refactor: apply repository naming contract

Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
Tianyi Cui
2026-08-13 00:36:22 +08:00
parent 101df7cf58
commit a2d0f7f411
3281 changed files with 21730 additions and 21592 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-06-16-typed-event-schemas.md
2026-06-16-typed-event-schemas.md: 8a73970d59fa75cc26e0518e441e704393c692f8
2026-06-16-typed-event-schemas.zh.md: 35afc9b977749b5b74b3634d14ee88f842d9bf33
2026-06-16-typed-event-schemas.md: 5082608f2ef4eb1d03d83e93de2814147a3a0b26
2026-06-16-typed-event-schemas.zh.md: 291799807cef3be02af846af0caa235e487143ca

View File

@@ -26,7 +26,7 @@ So the real proposal is: **replace the compile-time merge-extensible-map pattern
A migration of the event/vocabulary API to runtime schemas touches, at minimum:
- **Six merge-extensible maps** (~370 LOC of core types): `ContentBlockMap`, `MessageSourceMap`, `FinishReasonMap` (in `dsh-llm`); `TurnTriggerMap`, `TurnEndReasonMap`, `SessionEventMap` (in `dsh-session`).
- **~10 `declare module` augmentation sites** across `dsh-agent`, `dsh-agent-loop`, `dsh-bash`, `dsh-llm`, `dsh-session`, `dsh-session-persistence`, `dsh-system-prompt`, `dsh-tools` — each would move from declaration merging to a runtime `register()` call.
- **~10 `declare module` augmentation sites** across `dsh-agent`, `dsh-agent-loop`, `dsh-shell`, `dsh-llm`, `dsh-session`, `dsh-session-persistence`, `dsh-system-prompt`, `dsh-tools` — each would move from declaration merging to a runtime `register()` call.
- **The event producers** — 16 `session.append(...)` call sites in the loop — unchanged in shape but now validated at the boundary.
- **~7 switch-consumers** that branch on these unions: `deriveMessages` and the package-owned invariant companion (`dsh-session`), `BlockAssembler` (`dsh-llm`), both LLM adapters (`dsh-llm-deepseek`, `dsh-llm-pi-ai`), and the tool schema layer (`dsh-tools`). The `assertNever`-on-closed-unions vs fall-through-on-extensible-unions convention (a documented lint rule) would need rethinking — runtime variants are not statically exhaustive.
- **The `defineTool` `InferArgs` DSL** (`dsh-tools`), which derives zero-cast `execute` arg types from a compile-time schema spec — the showcase of the current approach.

View File

@@ -26,7 +26,7 @@ harness 将其核心词汇——内容块、消息来源、结束原因、轮次
将事件/词汇接口迁移到运行时 schema,至少涉及:
- **六个 merge-extensible map**(约 370 行核心类型):`ContentBlockMap`、`MessageSourceMap`、`FinishReasonMap`(位于 `dsh-llm`);`TurnTriggerMap`、`TurnEndReasonMap`、`SessionEventMap`(位于 `dsh-session`)。
- **约 10 处 `declare module` 声明增补位置**,分布在 `dsh-agent`、`dsh-agent-loop`、`dsh-bash`、`dsh-llm`、`dsh-session`、`dsh-session-persistence`、`dsh-system-prompt`、`dsh-tools` 各包中——每处都将从声明合并改为运行时 `register()` 调用。
- **约 10 处 `declare module` 声明增补位置**,分布在 `dsh-agent`、`dsh-agent-loop`、`dsh-shell`、`dsh-llm`、`dsh-session`、`dsh-session-persistence`、`dsh-system-prompt`、`dsh-tools` 各包中——每处都将从声明合并改为运行时 `register()` 调用。
- **事件生产者**——agent loop(智能体循环)中 16 处 `session.append(...)` 调用——形状不变,但现在在边界处被校验。
- **约 7 个 switch 消费方**,对这些联合类型进行分支:`deriveMessages` 与包自有的不变式 companion(`dsh-session`)、`BlockAssembler`(`dsh-llm`)、两个 LLM(大语言模型)适配器(`dsh-llm-deepseek`、`dsh-llm-pi-ai`)以及工具 schema 层(`dsh-tools`)。`assertNever` 对封闭联合类型的穷举 vs 对可扩展联合类型的 fall-through 约定(一条已记录的 lint 规则)需要重新考量——运行时变体在静态层面不可穷举。
- **`defineTool` 的 `InferArgs` DSL**(`dsh-tools`),它从编译期 schema 规范派生出零类型转换的 `execute` 参数类型——这是当前方案的标杆用例。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-07-19-required-cancellation-through-tool-capability-seams.md
2026-07-19-required-cancellation-through-tool-capability-seams.md: 1909b8451b6c70bde7c67f0cc0c1c3f4db443f7f
2026-07-19-required-cancellation-through-tool-capability-seams.md: 380204200711158fcda640fe25551293515fc49a
2026-07-19-required-cancellation-through-tool-capability-seams.zh.md: 631abfaa1cd21578437458cc194e277dc7883792

View File

@@ -18,7 +18,7 @@ Require an `AbortSignal` on every asynchronous same-process capability operation
Each direct caller supplies a signal it owns or propagates from its own required operation context. Implementations may derive a child deadline or cancellation scope, but the derived signal remains linked to the upstream signal for the delegated lifetime. Capability implementations do not synthesize never-abort signals, use ambient async-local cancellation, or validate `AbortSignal` at runtime solely to repeat the typed same-process contract.
The migration begins with an inventory from every first-party `ToolDefinition.execute()` through the capability calls it awaits. It then changes each coherent Service Definition / Service provider / Consumer seam together, including tests and generated API documentation. Separate PRs may migrate filesystem, bash/task, web/provider, workflow/subagent, code-runtime, and similar families so each change remains reviewable, but no migrated interface keeps an optional compatibility overload under the repository's pre-release policy.
The migration begins with an inventory from every first-party `ToolDefinition.execute()` through the capability calls it awaits. It then changes each coherent Service Definition / Service provider / Consumer seam together, including tests and generated API documentation. Separate PRs may migrate filesystem, shell/task, web/provider, workflow/subagent, code-runtime, and similar families so each change remains reviewable, but no migrated interface keeps an optional compatibility overload under the repository's pre-release policy.
### Scope boundary

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md
2026-07-24-domain-kv-storage-and-workspace.md: 2170534017ff81aeeb6c58ea04503b40ff5b5ee6
2026-07-24-domain-kv-storage-and-workspace.zh.md: c619ab3e47d6a3dea383bc7b78c42a4b4240799f
2026-07-24-domain-kv-storage-and-workspace.md: ff0db847f6f15130d38a3a001daf8b129311f3a3
2026-07-24-domain-kv-storage-and-workspace.zh.md: 4aff51448103367a6d3340ea9cd046b9edbd22a3

View File

@@ -25,11 +25,11 @@ Create the `packages/storage/` group — the `ctx.storage` hub (backend registry
| `@deepseek-ai/dsh-storage-json` | `packages/storage/storage-json/` | registers backend `json` | ✓ |
| `@deepseek-ai/dsh-storage-sqlite` | `packages/storage/storage-sqlite/` | registers backend `sqlite` | ✓ |
| `@deepseek-ai/dsh-storage-domain` | `packages/storage/storage-domain/` | mounts `ctx.storage.domain` | ✓ |
| `@deepseek-ai/dsh-workspace` | `packages/workspace/workspace/` | `ctx.workspace` | ✓ |
| `@deepseek-ai/dsh-workspace` | `packages/workspace/workspace/` | `ctx.workspaceRegistry` | ✓ |
| `SessionPersistence.delete` extension + cascade orchestration | `packages/session/session-persistence*` | new method on the existing seam | ✗ future work (session side untouched this phase) |
| `workspace.*` / `session.delete` RPC, GUI wiring, boot assembly | — | — | ✗ next phase |
(workspace lives in its own group rather than `packages/host/`: the host group's naming rule requires the `dsh-host-*` prefix while this package is named `dsh-workspace`; and the workspace entity is a domain concept, not bound to the host assembly tier. Unrelated to the existing `workspace-context` package — that is an AGENTS.md instruction loader.)
(workspace lives in its own group rather than `packages/host/`: the host group's naming rule requires the `dsh-host-*` prefix while this package is named `dsh-workspace`; and the workspace entity is a domain concept, not bound to the host assembly tier. Unrelated to the existing `agent-instructions` package — that is an AGENTS.md instruction loader.)
Dependency direction: `dsh-workspace` → `dsh-domain` → `dsh-storage` ← the two backends. `dsh-workspace` additionally depends on the read-only face of `ctx.sessionPersistence` (attach's cwd check reads the session header; when the service is absent, attach rejects outright — no verification, no bookkeeping). The `ctx.sessions` running-check for session deletion moves into future work together with the cascade.
@@ -192,7 +192,7 @@ Orchestration rules (implemented together with the cascade; the `session.delete`
### `dsh-workspace`
The package owns the `WorkspaceId` brand and exposes `ctx.workspace`. The record key is a generated uuid — path is not the key: normalization rewrites it, and reference anchors must be stable.
The package owns the `WorkspaceId` brand and exposes `ctx.workspaceRegistry`. The record key is a generated uuid — path is not the key: normalization rewrites it, and reference anchors must be stable.
```ts ignore-check
export type WorkspaceId = Branded<'WorkspaceId'>
@@ -229,7 +229,7 @@ export interface Workspace {
}
export class WorkspaceRegistry extends Service {
constructor(ctx: Context) // super(ctx, 'workspace')
constructor(ctx: Context) // super(ctx, 'workspaceRegistry')
// start(): this.domain = await ctx.storage.domain.open(workspaceDomainSpec)
// 实体缓存 Map<WorkspaceId, WorkspaceEntity> 重建
create(path: string, title?: string): Promise<Workspace> // realpath 后撞已有 → reject
@@ -243,7 +243,7 @@ export class WorkspaceRegistry extends Service {
- **Path canon**: the stored value = `fs.realpath(input)` (trailing slashes, `..`, and symlinks all resolved); uniqueness = string equality after normalization (a symlink resolving to the same directory counts as a collision). A missing directory makes create reject outright (realpath fails — a workspace must point at an existing directory; "Create new = make the directory" is upper-layer interaction: mkdir first, then create). The session cwd in attach checks follows the same canon. Single-valued cwd + unique path ⇒ one session structurally belongs to at most one workspace; double bookkeeping is impossible on the write side.
- **Title**: a display name, defaults to `basename(path)`, mutable, duplicates allowed. Ownership is never derived from cwd as a fallback — cwd cannot express ordering, and ownership is a workspace-side fact; sessions started headless belong to no workspace.
- Consumers see only the `Workspace` interface; `WorkspaceEntity` stays inside the package (a single implementation does not pre-split a seam). Entities are unique per id (registry cache); the record snapshot is swapped in place after each write, and the outside sees getters only. Every write funnels through the entity's internal `mutate(fn)` → `table.update`, with `updatedAt` refreshed inside mutate. Domain objects never cross RPC; next phase the wire layer projects records into zod wire schemas.
- **Session deletion remains future work.** The later [Workspace registration deletion decision](../../implemented/feature/2026-07-27-workspace-registration-deletion.md) ships `ctx.workspace.delete(id)` as a metadata-only operation that preserves Sessions and logs. Recursive Session deletion, running checks, and crash-rerun convergence belong to a separate `session.delete` capability.
- **Session deletion remains future work.** The later [Workspace registration deletion decision](../../implemented/feature/2026-07-27-workspace-registration-deletion.md) ships `ctx.workspaceRegistry.delete(id)` as a metadata-only operation that preserves Sessions and logs. Recursive Session deletion, running checks, and crash-rerun convergence belong to a separate `session.delete` capability.
Consistency doctrine (the ledger = the only ownership authority; the implementation and test baseline):
@@ -319,7 +319,7 @@ Snapshots: no model-visible or assembly surface this phase, none added; next pha
## Acceptance criteria
- This phase's four test suites all green: the shared backend contract suite on both json/sqlite, registry/mount disposer semantics, the domain layer (including the six open steps and fail-loud routing), and full workspace semantics (create/attach checks/consistency doctrine).
- `ctx.workspace` completes the create → attach → list → metadata-only delete lifecycle under a test assembly.
- `ctx.workspaceRegistry` completes the create → attach → list → metadata-only delete lifecycle under a test assembly.
- Zero diff in the session-persistence packages (the acceptance line for not touching the session side this phase).
- No new snapshots this phase (no model-visible or assembly surface); added next phase with the RPC wiring.

View File

@@ -25,11 +25,11 @@ host 侧唯一的持久化面是 session 事件日志(`packages/session/sessio
| `@deepseek-ai/dsh-storage-json` | `packages/storage/storage-json/` | 注册后端 `json` | ✓ |
| `@deepseek-ai/dsh-storage-sqlite` | `packages/storage/storage-sqlite/` | 注册后端 `sqlite` | ✓ |
| `@deepseek-ai/dsh-storage-domain` | `packages/storage/storage-domain/` | 挂载 `ctx.storage.domain` | ✓ |
| `@deepseek-ai/dsh-workspace` | `packages/workspace/workspace/` | `ctx.workspace` | ✓ |
| `@deepseek-ai/dsh-workspace` | `packages/workspace/workspace/` | `ctx.workspaceRegistry` | ✓ |
| `SessionPersistence.delete` 扩面 + 级联删编排 | `packages/session/*` | 既有 seam 新方法 | ✗ future work(本期不动 session 侧) |
| `workspace.*` / `session.delete` RPC、GUI 接线、boot 组装 | — | — | ✗ 下期 |
(workspace 放独立组不放 `packages/host/`:host 组命名规则要求 `dsh-host-*` 前缀,而包名定为 `dsh-workspace`;且 workspace 实体是领域概念,不绑定 host 装配层。与既有 `workspace-context` 包无关——那是 AGENTS.md 指令加载器。)
(workspace 放独立组不放 `packages/host/`:host 组命名规则要求 `dsh-host-*` 前缀,而包名定为 `dsh-workspace`;且 workspace 实体是领域概念,不绑定 host 装配层。与既有 `agent-instructions` 包无关——那是 AGENTS.md 指令加载器。)
依赖方向:`dsh-workspace` → `dsh-domain` → `dsh-storage` ← 两后端。`dsh-workspace` 另依赖 `ctx.sessionPersistence` 的只读面(attach 的 cwd 校验读 session header;服务缺席时 attach 直接拒绝——无法校验即不写账)。session 删除相关的 `ctx.sessions` 运行中检查随级联删一并归入 future work。
@@ -192,7 +192,7 @@ export abstract class SessionPersistence extends Service {
### `dsh-workspace`
包拥有 `WorkspaceId` brand,暴露 `ctx.workspace`。记录 key 为生成的 uuid——path 不做 key:规范化会改写它,引用锚点必须稳定。
包拥有 `WorkspaceId` brand,暴露 `ctx.workspaceRegistry`。记录 key 为生成的 uuid——path 不做 key:规范化会改写它,引用锚点必须稳定。
```ts ignore-check
export type WorkspaceId = Branded<'WorkspaceId'>
@@ -229,7 +229,7 @@ export interface Workspace {
}
export class WorkspaceRegistry extends Service {
constructor(ctx: Context) // super(ctx, 'workspace')
constructor(ctx: Context) // super(ctx, 'workspaceRegistry')
// start(): this.domain = await ctx.storage.domain.open(workspaceDomainSpec)
// 实体缓存 Map<WorkspaceId, WorkspaceEntity> 重建
create(path: string, title?: string): Promise<Workspace> // realpath 后撞已有 → reject
@@ -243,7 +243,7 @@ export class WorkspaceRegistry extends Service {
- **path 规范**:落盘值 = `fs.realpath(输入)`(尾斜杠、`..`、符号链接全解析);唯一性 = 规范化后字符串相等(符号链接指向同一目录算撞)。目录不存在时 create 直接 reject(realpath 失败——workspace 必须指向存在目录;"Create new = 建目录"是上层交互,先 mkdir 再 create)。attach 校验的 session cwd 同口径。cwd 单值 + path 唯一 ⇒ 一个 session 结构上最多归属一个 workspace,双重记账写侧不可能。
- **title**:显示名,默认 `basename(path)`,可改,允许重复。归属不用 cwd 派生兜底——cwd 表达不了排序,归属是 workspace 侧事实;headless 直开的 session 不属于任何 workspace。
- 消费方只见 `Workspace` 接口,`WorkspaceEntity` 不出包(单实现不预拆 seam);实体按 id 唯一(注册表缓存),记录快照写后原地换新,外部只见 getter;所有写收敛到实体内 `mutate(fn)` → `table.update`,`updatedAt` 在 mutate 内统一刷。领域对象不过 RPC,下期 wire 层把记录投影成 zod wire schema。
- **Session 删除仍属未来工作。** 后续的 [Workspace 注册记录删除决策](../../implemented/feature/2026-07-27-workspace-registration-deletion.md)已将 `ctx.workspace.delete(id)` 作为仅删除元数据、保留 Session 与日志的操作交付。递归删除 Session、运行中检查和崩溃重跑收敛属于独立的 `session.delete` 能力。
- **Session 删除仍属未来工作。** 后续的 [Workspace 注册记录删除决策](../../implemented/feature/2026-07-27-workspace-registration-deletion.md)已将 `ctx.workspaceRegistry.delete(id)` 作为仅删除元数据、保留 Session 与日志的操作交付。递归删除 Session、运行中检查和崩溃重跑收敛属于独立的 `session.delete` 能力。
一致性口径(账 = 归属唯一依据;实现与测试基准):
@@ -319,7 +319,7 @@ export class WorkspaceRegistry extends Service {
## 验收标准
- 测试矩阵本期四套件全绿:后端约定共享套件在 json/sqlite 双端、注册表/mount disposer 语义、domain 层(含 open 六步与路由 fail-loud)、workspace 全语义(create/attach 校验/一致性口径)。
- `ctx.workspace` 可在测试组装下完成 create → attach → list → 仅删除元数据的 delete 生命周期。
- `ctx.workspaceRegistry` 可在测试组装下完成 create → attach → list → 仅删除元数据的 delete 生命周期。
- session-persistence 包零 diff(本期不动 session 侧的验收线)。
- 本期无新快照(无模型可见面与组装面);下期 RPC 接线时补。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.md
2026-07-25-client-settings-locale-theme.md: 961a7d96705095486ed7e4b2044f8f0f4a3cd79c
2026-07-25-client-settings-locale-theme.zh.md: a615bdf9296c1e31a9c8ad258d6ce0829c2bc8b9
2026-07-25-client-settings-locale-theme.md: 9d7a61ae7ca5241145346fb38ed28836e75cb01e
2026-07-25-client-settings-locale-theme.zh.md: 312508c7f1175ec9396ce7ea7ba0d4a3047816ba

View File

@@ -10,7 +10,7 @@ The browser client's existing Settings is written directly inside the Sidebar, a
## Proposal
**Collaboration doctrine (how every later module joins Settings): feature owners self-register.** The Settings shell is a pure composition surface: it only declares slots and renders the chrome structure — zero copy, no locale dependency, and neither importing nor enumerating any feature; for a feature to appear in Settings, its own plugin registers into the corresponding slot — locale registers the Language row, ui-theme registers the Appearance row, ui-models registers the Models top-level panel. No separate `ui-settings-*` package is created for "a feature's settings page": the settings surface belongs to the feature package itself (shipping the Theme feature means Theme's settings choices ship with ui-theme). Content that belongs to no single feature (the trigger/title/close chrome copy, the General directory with its skeleton rows, the `settings` dictionary) is owned by `ui-settings-general` — the owner of the ownerless copy, not a feature satellite package.
**Collaboration doctrine (how every later module joins Settings): feature owners self-register.** The Settings shell is a pure composition surface: it only declares slots and renders the chrome structure — zero copy, no locale dependency, and neither importing nor enumerating any feature; for a feature to appear in Settings, its own plugin registers into the corresponding slot — locale registers the Language row, ui-theme registers the Appearance row, ui-settings-models registers the Models top-level panel. No separate `ui-settings-*` package is created for "a feature's settings page": the settings surface belongs to the feature package itself (shipping the Theme feature means Theme's settings choices ship with ui-theme). Content that belongs to no single feature (the trigger/title/close chrome copy, the General directory with its skeleton rows, the `settings` dictionary) is owned by `ui-settings-general` — the owner of the ownerless copy, not a feature satellite package.
The Sidebar declares the `sidebar.settings` single slot; `ui-settings` occupies it and declares four slots: `settings.trigger` / `settings.header` / `settings.close` (chrome content seats, single) and `settings.section` (top-level pages, list). Accessible names all resolve from slot content: the trigger's accessible name is its text content, the dialog points at the header content node via aria-labelledby, and close is a visually hidden text seat. Each section is contributed by a feature plugin; the shell only reads entry metadata from the slot ledger to build the navigation, rendering the current section via `only`. General is registered by `ui-settings-general` (order 0) and declares the `settings.general.item` list slot, into which the feature plugins' preference rows slot by order.
@@ -20,7 +20,7 @@ The Settings entry is the Settings row in the sidebar Foot; clicking it directly
Each feature row's apply layer subscribes to its own change event (locale to `locale/change`, ui-theme to `theme/change`) and projects the snapshot into the slot store declared when that row registered. React components only read `useStore` and write through the injected setter callbacks, never reading ctx or the services.
The theme preference has three states — `light`, `dark`, `system` — defaulting to `system` (when no persisted preference exists or the value is bad). Resolving system belongs to the theme domain: ThemeService holds the `prefers-color-scheme` matchMedia listener (environment sensing, not DOM presentation) and re-emits the snapshot when the preference is system and the system color scheme changes; the snapshot carries both `preference` and the resolved `active` definition.
The theme preference has three states — `light`, `dark`, `system` — defaulting to `system` (when no persisted preference exists or the value is bad). Resolving system belongs to the theme domain: ThemeRuntime holds the `prefers-color-scheme` matchMedia listener (environment sensing, not DOM presentation) and re-emits the snapshot when the preference is system and the system color scheme changes; the snapshot carries both `preference` and the resolved `active` definition.
The theme service never touches the DOM. `ui-layout` reads the Theme getter initially and then subscribes to `theme/change`; the presenter owned by Layout updates `body[data-ds-dark-theme]` and the theme tokens according to `active`. The presenter has no notion of system — it consumes only resolved results.
@@ -32,7 +32,7 @@ The theme service never touches the DOM. `ui-layout` reads the Theme getter init
| General section (order 0) | `ui-settings-general` | Permission and Tool Call visual skeletons (no write operations) plus the `settings.general.item` slot declaration |
| Language row (item order 0) | `locale` | Selector dropdown; 中文/English genuinely switch |
| Appearance row (item order 10) | `ui-theme` | Light/Dark/System three cubes genuinely switch (the selected state reflects preference) |
| Models section (order 10) | `ui-models` | Navigation item only, with an empty content area; later model-management features land in that package |
| Models section (order 10) | `ui-settings-models` | Navigation item only, with an empty content area; later model-management features land in that package |
| Plugin | none | Not built this phase, and the navigation does not show the item (once a later plugin feature package registers the section it appears automatically) |
The first phase localizes only the copy inside the Settings overlay; dictionaries stay close to their owners — the chrome plus the General skeletons live in `ui-settings-general`'s `settings` namespace, and feature-row copy lives in each feature package (`settings.locale`, `settings.theme`, `settings.models`).
@@ -52,14 +52,14 @@ root
│ └─ settings.general.item list/root
│ ├─ language (0) locale 注册
│ └─ appearance (10) ui-theme 注册
└─ models (order 10) ui-models 注册
└─ models (order 10) ui-settings-models 注册
```
Section and item contributions use `ctx.slots.inject()` and do not depend on the client manifest's apply order; localized labels ride the label thunk from the [full-rollout note](../../implemented/architecture/2026-07-30-client-locale-full-rollout.md). The SlotMap types split homes: trigger/header/close/section have their canonical home in the ui-settings contract (the consumers, general and models, both depend on the shell — no cycle); `settings.general.item`'s canonical home is the locale package — it is the lowest common dependency of all item registrants (a settings row always carries copy), while the declarer general's contract is unreachable from locale/ui-theme (it would form a cycle); ui-theme consumes it through a re-export outlet.
### Slot declarations are first-class injectable waits
`SlotsService.inject()` now waits on the typed ledger key directly; it does not bridge declarations into synthetic `slot:<name>` Cordis services. The callback follows declaration collapse and redeclaration while its controller remains owned by the contributing plugin fiber, and direct registration into an undeclared slot still fails loud. This removes the stale-disposer presence machine and the typo-prone parallel service namespace. The complete lifecycle and failure contract lives in the [slot declaration injection decision](../../implemented/architecture/2026-08-05-slot-declaration-injection.md).
`SlotRegistry.inject()` now waits on the typed ledger key directly; it does not bridge declarations into synthetic `slot:<name>` Cordis services. The callback follows declaration collapse and redeclaration while its controller remains owned by the contributing plugin fiber, and direct registration into an undeclared slot still fails loud. This removes the stale-disposer presence machine and the typo-prone parallel service namespace. The complete lifecycle and failure contract lives in the [slot declaration injection decision](../../implemented/architecture/2026-08-05-slot-declaration-injection.md).
### Service contracts
@@ -127,4 +127,4 @@ Locale ships with 中文 and English built in; `setLocale`/`setTheme` are the on
## Risks
The apply order of slot declarations and contributions is not fixed, so every section/item registrant must use `ctx.slots.inject()` rather than a service or local-disposer presence signal. Service events may fire before a row's first render, so both a feature row store's init and the inject attach must align to the current snapshot from the getter. The duplicated merge copies of `settings.general.item` (locale, ui-theme) must stay verbatim-identical to the ui-settings canonical home — any drift means changing all three together. Layout must clean up the global attributes it set on unmount, and ThemeService must remove its matchMedia listener on dispose, so nothing lingers after HMR.
The apply order of slot declarations and contributions is not fixed, so every section/item registrant must use `ctx.slots.inject()` rather than a service or local-disposer presence signal. Service events may fire before a row's first render, so both a feature row store's init and the inject attach must align to the current snapshot from the getter. The duplicated merge copies of `settings.general.item` (locale, ui-theme) must stay verbatim-identical to the ui-settings canonical home — any drift means changing all three together. Layout must clean up the global attributes it set on unmount, and ThemeRuntime must remove its matchMedia listener on dispose, so nothing lingers after HMR.

View File

@@ -10,7 +10,7 @@ Status: proposed
## 提案
**协作导向(后续所有模块接入 Settings 的方式):功能属主自注册。** Settings 壳是纯组合面:只声明 slot、渲染 chrome 结构,零文案、不依赖 locale、不 import 也不枚举任何功能;一个功能要出现在 Settings 里,由它自己的插件向对应 slot 注册——locale 注册 Language 行,ui-theme 注册 Appearance 行,ui-models 注册 Models 一级面板。不为「某功能的设置页」单开 `ui-settings-*` 包:设置面属于功能包本身(做 Theme 功能,Theme 的设置选择就随 ui-theme 一起交付)。不属于任何单一功能的内容(trigger/标题/close 的 chrome 文案、General 目录与骨架行、`settings` 字典)由 `ui-settings-general` 拥有——它是「无主文案」的属主,不是功能卫星包。
**协作导向(后续所有模块接入 Settings 的方式):功能属主自注册。** Settings 壳是纯组合面:只声明 slot、渲染 chrome 结构,零文案、不依赖 locale、不 import 也不枚举任何功能;一个功能要出现在 Settings 里,由它自己的插件向对应 slot 注册——locale 注册 Language 行,ui-theme 注册 Appearance 行,ui-settings-models 注册 Models 一级面板。不为「某功能的设置页」单开 `ui-settings-*` 包:设置面属于功能包本身(做 Theme 功能,Theme 的设置选择就随 ui-theme 一起交付)。不属于任何单一功能的内容(trigger/标题/close 的 chrome 文案、General 目录与骨架行、`settings` 字典)由 `ui-settings-general` 拥有——它是「无主文案」的属主,不是功能卫星包。
Sidebar 声明 `sidebar.settings` single slot,`ui-settings` 占用它并声明四个 slot:`settings.trigger` / `settings.header` / `settings.close`(chrome 内容座,single)与 `settings.section`(一级页面,list)。无障碍名称全部解析自 slot 内容:trigger 的无障碍名称即其文本内容,dialog 经 aria-labelledby 指向 header 内容节点,close 是视觉隐藏文本座。每个 section 由功能插件贡献;壳只从 slot ledger 读取 entry metadata 生成导航,通过 `only` 渲染当前 section。General 由 `ui-settings-general` 注册(order 0)并声明 `settings.general.item` list slot,功能插件的偏好行按 order 排入。
@@ -20,7 +20,7 @@ Settings 入口是 sidebar Foot 的 Settings 行,点击直接打开 1080×700
功能行的 apply 层各自订阅自家变更事件(locale 订 `locale/change`,ui-theme 订 `theme/change`),把快照投影到该行注册时声明的 slot store。React 组件只读 `useStore`、写注入的 setter callback,不读取 ctx 或服务。
Theme 偏好三态:`light`、`dark`、`system`,默认 `system`(无持久化偏好或无效值时)。system 的解析属主题领域:ThemeService 持有 `prefers-color-scheme` matchMedia 监听(环境感知,非 DOM 呈现),偏好为 system 且系统配色变化时重发快照;快照同时携带 `preference` 与解析后的 `active` 定义。
Theme 偏好三态:`light`、`dark`、`system`,默认 `system`(无持久化偏好或无效值时)。system 的解析属主题领域:ThemeRuntime 持有 `prefers-color-scheme` matchMedia 监听(环境感知,非 DOM 呈现),偏好为 system 且系统配色变化时重发快照;快照同时携带 `preference` 与解析后的 `active` 定义。
Theme 服务不操作 DOM。`ui-layout` 初始读取 Theme getter,随后订阅 `theme/change`,由 Layout 持有的 presenter 按 `active` 更新 `body[data-ds-dark-theme]` 和主题 token;presenter 不感知 system,只消费已解析结果。
@@ -32,7 +32,7 @@ Theme 服务不操作 DOM。`ui-layout` 初始读取 Theme getter,随后订阅
| General section(order 0)| `ui-settings-general` | Permission、Tool Call 视觉骨架(无写操作)+ `settings.general.item` slot 声明 |
| Language 行(item order 0)| `locale` | Selector 下拉,中文/English 真实可切 |
| Appearance 行(item order 10)| `ui-theme` | Light/Dark/System 三 cube 真实可切(选中态看 preference) |
| Models section(order 10)| `ui-models` | 仅导航项,内容区为空;后续模型管理功能落在该包 |
| Models section(order 10)| `ui-settings-models` | 仅导航项,内容区为空;后续模型管理功能落在该包 |
| 插件 | 无 | 首期不做,导航不出现该项(后续插件功能包注册 section 即自动出现) |
首期只对 Settings 浮层内文案进行本地化;字典就近存放——chrome + General 骨架归 `ui-settings-general` 的 `settings` namespace,功能行文案归各功能包(`settings.locale`、`settings.theme`、`settings.models`)。
@@ -52,14 +52,14 @@ root
│ └─ settings.general.item list/root
│ ├─ language (0) locale 注册
│ └─ appearance (10) ui-theme 注册
└─ models (order 10) ui-models 注册
└─ models (order 10) ui-settings-models 注册
```
section/item contribution 使用 `ctx.slots.inject()`,不依赖 client manifest(元数据清单)的 apply 顺序;本地化 label 走 [全量接入 Note](../../implemented/architecture/2026-07-30-client-locale-full-rollout.md) 的 label thunk。SlotMap 类型分家:trigger/header/close/section 正家在 ui-settings 约定(消费方 general/models 均依赖壳,无环);`settings.general.item` 正家在 locale 包——它是全部 item 注册方的最低公共依赖(设置行必带文案),而声明方 general 的约定对 locale/ui-theme 不可达(会成环);ui-theme 经 re-export 出口消费。
### slot 声明是一等可注入等待对象
`SlotsService.inject()` 直接等待有类型约束的 ledger key;它不会将声明桥接为合成的 `slot:<name>` Cordis 服务。回调会跟随声明折叠与重新声明,而其控制器仍归贡献方插件 fiber 所有;直接向未声明 slot 注册仍会直接报错。这删除了基于陈旧 disposer 的在位状态机,以及容易因拼写错误出错的平行服务命名空间。完整的生命周期与失败约定见 [slot 声明注入决策](../../implemented/architecture/2026-08-05-slot-declaration-injection.md)。
`SlotRegistry.inject()` 直接等待有类型约束的 ledger key;它不会将声明桥接为合成的 `slot:<name>` Cordis 服务。回调会跟随声明折叠与重新声明,而其控制器仍归贡献方插件 fiber 所有;直接向未声明 slot 注册仍会直接报错。这删除了基于陈旧 disposer 的在位状态机,以及容易因拼写错误出错的平行服务命名空间。完整的生命周期与失败约定见 [slot 声明注入决策](../../implemented/architecture/2026-08-05-slot-declaration-injection.md)。
### 服务约定
@@ -127,4 +127,4 @@ Locale 内置中文和 English;`setLocale`/`setTheme` 是唯一写入口,未
## 风险
slot 声明与 contribution 的 apply 顺序不固定,所有 section/item 注册方必须使用 `ctx.slots.inject()`,而不能以服务或本地 disposer 作为在位信号。service event 可能早于行首次渲染,功能行 store 的 init 与 inject attach 都必须从 getter 对齐当前快照。`settings.general.item` 的重复合并副本(locale、ui-theme)与 ui-settings 正家必须逐字一致,漂移即三处一起改。Layout 卸载时必须清理自己设置的全局属性,ThemeService dispose(资源释放)时必须移除 matchMedia 监听,避免 HMR(热模块替换)后残留。
slot 声明与 contribution 的 apply 顺序不固定,所有 section/item 注册方必须使用 `ctx.slots.inject()`,而不能以服务或本地 disposer 作为在位信号。service event 可能早于行首次渲染,功能行 store 的 init 与 inject attach 都必须从 getter 对齐当前快照。`settings.general.item` 的重复合并副本(locale、ui-theme)与 ui-settings 正家必须逐字一致,漂移即三处一起改。Layout 卸载时必须清理自己设置的全局属性,ThemeRuntime dispose(资源释放)时必须移除 matchMedia 监听,避免 HMR(热模块替换)后残留。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md
2026-07-27-session-projection-and-command-log.md: 01e77de47c7b5178ddc146e95487754a616639ec
2026-07-27-session-projection-and-command-log.zh.md: 85da237976e2e0764c02369d16540f6489a5a3b6
2026-07-27-session-projection-and-command-log.md: 7681ef8462ef194a2dd3dcbcd2cb84d354f98aef
2026-07-27-session-projection-and-command-log.zh.md: 6f6cc75994222025c412bec9f0b25f6d2dd982c3

View File

@@ -58,7 +58,7 @@ declare module 'cordis' {
### Shipped consumer: the subagent identity unit
The registry's two read faces already serve a shipped consumer beyond this RFC's wire plan: [subagent list identity via the projection unit](../../implemented/architecture/2026-08-06-subagent-list-identity-projection.md) registers a `subagent` unit — the durable mode/label identity folded last-wins from `subagent/descriptor` — and `SubagentService.listChildren` reads it through `snapshot()` for a live child (the watermark cache, zero log reads) and `restore({}, events, 0)` over one persistence inspection for a cold one. The registry contract is unchanged: no failure channel and no new read face — a unit never throws, an absent value is the signal, and how absence renders is that consumer's decision.
The registry's two read faces already serve a shipped consumer beyond this RFC's wire plan: [subagent list identity via the projection unit](../../implemented/architecture/2026-08-06-subagent-list-identity-projection.md) registers a `subagent` unit — the durable mode/label identity folded last-wins from `subagent/descriptor` — and `SubagentRuntime.listChildren` reads it through `snapshot()` for a live child (the watermark cache, zero log reads) and `restore({}, events, 0)` over one persistence inspection for a cold one. The registry contract is unchanged: no failure channel and no new read face — a unit never throws, an absent value is the signal, and how absence renders is that consumer's decision.
### Wire: projections block on the history tail page

View File

@@ -58,7 +58,7 @@ declare module 'cordis' {
### 已交付的消费方:subagent 身份单元
注册表的两处既有读法已经服务于本 RFC 协议计划之外的一个已交付消费方:[subagent 列表经投影单元读取身份](../../implemented/architecture/2026-08-06-subagent-list-identity-projection.md)注册了 `subagent` 单元——从 `subagent/descriptor` 按 last-wins 折叠出的持久化 mode/label 身份——`SubagentService.listChildren` 对 live child 经 `snapshot()` 读取(水位缓存,零日志读),对 cold child 则用一次持久化整读的结果调用 `restore({}, events, 0)` 读取。注册表约定不变:没有失败通道、没有新读法——单元永不抛错,值缺席本身就是信号,缺席如何呈现是该消费方自己的决定。
注册表的两处既有读法已经服务于本 RFC 协议计划之外的一个已交付消费方:[subagent 列表经投影单元读取身份](../../implemented/architecture/2026-08-06-subagent-list-identity-projection.md)注册了 `subagent` 单元——从 `subagent/descriptor` 按 last-wins 折叠出的持久化 mode/label 身份——`SubagentRuntime.listChildren` 对 live child 经 `snapshot()` 读取(水位缓存,零日志读),对 cold child 则用一次持久化整读的结果调用 `restore({}, events, 0)` 读取。注册表约定不变:没有失败通道、没有新读法——单元永不抛错,值缺席本身就是信号,缺席如何呈现是该消费方自己的决定。
### 协议层:历史尾页上的 projections 块

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md
2026-07-28-storage-root-and-derived-medium-recovery.md: 9463af00e2a2f5e2d3cee4cc7d386173007abf45
2026-07-28-storage-root-and-derived-medium-recovery.zh.md: 2466b8e3341d3b85beb23cca97f365d71658e79d
2026-07-28-storage-root-and-derived-medium-recovery.md: 0eb040007e928412d30cd19aa80554f5a02a9c2a
2026-07-28-storage-root-and-derived-medium-recovery.zh.md: b71b3d74d642c80166e32446f1c752c8963bd4d2

View File

@@ -18,7 +18,7 @@ Two independent changes, one per gap.
### One global storage root (shipped, amended form); resolved once at construction (still open)
- **Shipped**: the Web overlay anchors `storage-json.root` to `$DSH_HOME/storages` directly in the row through the app-boot-provided `dshHomePath('storages')` (`~/.dsh/storages` by default, beside `~/.dsh/sessions`; no leading dot — the home is already a hidden tree). The helper delegates to the canonical `dsh-paths` resolver, and the session root uses the same function without duplicating its fallback and tilde rules. The per-row form was chosen (user decision) over a launcher patch + `storageRoot` profile key (see Alternatives); per-row overrides ride the personal `~/.dsh/config.yaml` patch layer. The web e2e scaffold already patches the row to an absolute temp root, so tests never touch the user's home.
- **Shipped**: the Web overlay anchors `storage-json.root` to `$DSH_HOME/storages` directly in the row through the app-boot-provided `dshHomePath('storages')` (`~/.dsh/storages` by default, beside `~/.dsh/sessions`; no leading dot — the home is already a hidden tree). The helper delegates to the canonical `dsh-home-paths` resolver, and the session root uses the same function without duplicating its fallback and tilde rules. The per-row form was chosen (user decision) over a launcher patch + `storageRoot` profile key (see Alternatives); per-row overrides ride the personal `~/.dsh/config.yaml` patch layer. The web e2e scaffold already patches the row to an absolute temp root, so tests never touch the user's home.
- **Still open**: `JsonStorageBackend` resolves its configured root once at construction (`resolve(config.root)`), adopting the JSONL backend's recorded rationale verbatim: a later `process.cwd()` change must not split one backend across roots. The SQLite storage backend already resolves its path.
- Pre-release stance applies (and was executed): no migration shim. A deployment that cached under `<cwd>/.storages` re-derives everything (workspace re-bootstraps from the header index; the projection cache refolds lazily) or moves the two json files by hand once.

View File

@@ -18,7 +18,7 @@ Status: proposed
### 全局唯一存储根(已落地,形态修正);构造时 resolve 一次(仍开放)
- **已落地**:出厂 Web overlay 通过 app-boot 提供的 `dshHomePath('storages')`,直接在 `storage-json` 行内把 `root` 锚定到 `$DSH_HOME/storages`(默认 `~/.dsh/storages`,与 `~/.dsh/sessions` 并肩;目录名不带点——home 本身已是隐藏树)。该辅助函数委托给规范的 `dsh-paths` 解析器,会话根也使用同一个函数,无需重复其回退和波浪号规则。最终选用按行形态(用户决定)而非「launcher patch + `storageRoot` profile 键」(见 Alternatives);按行覆盖仍走个人 `~/.dsh/config.yaml` patch 层。web e2e scaffold 本就把该行 patch 到临时绝对根,测试不触用户 home。
- **已落地**:出厂 Web overlay 通过 app-boot 提供的 `dshHomePath('storages')`,直接在 `storage-json` 行内把 `root` 锚定到 `$DSH_HOME/storages`(默认 `~/.dsh/storages`,与 `~/.dsh/sessions` 并肩;目录名不带点——home 本身已是隐藏树)。该辅助函数委托给规范的 `dsh-home-paths` 解析器,会话根也使用同一个函数,无需重复其回退和波浪号规则。最终选用按行形态(用户决定)而非「launcher patch + `storageRoot` profile 键」(见 Alternatives);按行覆盖仍走个人 `~/.dsh/config.yaml` patch 层。web e2e scaffold 本就把该行 patch 到临时绝对根,测试不触用户 home。
- **仍开放**:`JsonStorageBackend` 在构造时对配置根 `resolve` 一次,原样采纳 JSONL 后端已记录的理由:后续 `process.cwd()` 变化不得把一个后端劈到多个根下。SQLite 存储后端已经 resolve 其路径。
- 适用 pre-release 立场(已按此执行):不做迁移垫片。曾在 `<cwd>/.storages` 下缓存过的部署要么全部重新派生(工作区从 header 索引重新 bootstrap;投影缓存惰性重折),要么手动把两个 json 文件挪一次。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.md
2026-08-10-unary-apiproxy-remote-migration.md: aa3546ec2b79a0cd6d2866c194e9d6c1c22f9e33
2026-08-10-unary-apiproxy-remote-migration.zh.md: 653f560b38a1e6862ef6131f9e8189ed3cb3d6f8
2026-08-10-unary-apiproxy-remote-migration.md: 6ed58647429182eb34015db246c83701f48bd19b
2026-08-10-unary-apiproxy-remote-migration.zh.md: f0793f712386cbe1e518e7d40d5e0406f3c3231a

View File

@@ -6,17 +6,17 @@ English | [中文](2026-08-10-unary-apiproxy-remote-migration.zh.md)
## Problem
The Host API Proxy still owns many unary methods whose implementation is only service lookup, argument projection, one business call, and response projection. That duplicates the contract across the business Service, API Proxy interface, Zod schemas, route table, client stub, and Client caller even though [TypeRT Remote calls](../../implemented/architecture/2026-08-02-typert-remote-method-calls.md) already let the business package own this class of call.
The Host API Proxy still owns many unary methods whose implementation is only service lookup, argument projection, one business call, and response projection. That duplicates the contract across the business Service, API Proxy interface, Zod schemas, route table, client stub, and Client caller even though [Typert Remote calls](../../implemented/architecture/2026-08-02-typert-remote-method-calls.md) already let the business package own this class of call.
Moving a method mechanically is not sufficient. Agent-bound API Proxy methods call `agentFor()`, which reuses a live Agent, resumes an ordinary cold Session with its recorded preset, deduplicates concurrent resumes, and rejects subagent-owned identities. A Remote method that resolved an `Agent` or `Session` differently would change lifecycle behavior even when the final business call looked identical.
The API Proxy also contains BFF operations whose contract is not a business method: Session lifecycle and transcript assembly, model-selection state, live-only input control, configuration filtering, skill presentation, Host composition facts, and native desktop operations. Stateful interactions and streams have different lifecycles again. Treating all unary syntax as evidence that a method is simple would move product policy into arbitrary Service packages or force new packages that have no independent business owner.
Finally, Connection currently applies its loopback-only privileged-method list inside the API Proxy fallback. A TypeRT interceptor claims its endpoint before that fallback, so migrating credential or preset authoring calls without moving the privilege check would grant trusted-LAN callers operations that are currently loopback-only.
Finally, Connection currently applies its loopback-only privileged-method list inside the API Proxy fallback. A Typert interceptor claims its endpoint before that fallback, so migrating credential or preset authoring calls without moving the privilege check would grant trusted-LAN callers operations that are currently loopback-only.
## Proposal
Migrate only unary calls whose business operation already has a natural Service owner and whose remaining adaptation is a small parameter or result projection. The Service binds a TypeRT namespace and decorates an existing method directly with `@Remote` when its signature is the intended consumer contract. A new method is justified only when it performs real adaptation; an identity `remote*` forwarding wrapper is not.
Migrate only unary calls whose business operation already has a natural Service owner and whose remaining adaptation is a small parameter or result projection. The Service binds a Typert namespace and decorates an existing method directly with `@Remote` when its signature is the intended consumer contract. A new method is justified only when it performs real adaptation; an identity `remote*` forwarding wrapper is not.
`@deepseek-ai/dsh-api-remotes/client` will mount each selected business package's generated `/remote` contribution. Client business packages will call `ctx.remote.<service>` and perform Client-owned joins or presentation projection there. The corresponding API Proxy interface member, schema, route, handler, generated client method, fixture implementation, and production invocation will be removed together in that Service's vertical commit.
@@ -27,9 +27,9 @@ Large BFF methods remain in `dsh-host-apiproxy`. A method leaves this migration
| Legacy RPC | Remote destination | Host method | Adaptation |
|---|---|---|---|
| `session.rename` | `ctx.remote.sessionTitle` in `@deepseek-ai/dsh-session-title` | `SessionTitleService.rename(Session, title)` | Direct `@Remote`; Client maps `eventSeq` to its title projection sequence. |
| `command.list`, `command.execute` | `ctx.remote.commands` in `@deepseek-ai/dsh-commands` | `CommandService.list(Agent)`, `execute(Agent, line, signal)` | Direct `@Remote`; Client maps `undefined` to unmatched and preserves caller cancellation. |
| `llm.providers` | `ctx.remote.llm` in `@deepseek-ai/dsh-llm` | `LlmService.listProviders()`, `listConfigurableProviders()` | Direct `@Remote` on both reads; the Client joins registration and configuration-directory rows. |
| `credentials.describe`, `credentials.set`, `credentials.unset` | `ctx.remote.credentials` in `@deepseek-ai/dsh-credentials-local` | `CredentialsLocal.describe(ref)`, `set(ref, value)`, `unset(ref)` | Direct `@Remote`; Client batches `describe` calls when its UI requests several refs. |
| `command.list`, `command.execute` | `ctx.remote.commands` in `@deepseek-ai/dsh-commands` | `CommandRuntime.list(Agent)`, `execute(Agent, line, signal)` | Direct `@Remote`; Client maps `undefined` to unmatched and preserves caller cancellation. |
| `llm.providers` | `ctx.remote.llm` in `@deepseek-ai/dsh-llm` | `LlmRuntime.listProviders()`, `listConfigurableProviders()` | Direct `@Remote` on both reads; the Client joins registration and configuration-directory rows. |
| `credentials.describe`, `credentials.set`, `credentials.unset` | `ctx.remote.credentials` in `@deepseek-ai/dsh-credentials-local` | `LocalCredentialProvider.describe(ref)`, `set(ref, value)`, `unset(ref)` | Direct `@Remote`; Client batches `describe` calls when its UI requests several refs. |
| `agentPreset.read`, `agentPreset.copy`, `agentPreset.remove` | `ctx.remote.agentPresets` in `@deepseek-ai/dsh-agent-presets` | `readDocument(id)`, `copy(from, id, name?)`, `remove(id)` | `copy` and `remove` are direct; `readDocument` combines stored content with metadata from one live discovery. |
| `subagent.interrupt` | `ctx.remote.subagents` in `@deepseek-ai/dsh-subagent` | `interruptByParent(targetSessionId, parentSessionId)` | Adapter constructs the internal user-authority variant without resolving or resuming either Agent. |
| `workspace.list`, `workspace.insertSessionBefore`, `workspace.archiveSession` | `ctx.remote.workspace` in `@deepseek-ai/dsh-workspace` | `snapshot()`, `insertSessionBefore(workspaceId, sessionId, before?)`, `archiveSession(sessionId)` | Registry adapters detach mutable entities and return the settled workspace or archive snapshot. |
@@ -64,21 +64,21 @@ The migration must pin these outcomes with integration tests:
- concurrent Agent and Session lookups for one id share one resume;
- a live or cold subagent-owned identity fails with `agent-busy` before business invocation;
- an id missing from durable persistence fails with `session-not-found`;
- resolver failures keep their existing `RpcError` through `TypeRTLookupFailure`.
- resolver failures keep their existing `RpcError` through `TypertLookupFailure`.
Lookup policy is key-wide, not endpoint-specific. Methods such as prompt, queue editing, cancellation, model selection, and skill listing cannot use the shared `agent` or `session` lookup while retaining live-only or no-resume behavior, so they remain in the API Proxy until TypeRT supports an explicit per-endpoint policy.
Lookup policy is key-wide, not endpoint-specific. Methods such as prompt, queue editing, cancellation, model selection, and skill listing cannot use the shared `agent` or `session` lookup while retaining live-only or no-resume behavior, so they remain in the API Proxy until Typert supports an explicit per-endpoint policy.
Methods whose signatures contain only branded ids do not invoke TypeRT object lookup. `subagents.interruptByParent()` must retain the existing process-local Activation lookup and parent-offline behavior: it does not call `agentFor`, read the catalog, inspect persistence, or cold-resume a parent or child.
Methods whose signatures contain only branded ids do not invoke Typert object lookup. `subagents.interruptByParent()` must retain the existing process-local Activation lookup and parent-offline behavior: it does not call `agentFor`, read the catalog, inspect persistence, or cold-resume a parent or child.
## Client and error behavior
Generated Remote methods return business values and throw an Error whose `cause` contains the existing RPC failure. Client business services own adaptation to their current result/store interfaces. They must settle successful results immediately exactly as they do today so event frames remain idempotent replays rather than the only update path.
Resolver-owned `session-not-found` and `agent-busy` errors remain stable because the shared resolver raises `TypeRTLookupFailure`. Ordinary business exceptions become the Gateway's existing `internal` RPC failure. A selected Client consumer may migrate only if it does not branch on a more specific legacy business error code; if implementation finds such a branch, that RPC leaves this set unless the business package gains a transport-independent typed failure.
Resolver-owned `session-not-found` and `agent-busy` errors remain stable because the shared resolver raises `TypertLookupFailure`. Ordinary business exceptions become the Gateway's existing `internal` RPC failure. A selected Client consumer may migrate only if it does not branch on a more specific legacy business error code; if implementation finds such a branch, that RPC leaves this set unless the business package gains a transport-independent typed failure.
## Privileged authority
Connection must enforce privileged endpoint authority before choosing the TypeRT interceptor or API Proxy fallback. The check must recognize both legacy dotted names and Remote slash endpoints and keep these migrated operations loopback-only:
Connection must enforce privileged endpoint authority before choosing the Typert interceptor or API Proxy fallback. The check must recognize both legacy dotted names and Remote slash endpoints and keep these migrated operations loopback-only:
- `agentPresets/readDocument`, `agentPresets/copy`, and `agentPresets/remove`;
- `credentials/describe`, `credentials/set`, and `credentials/unset`.
@@ -93,7 +93,7 @@ The final commit generates every `/remote` artifact from a clean state, updates
## Alternatives considered
**Keep simple methods in the central API Proxy.** This preserves one transport facade but continues the duplicated interfaces, schemas, route rows, stubs, and business projections that TypeRT was introduced to remove.
**Keep simple methods in the central API Proxy.** This preserves one transport facade but continues the duplicated interfaces, schemas, route rows, stubs, and business projections that Typert was introduced to remove.
**Move every unary API Proxy method.** Unary syntax does not imply single-owner behavior. Session orchestration, live-only control, configuration exposure, and native Host operations would either leak BFF policy into generic Services or create ownerless packages.
@@ -121,4 +121,4 @@ Generated Remote contracts add build ordering and publication entries to each bu
Moving privilege enforcement to composite dispatch changes security-sensitive carrier code. Tests must exercise both a Remote-owned endpoint and a legacy fallback endpoint so neither path can bypass the loopback decision.
This note applies the existing TypeRT Remote architecture rather than superseding it. It partially supersedes the central unary ownership and five-step extension checklist in the [GUI RPC protocol note](../../implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md) and the central wiring inventory in the [Web configuration plane note](../../implemented/architecture/2026-07-30-web-config-plane.md); those notes remain authoritative for Connection envelopes and configuration behavior outside the migrated methods. The title, command, configuration-boundary, subagent-interrupt, and archive notes continue to own their business behavior and require factual transport updates rather than archival. The [browser trust boundary](../../implemented/architecture/2026-07-28-api-browser-trust-boundary.md) and [generated-contract build order](../../implemented/process/2026-08-08-api-remotes-generated-contract-build.md) remain authoritative and require no archival action.
This note applies the existing Typert Remote architecture rather than superseding it. It partially supersedes the central unary ownership and five-step extension checklist in the [GUI RPC protocol note](../../implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md) and the central wiring inventory in the [Web configuration plane note](../../implemented/architecture/2026-07-30-web-config-plane.md); those notes remain authoritative for Connection envelopes and configuration behavior outside the migrated methods. The title, command, configuration-boundary, subagent-interrupt, and archive notes continue to own their business behavior and require factual transport updates rather than archival. The [browser trust boundary](../../implemented/architecture/2026-07-28-api-browser-trust-boundary.md) and [generated-contract build order](../../implemented/process/2026-08-08-api-remotes-generated-contract-build.md) remain authoritative and require no archival action.

View File

@@ -6,17 +6,17 @@ Status: proposed
## 问题
Host API Proxy 仍承载许多一元方法。这些方法的实现仅执行服务查找、参数投影、一次业务调用和响应投影。尽管 [TypeRT Remote 调用](../../implemented/architecture/2026-08-02-typert-remote-method-calls.md)已经允许业务包承载此类调用,这种做法仍会在业务服务、API Proxy 接口、Zod schema、路由表、客户端 stub 和 Client 调用方之间重复定义同一约定。
Host API Proxy 仍承载许多一元方法。这些方法的实现仅执行服务查找、参数投影、一次业务调用和响应投影。尽管 [Typert Remote 调用](../../implemented/architecture/2026-08-02-typert-remote-method-calls.md)已经允许业务包承载此类调用,这种做法仍会在业务服务、API Proxy 接口、Zod schema、路由表、客户端 stub 和 Client 调用方之间重复定义同一约定。
仅机械迁移方法并不足够。与 Agent 绑定的 API Proxy 方法会调用 `agentFor()`:它复用 live Agent,使用普通冷 Session 中记录的 preset 恢复该 Session,对并发恢复去重,并拒绝由 subagent 拥有的 identity。如果 Remote 方法以不同方式解析 `Agent` 或 `Session`,即使最终业务调用看起来相同,也会改变生命周期行为。
API Proxy 还包含一些不以业务方法为约定的 BFF 操作:Session 生命周期与 transcript(文本记录)组装、模型选择状态、仅限 live 的输入控制、配置过滤、skill(技能)呈现、Host 组合信息和原生桌面操作。有状态交互与流又具有不同的生命周期。若把一元调用的语法一概视为方法简单的依据,就会把产品策略移入任意服务包,或者迫使系统新增没有独立业务所有者的包。
最后,Connection 目前在 API Proxy 回退路径内执行仅限环回地址的特权方法清单。TypeRT interceptor 会先于该回退路径认领自己的端点,因此,如果迁移凭据或 preset 创作调用时不一并迁移权限检查,受信任的局域网调用方就会获得目前仅向环回调用方开放的操作权限。
最后,Connection 目前在 API Proxy 回退路径内执行仅限环回地址的特权方法清单。Typert interceptor 会先于该回退路径认领自己的端点,因此,如果迁移凭据或 preset 创作调用时不一并迁移权限检查,受信任的局域网调用方就会获得目前仅向环回调用方开放的操作权限。
## 提案
只迁移符合以下条件的一元调用:其业务操作已经有自然归属的服务,且其余适配只是少量参数或结果投影。当现有方法的签名就是预期的消费方约定时,服务应绑定 TypeRT namespace,并直接使用 `@Remote` 装饰现有方法。只有执行实质性适配时才有理由新增方法;不得添加只做恒等转发的 `remote*` 包装层。
只迁移符合以下条件的一元调用:其业务操作已经有自然归属的服务,且其余适配只是少量参数或结果投影。当现有方法的签名就是预期的消费方约定时,服务应绑定 Typert namespace,并直接使用 `@Remote` 装饰现有方法。只有执行实质性适配时才有理由新增方法;不得添加只做恒等转发的 `remote*` 包装层。
`@deepseek-ai/dsh-api-remotes/client` 将挂载所选各业务包生成的 `/remote` 贡献。Client 业务包将调用 `ctx.remote.<service>`,并在包内执行归 Client 所有的关联或呈现投影。对应的 API Proxy 接口成员、schema、路由、处理程序、生成的客户端方法、fixture(测试前置数据)实现和生产调用点,将在该服务的纵向提交中一并移除。
@@ -27,9 +27,9 @@ API Proxy 还包含一些不以业务方法为约定的 BFF 操作:Session 生
| 旧 RPC | Remote 目标 | Host 方法 | 适配 |
|---|---|---|---|
| `session.rename` | `ctx.remote.sessionTitle`,位于 `@deepseek-ai/dsh-session-title` | `SessionTitleService.rename(Session, title)` | 直接使用 `@Remote`;Client 将 `eventSeq` 映射到自身的标题投影序列。 |
| `command.list`、`command.execute` | `ctx.remote.commands`,位于 `@deepseek-ai/dsh-commands` | `CommandService.list(Agent)`、`execute(Agent, line, signal)` | 直接使用 `@Remote`;Client 将 `undefined` 映射为未匹配结果,并保留调用方的取消行为。 |
| `llm.providers` | `ctx.remote.llm`,位于 `@deepseek-ai/dsh-llm` | `LlmService.listProviders()`、`listConfigurableProviders()` | 两项读取都直接使用 `@Remote`;Client 关联注册行与配置目录行。 |
| `credentials.describe`、`credentials.set`、`credentials.unset` | `ctx.remote.credentials`,位于 `@deepseek-ai/dsh-credentials-local` | `CredentialsLocal.describe(ref)`、`set(ref, value)`、`unset(ref)` | 直接使用 `@Remote`;当 UI 请求多个 ref 时,Client 批量发起 `describe` 调用。 |
| `command.list`、`command.execute` | `ctx.remote.commands`,位于 `@deepseek-ai/dsh-commands` | `CommandRuntime.list(Agent)`、`execute(Agent, line, signal)` | 直接使用 `@Remote`;Client 将 `undefined` 映射为未匹配结果,并保留调用方的取消行为。 |
| `llm.providers` | `ctx.remote.llm`,位于 `@deepseek-ai/dsh-llm` | `LlmRuntime.listProviders()`、`listConfigurableProviders()` | 两项读取都直接使用 `@Remote`;Client 关联注册行与配置目录行。 |
| `credentials.describe`、`credentials.set`、`credentials.unset` | `ctx.remote.credentials`,位于 `@deepseek-ai/dsh-credentials-local` | `LocalCredentialProvider.describe(ref)`、`set(ref, value)`、`unset(ref)` | 直接使用 `@Remote`;当 UI 请求多个 ref 时,Client 批量发起 `describe` 调用。 |
| `agentPreset.read`、`agentPreset.copy`、`agentPreset.remove` | `ctx.remote.agentPresets`,位于 `@deepseek-ai/dsh-agent-presets` | `readDocument(id)`、`copy(from, id, name?)`、`remove(id)` | `copy` 和 `remove` 直接暴露现有方法;`readDocument` 将存储的内容与一次实时发现取得的元数据组合。 |
| `subagent.interrupt` | `ctx.remote.subagents`,位于 `@deepseek-ai/dsh-subagent` | `interruptByParent(targetSessionId, parentSessionId)` | 适配器构造内部的用户权限变体,不解析也不恢复任一 Agent。 |
| `workspace.list`、`workspace.insertSessionBefore`、`workspace.archiveSession` | `ctx.remote.workspace`,位于 `@deepseek-ai/dsh-workspace` | `snapshot()`、`insertSessionBefore(workspaceId, sessionId, before?)`、`archiveSession(sessionId)` | 注册表适配器分离可变实体,并返回已完成更新的 workspace 或归档快照。 |
@@ -64,21 +64,21 @@ Remote API 有意采用服务名称,而不保留旧 RPC 的点分名称。例
- 对同一个 id 并发执行 Agent 与 Session lookup 时,共享同一次恢复;
- 无论 live 还是 cold,由 subagent 拥有的 identity 都会在业务调用前以 `agent-busy` 失败;
- 持久化存储中不存在的 id 以 `session-not-found` 失败;
- resolver 失败会保留现有的 `RpcError`,并通过 `TypeRTLookupFailure` 传递。
- resolver 失败会保留现有的 `RpcError`,并通过 `TypertLookupFailure` 传递。
Lookup 策略作用于整个 key,而非特定端点。提示词输入、队列编辑、取消、模型选择和 skill 列表等方法如果使用共享 `agent` 或 `session` lookup,就无法保留仅限 live 或禁止恢复的行为,因此在 TypeRT 支持显式的逐端点策略之前,这些方法仍留在 API Proxy 中。
Lookup 策略作用于整个 key,而非特定端点。提示词输入、队列编辑、取消、模型选择和 skill 列表等方法如果使用共享 `agent` 或 `session` lookup,就无法保留仅限 live 或禁止恢复的行为,因此在 Typert 支持显式的逐端点策略之前,这些方法仍留在 API Proxy 中。
签名只包含 branded id 的方法不会调用 TypeRT 对象 lookup。`subagents.interruptByParent()` 必须保留现有的进程内 Activation lookup 和父级离线行为:它不会调用 `agentFor`、读取目录、检查持久化,也不会冷恢复父 Agent 或子 Agent。
签名只包含 branded id 的方法不会调用 Typert 对象 lookup。`subagents.interruptByParent()` 必须保留现有的进程内 Activation lookup 和父级离线行为:它不会调用 `agentFor`、读取目录、检查持久化,也不会冷恢复父 Agent 或子 Agent。
## Client 与错误行为
生成的 Remote 方法返回业务值,并抛出一个 Error,其 `cause` 包含现有的 RPC 失败。Client 业务服务负责适配到当前的结果/store 接口。它们必须像当前一样让成功结果立即生效,使事件帧仍是幂等回放,而非唯一的更新路径。
Resolver 拥有的 `session-not-found` 和 `agent-busy` 错误保持稳定,因为共享 resolver 会抛出 `TypeRTLookupFailure`。普通业务异常会变成 Gateway 现有的 `internal` RPC 失败。只有在选定的 Client 消费方不根据更具体的旧版业务错误码进行分支时,才能迁移该调用;如果实现过程中发现这种分支,除非业务包新增与传输无关的类型化失败,否则该 RPC 将退出此集合。
Resolver 拥有的 `session-not-found` 和 `agent-busy` 错误保持稳定,因为共享 resolver 会抛出 `TypertLookupFailure`。普通业务异常会变成 Gateway 现有的 `internal` RPC 失败。只有在选定的 Client 消费方不根据更具体的旧版业务错误码进行分支时,才能迁移该调用;如果实现过程中发现这种分支,除非业务包新增与传输无关的类型化失败,否则该 RPC 将退出此集合。
## 特权调用权限
Connection 必须在选择 TypeRT interceptor 或 API Proxy 回退路径之前检查调用方是否有权访问特权端点。该检查必须同时识别旧式点分名称和 Remote 斜杠端点,并保持以下已迁移操作仅限环回地址:
Connection 必须在选择 Typert interceptor 或 API Proxy 回退路径之前检查调用方是否有权访问特权端点。该检查必须同时识别旧式点分名称和 Remote 斜杠端点,并保持以下已迁移操作仅限环回地址:
- `agentPresets/readDocument`、`agentPresets/copy` 和 `agentPresets/remove`;
- `credentials/describe`、`credentials/set` 和 `credentials/unset`。
@@ -93,7 +93,7 @@ Connection 必须在选择 TypeRT interceptor 或 API Proxy 回退路径之前
## 考虑过的替代方案
**将简单方法保留在中央 API Proxy 中。** 这会保留统一的传输外观,但仍会延续 TypeRT 原本要消除的重复接口、schema、路由行、stub 和业务投影。
**将简单方法保留在中央 API Proxy 中。** 这会保留统一的传输外观,但仍会延续 Typert 原本要消除的重复接口、schema、路由行、stub 和业务投影。
**迁移每一个一元 API Proxy 方法。** 一元调用形式并不表示行为只有一个所有者。Session 编排、仅限 live 的控制、配置暴露和原生 Host 操作要么会把 BFF 策略泄漏到通用服务中,要么会产生没有所有者的包。
@@ -121,4 +121,4 @@ Connection 必须在选择 TypeRT interceptor 或 API Proxy 回退路径之前
将权限强制执行移至复合分发会改变安全敏感的载体代码。测试必须覆盖一个由 Remote 拥有的端点和一个旧版回退端点,确保两条路径都无法绕过环回判定。
本文应用现有 TypeRT Remote 架构,而非取代它。本文部分取代 [GUI RPC 协议笔记](../../implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md)中的中央一元调用所有权和五步扩展检查清单,以及 [Web 配置平面笔记](../../implemented/architecture/2026-07-30-web-config-plane.md)中的中央接线清单;对于已迁移方法之外的 Connection envelope 和配置行为,这些笔记仍具权威性。标题、命令、配置边界、subagent 中断和归档笔记继续负责各自的业务行为,只需如实更新传输相关事实,无需归档。[浏览器信任边界](../../implemented/architecture/2026-07-28-api-browser-trust-boundary.md)和[生成约定构建顺序](../../implemented/process/2026-08-08-api-remotes-generated-contract-build.md)仍具权威性,无需执行归档操作。
本文应用现有 Typert Remote 架构,而非取代它。本文部分取代 [GUI RPC 协议笔记](../../implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md)中的中央一元调用所有权和五步扩展检查清单,以及 [Web 配置平面笔记](../../implemented/architecture/2026-07-30-web-config-plane.md)中的中央接线清单;对于已迁移方法之外的 Connection envelope 和配置行为,这些笔记仍具权威性。标题、命令、配置边界、subagent 中断和归档笔记继续负责各自的业务行为,只需如实更新传输相关事实,无需归档。[浏览器信任边界](../../implemented/architecture/2026-07-28-api-browser-trust-boundary.md)和[生成约定构建顺序](../../implemented/process/2026-08-08-api-remotes-generated-contract-build.md)仍具权威性,无需执行归档操作。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md
2026-08-11-repository-naming-contract-and-rename-ledger.md: 8edb23f645cc34254cd49bed0d83767596ed42d7
2026-08-11-repository-naming-contract-and-rename-ledger.zh.md: 9879316d207508efd96cd5064e6268c47df772d4
2026-08-11-repository-naming-contract-and-rename-ledger.md: 440ab360e0bd34071de244120aca75116a56ff30
2026-08-11-repository-naming-contract-and-rename-ledger.zh.md: e5f8d37bdee790b374ecf18be41c04ad8588da46

View File

@@ -36,7 +36,7 @@ Use a common, concrete noun. Name the stable responsibility, not the first imple
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.
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.
@@ -106,7 +106,7 @@ Keep `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, and `deepse
| 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. |
| `packages/pty/tool-bash-persistent` | `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. |
| `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.
@@ -121,6 +121,7 @@ Keep the Bash- and PowerShell-specific leaf packages, plugin ids, types, and too
| 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. |
@@ -163,7 +164,7 @@ Keep `@deepseek-ai/dsh-tools` and `ctx.tools`. Keep `@deepseek-ai/dsh-api-gatewa
| Current | Proposed | Reason |
|---|---|---|
| Host `ctx.workspace` | Host `ctx.workspaces` | `WorkspaceRegistry` owns multiple workspaces. The plural key matches the registry role. The existing Client `ctx.workspaces` runs in a separate Cordis context, so the shared spelling is intentional and cannot collide at runtime. Keep `@deepseek-ai/dsh-workspace`, `WorkspaceRegistry`, `Workspace`, and `workspace.*` wire names. |
| 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. |
@@ -172,7 +173,7 @@ Keep `@deepseek-ai/dsh-tools` and `ctx.tools`. Keep `@deepseek-ai/dsh-api-gatewa
| `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` | `session/anonymous-user-id/`, `@deepseek-ai/dsh-anonymous-user-id` | The value is a random correlation id, not an authenticated user identity. |
| `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. |
@@ -182,6 +183,7 @@ Keep `@deepseek-ai/dsh-tools` and `ctx.tools`. Keep `@deepseek-ai/dsh-api-gatewa
| Current | Proposed | 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. |
@@ -206,6 +208,8 @@ Keep `/compact`, the command package, and the rejected decision to preserve sepa
| `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-download`, `session-log-download/`, Loader id `session-log-download`, `ctx.sessionLogDownload` | This package does not implement general Session export. It controls the Web download of one Host-streamed Session-log ZIP. The subject and browser side effect must be in its name. |
| `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. |
@@ -255,7 +259,7 @@ Keep `@deepseek-ai/dsh-subagent-dsh-sdk`, its provider id `dsh-sdk`, external AC
| `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. |
| `packages/support/invariants/` | `runtime-diagnostics/invariants/` | Invariants can run in production diagnostics even though shipped presets omit them. They are not test support. |
| `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. |
@@ -269,6 +273,7 @@ Keep MCP, Todo, and the Plan Mode package, key, events, and tool names. The acce
| `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. |
@@ -281,6 +286,7 @@ Keep atomic-write, brand, native-command, timeout utility, directory-picker, `ds
| `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. |
@@ -289,11 +295,16 @@ Keep atomic-write, brand, native-command, timeout utility, directory-picker, `ds
| `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`, `settings.pluginConfig` | `PluginsSettingsSection`, `PluginsSettingsSectionProps`, `PluginsSettingsSectionInjected`, `settings.plugins` | These names describe the Plugins settings presentation. Each card still edits one plugin's configuration, but the section itself is a settings UI. |
| `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. |
@@ -307,6 +318,7 @@ The following debated names stay unchanged because the current scope is accurate
- 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.
@@ -317,7 +329,15 @@ The following debated names stay unchanged because the current scope is accurate
- 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, directory picker, app boot, base bundle, web-app bundle, and CLI names.
- 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

View File

@@ -36,7 +36,7 @@ Status: proposed
接口包以能力命名。实现包增加机制、协议、环境或供应商限定词,以区分不同实现。只有同主机执行属于约定时,才能使用 `local`。如果提供方只是通过可替换的 `ctx.fs` 读取看似本地的路径,或通过可替换的 `ctx.subprocess` 启动工作,就不得使用该词。
如果对象是单个引擎、运行时、策略、控制器、解析器、存储或当前配置,使用单数 `ctx` 键。如果对象是注册表,或服务拥有多个具名成员,使用复数键。类的职责和键的单复数必须一致。复数键本身不能证明对象是注册表;应由其操作和所有权决定。
如果对象是单个引擎、运行时、策略、控制器、解析器、存储或当前配置,使用单数 `ctx` 键。如果对象是注册表,或服务拥有多个具名成员,使用复数键。类的职责和键的单复数必须一致。复数键本身不能证明对象是注册表;应由其操作和所有权决定。不得让不兼容的 host 与 client 声明复用同一个 Cordis `Context` 键。即使二者使用独立的运行时上下文,TypeScript 声明合并仍会同时看到两种类型。如果自然复数已经属于另一个端面,就增加职责后缀。
仅当没有更精确的职责词能够如实描述对象时,才使用 `Service`。`GoalService` 和 `SessionTitleService` 是保留的有效名称,因为它们各自拥有领域服务,其工作无法准确归约为存储、注册或单一执行机制。
@@ -106,7 +106,7 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
| 公开的高层 `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_*`;包应采用相同的产品名词。 |
| `packages/pty/tool-bash-persistent` | `shell/tool-bash-persistent/` | 该工具是 Bash 工具,应与 shell 工具放在一起。保留其 NPM 名称:`persistent` 将它与一次性 `bash` 区分开来,而 `bash-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、类型和工具。这些方言名称准确无误。
@@ -121,6 +121,7 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
| 公开的 `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` | 浏览器约定及其镜像应采用与注册表和工具相同的作业领域名称。 |
@@ -163,7 +164,7 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
| 当前名称 | 提议名称 | 理由 |
|---|---|---|
| Host `ctx.workspace` | Host `ctx.workspaces` | `WorkspaceRegistry` 拥有多个工作区。复数键与注册表职责一致。现有 Client `ctx.workspaces` 在独立的 Cordis 上下文中运行,因此共享拼写是有意设计的,不会在运行时冲突。保留 `@deepseek-ai/dsh-workspace`、`WorkspaceRegistry`、`Workspace` 和 `workspace.*` 协议名称。 |
| 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` | 该服务捕获会话账本遥测,并交给报告后端。它不是仓库级指标或追踪服务。 |
@@ -172,7 +173,7 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
| `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` | `session/anonymous-user-id/`, `@deepseek-ai/dsh-anonymous-user-id` | 该值是随机关联 id,并非经过身份验证的用户身份。 |
| `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` | 这些名称说明快照的生命周期和用途。 |
@@ -182,6 +183,7 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
| 当前名称 | 提议名称 | 理由 |
|---|---|---|
| `@deepseek-ai/dsh-tool-schedule`、`schedule/tool-schedule/`、插件 `tool-schedule` | `@deepseek-ai/dsh-schedule`、`schedule/schedule/`、插件 `schedule` | 该包拥有持久 Schedule 领域、持久化屏障、管理工具、定时器、后续轮次和运行时生命周期。`tool-` 只描述其中一部分。 |
| `ScheduleOwner` | `ScheduleRuntime` | 该逐 agent 对象运行实时定时器、持久化投影、分派、空闲等待和资源释放。`Owner` 没有说明这一执行职责。耦合的私有 `owner*` 名称也改用 `runtime*`。 |
| `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 机制,仓库拼写要求使用完整单词。 |
@@ -206,6 +208,8 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
| `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-download`, `session-log-download/`, Loader id `session-log-download`, `ctx.sessionLogDownload` | 该包不实现通用 Session 导出。它控制 Web 下载一个由 Host 流式输出的 Session 日志 ZIP。名称必须说明对象和浏览器副作用。 |
| `SessionExportDownloadController`, 其他 `SessionExport*` 浏览器类型、`useSessionExport`、`SessionExportHeader` | `SessionLogDownloadController`, 对应的 `SessionLogDownload*` 类型、`useSessionLogDownload`、`SessionLogDownloadHeaderAction` | 该 controller 拥有预检、重复请求合并、弹窗状态和浏览器保存。`ExportDownload` 重复表达同一动作,该组件贡献的是一个 Header action,不是整个 Header。 |
| 宿主命令包中的 `CommandService` | `CommandRuntime` | 该对象跨实时调用注册并执行宿主命令。保留其包、键、类型和事件。 |
| `TokenMeterService` | `TokenMeter` | 该对象测量 token 用量。`Service` 没有补充作用域信息。 |
| `LlmService` | `LlmRuntime` | 该对象选择提供方并运行实时模型请求。保留包、键、适配器和事件。 |
@@ -255,7 +259,7 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
| `PlanModeService` | `PlanModeController` | 该对象控制进入和退出计划模式的状态转换,而不是通用执行运行时。 |
| `packages/self-modification/` | `packages/extensions/` | 该组包含仓库插件检查和挂载工具。`extensions` 说明稳定的包职责,但不声称 agent 会修改自身。保留包名 `tool-cordis` 和仓库插件名称。 |
| `packages/support/` | `packages/test-support/` | 该组仅包含测试基础设施,其路径必须明确说明这一点。 |
| `packages/support/invariants/` | `runtime-diagnostics/invariants/` | 尽管交付预设未包含不变量检查,它们仍可在生产诊断中运行,因此不属于测试支持。 |
| 原 support 系列中的 `invariants/` | `runtime-diagnostics/invariants/` | 尽管交付预设未包含不变量检查,它们仍可在生产诊断中运行,因此不属于测试支持。 |
| `InvariantService` | `InvariantRegistry` | 该对象拥有已注册的不变量检查。保留 `@deepseek-ai/dsh-invariants` 和 `ctx.invariants`。 |
| `packages/client/test-runtime/` | `packages/test-support/client-runtime/` | 该包是客户端测试基础设施。如果现有 NPM 名称已经说明这一约定,则予以保留。 |
@@ -269,6 +273,7 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
| `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/`。 |
@@ -281,6 +286,7 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`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 布局状态。 |
@@ -289,11 +295,16 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
| `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`、`settings.pluginConfig` | `PluginsSettingsSection`、`PluginsSettingsSectionProps`、`PluginsSettingsSectionInjected`、`settings.plugins` | 这些名称描述 Plugins 设置呈现。每张卡片仍编辑一个插件的配置,但该分区本身是设置 UI。 |
| `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` 作为对外接口。 |
@@ -307,6 +318,7 @@ PascalCase 标识符内部使用 `Ui`,不要使用 `UI`。除非清单明确
- 保留完整的 sandbox 系列和 `ctx.sandbox`。不得引入 `processSandbox`。
- 保留 `@deepseek-ai/dsh-api-gateway`、`ctx.typertGateway` 和 `TypertGatewayService`。
- 保留会话投影名称。投影并不只是归约函数。
- 保留 `@deepseek-ai/dsh-session-stats`、`sessionStats` 和 `SessionStatsProjection`。这些名称准确表示全会话统计数据及承载它们的持续维护读模型。
- 保留 `GoalService`;它拥有目标状态机、裁决权、比较并设置行为、事件和远程操作,不只是存储。
- 保留 `SessionTitleService`;它的职责是由多个标题提供方共享的领域服务。
- 保留 `PermissionPresetSettingsController`,即使它很长。每个词都在限定其职责。
@@ -317,7 +329,15 @@ PascalCase 标识符内部使用 `Ui`,不要使用 `UI`。除非清单明确
- 保留已弃用的 Host `ApiProxy` 和客户端连接名称,直至 API 替代方案将其移除。
- Host 服务器和提供方无关的 Web 能力都保留 `Web`。仅直接抓取提供方使用 `HTTP`。
- 保留 `E2B` 作为包名和上下文名称,不改为 `E2B sandbox`。
- 保留 MCP、Todo、目录选择器、应用启动、基础组合包、web-app 组合包和 CLI 名称。
- 保留 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`。它们标识测试进入的编译端面,不声称产品职责。
## 考虑过的替代方案

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/feature/2026-07-06-recallable-compaction.md
2026-07-06-recallable-compaction.md: e0817346112d33bfb475430f1a84846aa465d1e0
2026-07-06-recallable-compaction.zh.md: db18ea93f84edae72aadb388378aab30e465eacd
2026-07-06-recallable-compaction.md: 377ced0b0ec001287c0c7a3f1900249b30872b95
2026-07-06-recallable-compaction.zh.md: a78760d6dcaf40f9a8439d61b5ebcdf8a7a97a77

View File

@@ -6,7 +6,7 @@ English | [中文](2026-07-06-recallable-compaction.zh.md)
## Problem
Compaction is irreversible from the model's current context. The summary the model sees carries no reference to what it shadows — `shadowedRange` lives only on the log-only `compact/summary` event — and no tool lets the model read a shadowed span back. Whatever the summarizer drops is unavailable to the model, even though the append-only log holds every byte. Repeated compaction compounds this: the head checkpoint is rewritten every pass, so the request prefix takes a full prompt-cache miss each time, and earlier summaries are re-summarized generation after generation.
Compaction is irreversible from the model's current context. The summary the model sees carries no reference to what it shadows — `shadowedRange` lives only on the log-only `compaction/summary` event — and no tool lets the model read a shadowed span back. Whatever the summarizer drops is unavailable to the model, even though the append-only log holds every byte. Repeated compaction compounds this: the head checkpoint is rewritten every pass, so the request prefix takes a full prompt-cache miss each time, and earlier summaries are re-summarized generation after generation.
The root cause is one artifact playing two conflicting roles. An **index** wants to be frozen, chronological, and cheap; the model's **working memory** wants a global view, re-prioritization, and mutability. A single summary can be neither well.
@@ -22,7 +22,7 @@ Newly stale history splits into chunks by deterministic policy: accumulate towar
- two or three lines of what happened;
- a keyword line of low-frequency literal anchors — exact error strings, values, config keys — grouped by kind;
- a code-composed footer: `[checkpoint c<summarySeq>: shadows conversation span #<start>–#<end>; originals retrievable via history_read]`. Code assembles these pointers from the `compact/summary` seq and `shadowedRange`; the model never writes them.
- a code-composed footer: `[checkpoint c<summarySeq>: shadows conversation span #<start>–#<end>; originals retrievable via history_read]`. Code assembles these pointers from the `compaction/summary` seq and `shadowedRange`; the model never writes them.
A committed stub is never rewritten and never re-enters a later compaction region. A stub call's input is layered: the fixed preamble and the byte-identical pass-start state checkpoint (the shared prefix across all calls in the phase), then the keyword lines of all previously committed stubs — so a new entry indexes what is distinctive to its chunk instead of repeating the directory — the one or two most recent committed stubs for chronological continuity, and the slice itself. Sibling stubs from the same pass are not inputs (the concurrent phase forbids it; turn-aligned boundaries carry local continuity instead), and the state checkpoint is background only, never material to summarize into the stub. A slice consisting of recalled content is stubbed by code alone — a pointer line, no LLM call. A failed stub call degrades the same way: its slice gets a code-only pointer stub and the pass continues, making the state rewrite the only hard LLM dependency in a pass.
@@ -37,16 +37,16 @@ An inflation guard bounds the whole pass: if the post-compaction size is not str
- Chunk slices are surface position ranges. A pass runs two phases: all summarize calls execute concurrently, buffered off-surface; then regions commit strictly left to right — chunks first, trailing slice last — so the state checkpoint lands after every stub through contiguous single-node replaces. Wall-clock stays near one summarize call.
- The superseded state checkpoint folds into the next pass's first chunk as ordinary history: no tombstone, no new primitive. Its stub omits it, `history_read` renders it labeled `[prior state checkpoint]`, and its footer travels with the rendered text, keeping every trailing slice reachable through the two-hop chain.
- Range selection is frozen-aware: the compactable span begins after the last committed index checkpoint, at the surface head only when none exists. A legacy session's existing head checkpoint is adopted as state-class — its text the merge base, its node folded like any superseded state.
- A crash in the summarize phase commits nothing; a crash mid-commit leaves a left-to-right prefix committed, and the resumed pass reads its merge base from the log's latest state-class `compact/summary` event and commits the remaining regions unconditionally — restoring `[stubs…][state][tail]` outranks shrinking.
- A crash in the summarize phase commits nothing; a crash mid-commit leaves a left-to-right prefix committed, and the resumed pass reads its merge base from the log's latest state-class `compaction/summary` event and commits the remaining regions unconditionally — restoring `[stubs…][state][tail]` outranks shrinking.
### The recall tools
A new package `@deepseek-ai/dsh-tool-recall` (consumer-only, over the `dsh-session` and `dsh-compact` vocabularies) registers two model-facing tools:
A new package `@deepseek-ai/dsh-tool-recall` (consumer-only, over the `dsh-session` and `dsh-compaction` vocabularies) registers two model-facing tools:
- `history_read(checkpoint, offset?)` — renders the shadowed span of any checkpoint in the log, including superseded ones, as `User:`/`Assistant:`/`Tool result:` transcript, paginated by a configured budget with a continuation cursor.
- `history_search(query, checkpoint?, limit?)` — case-insensitive literal scan over every shadowed span; returns snippets with checkpoint ids and coverage metadata (`scanned`/`matched`/`truncated`). The zero-match hint notes the scan is literal and points at direct `history_read` of a plausible checkpoint.
Both read `exec.agent.session.events` (the tool-todo access pattern; non-agent callers rejected), render only surface-type message events, and return ordinary `tool/result`s — recalled bytes land at the context tail, logged, so reconstructability holds with no special casing. There is no new storage and no sidecar index: the session log stores the content, `compact/summary.shadowedRange` and `shadowedSeqs` identify what each checkpoint replaced, and the tools read both. The tool schemas and the package's one system-prompt section are static strings; checkpoint ids reach the model only through footers. The transcript renderer moves from `compact-basic` into `dsh-session`, shared by summarizer and tools.
Both read `exec.agent.session.events` (the tool-todo access pattern; non-agent callers rejected), render only surface-type message events, and return ordinary `tool/result`s — recalled bytes land at the context tail, logged, so reconstructability holds with no special casing. There is no new storage and no sidecar index: the session log stores the content, `compaction/summary.shadowedRange` and `shadowedSeqs` identify what each checkpoint replaced, and the tools read both. The tool schemas and the package's one system-prompt section are static strings; checkpoint ids reach the model only through footers. The transcript renderer moves from `compaction-basic` into `dsh-session`, shared by summarizer and tools.
### Cache and cost
@@ -54,7 +54,7 @@ The request prefix after a pass is `[system][stubs…][state][tail]`. Frozen stu
### Packaging
The design ships as a new backend `dsh-compact-recallable` on the existing `ctx.compact` seam, enabled by default in the shipped example configs; `compact-basic` remains as the reference implementation and the seam's design twin, in the pattern of the paired LLM adapters. The seam JSDoc's "at most one auto-generated checkpoint, always at the head" clause is relaxed to name both backend behaviors.
The design ships as a new backend `dsh-compact-recallable` on the existing `ctx.compaction` seam, enabled by default in the shipped example configs; `compaction-basic` remains as the reference implementation and the seam's design twin, in the pattern of the paired LLM adapters. The seam JSDoc's "at most one auto-generated checkpoint, always at the head" clause is relaxed to name both backend behaviors.
### Relation to in-flight work
@@ -96,15 +96,15 @@ Deferred until observation calls for them:
- Auto-compaction over a long session yields `[stubs…][state][tail]` after every completed pass; prior stubs stay byte-identical across passes; committed stubs never fall inside a later region; the superseded state checkpoint folds without a tombstone, renders labeled, and stays reachable and searchable through the two-hop chain.
- Every checkpoint's surface text ends with the deterministic footer; footers round-trip through replay byte-identically; the state checkpoint's `shadowedRange` records its wider input range.
- Nothing commits before all summaries exist and the guard passes on like-for-like accounting; a guard failure commits nothing and does not fail the turn; a mid-commit kill resumed at the next pre-step completes the pass with the state region committed unconditionally, merge base read from the log; a legacy head checkpoint is adopted as state-class.
- `history_read` renders any logged checkpoint's span under budget with a working cursor; `history_search` covers every shadowed span with checkpoint-id snippets and coverage metadata, asserted in particular by finding content that exists only in a span shadowed by a superseded state checkpoint — the regression pin for trailing-slice reachability; both reject non-agent callers and never-existing ids or orphaned `compact/start` with typed errors; recalled content appears as ordinary `tool/result`s; request-reconstruction invariants pass over sessions with compaction plus recall; one keyless snapshot scenario covers compact-then-recall end to end; tool schemas and the prompt section are byte-identical across passes.
- On the long-horizon bench suite: task success does not regress against `compact-basic` at equal budgets; a handoff-fidelity probe (restate K known decisions and constraints after a pass) scores no worse; recall usage frequency and hit usefulness are reported per run via the dsh bench report pipeline, alongside the stub-directory attention measurement and cache-hit telemetry.
- `history_read` renders any logged checkpoint's span under budget with a working cursor; `history_search` covers every shadowed span with checkpoint-id snippets and coverage metadata, asserted in particular by finding content that exists only in a span shadowed by a superseded state checkpoint — the regression pin for trailing-slice reachability; both reject non-agent callers and never-existing ids or orphaned `compaction/start` with typed errors; recalled content appears as ordinary `tool/result`s; request-reconstruction invariants pass over sessions with compaction plus recall; one keyless snapshot scenario covers compact-then-recall end to end; tool schemas and the prompt section are byte-identical across passes.
- On the long-horizon bench suite: task success does not regress against `compaction-basic` at equal budgets; a handoff-fidelity probe (restate K known decisions and constraints after a pass) scores no worse; recall usage frequency and hit usefulness are reported per run via the dsh bench report pipeline, alongside the stub-directory attention measurement and cache-hit telemetry.
- Seam JSDoc, the compaction capability-seam Agent Note, `architecture.md`, and the generated tool, config, persistence, and module-graph catalogs update in the same change; all budgets live in config; new source directories hold per-file 100% coverage with HMR disposal tests.
## Risks
- **Recall is a learned behavior**: untrained models will under-use it, and the bench report exists to track the gap while training closes it. Until then the state checkpoint keeps the floor at today's summary quality.
- **Unknown unknowns remain**: a detail absent from summaries and keywords draws no recall. Recall converts "unreachable even when suspected" into "reachable when suspected".
- **The stub directory occupies attention**: dozens of stable index cards per request may dilute focus; the bench measurement in the acceptance criteria tracks it against `compact-basic`.
- **The stub directory occupies attention**: dozens of stable index cards per request may dilute focus; the bench measurement in the acceptance criteria tracks it against `compaction-basic`.
- **Cost**: per-pass summarize input is roughly twice today's; short sessions sit near today's cost and quality, and the design pays off with session length.
- **State drift and division-of-labor leakage** are observable through the handoff probe and stub review; their counters are specified follow-ups.
- **Two backends** are a maintenance burden; the seam contract and the shared recall consumer bound it, and the bench comparison decides the default over time.

View File

@@ -6,7 +6,7 @@ Status: proposed
## 问题
压缩(compaction)对模型的当前上下文不可逆。模型看到的摘要没有指向被其遮蔽内容的引用,因为 `shadowedRange` 只存在于仅写入日志、模型不可见的 `compact/summary` 事件上,也没有工具能让模型重新读取被遮蔽的区段。即使仅追加日志仍保存每一个字节,摘要器丢弃的内容也对模型不可用。重复压缩会进一步加重这些结果:每一轮都会重写头部检查点,因此请求前缀每次都会完全失去提示词缓存命中,而更早的摘要也会在后续每一轮中重新生成摘要。
压缩(compaction)对模型的当前上下文不可逆。模型看到的摘要没有指向被其遮蔽内容的引用,因为 `shadowedRange` 只存在于仅写入日志、模型不可见的 `compaction/summary` 事件上,也没有工具能让模型重新读取被遮蔽的区段。即使仅追加日志仍保存每一个字节,摘要器丢弃的内容也对模型不可用。重复压缩会进一步加重这些结果:每一轮都会重写头部检查点,因此请求前缀每次都会完全失去提示词缓存命中,而更早的摘要也会在后续每一轮中重新生成摘要。
根本原因是一个产物承担了两个互相冲突的角色。**索引**需要冻结、按时间排序且成本低廉;模型的**工作记忆**则需要全局视图、重新确定优先级并且可变。单一摘要无法同时胜任两者。
@@ -22,7 +22,7 @@ Status: proposed
- 用两三行说明发生了什么;
- 用一行关键词记录低频字面锚点,例如确切的错误字符串、值和配置键,并按类别分组;
- 由代码组装页脚:`[checkpoint c<summarySeq>: shadows conversation span #<start>–#<end>; originals retrievable via history_read]`。代码根据 `compact/summary` seq 和 `shadowedRange` 组装这些指针,模型绝不会编写它们。
- 由代码组装页脚:`[checkpoint c<summarySeq>: shadows conversation span #<start>–#<end>; originals retrievable via history_read]`。代码根据 `compaction/summary` seq 和 `shadowedRange` 组装这些指针,模型绝不会编写它们。
已经提交的存根永不重写,也绝不再次进入之后的压缩区域。存根调用采用分层输入:固定前导内容与逐字节相同的本轮开始状态检查点(该阶段所有调用共享的前缀);随后是先前所有已提交存根的关键词行,使新条目索引其分片的独特内容,而不是重复整个目录;再加最近一两个已提交存根以维持时间连续性;最后是切片本身。同一轮中的同级存根不作为输入,因为并发阶段禁止这种依赖,而与轮次对齐的边界已经维持局部连续性。状态检查点只作为背景,绝不能成为存根需要总结的材料。完全由回溯内容构成的切片只通过代码生成存根,即只写一行指针,不调用 LLM(大语言模型)。存根调用失败时采用相同降级方式:其切片获得一个仅由代码生成的指针存根,本轮继续执行,使状态重写成为一轮中唯一的强制 LLM 依赖。
@@ -37,16 +37,16 @@ Status: proposed
- 分片切片由表面位置范围表示。一轮分两个阶段运行:所有摘要调用先并发执行并在表面之外缓冲;随后严格从左到右提交区域,先提交各分片,最后提交尾随切片,使状态检查点通过连续的单节点替换落在所有存根之后。墙钟时间维持在接近一次摘要调用的水平。
- 被取代的状态检查点会作为普通历史折入下一轮的第一个分片,不需要墓碑或新原语。其存根省略该状态,`history_read` 会把它渲染为 `[prior state checkpoint]`,并让页脚随渲染文本一同传递,使每个尾随切片都可通过两跳链路触达。
- 范围选择会感知冻结边界:可压缩区段从最后一个已提交索引检查点之后开始;只有在不存在索引检查点时,才从表面头部开始。旧会话现有的头部检查点会被视为状态类检查点:其文本作为合并基线,其节点则像其他被取代状态一样折入历史。
- 摘要阶段发生崩溃时不会提交任何内容;提交中途崩溃会留下一个从左到右的已提交前缀,恢复后的一轮从日志中最新的状态类 `compact/summary` 事件读取合并基线,并无条件提交剩余区域。恢复 `[stubs…][state][tail]` 的优先级高于缩减大小。
- 摘要阶段发生崩溃时不会提交任何内容;提交中途崩溃会留下一个从左到右的已提交前缀,恢复后的一轮从日志中最新的状态类 `compaction/summary` 事件读取合并基线,并无条件提交剩余区域。恢复 `[stubs…][state][tail]` 的优先级高于缩减大小。
### 回溯工具
新增包 `@deepseek-ai/dsh-tool-recall`,它只是 `dsh-session` 与 `dsh-compact` 词汇之上的消费方,注册两个面向模型的工具:
新增包 `@deepseek-ai/dsh-tool-recall`,它只是 `dsh-session` 与 `dsh-compaction` 词汇之上的消费方,注册两个面向模型的工具:
- `history_read(checkpoint, offset?)`:把日志中任意检查点(包括已被取代的检查点)遮蔽的区段渲染为 `User:`/`Assistant:`/`Tool result:` transcript(文本记录),并按配置预算分页,提供续传游标。
- `history_search(query, checkpoint?, limit?)`:对每个被遮蔽区段进行不区分大小写的字面量扫描;返回带检查点 id 的片段与覆盖元数据(`scanned`/`matched`/`truncated`)。零匹配提示会说明扫描按字面量执行,并建议对可能的检查点直接使用 `history_read`。
两个工具都读取 `exec.agent.session.events`(沿用 tool-todo 访问模式;拒绝非 agent(智能体)调用方),只渲染表面类型的消息事件,并返回普通 `tool/result`:回溯字节会进入上下文尾部并记录到日志,因此无需特殊处理即可满足可重建性。系统不增加新存储或伴随索引:会话日志存储内容,`compact/summary.shadowedRange` 和 `shadowedSeqs` 指明每个检查点替换了什么,这些工具读取两者。工具 schema 与该包唯一的系统提示词章节都是静态字符串;检查点 id 只会通过页脚抵达模型。transcript 渲染器从 `compact-basic` 移入 `dsh-session`,供摘要器与工具共享。
两个工具都读取 `exec.agent.session.events`(沿用 tool-todo 访问模式;拒绝非 agent(智能体)调用方),只渲染表面类型的消息事件,并返回普通 `tool/result`:回溯字节会进入上下文尾部并记录到日志,因此无需特殊处理即可满足可重建性。系统不增加新存储或伴随索引:会话日志存储内容,`compaction/summary.shadowedRange` 和 `shadowedSeqs` 指明每个检查点替换了什么,这些工具读取两者。工具 schema 与该包唯一的系统提示词章节都是静态字符串;检查点 id 只会通过页脚抵达模型。transcript 渲染器从 `compaction-basic` 移入 `dsh-session`,供摘要器与工具共享。
### 缓存与成本
@@ -54,7 +54,7 @@ Status: proposed
### 打包方式
该设计以新的后端 `dsh-compact-recallable` 交付,挂在现有 `ctx.compact` seam 上,并在已交付的示例配置中默认启用。`compact-basic` 保留为参考实现和该 seam 的设计对照,与成对 LLM 适配器的模式一致。seam JSDoc 中「最多一个自动生成的检查点,始终位于头部」这一条会放宽,改为说明两个后端各自的行为。
该设计以新的后端 `dsh-compact-recallable` 交付,挂在现有 `ctx.compaction` seam 上,并在已交付的示例配置中默认启用。`compaction-basic` 保留为参考实现和该 seam 的设计对照,与成对 LLM 适配器的模式一致。seam JSDoc 中「最多一个自动生成的检查点,始终位于头部」这一条会放宽,改为说明两个后端各自的行为。
### 与进行中工作的关系
@@ -96,15 +96,15 @@ Status: proposed
- 长会话自动压缩后,每一轮完成时都会得到 `[stubs…][state][tail]`;先前存根在各轮之间保持逐字节相同;已提交存根绝不落入后续区域;被取代的状态检查点无需墓碑即可折入历史,渲染时带有标签,并可通过两跳链路触达和搜索。
- 每个检查点的表面文本都以确定性页脚结束;页脚通过回放逐字节往返;状态检查点的 `shadowedRange` 记录其更宽的输入范围。
- 在全部摘要就绪且保护逻辑通过同类核算前,不提交任何内容;保护失败不会提交任何内容,也不会让轮次失败;提交中途被终止后,下一次 pre-step 会恢复处理,从日志读取合并基线,并无条件提交状态区域以完成本轮;旧版头部检查点会被视为状态类。
- `history_read` 在预算内渲染任意已记录检查点的区段,并提供可用游标;`history_search` 覆盖每个被遮蔽区段,返回带检查点 id 的片段与覆盖元数据,测试尤其要能找到只存在于被已取代状态检查点遮蔽区段中的内容,这是锁定尾随切片可达性的回归用例;两个工具都会拒绝非 agent 调用方,并对从未存在的 id 或遗留 `compact/start` 返回类型化错误;回溯内容作为普通 `tool/result` 出现;请求重建不变量会在同时包含压缩与回溯的会话上通过;一项无密钥快照场景端到端覆盖先压缩再回溯;工具 schema 与提示词章节在各轮之间逐字节相同。
- 在长时间跨度 bench 套件中:任务成功率在预算相同的条件下不低于 `compact-basic`;交接保真探针(在一轮后重新陈述 K 项已知决策和约束)的得分不降低;每次运行都通过 dsh bench 报告流水线汇报回溯使用频率和命中有效性,并同时报告存根目录注意力度量与缓存命中遥测。
- `history_read` 在预算内渲染任意已记录检查点的区段,并提供可用游标;`history_search` 覆盖每个被遮蔽区段,返回带检查点 id 的片段与覆盖元数据,测试尤其要能找到只存在于被已取代状态检查点遮蔽区段中的内容,这是锁定尾随切片可达性的回归用例;两个工具都会拒绝非 agent 调用方,并对从未存在的 id 或遗留 `compaction/start` 返回类型化错误;回溯内容作为普通 `tool/result` 出现;请求重建不变量会在同时包含压缩与回溯的会话上通过;一项无密钥快照场景端到端覆盖先压缩再回溯;工具 schema 与提示词章节在各轮之间逐字节相同。
- 在长时间跨度 bench 套件中:任务成功率在预算相同的条件下不低于 `compaction-basic`;交接保真探针(在一轮后重新陈述 K 项已知决策和约束)的得分不降低;每次运行都通过 dsh bench 报告流水线汇报回溯使用频率和命中有效性,并同时报告存根目录注意力度量与缓存命中遥测。
- seam JSDoc、压缩能力 seam Agent Note、`architecture.md`,以及生成的工具、配置、持久化与模块图目录都在同一改动中更新;全部预算位于配置中;新源码目录具备逐文件 100% 覆盖率与 HMR(热模块替换)dispose(资源释放)测试。
## 风险
- **回溯属于学习到的行为**:未经训练的模型会低频使用它,bench 报告会持续追踪这项差距,直至训练弥合问题。在此之前,状态检查点会让质量下限保持在当前摘要水平。
- **未知的未知仍然存在**:如果某项细节既未出现在摘要中,也未出现在关键词中,就不会触发回溯。回溯把「即使已经怀疑也无法触达」变成「怀疑时可以触达」。
- **存根目录会占用注意力**:每次请求中包含数十张稳定的索引卡,可能稀释模型关注点;验收标准中的 bench 度量会将其与 `compact-basic` 对比。
- **存根目录会占用注意力**:每次请求中包含数十张稳定的索引卡,可能稀释模型关注点;验收标准中的 bench 度量会将其与 `compaction-basic` 对比。
- **成本**:每轮摘要输入大约是当前实现的两倍;短会话的成本和质量接近当前水平,而设计收益随会话长度增长。
- **状态漂移与职责分工泄漏**可以通过交接探针和存根评审观察;对应措施已列为后续事项。
- **两个后端**会扩大维护范围;seam 约定和共享回溯消费方会限制这一范围,bench 对比则用于逐步决定默认实现。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/feature/2026-08-04-task-surface.md
2026-08-04-task-surface.md: 72fc9e12f1a02f66a02d3335ad2e8d0d4c1c3dc2
2026-08-04-task-surface.md: d2c75d30f47076577534c239d4f1ed222851c45f
2026-08-04-task-surface.zh.md: 7d4b26218bc51da3182c4f4bacac8e8dc6807411

View File

@@ -252,7 +252,7 @@ The implementation depends on the existing message log, canonical tool output, t
**Extend `userInteraction.ask()` with a large form.** Rejected for this contract. `ask()` is a blocking request/response operation used when a running tool cannot continue without a short answer. A Task Surface ends the turn, may remain open across refreshes, and submits its result as the next visible user turn.
**Register one dynamic `conversation.view` per call.** Rejected because the view ledger is global while its render scope is per Session, and because transient task identity would become registration identity. One static Session-scoped Dock owns interaction, and one static keyed row summarizes the logged occurrence; neither registration uses occurrence identity.
**Register one dynamic `conversation.view` per call.** Rejected because the view ledger is global while its render scope is per Session, and because transient job identity would become registration identity. One static Session-scoped Dock owns interaction, and one static keyed row summarizes the logged occurrence; neither registration uses occurrence identity.
**Keep the model only in the canonical tool value.** Rejected because canonical values are not persisted. Replay requires the normalized model in `presentationMeta`.

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-api.md
2026-07-04-prune-dead-core-spine-api.md: ac0a72973f9c44e1f322eeff2e095d751497edf8
2026-07-04-prune-dead-core-spine-api.zh.md: aebc8cb7afbdcc85aab2d58b34fa0493ac7bcbb5
2026-07-04-prune-dead-core-spine-api.md: 64c9bb00a7f26463aea6767018da8814a13133ec
2026-07-04-prune-dead-core-spine-api.zh.md: 0666fa8b6b6228b97ade7ba91eb214db82f6bf94

View File

@@ -8,15 +8,15 @@ English | [中文](2026-07-04-prune-dead-core-spine-api.zh.md)
Several package-root exports, result fields, and convenience methods have no production consumer. They survive because tests import internals through public entry points or because a type anticipated a caller that never arrived. Each item is small in isolation, but together they enlarge the SDK contract, generated catalogs, documentation, and regression matrix without enabling a shipped path.
The production corpus is `packages/*/*/src`, example sources/config, and runtime scripts. Tests, package READMEs, and Agent Note prose are evidence of publication but not fixed callers. `cordis_inspect` makes `packages/self-modification/tool-cordis/src/api-catalog.ts` model-visible, and `cordis_mount` can invoke injected services through guarded real-service proxies, so catalogued service methods and returned shapes are a genuine dynamic product surface. The table therefore distinguishes absence of a fixed repository caller from unreachability: rows touching catalogued vocabulary intentionally contract what model-written mounts can discover and call, while package-root implementation helpers are not reached through that service façade. Exact-symbol searches produce the following inventory:
The production corpus is `packages/*/*/src`, example sources/config, and runtime scripts. Tests, package READMEs, and Agent Note prose are evidence of publication but not fixed callers. `cordis_inspect` makes `packages/extensions/tool-cordis/src/api-catalog.ts` model-visible, and `cordis_mount` can invoke injected services through guarded real-service proxies, so catalogued service methods and returned shapes are a genuine dynamic product surface. The table therefore distinguishes absence of a fixed repository caller from unreachability: rows touching catalogued vocabulary intentionally contract what model-written mounts can discover and call, while package-root implementation helpers are not reached through that service façade. Exact-symbol searches produce the following inventory:
| API element | Production evidence | Simplification |
| --- | --- | --- |
| `SurfaceManager.invalidate()` | Only its unit test calls it; seeding completes before the lazily-created manager exists and the session never replaces its log reference. | Delete it and its impossible wholesale-replacement contract. |
| `ToolExecutionResult.callId` | Every hook already receives the immutable `ToolExecution`; the loop and ACP correlate through the call/session event. No consumer reads the duplicate result field. | Remove the field, copy/mismatch guards, and tests that prove the duplicate cannot disagree. |
| `ReactLoopAgent` root export | Outside-package named imports are tests; production programs against `Agent` and creates/resumes through `ctx.agents`. | Return/interface-type `Agent` and make the concrete loop class package-internal; keep the deliberate synchronous config-only `AgentLoop.create()` path. |
| `workflow-workerthread` protocol/runtime/session re-exports and named `WorkerWorkflowEngine` | Every package-name consumer uses the default engine; the workflow Agent Note already defines the worker wire protocol as private. | Keep the default plugin class/config contract; drop the duplicate named class export and keep protocol modules source-private. |
| `code-runtime-worker` protocol/bootstrap re-exports | Outside-package production/e2e consumers use `WorkerCodeRuntime` and config, not `BootstrapPort`, `PatchableStream`, or worker message/boot types. | Keep the runtime class/config contract and make its wire/bootstrap vocabulary source-private. |
| `workflow-worker-thread` protocol/runtime/session re-exports and named `WorkerThreadWorkflowEngine` | Every package-name consumer uses the default engine; the workflow Agent Note already defines the worker wire protocol as private. | Keep the default plugin class/config contract; drop the duplicate named class export and keep protocol modules source-private. |
| `code-runtime-worker` protocol/bootstrap re-exports | Outside-package production/e2e consumers use `WorkerThreadCodeRuntime` and config, not `BootstrapPort`, `PatchableStream`, or worker message/boot types. | Keep the runtime class/config contract and make its wire/bootstrap vocabulary source-private. |
| ACP `agentOptions` root export | The helper has only same-file and ACP-test consumers; the sole outside-package production consumer mounts the plugin namespace. | Keep `name`, `inject`, `Config`, `AcpConfig`, and `apply`; make `agentOptions` source-private and test it through bridge behavior. |
| `providerWording` and `completedTurnPrefix` root exports | Each has one same-package production caller; only the balanced-prefix helper has a same-package white-box test. | Make them source-private and test provider behavior. |
| `depthOf`, `SubagentDepthError`, `waitForExit`, and `exitsWithin` root exports | Production subagent backends consume the in-process runner and subprocess construction/disposal helpers, not these enforcement/test internals. `SENSITIVE_ENV_PATTERN` is excluded because the SDK helper applies it to caller-supplied environments. | Keep depth and exit behavior but make the remaining helpers and error source-private; test through spawn and disposal. Keep the shared credential pattern public. |
@@ -25,7 +25,7 @@ The production corpus is `packages/*/*/src`, example sources/config, and runtime
| `BlockAssembler.push()` return value | Both production callers ignore the returned completed block. | Return `void`; keep the deliberately public `blocks()`/`message()` contract. |
| `compactRegion`'s separate `session` argument | The fixed caller passes the same object already present as `agent.session`; the model-visible mount API can also call the method, but accepting two identities permits a mounted plugin to provide an incoherent pair. | Keep the manual-region API while deliberately narrowing it to `agent.session` as the one source of truth. |
| `CompactionResult.startSeq`, `summarySeq`, `endSeq`, and `summary` | The production consumer reads only shadowed range/seq/token accounting; the durable log owns summary and event identity. | Remove the four result echoes while keeping both shared transcript renderers. |
| `BasicCompactService` estimation/summarization visibility | No outside production caller invokes the five methods; the implemented Agent Note names only `estimateContentTokens()` and `summarize()` as subclass hooks. | Make those two `protected` and the three orchestration-only estimators private. |
| `BasicCompactionEngine` estimation/summarization visibility | No outside production caller invokes the five methods; the implemented Agent Note names only `estimateContentTokens()` and `summarize()` as subclass hooks. | Make those two `protected` and the three orchestration-only estimators private. |
| `CodeLogEntry.source`/`level` and `RunCodeMeta.dispatches` | Every production consumer maps logs to text; no presenter/model path reads the other fields or the persisted dispatch count. | Make code-runtime logs strings (or text-only entries) and remove result-meta dispatch plumbing; keep the local counter that mints deterministic dispatch ids. |
| `CodeRuntime.language` and `CodeRuntime.isolation` | The worker backend supplies the only production values, while Code Mode and every other production caller invoke only `run()`. | Remove the unread descriptors while preserving the worker's language, isolation, budgets, cancellation, and disposal behavior. |
| `ToolNotFoundError.toolName`, `SystemPrompt.config`, and `BashTask.command` | Each stored public value has no production reader. | Drop the unread field while retaining error messages, resolved configuration behavior, and task lifecycle. |
@@ -37,9 +37,9 @@ The production corpus is `packages/*/*/src`, example sources/config, and runtime
- `dsh-llm-deepseek`: `httpErrorCode`, `serializeMessages`, `serializeRequest`, `DONE`, `parseSse`, `mapFinishReason`, `mapUsage`, and `translate`; `dsh-llm-pi-ai`: `buildModel`, `mapStopReason`, `mapUsage`, `toPiContext`, and `toStreamChunks`.
- `dsh-bash-local`: `DEFAULT_GRACE_MS`, `ENV_OVERRIDES`, `killGroup`, `OutputCollector`, and `runBash`; `dsh-bash-sandbox`: `shellQuote`, `classifyDenial`, and `classifyRunnerFailure`; `dsh-sandbox-local`: `bwrapProfileArgs`, `landlockProfileArgs`, and `seatbeltProfileArgs`. The public mutable test-injection fields and their types are outside this proposal.
- `dsh-fs-local`: `applyLiteralEdit`, `listDirectory`, `probe`, `readForEdit`, `readTextForDiff`, `readWholeText`, `resolveLocalTarget`, `restoreLineEndings`, `streamWholeText`, and `writeFileAtomic`.
- `dsh-web-fetch-local`: `classifyContentType`, `decoderForCharset`, `isSameOrigin`, `parseCharset`, and `validateFetchUrl`; `dsh-web-search-exa`: `mapExaResponse` and `mapExaResult`; `dsh-web-search-deepseek`: `citationSnippets` and `mapAnthropicResponse`; `dsh-web-search-perplexity`: `mapPerplexityResponse` and `mapPerplexityResult`.
- `dsh-web-fetch-http`: `classifyContentType`, `decoderForCharset`, `isSameOrigin`, `parseCharset`, and `validateFetchUrl`; `dsh-web-search-exa`: `mapExaResponse` and `mapExaResult`; `dsh-web-search-deepseek`: `citationSnippets` and `mapAnthropicResponse`; `dsh-web-search-perplexity`: `mapPerplexityResponse` and `mapPerplexityResult`.
- `dsh-tool-fs`: `READ_LIMIT`, `STREAM_MIN_SIZE`, `READ_MAX_BYTES`, `READ_MAX_LINE_LENGTH`, `DIFF_CONTEXT`, `applyReadTool`, `parseReadArgs`, `applyWriteTool`, `formatWriteOutput`, `parseWriteArgs`, `applyEditTool`, `formatEditOutput`, `parseEditArgs`, `buildWindow`, `formatReadOutput`, `computeHunkDiffs`, and `diffsFromMeta`.
- `dsh-tool-web`: `WEB_SEARCH_MAX_RESULTS`, `applyWebSearchTool`, `formatSearchOutput`, `parseSearchArgs`, `presentSearchCall`, `applyWebFetchTool`, `formatFetchOutput`, `parseFetchArgs`, `presentFetchCall`, `renderBody`, and `htmlToMarkdown`; `dsh-timeout-policy`: `toolTimeoutResult`; `dsh-compact-basic`: `resolveConfig`; `dsh-tool-bash`: `renderResult`.
- `dsh-tool-web`: `WEB_SEARCH_MAX_RESULTS`, `applyWebSearchTool`, `formatSearchOutput`, `parseSearchArgs`, `presentSearchCall`, `applyWebFetchTool`, `formatFetchOutput`, `parseFetchArgs`, `presentFetchCall`, `renderBody`, and `htmlToMarkdown`; `dsh-tool-call-timeout-policy`: `toolTimeoutResult`; `dsh-compaction-basic`: `resolveConfig`; `dsh-tool-bash`: `renderResult`.
## Proposal

View File

@@ -8,15 +8,15 @@ Status: proposed
若干包根导出、结果字段和便利方法没有生产消费方。它们之所以存活,要么是因为测试通过公开入口导入了内部实现,要么是因为某个类型预期了一个从未出现的调用者。每一项单独看都很小,但合在一起,它们扩大了 SDK 约定、生成的 catalog、文档和回归矩阵,却没有支撑任何已交付的路径。
生产语料库是 `packages/*/*/src`、示例源码/配置和运行时脚本。测试、包 README 和 Agent Note 行文是发布的证据,但不是固定调用者。`cordis_inspect` 使 `packages/self-modification/tool-cordis/src/api-catalog.ts` 对模型可见,`cordis_mount` 可以通过受保护的真实服务代理调用注入的服务,因此 catalog 中的服务方法和返回形状是真正的动态产品接口。下表因此区分「没有固定的仓库调用者」与「不可达」:涉及 catalog 词汇的行有意收缩模型编写的 mount 能发现和调用的内容,而包根实现辅助函数并不通过该服务门面可达。精确符号搜索得出以下清单:
生产语料库是 `packages/*/*/src`、示例源码/配置和运行时脚本。测试、包 README 和 Agent Note 行文是发布的证据,但不是固定调用者。`cordis_inspect` 使 `packages/extensions/tool-cordis/src/api-catalog.ts` 对模型可见,`cordis_mount` 可以通过受保护的真实服务代理调用注入的服务,因此 catalog 中的服务方法和返回形状是真正的动态产品接口。下表因此区分「没有固定的仓库调用者」与「不可达」:涉及 catalog 词汇的行有意收缩模型编写的 mount 能发现和调用的内容,而包根实现辅助函数并不通过该服务门面可达。精确符号搜索得出以下清单:
| 接口 | 生产证据 | 简化方式 |
| --- | --- | --- |
| `SurfaceManager.invalidate()` | 只有其单元测试调用它;seeding 在惰性创建的 manager 存在之前就已完成,且会话从不替换其日志引用。 | 删除它及其不可能触发的整体替换约定。 |
| `ToolExecutionResult.callId` | 每个钩子已经接收不可变的 `ToolExecution`;循环和 ACP(Agent Client Protocol)通过调用/会话事件关联。没有消费方读取这个重复的结果字段。 | 移除该字段、复制/不匹配守卫,以及证明该重复不可能不一致的测试。 |
| `ReactLoopAgent` 根导出 | 包外的命名导入都是测试;生产代码面向 `Agent` 编程,通过 `ctx.agents` 创建/恢复。 | 将返回类型和接口类型设为 `Agent`,将具体循环类改为包内部;保留有意设计的同步、仅配置的 `AgentLoop.create()` 路径。 |
| `workflow-workerthread` 的 protocol/runtime/session 再导出与命名的 `WorkerWorkflowEngine` | 所有通过包名导入的消费方都使用默认引擎;工作流 Agent Note 已将 worker 协议格式(wire format)定义为私有。 | 保留默认插件类/配置约定;移除重复的命名类导出,将协议模块保持为源码私有。 |
| `code-runtime-worker` 的 protocol/bootstrap 再导出 | 包外的生产/e2e 消费方使用 `WorkerCodeRuntime` 和配置,而非 `BootstrapPort`、`PatchableStream` 或 worker 消息/启动类型。 | 保留运行时类/配置约定,将其协议格式/bootstrap 词汇改为源码私有。 |
| `workflow-worker-thread` 的 protocol/runtime/session 再导出与命名的 `WorkerThreadWorkflowEngine` | 所有通过包名导入的消费方都使用默认引擎;工作流 Agent Note 已将 worker 协议格式(wire format)定义为私有。 | 保留默认插件类/配置约定;移除重复的命名类导出,将协议模块保持为源码私有。 |
| `code-runtime-worker` 的 protocol/bootstrap 再导出 | 包外的生产/e2e 消费方使用 `WorkerThreadCodeRuntime` 和配置,而非 `BootstrapPort`、`PatchableStream` 或 worker 消息/启动类型。 | 保留运行时类/配置约定,将其协议格式/bootstrap 词汇改为源码私有。 |
| ACP 的 `agentOptions` 根导出 | 该辅助函数只有同文件和 ACP 测试消费方;唯一的包外生产消费方挂载的是插件命名空间。 | 保留 `name`、`inject`、`Config`、`AcpConfig` 和 `apply`;将 `agentOptions` 改为源码私有,通过桥接层行为测试。 |
| `providerWording` 与 `completedTurnPrefix` 根导出 | 各有一个同包生产调用者;只有 balanced-prefix 辅助函数有一个同包白盒测试。 | 改为源码私有,测试提供方行为。 |
| `depthOf`、`SubagentDepthError`、`waitForExit` 与 `exitsWithin` 根导出 | 生产 subagent 后端消费的是进程内 runner 和子进程构造/dispose(资源释放)辅助函数,而非这些强制机制和测试内部实现。`SENSITIVE_ENV_PATTERN` 不在其中,因为 SDK helper 会将它应用于调用方传入的环境。 | 保留深度与退出行为,但将剩余辅助函数和 error 改为源码私有;通过 spawn 和 dispose 测试。保持共享凭据正则公开。 |
@@ -25,7 +25,7 @@ Status: proposed
| `BlockAssembler.push()` 返回值 | 两个生产调用者都忽略返回的已完成块。 | 返回 `void`;保留有意公开的 `blocks()`/`message()` 约定。 |
| `compactRegion` 的独立 `session` 参数 | 固定调用方传入的对象就是 `agent.session` 中已有的对象;模型可见的 mount API 也可以调用该方法,但同时接受两个独立对象,会让挂载的插件传入不一致的组合。 | 保留手动 region API,同时有意将其收窄为以 `agent.session` 为唯一真源。 |
| `CompactionResult.startSeq`、`summarySeq`、`endSeq` 与 `summary` | 生产消费方只读取 shadowed range/seq/token 统计;持久日志拥有 summary 和事件标识。 | 移除四个结果回显,保留两个共享的 transcript(文本记录)渲染器。 |
| `BasicCompactService` 的估算/摘要方法可见性 | 没有包外生产调用者调用这五个方法;已实现的 Agent Note 只将 `estimateContentTokens()` 和 `summarize()` 命名为子类钩子。 | 将这两个方法改为 `protected`,其余三个编排专用的估算器改为 private。 |
| `BasicCompactionEngine` 的估算/摘要方法可见性 | 没有包外生产调用者调用这五个方法;已实现的 Agent Note 只将 `estimateContentTokens()` 和 `summarize()` 命名为子类钩子。 | 将这两个方法改为 `protected`,其余三个编排专用的估算器改为 private。 |
| `CodeLogEntry.source`/`level` 与 `RunCodeMeta.dispatches` | 每个生产消费方都将日志映射为文本;没有 presenter/模型路径读取其他字段或持久化的 dispatch 计数。 | 将 code-runtime 日志改为字符串(或纯文本条目),移除 result-meta 的 dispatch 管道;保留用于生成确定性 dispatch id 的本地计数器。 |
| `CodeRuntime.language` 与 `CodeRuntime.isolation` | worker 后端提供唯一的生产值,而 Code Mode 及其他所有生产调用方只调用 `run()`。 | 移除未读描述符,同时保留 worker 的语言、隔离、预算、取消与资源释放行为。 |
| `ToolNotFoundError.toolName`、`SystemPrompt.config` 与 `BashTask.command` | 每个存储的公开值都没有生产读取者。 | 移除未读字段,保留错误消息、已解析的配置行为和任务生命周期。 |
@@ -37,9 +37,9 @@ Status: proposed
- `dsh-llm-deepseek`:`httpErrorCode`、`serializeMessages`、`serializeRequest`、`DONE`、`parseSse`、`mapFinishReason`、`mapUsage` 与 `translate`;`dsh-llm-pi-ai`:`buildModel`、`mapStopReason`、`mapUsage`、`toPiContext` 与 `toStreamChunks`。
- `dsh-bash-local`:`DEFAULT_GRACE_MS`、`ENV_OVERRIDES`、`killGroup`、`OutputCollector` 与 `runBash`;`dsh-bash-sandbox`:`shellQuote`、`classifyDenial` 与 `classifyRunnerFailure`;`dsh-sandbox-local`:`bwrapProfileArgs`、`landlockProfileArgs` 与 `seatbeltProfileArgs`。公开的可变测试注入字段及其类型不在本提案范围内。
- `dsh-fs-local`:`applyLiteralEdit`、`listDirectory`、`probe`、`readForEdit`、`readTextForDiff`、`readWholeText`、`resolveLocalTarget`、`restoreLineEndings`、`streamWholeText` 与 `writeFileAtomic`。
- `dsh-web-fetch-local`:`classifyContentType`、`decoderForCharset`、`isSameOrigin`、`parseCharset` 与 `validateFetchUrl`;`dsh-web-search-exa`:`mapExaResponse` 与 `mapExaResult`;`dsh-web-search-deepseek`:`citationSnippets` 与 `mapAnthropicResponse`;`dsh-web-search-perplexity`:`mapPerplexityResponse` 与 `mapPerplexityResult`。
- `dsh-web-fetch-http`:`classifyContentType`、`decoderForCharset`、`isSameOrigin`、`parseCharset` 与 `validateFetchUrl`;`dsh-web-search-exa`:`mapExaResponse` 与 `mapExaResult`;`dsh-web-search-deepseek`:`citationSnippets` 与 `mapAnthropicResponse`;`dsh-web-search-perplexity`:`mapPerplexityResponse` 与 `mapPerplexityResult`。
- `dsh-tool-fs`:`READ_LIMIT`、`STREAM_MIN_SIZE`、`READ_MAX_BYTES`、`READ_MAX_LINE_LENGTH`、`DIFF_CONTEXT`、`applyReadTool`、`parseReadArgs`、`applyWriteTool`、`formatWriteOutput`、`parseWriteArgs`、`applyEditTool`、`formatEditOutput`、`parseEditArgs`、`buildWindow`、`formatReadOutput`、`computeHunkDiffs` 与 `diffsFromMeta`。
- `dsh-tool-web`:`WEB_SEARCH_MAX_RESULTS`、`applyWebSearchTool`、`formatSearchOutput`、`parseSearchArgs`、`presentSearchCall`、`applyWebFetchTool`、`formatFetchOutput`、`parseFetchArgs`、`presentFetchCall`、`renderBody` 与 `htmlToMarkdown`;`dsh-timeout-policy`:`toolTimeoutResult`;`dsh-compact-basic`:`resolveConfig`;`dsh-tool-bash`:`renderResult`。
- `dsh-tool-web`:`WEB_SEARCH_MAX_RESULTS`、`applyWebSearchTool`、`formatSearchOutput`、`parseSearchArgs`、`presentSearchCall`、`applyWebFetchTool`、`formatFetchOutput`、`parseFetchArgs`、`presentFetchCall`、`renderBody` 与 `htmlToMarkdown`;`dsh-tool-call-timeout-policy`:`toolTimeoutResult`;`dsh-compaction-basic`:`resolveConfig`;`dsh-tool-bash`:`renderResult`。
## 提案

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.md
2026-07-19-make-jsonrpc-directional.md: 87c134ec1f412b1e4df439a5b6434f83e52b531e
2026-07-19-make-jsonrpc-directional.zh.md: b728f2335f1605f80b0b224fa2c889591c635645
2026-07-19-make-jsonrpc-directional.md: be0d8003e066b8695fb413d5a546e43694bf2225
2026-07-19-make-jsonrpc-directional.zh.md: d070983b14898078b481a930eb70e7f6db53714a

View File

@@ -20,7 +20,7 @@ Return the settled outcome directly from `session/prompt` as `{ status, reason }
## Implementation plan
1. In `packages/sdk/server/src/server.ts`, replace `SessionPromptResult.accepted` with `status: 'ok' | 'error' | 'aborted'` and the captured `TurnEndReason`. `HarnessSdkServer.prompt()` will return `completed` as `ok`, `aborted` as `aborted`, and every other current or merge-extensible reason as `error`; reaching idle without a `turn/end` remains an invariant error. Remove only `session.finished`, leaving `session.event`, `subagent.started`, and `subagent.finished` unchanged.
1. In `packages/sdk/server/src/server.ts`, replace `SessionPromptResult.accepted` with `status: 'ok' | 'error' | 'aborted'` and the captured `TurnEndReason`. `HarnessSdkJsonRpcServer.prompt()` will return `completed` as `ok`, `aborted` as `aborted`, and every other current or merge-extensible reason as `error`; reaching idle without a `turn/end` remains an invariant error. Remove only `session.finished`, leaving `session.event`, `subagent.started`, and `subagent.finished` unchanged.
2. In `packages/sdk/protocol/src/transport.ts`, narrow the shared class to the directions with consumers — inbound requests/outbound responses (the server) and outbound requests/inbound responses plus inbound notifications (the TypeScript SDK client) — removing only server-originated `request()` use and client-originated notification dispatch, or split the class into a server-side and client-side transport. Request result, method-not-found, and handler-error responses retain their current behavior and remain ordered after notifications emitted by the awaited handler.
3. In `python/sdk/src/deepseek_harness/client.py`, `models.py`, and `__init__.py`, remove `IncomingRequest`, `_requests`, `notify()`, `next_request()`, `respond()`, and `respond_error()`. Add a public validated `SessionPromptResponse` carrying status and reason, return it from `session_prompt()`, and keep an explicit reader guard that ignores unexpected server-request frames instead of allowing them to match a response waiter.
4. In `python/sdk/src/deepseek_harness/api.py`, build `TurnResult.status` and a new `TurnResult.reason` from `SessionPromptResponse`, then delete the `session.finished` branch and second completion loop. Keep the subscription open during the request and preserve `_request_raw()`'s final notification drain so the last `turn/end` event and any subagent notification written before the response are collected before `Session.run()` reconstructs the final assistant message.

View File

@@ -20,7 +20,7 @@ JSON-RPC 桥接层把两个端点都建模为对称的对等端,但实际协
## 实施计划
1. 在 `packages/sdk/server/src/server.ts` 中,用 `status: 'ok' | 'error' | 'aborted'` 和捕获的 `TurnEndReason` 替换 `SessionPromptResult.accepted`。`HarnessSdkServer.prompt()` 把 `completed` 映射为 `ok`,把 `aborted` 映射为 `aborted`,把其他当前已有或可通过声明合并扩展的原因映射为 `error`;进入空闲状态却没有 `turn/end` 仍视为不变量错误。只删除 `session.finished`,保持 `session.event`、`subagent.started` 和 `subagent.finished` 不变。
1. 在 `packages/sdk/server/src/server.ts` 中,用 `status: 'ok' | 'error' | 'aborted'` 和捕获的 `TurnEndReason` 替换 `SessionPromptResult.accepted`。`HarnessSdkJsonRpcServer.prompt()` 把 `completed` 映射为 `ok`,把 `aborted` 映射为 `aborted`,把其他当前已有或可通过声明合并扩展的原因映射为 `error`;进入空闲状态却没有 `turn/end` 仍视为不变量错误。只删除 `session.finished`,保持 `session.event`、`subagent.started` 和 `subagent.finished` 不变。
2. 在 `packages/sdk/protocol/src/transport.ts` 中,把共享类收窄到有消费者的方向——入站请求/出站响应(服务端)与出站请求/入站响应及入站通知(TypeScript SDK 客户端)——只删除服务端发起的 `request()` 用法与客户端发起的通知分发,或把该类拆分为服务端与客户端两个传输。请求结果、方法不存在与处理器错误响应保持原有行为,并继续排在被等待处理器发出的通知之后。
3. 在 `python/sdk/src/deepseek_harness/client.py`、`models.py` 和 `__init__.py` 中,删除 `IncomingRequest`、`_requests`、`notify()`、`next_request()`、`respond()` 和 `respond_error()`。新增公开且经过校验的 `SessionPromptResponse` 来携带状态与原因,由 `session_prompt()` 返回该对象,并保留明确的读取保护:忽略意外的服务端请求帧,避免它们命中响应等待器。
4. 在 `python/sdk/src/deepseek_harness/api.py` 中,根据 `SessionPromptResponse` 构造 `TurnResult.status` 和新增的 `TurnResult.reason`,再删除 `session.finished` 分支与第二个完成循环。请求期间保持订阅打开,并保留 `_request_raw()` 最后的通知排空步骤,确保写在响应前的最后一条 `turn/end` 事件与任何 subagent 通知,都会在 `Session.run()` 重建最终助手消息之前被收集。