Merge latest master into Codex subagent provider

This commit is contained in:
Tianyi Cui
2026-08-06 21:26:31 +08:00
2612 changed files with 56217 additions and 27538 deletions

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-06-16-typed-event-schemas.md
2026-06-16-typed-event-schemas.md: 97a7d0c3787eb5556696e25a6c8b1bb75642aba9
2026-06-16-typed-event-schemas.zh.md: c19f67c6ff058d42293ff2b6346630fe91c54dec
2026-06-16-typed-event-schemas.zh.md: d0c9ffb3640cb76f0c656e4ac841dc3817bcb86d

View File

@@ -13,13 +13,13 @@ harness 将其核心词汇——内容块、消息来源、结束原因、轮次
1. **持久化将 `event.data` 视为不透明 JSON。** JSONL/SQLite 后端对每个事件逐字 `JSON.stringify`/`JSON.parse`;唯一的运行时守卫是 `isJsonValue`(往返可序列化性检查:拒绝 BigInt、函数、循环引用、非有限数等),而非结构校验。一个损坏但仍为合法 JSON 的事件数据(字段类型错误、字段缺失)会静默往返,只有在后续消费方的 `switch` 中才可能被捕获。
2. **插件新增变体没有运行时契约。** 一个通过声明合并添加新 `SessionEventMap` 键的插件,在自身代码中获得了编译期类型,但没有任何机制校验它产出的值是否符合它所声明的形状——无论是在生产者处、持久化边界处还是重新加载时。
由此引出问题:事件词汇是否应迁移到 **Zod** 或其他运行时 schema 库,使持久化和插件边界拥有运行时 schema 而非被擦除的类型。
由此引出问题:事件词汇是否应迁移到 **Zod** 或其他运行时 schema 库,使持久化边界和插件边界拥有运行时 schema 而非被擦除的类型。
本 Agent Note(agent 决策记录)界定该问题的范围,不提出具体实现。
本 Agent Note 界定该问题的范围,不提出具体实现。
## 为什么这不是一个持久化层的改动
很容易把「用 Zod 做序列化」理解为对 `dsh-session-persistence-jsonl/src/format.ts` 的局部修改。但它不是,原因在于一个结构性事实:**插件无法对 Zod schema 进行声明合并。** 声明合并是 TypeScript 编译期机制;Zod schema 是运行时值。要用 Zod 校验事件,就需要一个**运行时注册表**,每个产出事件的包(package)向其贡献自己的 schema(如 `ctx.sessionEvents.register('compaction/marker', z.object({…}))`),每个消费方从中读取。这个注册表——而非持久化后端——将成为词汇的真源,取代 merge-extensible 接口。
很容易把「用 Zod 做序列化」理解为对 `dsh-session-persistence-jsonl/src/format.ts` 的局部修改。但它不是,原因在于一个结构性事实:**插件无法对 Zod schema 进行声明合并。** 声明合并是 TypeScript 编译期机制;Zod schema 是运行时值。要用 Zod 校验事件,就需要一个**运行时注册表**,每个产出事件的包向其贡献自己的 schema(如 `ctx.sessionEvents.register('compaction/marker', z.object({…}))`),每个消费方从中读取。这个注册表——而非持久化后端——将成为词汇的真源,取代 merge-extensible 接口。
因此,真正的提案是:**用运行时 schema 注册表替换编译期的 merge-extensible-map 模式,范围覆盖整个仓库。** 这是一次核心词汇的重新设计。
@@ -53,7 +53,7 @@ harness 将其核心词汇——内容块、消息来源、结束原因、轮次
### C. 为整个词汇建立运行时 schema 注册表(Zod 或 schemastery)
用运行时注册表替换 merge-extensible map,生产者向其贡献 schema,持久化/消费路径据此校验。
- **优点**:持久化边界和插件 seam 处获得真正的运行时校验;单一真源;可支撑通用工具(自动生成文档、模糊测试、协议格式检查)。
- **优点**:持久化边界和插件 seam 处获得真正的运行时校验;单一真源;可支撑通用工具(自动生成文档、模糊测试、协议格式(wire format)检查)。
- **缺点**:上述全部影响范围;**Zod 目前不是直接依赖**(仅作为 `@earendil-works/pi-ai` 的传递依赖),仓库选定的 schema 库是 **schemastery**——广泛引入 Zod 本身就是一个依赖决策;声明合并的易用性(一行插件扩展、完整推断)被运行时注册 + 手动类型接线取代;`assertNever` 穷举保证弱化(运行时变体在静态层面不可穷举)。
## 提案

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md
2026-07-15-sdk-project-editing-architecture.md: 8335af516dbaa85f4adb85286f976ce9be2c9da8
2026-07-15-sdk-project-editing-architecture.zh.md: bec39cc896887678b2d3f74832a9d13d7b354d6e
2026-07-15-sdk-project-editing-architecture.zh.md: 51f0c5e7f4f033d8149a7ae01aae7a8c4755b76c

View File

@@ -6,11 +6,11 @@ Status: proposed
## 问题
[开发者拥有的 SDK 工程](../feature/2026-07-14-sdk-developer-projects.md) 由 create 创建,可以通过 config 调整,并由 start 等命令构建和运行。初始创建、配置调整和编译运行都需要理解功能、功能选项、NPM 依赖、Cordis 配置项、环境变量、包管理器、本地插件和多个项目文件。如果读写项目的各个流程分别使用不同的解析协议,SDK 开发者流程会变得难以维护。
[开发者拥有的 SDK 工程](../feature/2026-07-14-sdk-developer-projects.md) 由 create 创建,可以通过 config 调整,并由 start 等命令构建和运行。初始创建、配置调整和编译运行都需要理解功能、功能选项、NPM 依赖、Cordis 配置项、环境变量、包管理器、本地插件和多个项目文件。如果各个项目读写工作流分别使用不同的解析协议,SDK 开发者工作流会变得难以维护。
## 提案
SDK 使用一个共享的面向对象工程模型。`SdkProject` 是只读快照,`ProjectEditSession` 是唯一修改与提交边界;功能对象负责自身的功能选项、关系、资源贡献和现状识别;create 与 config 只编排各自的用户流程,并通过同一组领域操作修改工程。
SDK 使用一个共享的面向对象工程模型。`SdkProject` 是只读快照,`ProjectEditSession` 是唯一修改与提交边界;功能对象负责自身的功能选项、关系、资源贡献和现状识别;create 与 config 只编排各自的用户工作流,并通过同一组领域操作修改工程。
结构化文件通过文档对象修改,一次性文本产物通过完整模板生成。问题由类型化对象表达,并使用 clack 交互。差异计算可以作为编辑会话的内部实现,但不成为要求调用方组装的公共执行协议。
@@ -18,26 +18,26 @@ SDK 使用一个共享的面向对象工程模型。`SdkProject` 是只读快照
| 名词 | 本文用词 | 含义 |
|---|---|---|
| Feature | 功能 | SDK 人工策划和管理的产品单元;一项功能可以包含多个功能选项,并贡献多个 Cordis 配置项、NPM 依赖、环境变量占位和独占文件 |
| Feature | 功能 | 由 SDK 策划和管理的产品单元;一项功能可以包含多个功能选项,并贡献多个 Cordis 配置项、NPM 依赖、环境变量占位和独占文件 |
| Feature option | 功能选项 | 一项功能内有限、可选择的实现或配置形状;根据功能规则可以固定、互斥或多选 |
| Cordis plugin | Cordis 插件 | Cordis 加载的插件实现,通常由一个 NPM 包导出;它不是 `cordis.yml` 中的一项配置 |
| Cordis config entry | Cordis 配置项 | `cordis.yml` 插件列表中的一项,通过 `id` 标识实例并通过 `name` 指向 Cordis 插件 |
| Cordis plugin config | Cordis 插件配置 | Cordis 插件公开的配置对象或配置结构;其中由功能拥有并更新的单个字段称为“配置键” |
| Cordis plugin config | Cordis 插件配置 | Cordis 插件公开的配置对象或配置结构;其中由功能拥有并更新的单个字段称为「配置键」 |
| config key | 配置键 | Cordis 插件配置中的单个字段;功能只更新自己声明拥有的配置键,并保留未知配置键 |
| npm dependency | NPM 依赖 | `package.json` 中的包关系;`dependencies`、`devDependencies` 等字段保持原样 |
| Feature requirement | 功能依赖 | 功能或功能选项通过 `requires` 声明的关系 |
## Package 边界
## 包边界
| Package | 责任 | 不负责 |
| 包 | 责任 | 不负责 |
|---|---|---|
| `@deepseek-ai/dsh-helper` | 编辑会话、功能配置、工程模板渲染、包管理适配和 prompt 交互适配 | 启动 Cordis 应用或决定 create/config 的终端流程 |
| `@deepseek-ai/dsh-scripts` | `dsh-sdk start/dev/build/config`、进程生命周期、项目入口加载、config 流程和所属终端文案模板 | 直接解释功能定义或修改 YAML/JSON AST |
| `@deepseek-ai/dsh-helper` | 编辑会话、功能配置、工程模板渲染、包管理器适配和 prompt 交互适配 | 启动 Cordis 应用或决定 create/config 的终端工作流 |
| `@deepseek-ai/dsh-scripts` | `dsh-sdk start/dev/build/config`、进程生命周期、项目入口加载、config 工作流和所属终端文案模板 | 直接解释功能定义或修改 YAML/JSON AST |
| `@deepseek-ai/create-sdk` | `npm create @deepseek-ai/sdk` 的参数、问题顺序、首次工程创建、安装收尾和所属终端文案模板 | 成为生成工程的运行时 NPM 依赖或提供库 API |
`@deepseek-ai/create-sdk` 是仓库 `@deepseek-ai/dsh-*` 命名规则的唯一例外;npm scoped initializer 约定要求 `npm create @deepseek-ai/sdk` 对应这个 package 名。该例外是仓库架构事实,不增加第三个开发者产品入口。
`@deepseek-ai/create-sdk` 是仓库 `@deepseek-ai/dsh-*` 命名规则的唯一例外;npm scoped initializer 约定要求 `npm create @deepseek-ai/sdk` 对应这个包名。该例外是仓库架构事实,不增加第三个开发者产品入口。
三个 package 只导出相邻层实际使用的最小入口,不提供 `src/*` 深路径。scripts 的库入口与构建配置子路径服务生成代码和项目构建配置,但开发者产品合同仍由 `dsh-sdk` 命令承担。
三个包只导出相邻层实际使用的最小入口,不提供 `src/*` 深路径。scripts 的库入口与构建配置子路径服务生成代码和项目构建配置,但开发者产品契约仍由 `dsh-sdk` 命令承担。
## 工程聚合与编辑会话
@@ -58,35 +58,35 @@ validate feature requirements and resource ownership
## 功能与资源所有权
功能是一等行为对象。浅层基类实现 install、configure、enable、disable、required/requires 校验和共同状态识别;固定功能选项、互斥功能选项与可多选功能选项共享这些生命周期。只有资源贡献依赖项目上下文或需要自定义 round-trip 的功能才使用专用行为类,其余功能通过标准化数据声明真正不同的部分。
功能是一等行为对象。浅层基类实现 install、configure、enable、disable、required/requires 校验和通用状态检查;固定功能选项、互斥功能选项与可多选功能选项共享这些生命周期。只有资源贡献依赖项目上下文或需要自定义 round-trip 的功能才使用专用行为类,其余功能通过标准化数据声明真正不同的部分。
每项功能贡献带稳定 key 的 Cordis 配置项、NPM 依赖、环境变量占位和独占文件。注册表初始化时拒绝不同功能声明同一个资源 key;同一功能的不同功能选项可以共享资源,并由该功能根据最终选项集合处理。
Cordis 配置项是功能安装锚点。NPM 包名判断配置项所属的功能,配置项 ID 区分同一插件包的多个实例;只有 NPM 依赖而没有功能拥有的 Cordis 配置项时,该功能仍视为未安装。Cordis 配置项存在后,缺失 NPM 依赖、无法读取的 Cordis 插件配置或资源冲突会使功能进入不一致状态,config 命令显示诊断并拒绝猜测式修改。
Cordis 配置项是功能安装锚点。NPM 包名用于确定配置项所属的功能,配置项 ID 区分同一插件包的多个实例;只有 NPM 依赖而没有功能拥有的 Cordis 配置项时,该功能仍视为未安装。Cordis 配置项存在后,缺失 NPM 依赖、无法读取的 Cordis 插件配置或资源冲突会使功能进入不一致状态,config 命令显示诊断并拒绝猜测式修改。
同一功能选项只更新其声明拥有的配置键,保留未知键。替换功能选项会删除旧功能选项独占且仍可确认的资源;无法确认旧资源或发现独占文件被用户修改时,整个操作失败。
配置同一功能选项时,只更新其声明拥有的配置键,保留未知键。替换功能选项会删除旧功能选项独占且仍可确认的资源;无法确认旧资源或发现独占文件被用户修改时,整个操作失败。
## 问题与 workflow
## 问题与工作流
问题由 TypeScript `Question<T>` 对象表达,默认值、校验、适用条件和类型留在同一个对象中。`PromptPort` 是领域层与终端库之间的唯一接口,helper 提供一份薄 `ClackPromptPort`;create 和 config 注入各自的命令行输入输出流,并在各自流程中决定取消、返回和收尾语义。
问题由 TypeScript `Question<T>` 对象表达,默认值、校验、适用条件和类型留在同一个对象中。`PromptPort` 是领域层与终端库之间的唯一接口,helper 提供一个轻量的 `ClackPromptPort`;create 和 config 注入各自的命令行输入输出流,并在各自工作流中决定取消、返回和收尾语义。
create 的有状态问题顺序留在一个向导中,config 的最终状态选择留在一个流程中。两者通过同一个功能配置器收集功能选项与专用输入,因此增加一项普通功能、功能选项或参数不要求同时修改两个入口。
create 的有状态问题顺序留在一个向导中,config 的最终状态选择留在一个工作流中。两者通过同一个功能配置器收集功能选项与专用输入,因此增加一项普通功能、功能选项或参数不要求同时修改两个入口。
## 项目文档与模板
只有需要读取或修改的结构化文件拥有具体文档对象,包括 `package.json`、`cordis.yml`、`.env`、`.env.example`、根 `tsconfig.json` 和 pnpm workspace 文件。文档对象拥有解析、克隆、校验和序列化行为;具体类与模块分别使用 `*File` 和 `*-file.ts` 命名,业务层不直接操作 YAML/JSON AST,异常形状在所属文档边界 fail loud。
只有需要读取或修改的结构化文件拥有具体文档对象,包括 `package.json`、`cordis.yml`、`.env`、`.env.example`、根 `tsconfig.json` 和 pnpm workspace 文件。文档对象拥有解析、克隆、校验和序列化行为;具体类与模块分别使用 `*File` 和 `*-file.ts` 命名,业务层不直接操作 YAML/JSON AST,异常结构会在所属文档边界明确报错。
README、入口代码、构建配置、`.gitignore` 和其他一次性文本产物使用与真实文件一一对应的完整模板。CLI usage、创建结果与恢复提示、安装与重试指导以及默认 persona 等完整产品文案也由所属 package 的本地模板提供。
README、入口代码、构建配置、`.gitignore` 和其他一次性文本产物使用与真实文件一一对应的完整模板。CLI(命令行界面)用法、创建结果与恢复提示、安装与重试指导以及默认 persona 等完整产品文案也由所属包的本地模板提供。
helper 提供通用的数据类型化 `TextTemplate` 模板渲染器,调用 package 通过本地 asset URL 加载自己的模板。
helper 提供通用的数据类型化 `TextTemplate` 模板渲染器,调用方包通过本地 asset URL 加载自己的模板。
模板使用 Handlebars strict mode 与 `noEscape`,不进行自定义处理。文件对象负责把类型化数据值编码成目标语言文本;如果不希望插值,则源码以 `\{{model}}` 等转义形式输出下游。
模板使用 Handlebars strict mode 与 `noEscape`,不进行自定义处理。文件对象负责把类型化数据值编码成目标语言文本;模板源码在必须原样输出下游字面量时,将插值转义为 `\{{model}}`。
## 命令与运行边界
scripts 支持 `dsh-sdk start/dev/build/config`。start 动态加载模块 target 并调用其命名入口;dev 在同一路径前增加 TypeScript 与本地 workspace 源码解析;build 调用工程安装的 tsdown;config 打开一个编辑会话并在 Review & Apply 后提交。typecheck 由生成工程直接执行 `tsc -b`。
scripts 支持 `dsh-sdk start/dev/build/config`。start 动态加载模块 target 并调用其命名入口;dev 在同一路径前增加 TypeScript 与本地 workspace 源码解析;build 调用工程安装的 tsdown;config 打开一个编辑会话并在 Review & Apply 后提交。类型检查由生成工程直接执行 `tsc -b`。
HMR 作为显式 Cordis 配置项由 dev 和 start 加载;它所需的 `node-addon-require-builtin` 由 scripts package 传递提供,不写入开发者工程的 `package.json`。
HMR(热模块替换)作为显式 Cordis 配置项由 dev 和 start 加载;它所需的 `node-addon-require-builtin` 由 scripts 包传递提供,不写入开发者工程的 `package.json`。
dev/start 会执行开发者入口,在开发者代码中处理命令行参数、cwd,由开发者自行传入 `--model=<name>` 与 `--resume=<session-id>` 启动标准流程。
@@ -94,12 +94,12 @@ dev/start 会执行开发者入口,在开发者代码中处理命令行参数
create-sdk 保留隐藏的 `--link-workspace` 选项供 Harness 仓库开发和 e2e 使用。该选项可以被解析,但不出现在 help、公开 flag 清单或普通用户文档中,也不接收仓库路径参数;仓库根从正在执行的 create-sdk 模块位置向上确定。
链接模式保持普通工程的文件形状。`@deepseek-ai/*` 指向 `packages/`,Cordis 相关 NPM 依赖指向 `vendor/`,共享底层 package 锚定到仓库实际使用的同一物理拷贝,避免 Cordis 类型合并产生多个模块类型定义。npm 使用 `file:`,pnpm 使用 `link:` 并关闭自动 peer 安装,Yarn 使用 `portal:` 与 resolutions;仓库 package 需要先构建。
链接模式保持普通工程的文件形状。`@deepseek-ai/*` 指向 `packages/`,Cordis 相关 NPM 依赖指向 `vendor/`,共享底层包锚定到仓库实际使用的同一物理拷贝,避免 Cordis 类型合并产生多个模块类型定义。npm 使用 `file:`,pnpm 使用 `link:` 并关闭自动 peer 安装,Yarn 使用 `portal:` 与 resolutions;仓库包需要先构建。
## 后续工作
- **可替换的 required 主干角色。** 当前 `spine` 以一个固定功能选项拥有整组实现,包含 SystemPrompt、LLMService 等。无法让开发者对其进行替换和切换,只能手工修改 Cordis 配置项。
- **Service contract 与 package 声明。** 替换特定内建服务时,Cordis 插件目前无法通过 `provides` 元数据声明其提供的服务,因此 SDK 无法在开发阶段辅助配置,也无法在运行时检查兼容性。后续需要设计相应协议。
- **服务契约与包声明。** 替换特定内建服务时,Cordis 插件目前无法通过 `provides` 元数据声明其提供的服务,因此 SDK 无法在开发阶段辅助配置,也无法在运行时检查兼容性。后续需要设计相应协议。
- **功能参数描述。** 当前功能的专用输入必须手工声明;SDK 无法从任意 Cordis 插件配置或 NPM package.json 信息中自动推导可交互参数。后续可以定义有限的声明式参数元数据,但不把任意 Cordis 插件配置转换成通用表单。
- **SDK 应用级配置。** 当前项目资源模型只描述 Cordis 配置项及单个 Cordis 插件拥有的配置键,因此所有受 SDK 管理的配置都必须归属某个插件。跨插件或面向整个 SDK 应用的设置没有独立持久化位置;后续需要定义应用级配置文档及其所有权、读取和修改边界。
@@ -107,23 +107,23 @@ create-sdk 保留隐藏的 `--link-workspace` 选项供 Harness 仓库开发和
**保留静态 Catalog 与中心 engine。** 该方案改动最小,但功能参数、round-trip、独占文件和 create/config 复用都会继续进入同一个协调中心;拆文件只能缩短单文件,不能收拢职责。
**使用 `wizard.json` 与通用 Questionnaire。** 静态表单无法直接表达功能依赖、选项切换、已有值回填和项目资源变化;类型、gate 和动态 option 最终仍要通过字符串 registry 与过程式 `run()` 连接,形成新的内部 DSL。
**使用 `wizard.json` 与通用 Questionnaire。** 静态表单无法直接表达功能依赖、选项切换、已有值回填和项目资源变化;类型、门禁和动态选项最终仍要通过字符串注册表与过程式 `run()` 连接,形成新的内部 DSL。
**公开本地链接 flag。** 该模式依赖 Harness monorepo 布局和未发布 package,只服务仓库开发;公开后会形成无法对外兑现的项目创建合同,因此保持隐藏。
**公开本地链接 flag。** 该模式依赖 Harness monorepo 布局和未发布包,只服务仓库开发;公开后会形成无法对外兑现的项目创建契约,因此保持隐藏。
## 验收标准
- create 与 config 只通过 `SdkProject` 和 `ProjectEditSession` 修改工程,写入前的任何业务、文件或并发校验失败都不产生磁盘变化
- 新增普通功能、功能选项或参数只扩展类型化 spec 或所属行为对象,create/config 流程不增加中央 switch
- create 与 config 只通过 `SdkProject` 和 `ProjectEditSession` 修改工程,写入前的任何业务、文档或并发校验失败都不产生磁盘变化
- 新增普通功能、功能选项或参数只扩展类型化 spec 或所属行为对象,create/config 工作流不增加中央 switch
- 功能模型、NPM 依赖与其他资源配置、不一致检测由 helper 统一实现
- 结构化文件通过 `*File` 文档对象修改;一次性文件和完整产品文案通过所属 package 的 Handlebars 模板生成,业务决策不进入模板 DSL
- `dsh-sdk start/dev/build/config` 是运行产品面,typecheck 直接使用 `tsc -b`,HMR 不通过命令隐式注入,`node-addon-require-builtin` 只由 scripts package 传递提供
- 结构化文件通过 `*File` 文档对象修改;一次性文件和完整产品文案通过所属包的 Handlebars 模板生成,业务决策不进入模板 DSL
- `dsh-sdk start/dev/build/config` 是运行时产品接口,类型检查直接使用 `tsc -b`,HMR 不通过命令隐式注入,`node-addon-require-builtin` 只由 scripts 包传递提供
- `--link-workspace` 只作为隐藏的仓库开发选项存在,并对 npm、pnpm 和 Yarn 保持单一模块身份
## 风险
- 行为对象与类型化 spec 并存会形成两种扩展形状;专用类必须只用于确实依赖项目上下文或自定义的功能,否则会重新产生无意义的类型层次
- 行为对象与类型化 spec 并存会形成两种扩展形状;专用类必须只用于确实依赖项目上下文或需要自定义行为的功能,否则会重新产生无意义的类型层次
- 乐观并发检查与写前校验不能解决写入中途的 I/O 故障,调用方仍需向开发者报告可能的部分提交
- 隐藏链接模式依赖仓库目录与 package manager 链接语义,仓库布局或工具行为变化时必须与实现一起更新
- Cordis loader 从自身模块路径加载 `node-addon-require-builtin`;npm、pnpm 或 Yarn 的 NPM 依赖布局变化时,scripts package 必须继续满足该可选对等依赖(optional peer dependency)
- 隐藏链接模式依赖仓库目录与包管理器链接语义,仓库布局或工具行为变化时必须与实现一起更新
- Cordis loader 从自身模块路径加载 `node-addon-require-builtin`;在 npm、pnpm 和 Yarn 的 NPM 依赖布局下,scripts 包必须持续满足该可选对等依赖(peer dependency)
- Handlebars 的 `noEscape` 把目标语言编码责任交给 typed model 构造方;新增模板字段时必须在 owner 处完成正确转义,下游 Handlebars 占位符必须在模板源码中显式转义

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
# 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: c2cfb09f27222136965058695e9b6b706ac688a9
2026-07-19-required-cancellation-through-tool-capability-seams.zh.md: f7a1d303212dfab6da27feba2d6e7195ea07bd50
2026-07-19-required-cancellation-through-tool-capability-seams.zh.md: 6aa7340d45e99b9072c28c6a1cfe6a769ff8f795

View File

@@ -1,4 +1,4 @@
# Agent Note: 工具可达能力接缝中的必填取消
# Agent Note: 工具可达能力 seam 中的必填取消
Status: proposed
@@ -8,17 +8,17 @@ Status: proposed
已经实现的[工具注册表取消契约](../../implemented/architecture/2026-07-19-cooperative-tool-cancellation.md)让每个工具主体中的 `exec.signal` 成为必填值,但许多由工具主体调用的异步能力接口仍接受可选信号。因此,工具可以满足自身类型,却在下一次同进程调用时意外丢失取消。
这项缺口会沿调用链传递。文件系统工具可能调用路径解析和 I/O,Web 工具可能调用提供方,Bash 工具可能调用执行器,组合工具可能启动或等待任务、subagent 或工作流。只要某个控制工具所持有工作的等待操作允许省略信号,TypeScript 就无法证明取消仍能到达拥有副作用的边界。
这项缺口会沿调用链传递。文件系统工具可能调用路径解析和 I/O,Web 工具可能调用提供方,Bash 工具可能调用执行器,组合工具可能启动或等待任务、subagent 或工作流。只要某个控制工具所持有工作的、且会被工具等待的操作允许省略信号,TypeScript 就无法证明取消仍能到达拥有副作用的边界。
要求仓库中所有异步函数都携带信号会过度扩张。有些操作无法从工具到达,有些同步查询不会等待或持有持续工作,而明确分离的工作在刻意交接后已经拥有新的所有者。
要求仓库中所有异步函数都携带信号会过度扩张。有些操作无法从工具到达,有些同步查询无法等待或持有持续工作,而明确分离的工作在刻意交接后已经拥有新的所有者。
## 提议
所有能从工具主体到达、且在工具仍持有或等待该操作期间执行的异步同进程能力操作,都必须接收 `AbortSignal`。根据所属接缝的既有形态,这项要求可以表现为位置参数,也可以表现为必填的只读请求字段,但省略信号必须导致 TypeScript 编译失败。
所有能从工具主体到达、且在工具仍持有或等待该操作期间执行的异步同进程能力操作,都必须接收 `AbortSignal`。根据所属 seam 的既有形态,这项要求可以表现为位置参数,也可以表现为必填的只读请求字段,但省略信号必须导致 TypeScript 编译失败。
每个直接调用方提供自己持有的信号,或从自身必填的操作上下文继续传递信号。实现可以派生子截止时间或取消作用域,但派生信号在委托期间仍须与上游信号关联。能力实现不得生成永不中止信号、使用环境式异步本地取消,也不得仅为重复类型化同进程契约而在运行时校验 `AbortSignal`。
迁移首先从每个第一方 `ToolDefinition.execute()` 出发,清点其等待的能力调用;随后把每个内聚的接口、实现和使用方接缝连同测试与生成的 API 文档一起修改。文件系统、Bash 与任务、Web 与提供方、工作流与 subagent、代码运行时等能力族可以通过独立 PR 迁移,以保持每项变更可审查;但根据仓库的预发布原则,已经迁移的接口不得保留可选兼容重载。
迁移首先从每个第一方 `ToolDefinition.execute()` 出发,清点其等待的能力调用;随后按内聚的接口/实现/消费方 seam,将测试与生成的 API 文档一并修改。文件系统、Bash 与任务、Web 与提供方、工作流与 subagent、代码运行时等能力族可以通过独立 PR(Pull Request)迁移,以保持每项变更可审查;但根据仓库的预发布原则,已经迁移的接口不得保留可选兼容重载。
### 范围边界
@@ -26,7 +26,7 @@ Status: proposed
本提议不包含同步注册表查询、可用性检查、schema 渲染、参数分类,以及其他无法保留异步工作的操作。明确交接所有权后的分离工作也不在范围内:任务、工作流、worker 或 subagent 成功发布给新的生命周期所有者后,其分离生命周期由新所有者的控制器管理。发起启动的操作在交接提交前仍须接收调用方信号;之后若另一次工具调用等待该分离工作,则必须使用该次调用自己的信号。
若外部协议本身允许省略取消,解析器、配置、模型与工具 JSON、持久化与文件格式、worker、进程或线协议输入仍可保留可选取消。所属边界必须先把该输入解析为必填的同进程信号,再调用已经迁移的能力接缝。
若外部协议本身允许省略取消,解析器、配置、模型与工具 JSON、持久化与文件格式、worker、进程或协议输入仍可保留可选取消。所属边界必须先把该输入解析为必填的同进程信号,再调用已经迁移的能力 seam。
## 考虑过的替代方案
@@ -34,19 +34,19 @@ Status: proposed
**通过 lint 规则或回调检查强制传递。** 不予采纳,因为语法检查无法可靠识别所有权、派生信号、抽象层或正确的完全停稳行为。必填接口参数可以在 TypeScript 能检查每个调用方的位置表达契约。
**把 `ToolRunContext` 传入所有能力。** 不予采纳,因为能力需要的是取消,而不是工具身份、agent 状态或上下文延后功能。传递更大的上下文会让可复用服务耦合到工具注册表,也会掩盖狭窄接缝。
**把 `ToolRunContext` 传入所有能力。** 不予采纳,因为能力需要的是取消,而不是工具身份、agent(智能体)状态或上下文延后功能。传递更大的上下文会让可复用服务耦合到工具注册表,也会掩盖狭窄的 seam。
**使用环境式异步本地信号。** 不予采纳,因为隐藏传递会让所有权和分离交接难以审计,使测试复杂化,并可能让调用静默绑定到错误的生命周期。
**在能力实现中加入默认或永不中止信号。** 不予采纳,因为默认值会抹去缺失的所有者,而不是在编译期暴露问题。
**在已经实现的工具注册表变更中迁移所有能力。** 不予采纳,因为传递性的接口修改横跨独立能力族。单独保留这项提议既能维持已实现的注册表决策,也能让每个深层接缝通过聚焦测试完成迁移。
**在已经实现的工具注册表变更中迁移所有能力。** 不予采纳,因为传递性的接口修改横跨独立能力族。单独保留这项提议既能维持已实现的注册表决策,也能让每个深层 seam 通过聚焦测试完成迁移。
## 验收标准
- 清单把每个第一方工具主体映射到所有权交接前可以到达的异步能力操作。
- 每个范围内的能力接口都要求 `AbortSignal`,并由编译期契约测试证明省略信号会失败。
- 接口、实现、直接使用方、测试辅助函数、示例和生成的 API 引用必须一起迁移,不保留兼容重载或生产环境永不中止哨兵。
- 接口、实现、直接消费方、测试辅助函数、示例和生成的 API 引用必须一起迁移,不保留兼容重载或生产环境永不中止哨兵。
- 派生截止时间和包装层作用域仍与调用方信号关联,集成测试证明取消到达副作用所有者,且等待的工作完全停稳。
- 同步查询和明确交接后的分离工作不受这项要求约束;存在歧义时,需要记录并测试所有权转换。
- 只有真实的无类型边界才添加运行时校验,不得重复校验 TypeScript 已要求的字段或参数。

View File

@@ -3,4 +3,4 @@
# 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: 230877628428dc88dbddeecfe5f4353cf15e151d
2026-07-24-domain-kv-storage-and-workspace.zh.md: 050f72cd3327f83e2c3f3cefcab63c01e8f112ee
2026-07-24-domain-kv-storage-and-workspace.zh.md: cb1fb1346a28b0c3249848705ca209f57cab766e

View File

@@ -1,29 +1,29 @@
# Agent Note: Domain KV storage capability seam and the workspace entity
# Agent Note: 领域 KV 存储能力 seam 与 workspace 实体
Status: proposed
[English](2026-07-24-domain-kv-storage-and-workspace.md) | 中文
## Problem
## 问题
host 侧唯一的持久化面是 session 事件日志(`packages/session-persistence`:append-only、一 session 一文件)。凡是"不属于某个 session"的信息就没有落盘处,眼下有两个真实需求:
host 侧唯一的持久化面是 session 事件日志(`packages/session-persistence`:仅追加、一 session 一文件)。凡是"不属于某个 session"的信息就没有落盘处,眼下有两个真实需求:
- **workspace 实体**。GUI 要把 workspace 做成真实对象:路径、标题、关联 session 清单。归属关系由 workspace 持有——"哪些 session 属于这个 workspace"不是任何单个 session 自己的事实,塞进 session log 语义不成立。此前 workspace 只是 sidebar 上按 cwd 分组的视觉概念,没有实体(该结论已被推翻)。
- **session 动态元信息**(可预见的第二个消费者)。冷会话列表只读日志首行 header(创建时的不可变快照),title、结束状态这类随会话推进变化的信息拿不到;补齐方向是 sidecar 元数据表——正是一张按 key 高频点更新的 KV 表。
- **session 动态元信息**(可预见的第二个消费方)。冷会话列表只读日志首行 header(创建时的不可变快照),title、结束状态这类随会话推进变化的信息拿不到;补齐方向是 sidecar 元数据表——正是一张按 key 高频点更新的 KV 表。
另外,Session 删除需要 `SessionPersistence` 删除原语和 `session.delete` 端点。该空白的设计随本 Note 定案,但实现仍属未来工作。
后续的 [Workspace 注册记录删除决策](../../implemented/feature/2026-07-27-workspace-registration-deletion.md)取代的仅是上述耦合关系:删除 Workspace 注册记录会保留相关 Session 及其日志,Session 删除仍是独立的未来工作。因此,下文的级联设计并不是 Workspace GUI 的删除语义。
## Proposal
## 方案
新建 `packages/storage/` 组——`ctx.storage` 存储枢纽(后端注册面 + 数据形式挂载面)、两个后端、domain 领域数据形式——及 workspace 消费者包;给 `SessionPersistence` 扩删除原语。
新建 `packages/storage/` 组——`ctx.storage` 存储枢纽(后端注册面 + 数据形式挂载面)、两个后端、domain 领域数据形式——及 workspace 消费方包;给 `SessionPersistence` 扩删除原语。
| 包 | 路径 | ctx 面 | 本期 |
| --- | --- | --- | --- |
| `@deepseek-ai/dsh-storage` | `packages/storage/storage/` | `ctx.storage`(枢纽) | ✓ |
| `@deepseek-ai/dsh-storage-json` | `packages/storage/storage-json/` | 注册 backend `json` | ✓ |
| `@deepseek-ai/dsh-storage-sqlite` | `packages/storage/storage-sqlite/` | 注册 backend `sqlite` | ✓ |
| `@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` | ✓ |
| `SessionPersistence.delete` 扩面 + 级联删编排 | `packages/session-persistence/*` | 既有 seam 新方法 | ✗ future work(本期不动 session 侧) |
@@ -35,13 +35,13 @@ host 侧唯一的持久化面是 session 事件日志(`packages/session-persis
### `dsh-storage`:存储枢纽
纯注册枢纽,自身不做 IO,无 Config。`Storage` service 挂 `ctx.storage`,两个面:`backend`(`BackendRegistry`:`register(name, backend)` 返回 disposer、重名 throw;`get(name)` 未知名 throw `backend-not-found`)与数据形式挂载(`mount(form, facility)` 配 merge-extensible 的 `StorageForms` map,`dsh-domain` merge 进 `domain` 键;未挂载访问 throw `form-not-mounted`)。签名正文见 `packages/storage/storage/src/index.ts` 与 `src/registry.ts`。
纯注册枢纽,自身不做 IO,无 Config。`Storage` 服务挂 `ctx.storage`,两个面:`backend`(`BackendRegistry`:`register(name, backend)` 返回 disposer、重名 throw;`get(name)` 未知名 throw `backend-not-found`)与数据形式挂载(`mount(form, facility)` 配 merge-extensible 的 `StorageForms` map,`dsh-domain` merge 进 `domain` 键;未挂载访问 throw `form-not-mounted`)。签名正文见 `packages/storage/storage/src/index.ts` 与 `src/registry.ts`。
**多后端同时挂载**;域→后端的选择是 `dsh-domain` 的配置(见下),不是全局二选一。disposer 语义 = 从表中摘名;后端自身的 close 由后端包的 effect 闭包负责,顺序先摘名后 close。
一个后端是一个**介质 owner**(一棵文件树 root / 一个 db 文件),通过**数据形状 facet** 暴露原语——本期只有 `kv`;session 迁移期加 `log`(见迁移节)。facet 是可选成员,缺席即该后端不支持该形状,解析时 fail loud。`kv` facet 的原语面:`open(descriptor)`(descriptor = 名字/版本/表名清单/有无 global,名字与表名限 `^[a-z][a-z0-9_]*$` 兼作文件名与 SQL 表名段)返回 unit,unit 提供 `loadAll` / `putRecord` / `deleteRecord`(缺 key 为 no-op)/ `setGlobal` / `close`(幂等);值对后端是不透明 JSON。规范正文(含逐方法 JSDoc)在 `packages/storage/storage/src/backend.ts`。
backend 契约(共享契约测试逐条断言,两后端同套件):
后端契约(共享契约测试逐条断言,两后端同套件):
1. `open` 对不存在的介质创建(懒物化允许:可延迟到首写,但 `loadAll` 立即可用返回空表);对已存在介质载入。
2. 介质上版本 ≠ descriptor.version → `StorageError('version-mismatch')`,不迁移不重建。
@@ -93,7 +93,7 @@ CREATE TABLE IF NOT EXISTS "u_<unit>_<table>" (
### `dsh-domain`:领域数据形式
单实现不抽象;消费者只依赖这层,不直接触后端。
单实现不抽象;消费方只依赖这层,不直接触后端。
```ts ignore-check
export const Config = z.object({
@@ -108,7 +108,7 @@ export function apply(ctx: Context, config: Config) {
(facility 卸载顺序:先 dispose 各域(排空写链)再从枢纽摘名——排空期间在途写仍发 `domain/changed`,事件一致性 invariant 经 facility 反查域,要求此时域名仍可解析。)
域声明(spec 对象由拥有该域的包定义导出,是类型与运行时的单一来源;schema 用 zod,`z.infer` 推导类型不重复声明——记录模型下期要投影成 RPC wire schema,wire 边界全是 zod;schemastery 仍只管插件 Config):
域声明(spec 对象由拥有该域的包定义导出,是类型与运行时的唯一真源;schema 用 zod,`z.infer` 推导类型不重复声明——记录模型下期要投影成 RPC wire schema,wire 边界全是 zod;schemastery 仍只管插件 Config):
```ts ignore-check
export interface DomainGlobalSpec<G> { readonly schema: ZodType<G>; readonly initial: G }
@@ -156,7 +156,7 @@ export interface KvTable<K extends string, V> {
规则:
- **一级 mapping**:key → 记录,不做嵌套表;层级需求用复合 key 或值内字段。两后端因此同构(JSON object 一层 ↔ SQLite 一行)。
- **记录是纯数据**:可直接 JSON 序列化的不可变 POJO;`get`/`entries` 返回值不得原地改(TypeScript readonly 投影,不做运行时冻结)。带行为的领域对象属于消费者包。
- **记录是纯数据**:可直接 JSON 序列化的不可变 POJO;`get`/`entries` 返回值不得原地改(TypeScript readonly 投影,不做运行时冻结)。带行为的领域对象属于消费方包。
- **写串行**:域内一条 promise 链,`put`/`delete`/`update`/`global.set` 全排队;`update` 的 fn 在链上执行,并发不交错。不做 active-record(取出可变对象自动落盘——落盘时机不可控,与整域原子覆写冲突)。
- **版本 fail loud**:盘上版本与 spec 不符直接报错,不迁移不重建(数据不可再生,pre-release 拒绝旧格式)。
- **变更事件**:每次写落盘 resolve 后 emit `domain/changed`(`@mode emit`),逐条发、不带旧值(对齐仓库"新快照 + 操作判别"惯例,范本 `goal/changed`);payload `DomainChanged` 是 put/deleted 判别联合——域名 + 表名 + key(global 变更两者为 `''`)+ operation,put 支带新快照 value、deleted 支无 value(`packages/storage/storage-domain/src/events.ts`)。此为下期 RPC 推帧的事件源。错误词汇 `DomainError`,码表:`already-open` / `facet-unsupported` / `invalid-record`(带 `{ table, key }`)/ `missing-key` / `closed`。
@@ -242,7 +242,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 唯一(registry 缓存),记录快照写后原地换新,外部只见 getter;所有写收敛到实体内 `mutate(fn)` → `table.update`,`updatedAt` 在 mutate 内统一刷。领域对象不过 RPC,下期 wire 层把记录投影成 zod wire schema。
- 消费方只见 `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` 能力。
一致性口径(账 = 归属唯一依据;实现与测试基准):
@@ -256,7 +256,7 @@ export class WorkspaceRegistry extends Service {
### 复用与 session 后端迁移展望
**长期方向**:session-persistence 的 JSONL/SQLite 后端里"纯介质操作"下沉到 `dsh-storage` 后端(session 包不删,`SessionPersistence` seam 与 coordinator 语义不动;动的只是它们脚下的文件/db 操作层)。复用的动机:介质层全是文件系统操作、数据库调用与跨平台兼容的脏活(Windows 权限与原子发布变体、fsync 语义、独占建文件……),这些只应写一遍;业务语义(session 怎么 append、何时 append、append 什么)留在上层——而"底下这次 append 是否正常完成"(持久性/原子性/平台正确性)是底层的责任,责任界面就是 facet 原语的契约。为此后端接口按**介质 owner + 数据形状 facet** 设计:session 日志是 append-only 流,与 KV 形状不同——强行统一进 KV 原语会两头变形,所以按 facet 分开(`kv` 本期、`log` 迁移期),介质与生命周期共享。
**长期方向**:session-persistence 的 JSONL/SQLite 后端里"纯介质操作"下沉到 `dsh-storage` 后端(session 包不删,`SessionPersistence` seam 与 coordinator 语义不动;动的只是它们脚下的文件/db 操作层)。复用的动机:介质层全是文件系统操作、数据库调用与跨平台兼容的脏活(Windows 权限与原子发布变体、fsync 语义、独占建文件……),这些只应写一遍;业务语义(session 怎么 append、何时 append、append 什么)留在上层——而"底下这次 append 是否正常完成"(持久性/原子性/平台正确性)是底层的责任,责任界面就是 facet 原语的契约。为此后端接口按**介质 owner + 数据形状 facet** 设计:session 日志是仅追加流,与 KV 形状不同——强行统一进 KV 原语会两头变形,所以按 facet 分开(`kv` 本期、`log` 迁移期),介质与生命周期共享。
现状复用审计(迁移前就能看清的账):
@@ -266,7 +266,7 @@ export class WorkspaceRegistry extends Service {
| JSONL:逐行 append、首行 header 快读、zstd 逐帧压缩 | log 形状 | 留在原地;迁移期进 `log` facet |
| SQLite:openDatabase(mkdir/独占建文件/PRAGMA 序列/user_version 检查) | 纯介质 | 本期 `dsh-storage-sqlite` 抄用——两处 openDatabase 已几乎逐行同构,本组是第三个使用者;先抄后提,提取放迁移期 |
| SQLite:events/sessions 表结构、同事务物化 | log 形状 | 留在原地;迁移期进 `log` facet |
| coordinator(per-id 写链、懒物化、崩溃修复、flush 屏障) | session 语义 | 永不下沉——事件日志的领域逻辑,对应物在 domain 层(写串行链),各归各 |
| coordinator(per-id 写链、懒物化、崩溃修复、flush 屏障) | session 语义 | 永不下沉——事件日志的领域逻辑,在 domain 层对应的是写串行链,各归各 |
| encodeSegment(id 进路径转义) | 介质工具 | domain 侧 key 不进路径用不到;`log` facet(一 session 一文件)迁移时随之下沉 |
**本期不改 session-persistence 的介质代码**(只加 delete 原语);上表是迁移期的施工清单,也是后端接口"必须装得下 log 形状"的设计依据。
@@ -275,8 +275,8 @@ export class WorkspaceRegistry extends Service {
| 套件 | 覆盖 | 后端 |
| --- | --- | --- |
| backend 契约(共享套件,一次编写两端跑) | 七条契约 + 版本拒绝 + close 幂等 | json、sqlite(`:memory:` + 临时目录) |
| registry/mount | 重复注册、未挂载访问、disposer 摘除 | — |
| 后端契约(共享套件,一次编写两端跑) | 七条契约 + 版本拒绝 + close 幂等 | json、sqlite(`:memory:` + 临时目录) |
| 注册表/mount | 重复注册、未挂载访问、disposer 摘除 | — |
| domain 层 | open 六步语义、schema 拒绝、update 串行(并发交错压测)、`domain/changed` 逐条、global 初值懒物化、路由与 `facet-unsupported` | 任一(json) |
| workspace | create/唯一性/realpath、attach 校验(含 sessionPersistence 缺席拒绝)、一致性口径四情形 | mock domain 或 json |
| session delete 契约(future work,随实施并入 runPersistenceContract) | 未知 id、已删 id 复用、未物化 intent、与在途 append 串行、deleted 事件 | jsonl、sqlite |
@@ -292,40 +292,40 @@ export class WorkspaceRegistry extends Service {
| 多进程并发写保护 | 两 host 进程同写一介质 | JSON 后端文件锁;SQLite WAL 天然多进程 | 写全经 domain 单点串行,加锁只动后端 |
| 跨进程变更观测 | GUI 断线重连感知 | revision 模式(抄 session-persistence) | 进程内已有 `domain/changed` |
| 数据迁移 | 首个 tagged release 后模型再变 | 版本号驱动逐域迁移 | 版本号自第一天入介质 |
| 大表性能 | 千级记录域挂 json | `routes` 改指 sqlite,数据手工导一次 | 路由即配置,消费者零改动 |
| 多段 key | 两段 key 消费者出现(每 workspace 每 session 维度数据) | key 泛型换 tuple、SQLite 复合主键、JSON 嵌套层 | 一级表 = 段数 1 特例;不做任意深度嵌套;不拼字符串 key |
| 大表性能 | 千级记录域挂 json | `routes` 改指 sqlite,数据手工导一次 | 路由即配置,消费方零改动 |
| 多段 key | 两段 key 消费方出现(每 workspace 每 session 维度数据) | key 泛型换 tuple、SQLite 复合主键、JSON 嵌套层 | 一级表 = 段数 1 特例;不做任意深度嵌套;不拼字符串 key |
| scope 维度 | "每 workspace 一份"的域出现且复合 key 表达不动 | DomainSpec 加 scope + 文件名 scope 段(encodeSegment) | 名字字符集已收紧,文件名不冲突 |
| 跨表原子事务 | 同域两表一次原子操作需求 | `domain.transact(fn)`;JSON 天然原子,SQLite 包事务 | — |
| 二级索引/条件查询 | 内存过滤不动(万级记录) | SQLite JSON1 查 value 列,加只读 query 面 | JSON 后端不陪跑 |
| session 跨 workspace 移动 | 产品需求出现 | attach 校验放宽为"先 detach 后 attach"编排 | — |
| Session 删除 RPC/GUI | 破坏性的 Session 删除产品流启动 | `session.delete` 端点、wire schema 与明确的确认 UI | Workspace RPC/GUI 已独立交付,不再存在级联耦合 |
## Alternatives considered
## 备选方案
- **复用 session-persistence 的 coordinator/后端**:事件日志语义(append-only、turn 崩溃修复、懒物化)与 KV 覆写语义不匹配;只借其分层思想(协调层持写序、后端只实现最小原语)。
- **workspace 专用存储包,后续再抽 seam**:第二个消费者(session sidecar)已可预见,届时泛化要再动一次接口。
- **复用 session-persistence 的 coordinator/后端**:事件日志语义(仅追加、turn 崩溃修复、懒物化)与 KV 覆写语义不匹配;只借其分层思想(协调层持写序、后端只实现最小原语)。
- **workspace 专用存储包,后续再抽 seam**:第二个消费方(session sidecar)已可预见,届时泛化要再动一次接口。
- **domain 与 storage 合为一层**:后端会被迫接触 schema 校验、变更事件、写串行等领域关切;拆开后 storage 后端只做不透明原语(可替换面最小),domain 单实现收敛全部领域逻辑(zod/事件/串行化只写一遍,不随后端翻倍)。
- **整库单后端二选一(学 session-persistence 单坑位模式)**:曾是初版方案;改为多后端并存 + 配置路由,因为存储枢纽要承载多种数据形式,不同形式/域对后端的偏好(肉眼可读 vs 高频点更新)注定分化,单坑位会逼出"整体换挂 + 手工导数据"的粗粒度动作。代价是按名查找多一步,fail-loud 兜底。
- **JSON 后端 jsonl 追加 + 墓碑 + 压实**:temp+fsync+rename 的崩溃安全与 append 等价;覆写让文件永远是净值、肉眼可读,免掉折叠/压实/断行容错。域规模下整写与追加一行同量级。
- **整库单后端二选一(学 session-persistence 单 slot 模式)**:曾是初版方案;改为多后端并存 + 配置路由,因为存储枢纽要承载多种数据形式,不同形式/域对后端的偏好(肉眼可读 vs 高频点更新)注定分化,单 slot 会逼出"整体换挂 + 手工导数据"的粗粒度动作。代价是按名查找多一步,fail-loud 兜底。
- **JSON 后端 jsonl 追加 + 墓碑 + 压实(compaction)**:temp+fsync+rename 的崩溃安全与 append 等价;覆写让文件永远是净值、肉眼可读,免掉折叠/压实/断行容错。域规模下整写与追加一行同量级。
- **JSON 一表一文件**:覆写下文件粒度不影响写成本,按域合并文件更少,global 单例有落点。
- **SQLite 整域存单行 blob**:任何一条记录变更都重写整域,失去按 key 精确更新——SQLite 相对 JSON 的唯一优势归零。
- **SQLite 按 schema 生成 typed columns**:DDL 生成器过度建设;document-per-row 足够,查询需求出现再议。
- **每域独立 sqlite db 文件**:与仓库一库多表惯例相反。
- **path 作为 workspace key**:规范化/符号链接解析会改写 path;引用锚点必须稳定。
- **归属用 cwd 派生(或与账合并)**:双真相源;cwd 表达不了排序;归属本就是 workspace 侧事实。
- **变更事件带旧值**:仓库变更事件惯例是"新快照 + 操作判别"(唯一例外 fs 的 before/after 是方法返回值而非事件,因旧值事后不可重建且有 diff 消费者);需要 diff 的消费者自己持有上次快照。
- **变更事件带旧值**:仓库变更事件惯例是"新快照 + 操作判别"(唯一例外 fs 的 before/after 是方法返回值而非事件,因旧值事后不可重建且有 diff 消费方);需要 diff 的消费方自己持有上次快照。
- **删除自动 cancel 运行中 session**:持久层/编排层反向牵动运行时,层次变脏;cancel 机制已存在,调用方组合即可。
## Acceptance criteria
## 验收标准
- 测试矩阵本期四套件全绿:backend 契约共享套件在 json/sqlite 双端、registry/mount disposer 语义、domain 层(含 open 六步与路由 fail-loud)、workspace 全语义(create/attach 校验/一致性口径)。
- 测试矩阵本期四套件全绿:后端契约共享套件在 json/sqlite 双端、注册表/mount disposer 语义、domain 层(含 open 六步与路由 fail-loud)、workspace 全语义(create/attach 校验/一致性口径)。
- `ctx.workspace` 可在测试组装下完成 create → attach → list → 仅删除元数据的 delete 生命周期。
- session-persistence 包零 diff(本期不动 session 侧的验收线)。
- 本期无新快照(无模型可见面与组装面);下期 RPC 接线时补。
## Risks
## 风险
- **仓库持久化面第一个推式变更事件**(session-persistence 靠 revision 轮询):形态虽有 `goal/changed` 范本,但"存储层发事件"是新先例,下期 RPC 消费时才能验证形态是否合适。
- **JSON 后端整域覆写的规模前提**:若第二个消费者(session sidecar)在路由到 SQLite 前就以千级记录落在 JSON 后端,整写成本会先于预期显现;缓解即 `routes` 改指 sqlite。
- **删除语义的编排层检查依赖 `ctx.sessions` 弱依赖**:headless 组装拿不到运行时注册表时按"无热 session"处理,存在窗口(外部进程正在跑该 session);多进程本就在不做清单内,接受。
- **JSON 后端整域覆写的规模前提**:若第二个消费方(session sidecar)在路由到 SQLite 前就以千级记录落在 JSON 后端,整写成本会先于预期显现;缓解即 `routes` 改指 sqlite。
- **删除编排对 `ctx.sessions` 的弱依赖**:headless 组装拿不到运行时注册表时按"无热 session"处理,存在窗口(外部进程正在跑该 session);多进程本就在不做清单内,接受。
- **facet 泛化以未来的 `log` facet 为设计依据但本期不实现它**:存在"预留形状不合身"的风险;缓解是本期后端介质代码按复用审计表的下沉形状组织,`log` facet 真正落地时只动 facet 层。

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: c86d6ac053f7bb87ce758613a5f3a0a34951e428
2026-07-25-client-settings-locale-theme.zh.md: 05edbb3c550828832a390e3cf4fad3262b5be196
2026-07-25-client-settings-locale-theme.md: fdcdcff92fa324803d5f2343f3fc3d9dcca069c7
2026-07-25-client-settings-locale-theme.zh.md: b1536a1515fa7022320934dfdabf94c815e8ba04

View File

@@ -55,11 +55,11 @@ root
└─ models (order 10) ui-models 注册
```
Section and item contributions both use declaration-aware deferral (ui-slots' `deferRegistration()`: ledger-judged presence, one-call disposal; localized labels ride the label thunk from the [full-rollout note](../../implemented/architecture/2026-07-30-client-locale-full-rollout.md), not `refresh()`) and do not depend on the client manifest's apply order. 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 seam.
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 seam.
### Future work: promote slot declarations to first-class injectable waits
### Slot declarations are first-class injectable waits
`deferRegistration()` is behaviorally isomorphic to `ctx.inject` — one waits on a ledger declaration, the other on service presence, with matching disappear/reappear lifecycle semantics; the difference is that the fiber form's disposer lifetime naturally equals the declaration's lifetime, so the stale-disposer presence-judging machinery disappears entirely. Direction (a separate PR): SlotsService bridges each slot into a `slot:<name>` service (value = the slot spec) at declaration commit / cascade removal, registrants migrate from `deferRegistration()` to a nested `ctx.inject(['slot:<name>'], cb)`, then `deferRegistration()` is deleted and packages/client/AGENTS.md checklist item 4 is rewritten. Boundaries to pin down: the nested fiber's harmless wait must not be named by the boot fail-loud scan (needs a test); the `slot:` namespace and the silent-wait-on-typo stance; provide keys are flat names (`slot:a.b` is one key, not a property path on `ctx.slots`). This phase keeps the `deferRegistration()` function form.
`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).
### 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 keep declaration-aware registration and judge presence by the ledger, not by a local disposer. 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 ThemeService must remove its matchMedia listener on dispose, so nothing lingers after HMR.

View File

@@ -4,40 +4,40 @@ Status: proposed
[English](2026-07-25-client-settings-locale-theme.md) | 中文
## Problem
## 问题
浏览器端已有的 Settings 直接写在 Sidebar 内,语言和主题也由组件本地状态直接改 DOM。这使 Settings 无法由独立插件扩展,偏好状态没有稳定的跨插件服务契约,主题 registry 同时承担状态与呈现职责。
浏览器端已有的 Settings 直接写在 Sidebar 内,语言和主题也由组件本地状态直接改 DOM。这使 Settings 无法由独立插件扩展,偏好状态没有稳定的跨插件服务契约,主题注册表同时承担状态与呈现职责。
## Proposal
## 提案
**协作导向(后续所有模块接入 Settings 的方式):功能属主自注册。** Settings 壳是纯组合面:只声明坑位、渲染 chrome 结构,零文案、不依赖 locale、不 import 也不枚举任何功能;一个功能要出现在 Settings 里,由它自己的插件向对应坑位注册——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-models 注册 Models 一级面板。不为「某功能的设置页」单开 `ui-settings-*` 包:设置面属于功能包本身(做 Theme 功能,Theme 的设置选择就随 ui-theme 一起交付)。不属于任何单一功能的内容(trigger/标题/close 的 chrome 文案、General 目录与骨架行、`settings` 字典)由 `ui-settings-general` 拥有——它是「无主文案」的属主,不是功能卫星包。
Sidebar 声明 `sidebar.settings` 单坑位,`ui-settings` 占用它并声明四个坑:`settings.trigger` / `settings.header` / `settings.close`(chrome 内容座,single)与 `settings.section`(一级页面,list)。无障碍名全部解析自坑内容:trigger 的可达名即其文本内容,dialog 经 aria-labelledby 指向 header 内容节点,close 是视觉隐藏文本座。每个 section 由功能插件贡献;壳只从 slot ledger 读取 entry metadata 生成导航,通过 `only` 渲染当前 section。General 由 `ui-settings-general` 注册(order 0)并声明 `settings.general.item` list 坑位,功能插件的偏好行按 order 排入。
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 排入。
Settings 入口是 sidebar Foot 的 Settings 行,点击直接打开 1080×700 居中浮层(黑 24% 遮罩);close 按钮、点击遮罩、ESC 均关闭。无任何中间菜单形态。
`@deepseek-ai/dsh-client-locale` 提供 `ctx.locale`,`ui-theme` 提供 `ctx.theme`。两个 service 都以 getter 读取、setter 写入并用 typed Cordis change event 发布 immutable snapshot;service 自己持久化偏好(只存 id,坏值回退默认)。
`@deepseek-ai/dsh-client-locale` 提供 `ctx.locale`,`ui-theme` 提供 `ctx.theme`。两个服务都以 getter 读取、setter 写入并用 typed Cordis 变更事件发布 immutable snapshot;服务自己持久化偏好(只存 id,坏值回退默认)。
功能行的 apply 层各自订阅自家 change event(locale 订 `locale/change`,ui-theme 订 `theme/change`),把 snapshot 投影到该行注册时声明的 slot store。React 组件只读 `useStore`、写注入的 setter callback,不读取 ctx 或 service。
功能行的 apply 层各自订阅自家变更事件(locale 订 `locale/change`,ui-theme 订 `theme/change`),把 snapshot 投影到该行注册时声明的 slot store。React 组件只读 `useStore`、写注入的 setter callback,不读取 ctx 或服务。
Theme 偏好三态:`light`、`dark`、`system`,默认 `system`(无持久化偏好或坏值时)。system 的解析属主题领域:ThemeService 持有 `prefers-color-scheme` matchMedia 监听(环境感知,非 DOM 呈现),偏好为 system 且系统配色变化时重发 snapshot;snapshot 同时携带 `preference` 与解析后的 `active` 定义。
Theme service 不操作 DOM。`ui-layout` 初始读取 Theme getter,随后订阅 `theme/change`,由 Layout 持有的 presenter 按 `active` 更新 `body[data-ds-dark-theme]` 和主题 token;presenter 不感知 system,只消费已解析结果。
Theme 服务不操作 DOM。`ui-layout` 初始读取 Theme getter,随后订阅 `theme/change`,由 Layout 持有的 presenter 按 `active` 更新 `body[data-ds-dark-theme]` 和主题 token;presenter 不感知 system,只消费已解析结果。
### 首期注册面
| 注册面 | 属主插件 | 首期内容 |
|---|---|---|
| chrome 内容(trigger/header/close)| `ui-settings-general` | 设置入口行图标+文案、面板标题、close 隐藏文本 |
| General section(order 0)| `ui-settings-general` | Permission、Tool Call 视觉骨架(无写操作)+ `settings.general.item` 坑位声明 |
| 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` | 仅导航项,内容区为空;后续模型管理功能落在该包 |
| Plugin | 无 | 首期不做,导航不出现该项(后续插件功能包注册 section 即自动出现) |
| 插件 | 无 | 首期不做,导航不出现该项(后续插件功能包注册 section 即自动出现) |
首期只翻译 Settings 浮层内文案;字典就近——chrome + General 骨架归 `ui-settings-general` 的 `settings` namespace,功能行文案归各功能包(`settings.locale`、`settings.theme`、`settings.models`)。
首期只对 Settings 浮层内文案进行本地化;字典就近——chrome + General 骨架归 `ui-settings-general` 的 `settings` namespace,功能行文案归各功能包(`settings.locale`、`settings.theme`、`settings.models`)。
### Slot topology
### slot 拓扑
```text
root
@@ -55,13 +55,13 @@ root
└─ models (order 10) ui-models 注册
```
section/item contribution 均使用 declaration-aware deferral(ui-slots 的 `deferRegistration()`:ledger 判在位、一键 dispose;本地化 label 走 [全量接入 Note](../../implemented/architecture/2026-07-30-client-locale-full-rollout.md) 的 label thunk,不再 `refresh()`),不依赖 client manifest 的 apply 顺序。SlotMap 类型分家:trigger/header/close/section 正家在 ui-settings contract(消费者 general/models 均依赖壳,无环);`settings.general.item` 正家在 locale 包——它是全部 item 注册方的最低公共依赖(设置行必带文案),而声明方 general 的 contract 对 locale/ui-theme 不可达(会成环);ui-theme 经 re-export seam 消费。
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 contract(消费者 general/models 均依赖壳,无环);`settings.general.item` 正家在 locale 包——它是全部 item 注册方的最低公共依赖(设置行必带文案),而声明方 general 的 contract 对 locale/ui-theme 不可达(会成环);ui-theme 经 re-export seam 消费。
### Future work:坑位声明升格为可 inject 的一等等待物
### slot 声明是一等可注入等待对象
`deferRegistration()` 与 `ctx.inject` 行为同构——一个等 ledger 声明、一个等服务在场,消失/重现的生命周期语义一致;差别只在 fiber 版的 disposer 生命周期天然等于声明生命周期,stale-disposer 判在位机器可整体消失。方向(另开 PR):SlotsService 在声明落账/级联拆除处把每个坑位桥接成 `slot:<name>` 服务(value 为坑位 spec),注册方从 `deferRegistration()` 迁为嵌套 `ctx.inject(['slot:<name>'], cb)`,随后删除 `deferRegistration()` 并改写 packages/client/AGENTS.md checklist 第 4 条。待钉死的边界:嵌套 fiber 的无害等待不被 boot fail-loud 扫描点名(需测试);`slot:` 名字空间与 typo 静默等待的口径;provide 键是平面名(`slot:a.b` 是一个键,不是 `ctx.slots` 的属性路径)。本期维持 `deferRegistration()` 函数形式。
`SlotsService.inject()` 直接等待有类型约束的 ledger key;它不会将声明桥接为合成的 `slot:<name>` Cordis 服务。回调会跟随声明折叠与重新声明,而其控制器仍归贡献方插件 fiber 所有;直接向未声明 slot 注册仍会大声失败。这删除了陈旧 disposer 判在位机器和容易因拼写错误出错的平行服务命名空间。完整的生命周期与失败契约见 [slot 声明注入决策](../../implemented/architecture/2026-08-05-slot-declaration-injection.md)。
### Service contracts
### 服务契约
```ts
export type ThemePreference = 'light' | 'dark' | 'system'
@@ -100,31 +100,31 @@ export interface Events {
Locale 内置中文和 English;`setLocale`/`setTheme` 是唯一写入口,未知 id 失败。
## Alternatives considered
## 曾考虑的替代方案
**由 app shell 统一订阅偏好并重渲染 root slot tree。** 语言和主题变化只需要更新实际消费者;全树刷新放大影响面,也把业务偏好接入 shell。
**由 app shell 统一订阅偏好并重渲染 root slot tree。** 语言和主题变化只需要更新实际消费方;全树刷新放大影响面,也把业务偏好接入 shell。
**Theme service 直接修改 DOM。** registry service 因此依赖呈现环境,生命周期与全局样式所有权不清;Layout 已经拥有页面根呈现边界。
**Theme 服务直接修改 DOM。**注册表服务因此依赖呈现环境,生命周期与全局样式所有权不清;Layout 已经拥有页面根呈现边界。
**system 由 Layout presenter 解析。** presenter 需自带 matchMedia 订阅并在 themes 列表里挑选具体定义,呈现层被迫理解偏好语义;解析放服务侧则所有消费者拿到一致的已解析 snapshot。
**system 由 Layout presenter 解析。** presenter 需自带 matchMedia 订阅并在 themes 列表里挑选具体定义,呈现层被迫理解偏好语义;解析放服务侧则所有消费方拿到一致的已解析 snapshot。
**Settings import 并枚举各 section。** 新增页面必须修改壳插件,破坏「每个功能由自己的插件占坑」的组合模型。
**Settings import 并枚举各 section。** 新增页面必须修改壳插件,破坏「每个功能由自己的插件占用 slot」的组合模型。
**按功能为每个 section 单开 `ui-settings-*` 卫星包。** 设置面与功能本体分家:改 Theme 行为要动两个包,包数随设置项线性膨胀,且卫星包反向依赖 locale/theme 服务,形成纯粹为拆包而生的中间层。功能属主自注册下不存在这层:preference 行随功能包交付;`ui-settings-general` 只收无主文案(chrome 与 General 骨架),不承载任何功能的设置面。
**把 Locale/Theme snapshot 直接注入 React。** inject 结果按 entry identity 缓存,易变值会陈旧;为每个 service 自造 React hook 也绕开 slot store 的统一绑定。
**把 Locale/Theme snapshot 直接注入 React。** inject 结果按 entry identity 缓存,易变值会陈旧;为每个服务自造 React 钩子也绕开 slot store 的统一绑定。
## Acceptance criteria
## 验收标准
- Settings 壳只依赖 slot ledger,不依赖任一功能实现;General 的 item 列表同样只依赖 ledger。
- 新增一个设置项 = 功能包自己注册(section 或 general item),零壳改动。
- Locale 与 Theme 的写入只走 setter,持续同步只走 change event。
- 功能行 store 初始化走 getter,后续由自家 change event 更新并局部重渲染。
- Layout 独立应用 Theme snapshot,Theme service 不访问 DOM;presenter 不出现 system 分支。
- Locale 与 Theme 的写入只走 setter,持续同步只走变更事件。
- 功能行 store 初始化走 getter,后续由自家变更事件更新并局部重渲染。
- Layout 独立应用 Theme snapshot,Theme 服务不访问 DOM;presenter 不出现 system 分支。
- 中文/English 与 Light/Dark/System 能切换并刷新后恢复;偏好为 system 时系统配色变化即时生效。
- Models 只有导航项与空内容区;Permission、Tool Call 骨架无写操作。
- 浮层经 close 按钮、遮罩点击、ESC 均可关闭。
## Risks
## 风险
slot 声明与 contribution 的 apply 顺序不固定,所有 section/item 注册方必须保留 declaration-aware registration,并以 ledger(而非本地 disposer)判定在位。service event 可能早于行首次渲染,功能行 store 的 init 与 inject attach 都必须从 getter 对齐当前 snapshot。`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 对齐当前 snapshot。`settings.general.item` 的重复合并副本(locale、ui-theme)与 ui-settings 正家必须逐字一致,漂移即三处一起改。Layout 卸载时必须清理自己设置的全局属性,ThemeService dispose 时必须移除 matchMedia 监听,避免 HMR 后残留。

View File

@@ -3,4 +3,4 @@
# 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: 6a073c956c27bbfc65cff2d4f44ca12023df0cd5
2026-07-27-session-projection-and-command-log.zh.md: 500f07968db049e4a174ff3b7a075bfe095283db
2026-07-27-session-projection-and-command-log.zh.md: a22ebe57811339a0e583ae00909e60482ddb57b1

View File

@@ -1,10 +1,10 @@
# Agent Note: Session projections and command lifecycle logging
# Agent Note: 会话投影与命令生命周期日志记录
Status: proposed
[English](2026-07-27-session-projection-and-command-log.md) | 中文
## Problem
## 问题
三个在途的 web 功能——todo(#497)、goal(#527)、plan mode(#587)——都要从会话日志推导按会话的状态并呈现到浏览器客户端,而三者各自发明了一套同样的机制:
@@ -14,7 +14,7 @@ Status: proposed
底层缺口是架构性的:客户端没有一个 seam 让插件在会话 scope 内观察会话事件并维护自己的派生状态;host 侧也没有统一的方式把日志派生状态的当前值交给客户端——而该状态的历史可能已被分页挤出客户端窗口之外。
## Proposal
## 提案
先立四件基础设施,之后各领域都退化为纯贡献方。
@@ -24,7 +24,7 @@ Status: proposed
### host 侧投影注册表(`dsh-session-projection`,新包)
一个轻量的接口包(package):merge-extensible 类型表、注册表服务、边界上的 zod 校验。能力 seam 三方拆分:领域 host 插件负责贡献,载体负责消费,两侧互不相识。
一个轻量的接口包:merge-extensible 类型表、注册表服务、边界上的 zod 校验。能力 seam 三方拆分:领域 host 插件负责贡献,载体负责消费,两侧互不相识。
领域注册的是一个**状态驱动计算单元(state-driven computation unit)**——三个纯函数外加若干声明——绝不是一个不透明的 getter。驱动它是框架的职责(订阅、水位线(watermark)、缓存,以及后续的检查点机制),领域只负责数学本身。投影服务于所有业务领域(会话标题、plan、goal、权限、todos);命令只是其中一条触发路径,在本契约中没有任何特殊地位。
@@ -50,7 +50,7 @@ declare module 'cordis' {
```
- 值就是协议层的 JSON 载荷;同一张类型表经 `import type` 端到端贯通(host 侧单元、协议块、React 钩子)——没有第二张 DTO 表,也没有独立的客户端「views」表。值如何*渲染*是 slot 体系的事,永远不归投影层管。
- **host 是投影唯一的计算地点。** 框架正向驱动(eager drive)每个已注册的单元:每个已提交的会话事件都经过 `apply`;对某事件不感兴趣的单元返回同一个状态引用,而引用未变(`Object.is`)就不产生任何下游工作。客户端从不折叠领域事件——它们收到的是成品值(基线块 + 下文的推送帧)。这消除了双重实现陷阱(plan 的双事件折叠只在 host 写一遍),也消除了一切客户端侧领域代码。
- **host 是投影唯一的计算地点。** 框架主动驱动(eager drive)每个已注册的单元:每个已提交的会话事件都经过 `apply`;对某事件不感兴趣的单元返回同一个状态引用,而引用未变(`Object.is`)就不产生任何下游工作。客户端从不折叠领域事件——它们收到的是成品值(基线块 + 下文的推送帧)。这消除了双重实现陷阱(plan 的双事件折叠只在 host 写一遍),也消除了一切客户端侧领域代码。
- **状态永远靠计算得出,绝不入日志。** 日志只存事件;单元的状态住在框架的按会话水位线缓存里(每单元一份 `{state, observedSeq}`),并在后续阶段进入 domain-KV 存储 seam 上的**持久投影缓存(persisted projection cache)**:形如 `(sessionId, key, ver, seq, val)` 的行(`ver` = 单元的 `stateVersion`,`seq` = 水位线,`val` = 状态 JSON)。一行永远不会是错的,至多是陈旧的——其 `seq` 精确说明陈旧到哪。冷读与活读共用同一套读取配方:取缓存状态(或 `init()`),只对超出其水位线的事件做正向 `apply`,再对结果做 `view`。冷列表(跨全部 workspace 列出每个会话的标题)变成一次索引读,至多外加一小段尾部回放;session-persistence seam 在同一后续阶段为这段尾部补一个按 seq 起读的原语。写入策略:节流(次数/间隔,可配置)外加两个强制点——`turn/end` 与 detach(由活转冷的时刻)。两次写入之间崩溃的代价是尾部回放更长一些,绝不会是值出错。
- 领域的输入事件集由领域自己选择:todos 只折叠 `todo/write`;plan 折叠 `plan/mode` 外加它自己的 `/plan` `command/run` 记录(见 plan 一节);goal 折叠 `goal/change` 元数据;会话标题折叠其标题事件(顺带下线专设的 `session/title` 帧与客户端的标题快照表——这是该 seam 收编的第四个手工投影)。
- 注册是 effect(disposer 随 fiber 走):插件卸载后其 key 从后续响应中消失,客户端将其读作能力缺失——HMR(热模块替换)语义随之自动成立。key 重复直接 throw。领域插件在 `ctx.inject(['sessionProjections'], …)` 下注册,因此不带注册表的 headless 组装完全不受影响。
@@ -125,17 +125,17 @@ host 侧命令执行器(`packages/ui/commands`)在调用处理器前追加 `
客户端 flow 构建器新增一个通用命令节点(run/done 按 `commandId` 配对;跨窗口截断时与工具配对同样软降级)。渲染走一个新的 keyed slot `'conversation.chat.commandview'`,key = 命令名,**兜底 = 通用命令卡片**(零注册即可用——从前的通知文本现在持久地渲染在 flow 里)。领域要升级展示,只需注册一个行组件,取材于 `command/run` 的结构化字段与自己的投影值(`useProjection`)——与 toolview 解散之后的工具行同一形状。
## Delivery plan
## 交付计划
基础设施先行;三个在途 PR(Pull Request)原样不动,待基座落地后重新对接(它们的迁移映射即指南):
1. **host 基座**:`dsh-session-projection`(单元契约、正向驱动、水位线缓存)+ api-proxy 的 projections 块 + `session/projection` 推送帧。零领域注册也可合入(此时块与帧直接缺席)。
1. **host 基座**:`dsh-session-projection`(单元契约、主动驱动、水位线缓存)+ api-proxy 的 projections 块 + `session/projection` 推送帧。零领域注册也可合入(此时块与帧直接缺席)。
2. **客户端基座**:通用值仓 + `useProjection` 席位;下线按领域的 cell 机制,并在标题单元注册后一并下线 `session/title` 帧与标题快照表。帧的形状依赖 1(在此之前 fixture(测试前置数据)喂合成帧)。
3. **命令通道**:两个事件、执行器落日志、通用节点 + keyed slot、通知通道下线、`{matched, commandId?}` 准入。与 1 并行。
4. **领域重新对接**(在 1+2 之后):先 todo(单元进 `tool-todo`,删掉搭载字段),再 plan(双事件单元、RPC 下线、开关改发 `/plan`),最后 goal(`goal/change` 单元,删掉 `goals.get`,把六个 `Session` 方法移入领域插件的 inject)。
5. **持久投影缓存**(后续阶段,待 domain-KV 存储 seam 就绪后):`(sessionId, key, ver, seq, val)` 行、带 turn/end 与 detach 强制点的节流写入,以及持久化侧供冷尾部回放用的按 seq 起读原语。
## Alternatives considered
## 备选方案
**专设一个 `session.projections` RPC**——不予采纳:基线刷新时刻与尾页拉取精确重合,单独的一元 RPC 只会换来第二次往返、第二个待调和的 seq,以及一个客户端「何时重取」决策——而搭载设计把这个决策整个删掉了。
@@ -147,7 +147,7 @@ host 侧命令执行器(`packages/ui/commands`)在调用处理器前追加 `
**客户端侧折叠(带 `fromEvent` 的按领域投影 cell)**——曾是第二稿,后被否决:一旦 plan 的单元要折叠两种事件,客户端 cell 就必须在浏览器里复刻 host 的状态转移逻辑——同一个折叠写两遍、各自演化。推送成品值(标题帧先例的泛化)保住唯一计算地点,并把客户端简化为一个由 seq 把守的通用值仓;领域零客户端代码。
**对日志尾部的有界反向扫描(absorber 声明)**——暂不采纳:今天没有任何东西需要它,它只服务于「每个事件都携带完整折叠状态」的领域,而持久投影缓存以统一方式覆盖同一冷读需求(缓存行 + 正向尾部回放——与客户端的基线 + 追赶、与分页加载是同一套配方)。只有当出现检查点机制服务不了的真实冷读路径时才重议。
**对日志尾部的有界反向扫描(absorber 声明)**——暂不采纳:今天没有任何东西支持它,它只服务于「每个事件都携带完整折叠状态」的领域,而持久投影缓存以统一方式覆盖同一冷读需求(缓存行 + 正向尾部回放——与客户端的基线 + 追赶、与分页加载是同一套配方)。只有当出现检查点机制服务不了的真实冷读路径时才重议。
**`invalidate` 式 cell(标脏,遇领域事件就重取)**——不予采纳:它的存在只为伺候增量事件。全量值规则让每个领域都是 last-wins;goal 的重取循环、合并逻辑、陈旧读栅栏随之全部消失。
@@ -163,7 +163,7 @@ host 侧命令执行器(`packages/ui/commands`)在调用处理器前追加 `
**让变更 RPC 的响应喂 cell 状态**——不予采纳:已提交的 mux 事件即刻到达,携带同一个全量值外加 seq;「响应喂状态」正是当初逼出 #527 写 revision 栅栏的根源。
## Acceptance criteria
## 验收标准
- 领域插件把按会话的日志派生状态送达 React,只需写:全量值事件声明、一次 host 侧单元 `register`、自己那份 `SessionProjectionMap` merge、以及 inject 回调——零客户端侧代码,不改客户端 `Session` 类、`ConversationSnapshot`、api-proxy 或任何协议 schema 文件。
- 历史尾页携带 `projections`,其 `asOfSeq` 等于窗口尾部 seq;loadOlder 页永不携带;未装注册表的部署照常返回不带该块的历史,客户端把所有 key 视为缺席。
@@ -172,12 +172,12 @@ host 侧命令执行器(`packages/ui/commands`)在调用处理器前追加 `
- `useProjection` 经标准 props 套件抵达组件;没有任何钩子穿过 inject 契约(包括 `useSelection`)。
- 会话标题搭乘这对通用机制(基线块 + 投影帧);专设的 `session/title` 帧与客户端标题快照表彻底移除。
## Risks
## 风险
- **全量值规则是承重结构**:未来某个领域若只记裸增量,就无法凭其最新事件服务消费方,还会让自己的单元复杂化。缓解:该规则写明在本 Note 与投影包的 README 里;单元契约让完整状态在每次转移处都是显式的。
- **单元的同步纪律**:`init`/`apply`/`view` 一旦 await 就会撕裂一致性切面。注册表在文档中申明这条纪律,invariant 配套在可行范围内断言同步性;其余由评审把关。
- **注册表的实时增删不做推送**:会话中途加载或卸载领域插件会改变键集,但不会触发任何会话事件、也不会推任何帧;开着的客户端持有陈旧的 key 直到下次尾页拉取(重连、缺口修补、打开)。接受为仅开发期(HMR)的陈旧时窗——日后可以在变更流上加一个注册表变更推送,契约不受影响。
- **忙碌会话上的正向驱动开销**:每个已提交事件都要过每个已注册单元的 `apply`。按构造,单元的逐事件开销很低(全量值规则),不匹配的事件返回同一引用,且已注册领域的数量很小;若真出现热点路径,可以加按单元的事件类型预过滤,契约不变。
- **忙碌会话上的主动驱动开销**:每个已提交事件都要过每个已注册单元的 `apply`。按构造,单元的逐事件开销很低(全量值规则),不匹配的事件返回同一引用,且已注册领域的数量很小;若真出现热点路径,可以加按单元的事件类型预过滤,契约不变。
- **投影载荷膨胀**:每个尾页携带每个已注册的 key。载荷是 UI 量级状态的全量值(一张 todo 清单、一份 goal 快照);将来若某领域的值很大,可以在请求上加逐 key 的 opt-out 或惰性 key,模型本身不用改。
- **命令日志体量**:每条斜杠命令两个仅日志事件;上限由人敲命令的频率决定,相对分片体量可忽略不计。
- **重新对接的返工**:三个未合入的 PR 要变基到挪动后的地基上。这是基础设施先行的既定代价;设计台账中的迁移映射一节逐一列出每个 PR 的保留/删除清单。

View File

@@ -3,4 +3,4 @@
# 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: 2edb60d204da736bf14ace6278fa4ee7b9e4b8f7
2026-07-28-storage-root-and-derived-medium-recovery.zh.md: daef4f7661df069df3438f909f2744880a93a873
2026-07-28-storage-root-and-derived-medium-recovery.zh.md: 802ca16f399446777203581d246444e53d632a34

View File

@@ -1,10 +1,10 @@
# Agent Note:存储根目录落点与派生介质恢复
# Agent Note: 存储根目录落点与派生介质恢复
Status: proposed
[English](2026-07-28-storage-root-and-derived-medium-recovery.md) | 中文
## Problem
## 问题
持久投影缓存([RFC](2026-07-27-session-projection-and-command-log.md),已作为 `dsh-session-projection-cache` 落地)暴露了它所依托的存储基座的两个缺口。二者都是 domain-KV 栈([设计](2026-07-24-domain-kv-storage-and-workspace.md))的属性而非缓存自身的问题,且都首先咬到缓存——因为它是这条栈上第一个*派生*介质。
@@ -12,7 +12,7 @@ Status: proposed
**现在是怎么恢复的。** 在健康介质内部,缓存按设计完全自愈:`stateVersion` 不匹配的行被丢弃重折,日志缩短到行水位以下由带锚的 restore floor 检出并以一次全量重读回答,每次后台写都是 fail-soft。但在*介质*层面完全没有恢复:被截断、被手改或版本被 bump 的 `session_projcache.json` 会让 `openJsonUnit` 以 `malformed-medium`/`version-mismatch` 失败(packages/storage/storage-json/src/format.ts),schema 漂移的记录让域 open 以 `invalid-record` 失败(packages/storage/storage-domain/src/index.ts),拒绝一路穿过 `SessionProjectionCache[Service.init]`,在 CLI 的 fail-loud 启动下整个组装拒绝启动。一个内容完全可从会话日志重建的文件能把启动搞死。这与缓存包自己声明的立场("a stale or unreadable cache costs a longer tail replay, never a wrong value")和缓存域 spec 的 JSDoc("version bumps discard the whole medium")相矛盾——后者今天描述的是愿望而非实现。同一条 fail-loud 路径对 `workspace.json` 却是*正确*的——工作区记录是权威数据,不可派生——所以缺的概念是按域声明权威性,而不是全局改行为。
## Proposal
## 提案
两个独立改动,一个缺口一个。
@@ -24,12 +24,12 @@ Status: proposed
### 声明派生介质:损坏时重置而非拒绝
- `DomainSpec` 增加 `recovery?: 'reject' | 'reset'`(默认 `'reject'`)。spec 对象已经是一个域的身份与布局的单一来源;其介质是权威还是派生属于同类事实,落在同一处。`session_projcache` 声明 `'reset'`;`workspace` 保持默认。
- `DomainSpec` 增加 `recovery?: 'reject' | 'reset'`(默认 `'reject'`)。spec 对象已经是一个域的身份与布局的真源;其介质是权威还是派生属于同类事实,落在同一处。`session_projcache` 声明 `'reset'`;`workspace` 保持默认。
- `KvFacet` 增加一个原语:`destroy(descriptor): Promise<void>`——整体移除该 unit 的介质(json:删文件;sqlite:drop 该 unit 的表)。与 `open` 一样,它是后端存储原语,不是策略。
- `DomainFacility.open` 在 spec 声明 `'reset'` 且 open 恰以损坏类错误失败时——`StorageError('version-mismatch' | 'malformed-medium')` 或 `DomainError('invalid-record')`——记一条命名该域和被丢弃介质的警告,调用 `destroy`,再空开一次。其余一切失败(`backend-not-found`、`facet-unsupported`、`already-open`、I/O 错误)无论声明与否都保持大声:配置错误和环境故障不是介质损坏。重试单发——第二次失败原样传播,持续失败的介质不会成环。
- 有了这个,缓存域 spec 的 version 字段才获得其本意:bump `version`(或让 zod 拒绝漂移行)真正丢弃整个介质,缓存经正常写点和冷读重建——恢复阶梯的最外一档,与已落地的行级各档对齐。
## Alternatives considered
## 备选方案
**保持按启动目录的 `.storages`(改动前现状)**——拒绝:会话是全局的,所以每个从会话派生的介质都与自己的真源劈叉;缓存的动机场景(一次列出全部会话)结构性丢行,工作区注册表索引着从另一个启动目录看不见的会话。
@@ -45,15 +45,15 @@ Status: proposed
**所有域一律自动重置(不加 spec 字段)**——断然拒绝:`workspace.json` 是权威用户数据;版本 bump 时静默重置会毁掉工作区。权威性是域的属性,必须由其所有者声明。
## Acceptance criteria
## 验收标准
- 从任意目录启动 `dsh` 都读写同一份 `$DSH_HOME/storages/*.json`(默认 `~/.dsh/storages`)——已由 overlay 表达式满足,按行覆盖走个人 config.yaml patch 层;后端对相对根在构造时 resolve 一次(待做)。
- `session_projcache.json` 被截断、版本 bump 或 schema 漂移时,组装干净启动:一条警告命名被丢弃的介质,文件消失,缓存经正常运转重建,冷列表列随会话重新 checkpoint 逐步回归。
- 同样的损坏发生在 `workspace.json` 上仍大声拒绝启动。
- facility 测试覆盖:每个损坏类恰好重置一次 `'reset'` 域;非损坏失败在 `'reset'` 域上保持大声;`'reject'` 域传播一切失败;`destroy` 在两个出厂后端上都移除介质。
## Risks
## 风险
- **错误分类失误导致自动删除健康文件。** 由封闭的损坏类清单缓解:重置只在三个确定性解析期代码上触发;ENOENT 本来就是「空 unit」,一切 I/O 错误(EACCES、EIO)大声传播。单发重试把爆炸半径限定为每次 open 至多一删。
- **根迁移改变既有 checkout 的查找位置。** 在 pre-release 立场下接受(后端拒绝旧格式、无外部消费者);上文为在乎 per-cwd `workspace.json` 内容的人记录了一次性手动搬移。
- **`destroy` 是存储 seam 上新增的破坏性原语。** 唯一调用方是 facility 的声明重置路径;后端契约将其记档为 facility 专属,任何面向模型或面向用户的路径都触不到它。
- **错误分类失误导致自动删除健康文件。** 由封闭的损坏类清单缓解:重置只在三个确定性解析期代码上触发;ENOENT 本来就是「空 unit」,一切 I/O 错误(EACCES、EIO)大声传播。单发重试把爆炸半径限定为每次 open 至多一删。
- **根迁移改变既有 checkout 的查找位置。** 在 pre-release 立场下接受(后端拒绝旧格式、无外部消费方);上文为在乎 per-cwd `workspace.json` 内容的人记录了一次性手动搬移。
- **`destroy` 是存储 seam 上新增的破坏性原语。** 唯一调用方是 facility 的声明重置路径;后端契约将其记档为 facility 专属,任何面向模型或面向用户的路径都触不到它。

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
# pnpm run verify-translation-pairing --write .agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md
2026-06-30-pre-tool-input-rewrite.md: f35e6af465ce8cec5685911c43e12b8dd66f2e6a
2026-06-30-pre-tool-input-rewrite.zh.md: c94c647bb6867199b72528bc84c58a08ae93e27e
2026-06-30-pre-tool-input-rewrite.zh.md: 5793b38293e117e328350722ad1efb3126fd176c

View File

@@ -6,7 +6,7 @@ Status: proposed
## 问题
[拦截 seam Agent Note(agent 决策记录)](../../implemented/feature/2026-06-30-interception-seams.md) 将 `tools/pre-execute` 定义为一道针对执行的允许/拒绝/询问门禁,此时执行的身份标识已受保护、参数已被深度冻结。Claude Code 的 `PreToolUse` 钩子还提供了 `updatedInput`,因此忠实的桥接需要一个显式的重写机制。重写不能是对现有执行对象的可变逃逸口:它必须保持持久化历史、审计记录、展示层与实际执行值之间的一致性。
[拦截 seam Agent Note](../../implemented/feature/2026-06-30-interception-seams.md) 将 `tools/pre-execute` 定义为一道针对执行的允许/拒绝/询问门禁,此时执行的身份标识已受保护、参数已被深度冻结。Claude Code 的 `PreToolUse` 钩子还提供了 `updatedInput`,因此忠实的桥接需要一个显式的重写机制。重写不能是对现有执行对象的可变逃逸口:它必须保持持久化历史、审计记录、展示层与实际执行值之间的一致性。
## 问题本质:执行前参数的三个读取方
@@ -42,7 +42,7 @@ Status: proposed
## 风险
- 重写 `assistant/message` 中的工具调用块会改变模型「看到自己说了什么」;是否有提供方在回放时拒绝这种改动,是一个需要通过实验确定的开放问题,必须在决策形状冻结之前解决。
- 重写 `assistant/message` 中的工具调用块会改变模型「看到自己说了什么」;是否有提供方在回放时拒绝这种改动,是一个需要通过实验确定的开放问题,必须在决策结构定型之前解决。
- 更早的重写阶段改变了 `assistant/message`、`tool/call`、钩子审计事件与执行之间的顺序关系;设计必须固定这一顺序,同时不削弱轮次封闭性或调用/结果邻接性。
## 开放问题

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
# pnpm run verify-translation-pairing --write .agents/notes/proposed/feature/2026-07-06-recallable-compaction.md
2026-07-06-recallable-compaction.md: ed5491e642ea7ac99fd9f4ba071a61e655f968d3
2026-07-06-recallable-compaction.zh.md: 4060df2c2550f9ea3287adfb51d097c1a60baf71
2026-07-06-recallable-compaction.zh.md: ad3cba250973a8c8d9545a31b312cd4d3f23725f

View File

@@ -1,4 +1,4 @@
# Agent Note: 可回溯压缩(compaction):索引检查点、状态检查点与会话内历史回溯
# Agent Note: 可回溯压缩:索引检查点、状态检查点与会话内历史回溯
Status: proposed
@@ -6,7 +6,7 @@ Status: proposed
## 问题
压缩是一扇单向门。模型看到的摘要没有指向被其遮蔽内容的引用,因为 `shadowedRange` 来源只存在于仅写入日志、模型不可见的 `compact/summary` 事件上,也没有工具能让模型重新读取被遮蔽的区段。即使仅追加日志仍保存每一个字节,摘要器丢弃的内容也会离开模型可触达的世界。重复压缩会进一步放大问题:每一轮都会重写头部检查点,因此请求前缀每次都会完全失去提示词缓存命中,而更早的摘要也会一代又一代地被重新摘要。
压缩(compaction)是一扇单向门。模型看到的摘要没有指向被其遮蔽内容的引用,因为 `shadowedRange` 来源只存在于仅写入日志、模型不可见的 `compact/summary` 事件上,也没有工具能让模型重新读取被遮蔽的区段。即使仅追加日志仍保存每一个字节,摘要器丢弃的内容也会离开模型可触达的世界。重复压缩会进一步放大问题:每一轮都会重写头部检查点,因此请求前缀每次都会完全失去提示词缓存命中,而更早的摘要也会一代又一代地被重新摘要。
根本原因是一个产物承担了两个互相冲突的角色。**索引** 需要冻结、按时间排序且成本低廉;模型的**工作记忆** 则需要全局视图、重新确定优先级并且可变。单一摘要无法同时胜任两者。
@@ -24,7 +24,7 @@ Status: proposed
- 用一行关键词记录低频字面锚点,例如确切的错误字符串、值和配置键,并按类别分组;
- 由代码组装页脚:`[checkpoint c<summarySeq>: shadows conversation span #<start>–#<end>; originals retrievable via history_read]`。指针根据来源组装,绝不由模型编写。
已经提交的存根永不重写,也绝不再次进入之后的压缩区域。存根调用采用分层输入:固定前导内容与逐字节相同的本轮开始状态检查点(该阶段所有调用共享的前缀);随后是先前所有已提交存根的关键词行,使新条目索引其分片的独特内容,而不是重复整个目录;再加最近一两个已提交存根以维持时间连续性;最后是切片本身。同一轮中的同级存根不作为输入,因为并发阶段禁止这种依赖,而与轮次对齐的边界已经维持局部连续性。状态检查点只作为背景,绝不能成为存根需要总结的材料。完全由回溯内容构成的切片只通过代码生成存根,即只写一行指针,不调用 LLM(大语言模型)。存根调用失败时采用相同降级方式:其切片获得一个仅包含代码指针的存根,本轮继续执行,使状态重写成为一轮中唯一的强制 LLM 依赖。
已经提交的存根永不重写,也绝不再次进入之后的压缩区域。存根调用采用分层输入:固定前导内容与逐字节相同的本轮开始状态检查点(该阶段所有调用共享的前缀);随后是先前所有已提交存根的关键词行,使新条目索引其分片的独特内容,而不是重复整个目录;再加最近一两个已提交存根以维持时间连续性;最后是切片本身。同一轮中的同级存根不作为输入,因为并发阶段禁止这种依赖,而与轮次对齐的边界已经维持局部连续性。状态检查点只作为背景,绝不能成为存根需要总结的材料。完全由回溯内容构成的切片只通过代码生成存根,即只写一行指针,不调用 LLM(大语言模型)。存根调用失败时采用相同降级方式:其切片获得一个仅由代码生成的指针存根,本轮继续执行,使状态重写成为一轮中唯一的强制 LLM 依赖。
### 状态检查点
@@ -41,7 +41,7 @@ Status: proposed
### 回溯工具
新增包(package)`@deepseek-ai/dsh-tool-recall`,它只是 `dsh-session` 与 `dsh-compact` 词汇之上的消费方,注册两个面向模型的工具:
新增包`@deepseek-ai/dsh-tool-recall`,它只是 `dsh-session` 与 `dsh-compact` 词汇之上的消费方,注册两个面向模型的工具:
- `history_read(checkpoint, offset?)`:把日志中任意检查点(包括已被取代的检查点)遮蔽的区段渲染为 `User:`/`Assistant:`/`Tool result:` transcript(文本记录),并按配置预算分页,提供续传游标。
- `history_search(query, checkpoint?, limit?)`:对每个被遮蔽区段进行不区分大小写的字面量扫描;返回带检查点 id 的片段与覆盖元数据(`scanned`/`matched`/`truncated`)。零匹配提示会说明扫描按字面量执行,并建议对可能的检查点直接使用 `history_read`。
@@ -60,7 +60,7 @@ Status: proposed
- **工具结果裁剪**(进行中的裁剪服务):其替换节点携带 `sourceEventSeqs`;同一注册表折叠会把经过裁剪的结果列为可回溯。它属于后续范围,两项工作互不阻塞。
- **提供方 token 用量核算**(正在把压缩压力迁移至提供方报告用量的工作):为保护逻辑提供核算基础;本实现堆叠在它之后。
- **「查询会话」backlog(待翻清单)条目**:它是跨会话的泛化方案;本 Agent Note(agent 决策记录)把范围限定在实时会话内,并选择工具名称与渲染方式,使该工作能够扩展本设计而不产生冲突。
- **「查询会话」backlog(待办清单)条目**:它是跨会话的泛化方案;本 Agent Note 把范围限定在实时会话内,并选择工具名称与渲染方式,使该工作能够扩展本设计而不产生冲突。
- **训练**:何时回溯属于学习到的行为。确定性页脚与关键词锚点为训练提供稳定目标,而回溯使用情况在会话日志中完全可见,可供轨迹导出;基准测试与 RL 设计由后训练侧推进。
### 后续事项
@@ -72,19 +72,19 @@ Status: proposed
- 定期使用分片原文刷新状态:触发条件是交接探针观察到漂移。
- `stateFallbackThreshold`(存根数量低于阈值时使用完整细节的状态提示词):触发条件是短会话回归。
- 延迟注册回溯工具:触发条件是在从不进行压缩的会话中测得上下文开销。
- 在 pre-step 分摊存根起草工作:一旦已经陈旧但尚未压缩的内容积累超过 `chunkTokens`,就在下一个 pre-step 起草该分片的存根(一个仅写入日志的草稿事件,在分片周围的上下文仍然存活时写入),让压缩轮提交草稿,而不是集中执行摘要。这是后台压缩的确定性、精确回放等价形式(Claude Code 会话记忆采用这种模式;OpenClaw 证明同步语义完全相同)。触发条件是观察到一轮延迟,或近实时起草带来的存根质量收益得到验证。
- 在 pre-step 分摊存根起草工作:一旦已经陈旧但尚未压缩的内容积累超过 `chunkTokens`,就在下一个 pre-step 起草该分片的存根(一个仅写入日志的草稿事件,在分片周围的上下文仍然存活时写入),让压缩轮提交草稿,而不是集中执行摘要。这是后台压缩的确定性、精确回放等价形式(Claude Code 会话记忆采用这种模式;OpenClaw 证明同步语义完全相同)。触发条件是观察到压缩轮次的执行延迟,或近实时起草带来的存根质量收益得到验证。
- 拆分摘要模型;由模型选择分片边界;跨会话回溯;语义搜索回退:每项都必须由各自证据支持。
- 更丰富的 `history_search` 查询形式:正则表达式,以及对日志 JSON 工具结果执行的结构化查询(sql/jq 风格,或由 agent 针对索引存储编写查询)。触发条件是观察到搜索漏检;首版先交付字面量匹配,使回溯路径保持为日志的纯函数。
## 考虑过的替代方案
- **分阶段交付**(先在当前后端之上单独交付回溯工具;观察到回溯使用后,再决定是否拆分检查点):不予采纳。未经训练的模型会低频使用任何新工具,因此该条件测量的是训练缺失,而不是设计价值;训练侧需要完整机制来构建环境;预发布阶段修改持久化格式的成本最低;缓存经济性则属于第一方已经掌握的知识,不是等待遥测验证的假设。实现仍以堆叠 PR 方式落地,并先交付回溯工具,但这只是构建顺序,不是决策门槛。
- **分阶段交付**(先在当前后端之上单独交付回溯工具;观察到回溯使用后,再决定是否拆分检查点):不予采纳。未经训练的模型会低频使用任何新工具,因此该条件测量的是训练缺失,而不是设计价值;训练侧需要完整机制来构建环境;预发布阶段修改持久化格式的成本最低;缓存经济性则属于第一方已经掌握的知识,不是等待遥测验证的假设。实现仍以堆叠 PR(Pull Request)方式落地,并先交付回溯工具,但这只是构建顺序,不是决策门槛。
- **只保留冻结的全尺寸摘要,不设状态检查点**:不予采纳,因为永久前缀会无界增长、自我加速并最终发生颠簸,而且没有任何内容可以重新确定优先级。
- **只保留纯存根,不设状态检查点**:不予采纳,因为这假定模型知道自己缺少什么,在面对未知的未知时会失败。
- **由 LLM 老化/整合冻结分片**:不作为常规机制,因为摘要的摘要会丢失信息,并使冻结前缀频繁变化;其保留下来的形式是由代码汇总,且延后实现。
- **把完整前缀作为分片摘要器输入**:不予采纳,因为成本为 O(N²);状态文档以 O(state) 提供相同背景。
- **一次摘要调用输出全部结果**:不予采纳,因为摘要路径没有结构化输出约束;解析一份自由文本响应并将其拆开,正是保守失败设计要避免的脆弱 seam。
- **由模型选择分片边界**:延后实现,因为相对于未经证明的收益,解析与校验成本过高;分片策略位于配置之后。
- **由模型选择分片边界**:延后实现,因为相对于未经证明的收益,解析与校验成本过高;分片策略由配置控制。
- **由模型编写指针**:不予采纳,因为指针必须精确,应由确定性代码组装。
- **FTS/向量索引伴随存储**:在会话内不予采纳,因为实时日志已在内存中且大小有界,在预算内进行字面量扫描已经足够;只有跨会话范围才能证明索引的价值。
- **回溯路径中的语义搜索回退/次级模型提取**:不予采纳,因为其中的 LLM 或嵌入调用会破坏无密钥回放的确定性;回溯必须保持为日志的纯函数。
@@ -107,4 +107,4 @@ Status: proposed
- **存根目录会占用注意力**:每次请求中包含数十张稳定的索引卡,可能稀释模型关注点;验收标准中的 bench 度量会将其与 `compact-basic` 对比。
- **成本**:每轮摘要输入大约是当前实现的两倍;短会话的成本和质量接近当前水平,而设计收益随会话长度增长。
- **状态漂移与职责分工泄漏** 可以通过交接探针和存根评审观察;对应措施已列为后续事项。
- **两个后端** 会增加维护接口;seam 契约和共享回溯消费方会约束该成本,bench 对比则用于逐步决定默认实现。
- **两个后端** 会扩大维护范围;seam 契约和共享回溯消费方会限制这一范围,bench 对比则用于逐步决定默认实现。

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
# pnpm run verify-translation-pairing --write .agents/notes/proposed/feature/2026-07-08-interactive-side-sessions.md
2026-07-08-interactive-side-sessions.md: dfd325babe215782c9c1cbec3fd9f874783af7ab
2026-07-08-interactive-side-sessions.zh.md: 9bc9d5c94fdef893134844551a594acc39a99d3b
2026-07-08-interactive-side-sessions.zh.md: a1441cfe12df933855c7b9fe08c5e09f9e6107d3

View File

@@ -14,8 +14,8 @@ Status: proposed
- **Fork 并绑定:** 以父会话的平衡已完成轮次前缀创建子会话,并在其元数据中标记 `parentSession` 与 `seedLength`。这组合了 `ctx.agents.create({ seed, meta })`;不新增核心服务或会话存储方法。
- **顾问定位:** 创建后注入一条插件来源的 `context/message`,告知子会话只做解释,不执行变更或继续任务。保持系统提示词逐字节一致,可在继承的历史上保留提供方的前缀缓存。
- **合并回写:** 向子会话请求一条有长度上限的 handback,然后向父会话注入一条插件来源的 `context/message`。父会话的下一次请求在其日志位置看到该消息,保持回放与[请求可重建性](../../implemented/architecture/2026-07-05-reconstructable-requests.md),无需新增会话事件。
- **呈现:** 调用方式、会话切换与 handback 渲染属于首个客户端拥有的界面。本 Agent Note(agent 决策记录)仅规定与界面无关的机制。
- **合并回写:** 向子会话请求一条有长度上限的 handback,然后向父会话注入一条插件来源的 `context/message`。父会话的下一次请求在日志所记录的位置看到该消息,保持回放与[请求可重建性](../../implemented/architecture/2026-07-05-reconstructable-requests.md),无需新增会话事件。
- **呈现:** 调用方式、会话切换与 handback 渲染属于首个客户端拥有的界面。本 Agent Note 仅规定与界面无关的机制。
回退产品化、会话树视图、面向模型的侧会话工具,以及 `forkName`/`mergedInto` 元数据均不在本 Agent Note 范围内。一次真实适配器 spike 已验证了源日志隔离、继承上下文、多轮子会话交互,以及合并回写在父会话下一轮次中的可见性。

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
# pnpm run verify-translation-pairing --write .agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.md
2026-07-14-sdk-developer-projects.md: 65d2bf66232993222832eb0f2f4f56cfcf7afd16
2026-07-14-sdk-developer-projects.zh.md: 8435a07d9b2545a8f41a1f96743c9c7e6d4daf3d
2026-07-14-sdk-developer-projects.zh.md: 07bf898a7ef672a21951afaa9038a86878ae2900

View File

@@ -10,19 +10,19 @@ DeepSeek Harness 通过 Cordis 插件对功能进行组合,但从空目录开
一次性生成器只能降低首次创建成本。若生成结果隐藏在 preset 或不可编辑的 CLI(命令行界面)内部,高级开发者无法调整插件树、修改 Cordis 插件配置或增加项目特有行为;若创建后的工程完全脱离工具管理,开发者又必须重新承担所有 NPM 依赖和 Cordis 插件配置的一致性工作。
初始创建和后续配置面对同一组内置功能。两条流程各自维护功能列表、功能选项和 NPM 依赖时,新增 Cordis 插件、NPM 包或调整配置会使二者逐渐分叉。工程还需要一条普通的本地插件开发路径,参与开发、构建和启动流程。
初始创建和后续配置面对同一组内置功能。两条流程各自维护功能列表、功能选项和 NPM 依赖时,新增 Cordis 插件、NPM 包或调整 Cordis 插件配置会使二者逐渐分叉。工程还需要一条普通的本地插件开发路径,参与开发、构建和启动流程。
## 提案
SDK 创建一个普通、显式且归开发者所有的 TypeScript/Cordis 工程。`cordis.yml` 是唯一的运行时插件树;开发和生产读取同一份文件。工程中的 `package.json`、`cordis.yml`、TypeScript 入口、构建配置和 `plugins/*` 均可直接编辑,SDK 不把它们封装成不可见的 preset。
开发者产品入口只有 `npm create @deepseek-ai/sdk` 和 `dsh-sdk` 命令。前者负责首次创建,`dsh-sdk config` 在创建后管理 SDK 能识别的内置功能,`dsh-sdk dev`、`dsh-sdk build` 与 `dsh-sdk start` 负责开发、构建和启动;本期不提供 `dsh-sdk create`。create 与 config 使用同一份人工编写的功能定义,因此一项功能的功能选项、NPM 依赖、Cordis 配置项、相关文件和识别规则只有一个来源。功能、功能选项等名词由 [SDK 工程编辑架构](../architecture/2026-07-15-sdk-project-editing-architecture.md) 的术语表定义。
开发者产品入口只有 `npm create @deepseek-ai/sdk` 和 `dsh-sdk` 命令。前者负责首次创建,`dsh-sdk config` 在创建后管理 SDK 能识别的内置功能,`dsh-sdk dev`、`dsh-sdk build` 与 `dsh-sdk start` 负责开发、构建和启动;本期不提供 `dsh-sdk create`。create 与 config 使用同一份人工编写的功能定义,因此一项功能的功能选项、NPM 依赖、Cordis 配置项、相关文件和识别规则只有一个来源。[SDK 工程编辑架构](../architecture/2026-07-15-sdk-project-editing-architecture.md) 定义了功能、功能选项等术语。
SDK 只为功能选择和有限功能选项提供交互,不尝试把任意 Cordis 插件配置变成通用表单。功能选项所需的少量专用输入由所属功能收集;其余 Cordis 插件配置留在 `cordis.yml` 中,并通过注释指明常用改法,由开发者直接修改。
## 开发者流程
首次创建按会影响后续问题集合的顺序收集信息:目标目录与 package 身份、模型提供方与凭据、运行接口、内置功能与功能选项、可选本地插件、包管理器,以及是否安装 NPM 依赖并构建。命令参数已提供的答案不重复询问;本期 create 和 config 都要求交互式 TTY,取消创建时不写入目标目录。
首次创建按会影响后续问题集合的顺序收集信息:目标目录与包身份、模型提供方与凭据、运行接口、内置功能与功能选项、可选本地插件、包管理器,以及是否安装 NPM 依赖并构建。命令参数已提供的答案不重复询问;本期 create 和 config 都要求交互式 TTY,取消创建时不写入目标目录。
```sh
npm create @deepseek-ai/sdk my-agent
@@ -39,13 +39,13 @@ create 还提供一次 `none / plugin / tool` 选择。`plugin` 固定生成 `pl
## 创建时支持的功能
下表是本期 create 面向开发者展示的支持集。`required` 始终存在但仍可切换有限功能选项;`default` 在选择树中预选;`optional` 由开发者主动选择。表格说明产品支持集,运行时注册表是实现的事实源。
下表是本期 create 面向开发者展示的支持集。`required` 始终存在但仍可切换有限功能选项;`default` 在选择树中预选;`optional` 由开发者主动选择。表格说明产品支持集,运行时注册表是实现的真源。
| 功能 | create 状态 | 功能选项 | 限制与关系 |
|---|---|---|---|
| `provider` | required | `deepseek`(默认)/ `custom` | DeepSeek 收集 API key;custom 另收集 base URL,模型名可由 CLI 参数覆盖 |
| `app` | required | `tui`(默认)/ `acp` / `embed` | 选择运行接口 |
| `spine` | required | `default` | timer、LLM seam、会话存储、系统提示词、工具注册表、agent 注册表,以及 agent loop |
| `spine` | required | `default` | timer、LLM(大语言模型)seam、会话存储、系统提示词、工具注册表、agent 注册表,以及 agent loop(智能体循环) |
| `bash` | required | `local`(默认)/ `sandbox` | 两个功能选项互斥、与运行接口正交,且都安装面向模型的 bash 工具;sandbox 安装本地沙箱提供方和沙箱 bash 后端 |
| `persistence` | required | `jsonl`(默认)/ `sqlite` | 每个工程恰好选择一个持久化后端 |
| `hmr` | default | `default` | 加载 `@cordisjs/plugin-hmr`;dev 和 start 都启用,使用插件默认配置 |
@@ -55,11 +55,11 @@ create 还提供一次 `none / plugin / tool` 选择。`plugin` 固定生成 `pl
| `web` | optional | `deepseek`(默认)/ `exa` / `perplexity` / `fetch-only` | 搜索功能选项互斥;Exa/Perplexity 收集各自 API key;建议同时启用 timeout policy |
| `subagent` | optional | `spawn`(默认)/ `fork`,可多选 | 本期只提供进程内后端 |
| `workflow` | optional | `workerthread` | 要求 subagent 的 `spawn` 功能选项 |
| `compact` | optional | `basic` | 使用 SDK 提供的上下文压缩参数 |
| `compact` | optional | `basic` | 使用 SDK 提供的上下文压缩(context compaction)参数 |
| `hooks` | optional | `claude`(默认)/ `codex`,可多选 | 各功能选项生成独立的可编辑配置文件 |
| `guard` | optional | `repeat-tool` | 提供重复工具调用提醒 |
| `timeout-policy` | optional | `default` | 对声明超时预算的工具执行统一策略 |
| `ask-user` | optional | `default` | 提供 `ask_user_question` 工具;只有 `tui` 可选,因为 ACP 是自动化传输,而 embed 不提供人类交互服务 |
| `ask-user` | optional | `default` | 提供 `ask_user_question` 工具;只有 `tui` 可选,因为 ACP(Agent Client Protocol)是自动化传输,而 embed 不提供人类交互服务 |
`bash` 的两个功能选项都适用于 ACP、TUI 和 embed,不由运行接口决定。sandbox 功能选项不写任何生效的配置键,因而沿用 `dsh-bash-sandbox` 的 `read-only` 默认值;生成的 `cordis.yml` 保留注释示例,开发者可以显式改为 `workspace-write`:
@@ -76,7 +76,7 @@ create 还提供一次 `none / plugin / tool` 选择。`plugin` 固定生成 `pl
## 生成工程
使用默认答案创建 npm 工程时,provider 为 DeepSeek,运行接口为 TUI,bash 为 local,持久化为 JSONL,hmr、fs、todo 与 skill 处于选中状态。初始目录树为:
使用默认答案创建 npm 工程时,提供方为 DeepSeek,运行接口为 TUI,bash 为 local,持久化为 JSONL,hmr、fs、todo 与 skill 处于选中状态。初始目录树为:
```text
my-agent/
@@ -99,7 +99,7 @@ my-agent/
| script | 行为 |
|---|---|
| `dev` | 运行 `dsh-sdk dev index.ts`,为 TypeScript 和本地 workspace 插件注册开发期解析 |
| `build` | 运行 `dsh-sdk build`,调用工程安装的 tsdown 构建根入口和 `plugins/*` package |
| `build` | 运行 `dsh-sdk build`,调用工程安装的 tsdown 构建根入口和 `plugins/*` 包 |
| `typecheck` | 直接运行 `tsc -b` |
| `start` | 运行 `dsh-sdk start index.js`,启动已构建入口且不隐式构建 |
| `config` | 运行 `dsh-sdk config`,修改当前工程功能树 |
@@ -118,39 +118,39 @@ my-agent/
`dsh-sdk config` 可以安装缺失功能、启停已安装功能和切换有限功能选项。required 功能不能取消。改变 NPM 依赖后只运行一次项目包管理器安装;安装失败不回滚已经提交的工程文件。
SDK 只修改功能明确拥有的 Cordis 配置项、配置键、NPM 依赖、`.env.example` 占位和独占文件。同一功能选项的更新保留 Cordis 配置项中的未知配置键;手写或第三方插件只支持按稳定 ID 启停。已知功能被手改成不完整、歧义或无法读取的形状时,`dsh-sdk config` 显示诊断并拒绝自动修改,直到开发者手工修复。
SDK 只修改功能明确拥有的 Cordis 配置项、配置键、NPM 依赖、`.env.example` 占位和自有文件。同一功能选项的更新保留 Cordis 配置项中的未知配置键;手写或第三方插件只支持按稳定 ID 启停。已知功能被手改成不完整、歧义或无法读取的形状时,`dsh-sdk config` 显示诊断并拒绝自动修改,直到开发者手工修复。
一次 config 会话在内存工作区上累计全部修改。Apply 前完成功能关系、资源冲突和文件形状校验,并比较受影响文件与会话打开时的原文;校验失败或检测到外部修改时不写盘。实际写盘开始后不提供跨文件事务回滚。
## 维护模型
Builtin 支持集由 SDK 人工策划,不根据 NPM 依赖名称或目录约定自动暴露。一个功能可以组合多个 Cordis 配置项,功能选项可以共享资源,并声明对其他功能或特定功能选项的功能依赖;新增普通功能或功能选项不应要求同时修改 create 和 config 两个命令流程。
Builtin 支持集由 SDK 人工策划,不根据 NPM 依赖名称或目录约定自动暴露。一个功能可以组合多个 Cordis 配置项,功能选项可以共享资源,并声明对其他功能或特定功能选项的功能依赖;新增普通功能或功能选项无需同时修改 create 和 config 两个命令流程。
## 后续工作
- `dsh-sdk add [package-spec]`:统一本地插件创建与外部 Cordis 插件接入;未指定 package 或仓库来源时创建本地 plugin/tool,指定来源时增加 NPM 依赖和 `cordis.yml` 配置项,来源模型为 GitHub 仓库等扩展保留空间
- 非交互 create/config:本期两个流程都要求 TTY,不提供供自动化调用的完整输入合同
- `dsh-sdk add [package-spec]`:统一本地插件创建与外部 Cordis 插件接入;未指定包或仓库来源时创建本地插件/工具,指定来源时增加 NPM 依赖和 `cordis.yml` 配置项,来源模型为 GitHub 仓库等扩展保留空间
- 非交互 create/config:本期两个流程都要求 TTY,不提供供自动化调用的完整输入契约
- 更多功能专用参数输入:本期产品只展示有限功能选项、secret 和少量专用值,不为 Cordis 插件配置提供通用参数界面
## 曾考虑的替代方案
**不可编辑的 preset 或生成器托管工程。** 该方案可以缩短初次创建路径,但会隐藏真实插件树和构建边界,使高级开发者无法直接组合 Cordis 插件,也让项目行为依赖 CLI 版本而不是检入的工程文件。
**只提供一次性生成器。** 创建后完全依赖手工维护,会让功能依赖、功能选项切换和多文件更新再次分散;共享 registry 的 config 流程为生成工程保留持续管理机制。
**只提供一次性生成器。** 创建后完全依赖手工维护,会让功能依赖、功能选项切换和多文件更新再次分散;共享注册表的 config 流程为生成工程保留持续管理机制。
**为开发和生产维护两份 `cordis.yml`。** 两份插件树会使开发成功无法证明生产加载相同功能;dev 只增加 TypeScript 与本地 workspace 解析,运行配置保持唯一。
**为任意 Cordis 插件配置生成通用表单。** Cordis 插件配置包含嵌套结构、表达式和插件特有语义,通用表单会形成第二套不完整 schema。SDK 只管理有限功能选项和专用 secret,复杂配置继续由开发者直接编辑。
**使用私有协议发现本地插件。** 普通 package manager workspace、根 NPM 依赖、TypeScript references 和 Cordis 配置项已能表达完整关系;额外发现协议会创造只能由 SDK 理解的隐藏状态。
**使用私有协议发现本地插件。** 普通包管理器 workspace、根 NPM 依赖、TypeScript references 和 Cordis 配置项已能表达完整关系;额外发现协议会创造只能由 SDK 理解的隐藏状态。
**在现有工程中提供 `dsh-sdk create`。** create 已能生成一种可编辑的本地插件骨架,后续插件可以沿用普通 workspace 和 Cordis 机制手工添加;再提供同构命令会增加第二条脚手架产品面,却不增加新的组合功能。
**在现有工程中提供 `dsh-sdk create`。** create 已能生成一个可编辑的本地插件骨架,后续插件可以沿用普通 workspace 和 Cordis 机制手工添加;再提供并行命令会增加第二条脚手架产品面,却不增加新的组合功能。
**把每个新 Cordis 插件自动暴露为 builtin。** package 无法说明多个插件如何组合成一项产品功能,也无法推导互斥关系、功能依赖、secret、接口适用性和安全限制;支持集需要人工策划,自动化只适合检查候选是否完成分类。
**把每个新 Cordis 插件自动暴露为 builtin。** 包无法说明多个插件如何组合成一项产品功能,也无法推导互斥关系、功能依赖、secret、接口适用性和安全限制;支持集需要人工策划,自动化只适合检查候选是否完成分类。
## 验收标准
- `npm create @deepseek-ai/sdk` 按本文顺序收集项目身份、provider、interface、功能、可选本地插件、包管理器和安装选择,并在取消时保持目标路径不存在
- `npm create @deepseek-ai/sdk` 按本文顺序收集项目身份、提供方、接口、功能、可选本地插件、包管理器和安装选择,并在取消时保持目标路径不存在
- 默认 npm 工程具有本文目录树和 `dev`、`build`、`typecheck`、`start`、`config` scripts,且 dev/start 使用同一份 `cordis.yml`
- create 展示本文功能及功能选项;`bash` 的 local/sandbox 二选一且默认 local,sandbox Cordis 配置项保留可编辑的注释配置示例;HMR 默认选中并同时由 dev/start 加载
- create 的 `plugin` 或 `tool` 选择至多生成一个固定名称的本地插件,并原子更新插件文件与根工程关系;本期不提供 `dsh-sdk create`
@@ -160,8 +160,8 @@ Builtin 支持集由 SDK 人工策划,不根据 NPM 依赖名称或目录约
## 风险
- 开发者可以把 builtin 手改成 registry 无法识别的形状;SDK 选择停止自动化而不是猜测并覆盖配置
- 开发者可以把 builtin 手改成注册表无法识别的形状;SDK 选择停止自动管理该功能而不是猜测并覆盖配置
- 多文件写入前的校验和外部修改检测不能提供写入阶段的事务回滚;I/O 中途失败可能留下需要人工修复的部分提交
- sandbox 功能选项依赖目标平台存在可用的本地沙箱后端;后端不可用时必须 fail closed,不能退回无沙箱执行
- HMR 在生产启动中也保持文件 watcher 和热重载行为;这是显式插件选择的结果,不是仅限开发环境的隐式服务
- `.env` 的仅追加策略会保留已经不用的凭据,SDK 不判断这些用户数据何时可以安全删除
- `.env` 的仅追加策略会保留已经不用的凭据,SDK 不判断这些用户拥有的密钥数据何时可以安全删除

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-01-windows-pwsh-default.md
2026-08-01-windows-pwsh-default.md: a310174b6864bb880070280835ccfd8623e26342
2026-08-01-windows-pwsh-default.zh.md: 079c1e3cac789a5e3fa4d0bb889026b3fb69f78c
2026-08-01-windows-pwsh-default.md: 1c3ccef23bb5cd9bc37237bd69aac2e2c56649a8
2026-08-01-windows-pwsh-default.zh.md: 3958d21eb8a9d306009b11d6e9806a1654a8958e

View File

@@ -13,9 +13,9 @@ The harness's shipped execution profile is bash-first on every platform. Windows
Two follow-up stages, each independently shippable. The former stage 2 (bash-tool parity twin) shipped with the [pwsh tool bash parity decision](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md): `tool-pwsh` now mirrors `tool-bash` for foreground and background work minus the sandbox surface, shares the `DSH_*` environment through `dsh-bash-env`, and carries a keyless application snapshot of its assembled surface.
1. **Windows default composition** — the shipped CLI compositions mount `dsh-pwsh-local` as the `ctx.bash` executor and `dsh-tool-pwsh` as the model-facing shell tool on Windows hosts (bash unmounted there), while POSIX hosts keep the bash stack. This is a composition/roster decision in `base.cordis.yml` and the surface overlays, gated by platform; it makes the shipped Windows experience PowerShell-native end to end.
2. **pwsh TUI/GUI rendering** — the TUI and Web surfaces render pwsh output with PowerShell-aware presentation (native path display, `$env:` facts), the counterpart of the bash terminal cards. This is where terminal/console rendering conventions get a PowerShell twin.
2. **pwsh GUI rendering** — the Web surface renders pwsh calls with the bash-shaped terminal presentation (terminal card with exit-status pill), the counterpart of the bash terminal cards. Shipped in the [pwsh UI presentation matches bash decision](../../implemented/feature/2026-08-05-pwsh-ui-bash-parity.md) with a keyless web lane; the TUI was removed, so no terminal twin remains. A PowerShell-aware presentation beyond bash parity (native path display, `$env:` facts) remains unclaimed.
The stages are deliberately sequenced: composition first (a Windows user gets PowerShell without choosing), then rendering. Nothing in this proposal changes POSIX behavior.
The stages are ordered by dependency only where one exists: the rendering stage shipped first with the [pwsh UI presentation matches bash decision](../../implemented/feature/2026-08-05-pwsh-ui-bash-parity.md) because it is platform-independent and its keyless web lane runs on any host, while the Windows default composition remains the only unshipped stage. Nothing in this proposal changes POSIX behavior.
## Alternatives considered
@@ -30,10 +30,10 @@ The stages are deliberately sequenced: composition first (a Windows user gets Po
- A Windows host running the shipped `dsh` TUI/Web gets `pwsh` as its shell tool and PowerShell as the `ctx.bash` executor without configuration, and `bash` is absent from the model-visible roster there.
- POSIX hosts are byte-for-byte unaffected (same roster, same executor).
- The shipped-composition e2es assert the platform-gated roster on both families.
- Stage 1 lands with the keyless pwsh-tool snapshot already in place from the parity change; stage 2 lands with TUI/Web rendering snapshots for pwsh output.
- Stage 1 lands with the keyless pwsh-tool snapshot already in place from the parity change; stage 2 landed with the web `pwsh-terminal` rendering lane (the TUI's removal left no terminal surface to snapshot).
## Risks
- **Bash-dependent composition rows** — any shipped plugin that assumes `bash` semantics (hook bridges executing shell hooks, workspace tooling) must be audited per stage; the audit may force a staged rollout rather than one switch.
- **Windows CI coverage gap** — unit coverage runs on Linux; Windows-only regressions in the pwsh stack surface through the Windows build/static lane and e2es, which must be extended per stage rather than assumed.
- **Rendering conventions** — a PowerShell twin for terminal cards is a UI design decision with snapshot surface; deferring it (stage 2) keeps stage 1 shippable without UI churn.
- **Rendering conventions** — the bash-shaped terminal twin shipped with the Web lane; a PowerShell-aware presentation beyond bash parity (native path display, `$env:` facts) remains a UI design decision with snapshot surface, deferred with stage 1.

View File

@@ -13,9 +13,9 @@ harness 交付的执行画像在每个平台都是 bash 优先。Windows 主机
两个阶段,各自可独立交付。原阶段 2(bash 工具对等孪生)已随 [pwsh 工具与 bash 对齐决策](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) 交付:`tool-pwsh` 现在在前台与后台工作(减 sandbox 面)上镜像 `tool-bash`,通过 `dsh-bash-env` 共享 `DSH_*` 环境,并携带其组装表面的 keyless 应用快照。
1. **Windows 默认组合**——交付的 CLI 组合在 Windows 主机上挂载 `dsh-pwsh-local` 作为 `ctx.bash` 执行器、`dsh-tool-pwsh` 作为面向模型的 shell 工具(那里不挂载 bash),POSIX 主机保持 bash 栈。这是 `base.cordis.yml` 与 surface 覆盖层里按平台门控的组合/清单决策;它让交付的 Windows 体验端到端 PowerShell 原生。
2. **pwsh TUI/GUI 渲染**——TUI 与 Web 表面以 PowerShell 感知的呈现渲染 pwsh 输出(原生路径显示、`$env:` 实情),即 bash 终端卡片的对应物。这是终端/控制台渲染约定获得 PowerShell 孪生的地方。
2. **pwsh GUI 渲染**——Web 表面以 bash 形状的终端呈现渲染 pwsh 调用(带退出状态 pill 的 terminal 卡),即 bash 终端卡片的对应物。已随 [pwsh UI 呈现与 bash 对齐决策](../../implemented/feature/2026-08-05-pwsh-ui-bash-parity.md) 及 keyless web 通道交付;TUI 已移除,不再有终端孪生。超出 bash 对齐的 PowerShell 感知呈现(原生路径显示、`$env:` 实情)仍无人认领。
各阶段刻意排序:先组合(Windows 用户无需选择即获得 PowerShell),再渲染。本提案不改变任何 POSIX 行为。
各阶段仅在有依赖关系时排序:渲染阶段已随 [pwsh UI 呈现与 bash 对齐决策](../../implemented/feature/2026-08-05-pwsh-ui-bash-parity.md) 先行交付(平台无关,其 keyless web 通道可在任意宿主运行),而 Windows 默认组合仍是唯一未交付的阶段。本提案不改变任何 POSIX 行为。
## 备选方案
@@ -30,10 +30,10 @@ harness 交付的执行画像在每个平台都是 bash 优先。Windows 主机
- 运行交付版 `dsh` TUI/Web 的 Windows 主机无需配置即获得 `pwsh` 作为其 shell 工具、PowerShell 作为 `ctx.bash` 执行器,且那里的模型可见清单中没有 `bash`。
- POSIX 主机逐字节不受影响(清单相同,执行器相同)。
- 交付组合 e2e 在两个平台族上断言按平台门控的清单。
- 阶段 1 落地时,parity 变更带来的 keyless pwsh 工具快照已经就位;阶段 2 附带 pwsh 输出的 TUI/Web 渲染快照落地。
- 阶段 1 落地时,parity 变更带来的 keyless pwsh 工具快照已经就位;阶段 2 已随 web `pwsh-terminal` 渲染通道落地(TUI 的移除让终端表面无快照可做)。
## 风险
- **依赖 bash 的组合行**——任何假设 bash 语义的交付插件(执行 shell hooks 的 hooks 桥、工作区工具)必须按阶段审计;审计可能迫使分阶段推出而非一次切换。
- **Windows CI 覆盖缺口**——单元覆盖在 Linux 上运行;pwsh 栈里仅 Windows 的回归通过 Windows 构建/静态通道与 e2e 浮出,必须按阶段扩展而不是想当然。
- **渲染约定**——终端卡片的 PowerShell 孪生是带快照表面的 UI 设计决策;把它延期(阶段 2)让阶段 1 无需 UI 翻动即可交付。
- **渲染约定**——bash 形状的终端孪生已随 web 通道交付;超出 bash 对齐的 PowerShell 感知呈现(原生路径显示、`$env:` 实情)仍是带快照表面的 UI 设计决策,随阶段 1 一起延期。

View File

@@ -3,4 +3,4 @@
# 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-claude-code-and-codex-subagent-backends.md
2026-08-04-claude-code-and-codex-subagent-backends.md: f0b642d488ce585879deab91bd152b2ca4e68ea6
2026-08-04-claude-code-and-codex-subagent-backends.zh.md: f3d0010163b402e79c78188d4686963abfa21569
2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 487e7e9ee19761d047ac838709799287cf3d4679

View File

@@ -16,7 +16,7 @@ harness 将两个一次性兄弟提供方发布为可独立安装的可选包。
Codex 提供方基于 Codex 0.146.0 实现。Claude Code 提供方仍未实现。在两个兄弟提供方及其组合证据全部具备之前,本 Agent Note 将保持提案状态。
这两个提供方都报告 `inheritsParentContext: false`,不声明任何可选的启动时功能,并传递父会话 cwd,但不会复制父级对话。文档中的工具会关闭后台执行并使用 `maxDepth: 'provider-managed'`,让进程外产品自行负责递归策略,而不会向提供方发送其无法执行的限制。每次调用都会创建一个全新的产品进程和一次不可续接的产品对话。共享 subagent 服务继续负责请求解析、生命周期事件、结果结算和前台收集;共享子进程服务负责凭证清洗、进程树终止以及整棵进程树的退出观测。
这两个提供方都报告 `inheritsParentContext: false`,不声明任何可选的启动能力,并传递父会话 cwd,但不会复制父级对话。文档中的工具会关闭后台执行并使用 `maxDepth: 'provider-managed'`,让进程外产品自行负责递归策略,而不会向提供方发送其无法执行的限制。每次调用都会创建一个全新的产品进程和一次不可续接的产品对话。共享 subagent 服务继续负责请求解析、生命周期事件、结果结算和前台收集;共享子进程服务负责凭证清洗、进程树终止以及整棵进程树的退出观测。
```text
fixed tool → shared subagent service → product provider → official product process
@@ -53,7 +53,7 @@ Claude Code 兄弟提供方尚未实现。其中间提案不固定产品版本
## 证据契约
每个产品都负责覆盖所有分支的包(package)测试、一项必跑的无密钥真实产品测试、一项 Loader 装配 e2e 和一项带密钥的 DeepSeek e2e。无密钥产品层级使用被测的确切官方发行版、非空的伪产品密钥、隔离的临时工作区与产品主目录,以及能返回固定答案的回环模型。产品请求缺失、身份验证错误、任务文本被改动、答案不完全一致、真实产品被跳过或受管句柄仍存活,都会使这项必跑测试失败。独立的 Loader 层级会启动与 README 同形的用户配置,验证固定提供方与只支持前台执行的通用工具,并且不得启动产品进程。带密钥层级使用仅在运行时提供的密钥启动相同的生产提供方与真实产品,要求从固定的 DeepSeek 官方服务获得一个唯一的随机数,并再次证明完全停稳;只有本地操作方未提供密钥时才会自行跳过,受信 CI 则会对该 secret 执行 preflight 检查。
每个产品都负责覆盖所有分支的包测试、一项必跑的无密钥真实产品测试、一项 Loader 装配 e2e 和一项带密钥的 DeepSeek e2e。无密钥产品层级使用被测的确切官方发行版、非空的伪产品密钥、隔离的临时工作区与产品主目录,以及能返回固定答案的回环模型。产品请求缺失、身份验证错误、任务文本被改动、答案不完全一致、真实产品被跳过或受管句柄仍存活,都会使这项必跑测试失败。独立的 Loader 层级会启动与 README 同形的用户配置,验证固定提供方与只支持前台执行的通用工具,并且不得启动产品进程。带密钥层级使用仅在运行时提供的密钥启动相同的生产提供方与真实产品,要求从固定的 DeepSeek 官方服务获得一个唯一的随机数,并再次证明完全停稳;只有本地操作方未提供密钥时才会自行跳过,受信 CI 则会对该 secret 执行 preflight 检查。
Codex 证据锁定 `@openai/codex@0.146.0` 与 `codex-cli 0.146.0`。其真实产品测试会观测确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、不会产生文件副作用的无人值守命令拒绝、本地取消以及整棵进程树退出。其 Loader e2e 会按包名解析 `@deepseek-ai/dsh-subagent-codex`,验证 `codex` 注册与省略后台参数的 `subagent_codex` schema,接受 `maxDepth: 'provider-managed'`,并在环境中没有可用 `codex` 命令时记录零次子级启动。该 NPM 包是用于复现真实产品证据的开发依赖;生产环境仍提供 `codex`,并通过 `PATH` 解析。

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
# pnpm run verify-translation-pairing --write .agents/notes/proposed/process/2026-06-11-api-extractor-reports.md
2026-06-11-api-extractor-reports.md: 03f512992fe87ea3d0f8d51a1772ce1ec89a5c0d
2026-06-11-api-extractor-reports.zh.md: a8180124c5e5402dce3c28c1bd8c54219d5b68fc
2026-06-11-api-extractor-reports.zh.md: e3b40f29908ebe1096a02dd4ee5f4886bb5a4537

View File

@@ -4,7 +4,7 @@ Status: proposed
[English](2026-06-11-api-extractor-reports.md) | 中文
> 从最初的「doc-sync(文档同步门禁)与 API 报告」Agent Note(agent 决策记录)中拆出(首次提出于 2026-06-11)。第 1 至第 2 部分(文档块类型检查、事件分类体系校验)已交付,见 [doc-sync 强制](../../archived/process/2026-06-11-doc-sync-enforcement.md)。本文是被推迟的第 3 部分,作为独立提案保留。
> 从最初的「doc-sync(文档同步门禁)与 API 报告」Agent Note 中拆出(首次提出于 2026-06-11)。第 1 至第 2 部分(文档块类型检查、事件分类体系校验)已交付,见 [doc-sync 强制](../../archived/process/2026-06-11-doc-sync-enforcement.md)。本文是被推迟的第 3 部分,作为独立提案保留。
## 问题
@@ -12,11 +12,11 @@ Status: proposed
## 提案
使用 api-extractor(或 `tsc --emitDeclarationOnly` 加一份规范化的公开接口导出)为每个包(package)生成一份签入仓库的 `etc/<pkg>.api.md`;CI 在重新生成结果与已签入报告不一致时失败。这样,每一次公开 API 变更都会成为评审者(或评审 agent(智能体))必须看到的一行 diff。
使用 api-extractor(或 `tsc --emitDeclarationOnly` 加一份规范化的公开 API 清单)为每个包生成一份签入仓库的 `etc/<pkg>.api.md`;CI 在重新生成结果与已签入报告不一致时失败。这样,每一次公开 API 变更都会成为评审者(或评审 agent(智能体))必须看到的一行 diff。
## 曾考虑的替代方案
**`tsc --emitDeclarationOnly` 加规范化的公开接口导出**:如果 api-extractor 过于笨重,这是更轻量的机制;两者都能满足提案所需的「签入仓库、可 diff」的报告形态。
**`tsc --emitDeclarationOnly` 加规范化的公开 API 清单**:如果 api-extractor 过于笨重,这是更轻量的机制;两者都能满足提案所需的「签入仓库、可 diff」的报告形态。
## 验收标准

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
# pnpm run verify-translation-pairing --write .agents/notes/proposed/process/2026-06-11-architectural-conformance.md
2026-06-11-architectural-conformance.md: f7cb0d7397d4e03df225f68417da43b1fec8de62
2026-06-11-architectural-conformance.zh.md: aa25ef6d2772642885ef268bd548fc6dad40d3cf
2026-06-11-architectural-conformance.zh.md: c82297fcbe02feed4cd04bc60730a3494b7faa4d

View File

@@ -6,15 +6,15 @@ Status: proposed
## 问题
目前有两项架构保证仅存在于行文中:(1)没有任何组件依赖具体的 loop 包(package)([微内核承诺](../../implemented/architecture/2026-06-11-microkernel-event-taxonomy.md));(2)每个 LlmAdapter 都正确遵循分片协议。二者都应由机制强制执行([质量门禁原则](../../implemented/process/2026-06-11-quality-gates.md))。
目前有两项架构保证仅存在于行文中:(1)没有任何组件依赖具体的 agent loop(智能体循环)包([微内核承诺](../../implemented/architecture/2026-06-11-microkernel-event-taxonomy.md));(2)每个 LlmAdapter 都正确遵循分片协议。二者都应由机制强制执行([质量门禁原则](../../implemented/process/2026-06-11-quality-gates.md))。
## 提案
**dependency-cruiser** 配合以下规则:
- `packages/*`(除 agent-loop(智能体循环)自身的 tests 和 examples/ 外)禁止导入 `@deepseek-ai/dsh-agent-loop`。
- `packages/*`(除 agent-loop 自身的测试和 examples/ 外)禁止导入 `@deepseek-ai/dsh-agent-loop`。
- 禁止跨包深层导入(`@deepseek-ai/dsh-*/src/...` 路径)——只允许使用公开入口点。
- packages/ 内禁止导入循环。
- packages/ 内禁止出现循环依赖。
- `vendor/*` 禁止从 `packages/*` 导入。
- 分层:dsh-llm 不导入其他 dsh 包;dsh-session 仅导入 dsh-llm;以此类推(packages/README.md 中的依赖表,强制执行)。

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
# pnpm run verify-translation-pairing --write .agents/notes/proposed/process/2026-06-11-supply-chain-and-vendor-drift.md
2026-06-11-supply-chain-and-vendor-drift.md: a27ae64556dc7366279824f1480e5681b1e86bf1
2026-06-11-supply-chain-and-vendor-drift.zh.md: 25c27650709faf1a462ce9779ee6f0a909746311
2026-06-11-supply-chain-and-vendor-drift.zh.md: b52748e4b4c92a161c5b3ff49c079ff909d11c01

View File

@@ -6,14 +6,14 @@ Status: proposed
## 问题
vendor manifest(元数据清单)(见[引入 vendor 的决策](../../implemented/process/2026-06-11-vendor-cordis-as-source.md))在提交时仅在*正向*强制执行(vendor 变更 ⇒ manifest 更新),但没有任何机制验证 manifest 的*声明*:即 vendor/ 确实等于上游指定 SHA 的内容加上所记录的修改。此外,少量真正的 npm 依赖也没有安全公告监控或更新节奏。
vendor manifest(元数据清单)(见[引入 vendor 的决策](../../implemented/process/2026-06-11-vendor-cordis-as-source.md))在提交时仅在*正向*强制执行(vendor 变更 ⇒ manifest 更新),但没有任何机制验证 manifest 的*声明*:即 vendor/ 确实等于上游指定 SHA 的内容加上所记录的修改。此外,少量真正的 NPM 依赖也没有安全公告监控或更新节奏。
## 提案
1. **Vendor 漂移检查**(夜间 CI):以 manifest 中记录的 SHA 浅克隆上游仓库,复制对应的包(package)源码,与 `vendor/*/src` 做 diff。除非 diff 与已记录的本地修改一致(每项修改以签入的 patch 文件保存——日志条目从行文描述变为可验证的产物),否则任务失败。
1. **Vendor 漂移检查**(夜间 CI):以 manifest 中记录的 SHA 浅克隆上游仓库,复制对应的包源码,与 `vendor/*/src` 做 diff。除非 diff 与已记录的本地修改一致(每项修改以签入的 patch 文件保存——日志条目从行文描述变为可验证的产物),否则任务失败。
2. **依赖安全公告**:对 lockfile 运行 osv-scanner(或 `pnpm audit`),按计划定期执行,并在涉及 lockfile 变更的 PR(Pull Request)上触发。
3. **许可证清单**:一个脚本断言每个 vendor 包都携带其 LICENSE 文件,且 package.json 的 `license` 字段与 vendor/README.md 中的清单一致(我们混合了 vendor 的 MIT 与自有的 BSD-3)——作为 CI 步骤运行。
4. **Renovate**(或定时 agent(智能体)任务)以小 PR 的形式提议 npm 依赖更新,这些 PR 走完整门禁套件;vendor 包不在其列(它们的更新遵循 manifest 同步流程,理想情况下是半自动化的 agent 工作流:拉取上游、重新应用 patch、运行门禁、以更新后的 manifest 表格开 PR)。
4. **Renovate**(或定时 agent(智能体)任务)以小 PR 的形式提议 NPM 依赖更新,这些 PR 走完整门禁套件;vendor 包不在其列(它们的更新遵循 manifest 同步流程,理想情况下是半自动化的 agent 工作流:拉取上游、重新应用 patch、运行门禁、以更新后的 manifest 表格开 PR)。
## 计划

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/process/2026-06-20-discover-package-inventory.md
2026-06-20-discover-package-inventory.md: 7de865e43f87f0e41fad43b3786b9825509e9d50
2026-06-20-discover-package-inventory.zh.md: 00195981a1f8036121e3895a0ff8ac342f54dff9
2026-06-20-discover-package-inventory.zh.md: fed3d9dd8740b2dc22434f8af7e1e8c025502f61

View File

@@ -6,15 +6,15 @@ Status: proposed
## 问题
包(package)与门禁清单在 TypeScript project references、包文档、CI 描述和 Knip 覆盖项中反复出现。大多数只是重述包布局、manifest(元数据清单)数据或聚合命令内容。因此每新增一个包都会产生本可避免的同步点。
包与门禁清单在 TypeScript project references、包文档、CI 描述和 Knip 覆盖项中反复出现。大多数只是重述包布局、manifest(元数据清单)数据或聚合命令内容。因此每新增一个包都会产生本可避免的同步点。
[包层级结构](../../archived/architecture/2026-06-20-package-hierarchy.md)已经手动消除了其中若干:`scripts/publint-all.ts` 现在从 `packages/<group>/<pkg>` 布局推导列表,两份 `tsconfig` 的 `paths` 映射也合并为一个 `@deepseek-ai/dsh-*` 通配符。剩下的是无法用 glob 消除的清单,主要是聚合配置(`tsconfig.host.json`、`tsconfig.client.json`)的 project `references`——TypeScript 要求它们是显式数组(没有通配符形式)。
[包层级结构](../../archived/architecture/2026-06-20-package-hierarchy.md)已经手动消除了其中若干:`scripts/publint-all.ts` 现在从 `packages/<group>/<pkg>` 布局推导列表,两份 `tsconfig` 的 `paths` 映射也合并为一个 `@deepseek-ai/dsh-*` 通配符。剩下的是无法用 glob 消除的清单,主要是聚合配置(`tsconfig.host.json`、`tsconfig.client.json`)中的项目引用(`references`)——TypeScript 要求它们是显式数组(没有通配符形式)。
当静态列表编码的是策略时,它们是合理的;当它们只是重复 `package.json`、workspace glob 或包层级结构中已有的 manifest 数据或布局事实时,就是不必要的摩擦。
## 提案
让剩余的包与门禁清单可被发现。一个唯一的权威来源,即 `packages/<group>/<pkg>` 层级结构加上包 manifest,应当驱动聚合配置的 `references`、模块图以及任何全量包列表,并配合一个生成加校验步骤(沿用现有的 `gen-module-graph` / `gen-cordis-catalog` 模式:生成器写出产物,`--check` 模式在 `hygiene` / `doc-sync`(文档同步门禁)中发现已提交副本陈旧时失败)。模块图生成已经在读取包 manifest。`doc-sync` 应当成为定义并打印其子门禁的唯一命令,文档链接到该命令,而非重述第二份列表。
让剩余的包与门禁清单可被发现。唯一真源,即 `packages/<group>/<pkg>` 层级结构加上包 manifest,应当驱动聚合配置的 `references`、模块图以及任何全量包列表,并配合一个生成加校验步骤(沿用现有的 `gen-module-graph` / `gen-cordis-catalog` 模式:生成器写出产物,`--check` 模式在 `hygiene` / `doc-sync`(文档同步门禁)中发现已提交副本陈旧时失败)。模块图生成已经在读取包 manifest。`doc-sync` 应当成为定义并打印其子门禁的唯一命令,文档链接到该命令,而非重述第二份列表。
层级结构不需要编码关于包的所有事实,但应当编码宽泛的维护策略:core/product 包、集成包、能力 seam 包与 support/test/example 包不应在脚本能区分它们之前先要求一份手工维护的例外列表。
@@ -22,7 +22,7 @@ Status: proposed
## 验收标准
- 聚合配置的 project `references` 由层级结构生成(生成器输出它们;`--check` 门禁在提交副本陈旧时报错),而非手工维护。
- 聚合配置的项目引用(`references`)由层级结构生成(生成器输出它们;`--check` 门禁在提交副本陈旧时报错),而非手工维护。
- 新增一个包时,不需要为任何门禁编辑静态包列表。
- 文档描述真源,而非重复生成的清单。
- CI 调用聚合命令,由这些命令自行管理其子门禁列表。

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
# pnpm run verify-translation-pairing --write .agents/notes/proposed/process/2026-07-13-human-review-skill-maintenance.md
2026-07-13-human-review-skill-maintenance.md: 76391bd110b7a86b194a9340ffcf7cc4602d1d2e
2026-07-13-human-review-skill-maintenance.zh.md: 67d68d07cc7f467a310b64d833a28da6f04bcc8e
2026-07-13-human-review-skill-maintenance.zh.md: e442a39618a53ffdc130c94874a482315943caff

View File

@@ -34,17 +34,17 @@ flowchart TD
### 采纳证据
每条反馈都携带稳定的来源 ID 和有界的变更证据。评审人的 `commit_id` 仍属于该 PR 时(强制推送场景无法确认即排除),工具会选择 committer 时间戳严格早于反馈的最新 PR commit 作为基线,而不是采用评审人点击的 commit,因为后者可能更旧。工具绝不会直接比较该基线与落地 merge:这种 diff 会混入不断前移的目标分支中的无关变更。相反,它会向采纳评审人提供两份 PR 专用 patch 快照。令 `B` 为反馈基线,`T` 为落地 merge 的目标父级,`M` 为落地 merge。反馈时快照是从 `merge-base(B, T)` 至 `B` 的树 diff;最终快照是从 `T` 至 `M` 的树 diff。因此,目标分支专有变更不会出现在任一 PR patch 中,而反馈后加入 PR 的变更只会出现在最终快照中。遭到强制推送的评审、早于全部现存 PR commit 的反馈,以及无法重建目标父级的落地形态,会在任何评审人看到之前确定性地归类为 `unclear`。合并状态、已解决讨论串、作者回复「已修复」或同文件编辑只是上下文,不是采纳证明;PR 作者自己的评论绝不会进入适配器,因为它们不可能构成对作者自身意见的采纳。
每条反馈都携带稳定的来源 ID 和有界的变更证据。评审人的 `commit_id` 仍属于该 PR 时(强制推送场景无法确认即排除),工具会选择 committer 时间戳严格早于反馈的最新 PR commit 作为基线,而不是采用评审人点击的 commit,因为后者可能更旧。工具绝不会直接比较该基线与落地 merge:这种 diff 会混入不断前移的目标分支中的无关变更。相反,它会向采纳评审人提供两份 PR 专用 patch 快照。令 `B` 为反馈基线,`T` 为落地 merge 的目标父级,`M` 为落地 merge。反馈时快照是从 `merge-base(B, T)` 至 `B` 的树 diff;最终快照是从 `T` 至 `M` 的树 diff。因此,目标分支专有变更不会出现在任一 PR patch 中,而反馈后加入 PR 的变更只会出现在最终快照中。关联 commit 已因强制推送而不再属于该 PR 的评审、早于全部现存 PR commit 的反馈,以及无法重建目标父级的落地形态,会在任何评审人看到之前确定性地归类为 `unclear`。合并状态、已解决讨论串、作者回复「已修复」或同文件编辑只是上下文,不是采纳证明;PR 作者自己的评论绝不会进入适配器,因为它们不可能构成对作者自身意见的采纳。
### 双评审人分类与起草
两个独立配置的评审适配器,会从来源(`human-authored`、`forwarded-automation` 或 `unclear`)和采纳情况(`adopted`、`rejected` 或 `unclear`)两个维度,对每个符合条件的条目进行分类。只有两个适配器都判定为 `human-authored` 加 `adopted` 的条目才会继续。采纳集合随后会针对当前 skill 接受第二次独立分类:候选项、已经覆盖、实现专用或并非反馈。单个条目即可符合要求,不要求重复出现。意见分歧会得到一次有界的重新评估;如果仍未解决,则继续保留在运行产物中。单个批次的适配器输出如果未通过 schema 或 id 校验,系统会在批次层按不采纳处理:其中的每条反馈都标记为 unclear 并路由到 `excluded`,而不是中止整次运行;有问题的原始输出会保存在该次运行的私有产物中,供调试使用。如果任一适配器在某项操作的任何非空批次中都没有返回有效结果,运行会以非零状态退出并发出失败记录,而不会报告「没有候选项」。
主适配器只根据结构化的共同指引起草,绝不接收原始评审文本。根据适配器作者的契约,它保持无工具且只读:返回完整的候选文件内容,由工具校验后写入唯一目标。两个适配器随后评审同一份完整 skill diff;阻塞性问题会进入有界修订循环,而且两者必须批准同一版修订。工具会在运行文档和 lint 检查前,以及报告成功前,再次拒绝暂存改动和目标 skill 之外的编辑,因此检查或并发进程无法通过添加其他路径混入。失败时,工具使用尽力而为的比较并交换恢复自身写入,避免覆盖维护者的并发编辑。成功时,它保存一份候选资料包,其中包含源 `origin/master` commit、源 skill blob ID、已评审 diff、完整候选文件、源反馈 ID 与 URL、落地证据范围、适配器判定和检查结果;它绝不提交、推送、打开或合并 PR。
主适配器只根据结构化的共同指引起草,绝不接收原始评审文本。根据适配器作者的契约,它保持无工具且只读:返回完整的候选文件内容,由工具校验后写入唯一目标。两个适配器随后评审同一份完整 skill diff;阻塞性问题会进入有界修订循环,而且两者必须批准同一版修订。工具会在运行文档和 lint 检查前,以及报告成功前,再次拒绝暂存改动和目标 skill 之外的编辑,因此检查或并发进程无法通过添加其他路径混入。失败时,工具使用尽力而为的比较并交换回滚自身写入,以免覆盖维护者的并发编辑。成功时,它保存一份候选资料包,其中包含源 `origin/master` commit、源 skill blob ID、已评审 diff、完整候选文件、源反馈 ID 与 URL、落地证据范围、适配器判定和检查结果;它绝不提交、推送、打开或合并 PR。
### 评审适配器协议
每个私有可执行文件从 stdin 接收有字节上限、带版本的 JSON 请求,并在 stdout 返回有字节上限且符合 schema 的 JSON。两个评审命令解析为逐字节相同的可执行文件时,工具拒绝运行;这是机械性的最低标准,保证主适配器与次适配器由独立提供方或模型驱动,仍是部署运维方的责任。`access` 与 `tools` 字段是适配器作者承担的契约标记,不是 OS 沙箱:评审子进程在清理后的环境中 spawn,其 `cwd` 指向私有运行目录而非仓库根目录;反馈包装在带随机数的 `<untrusted-feedback nonce="…">` 块中,每个提示词都会要求模型把它视为数据;128 位随机数防止不受信任的正文伪造结束标签。每个子进程都采用有界、感知中止的进程树清理。适配器作者把每项操作实现为纯只读推理(inference);即使 `edit` 操作也只会在 JSON 中返回完整候选内容,由工具校验后写入唯一目标。每个生产 `git`/`gh`/检查命令同样在清理后的环境中 spawn,避免 pre-push 钩子的路由变量无提示地重定向维护工具。候选写入与失败回滚会针对最近一次写入内容使用尽力而为的比较并交换;回滚还会取消暂存目标,避免由适配器或检查暂存的候选项在失败运行后遗留并进入之后的 commit。
每个私有可执行文件从 stdin 接收有字节上限、带版本的 JSON 请求,并在 stdout 返回有字节上限且符合 schema 的 JSON。两个评审命令解析为逐字节相同的可执行文件时,工具拒绝运行;这只是最低限度的机械检查;确保主适配器与次适配器由独立提供方或模型驱动,仍是部署运维方的责任。`access` 与 `tools` 字段是适配器作者承担的契约标记,不是 OS 沙箱:评审子进程在清理后的环境中 spawn,其 `cwd` 指向私有运行目录而非仓库根目录;反馈包装在带随机数的 `<untrusted-feedback nonce="…">` 块中,每个提示词都会要求模型把它视为数据;128 位随机数防止不受信任的正文伪造结束标签。每个子进程都采用有界、感知中止的进程树清理。适配器作者把每项操作实现为纯只读推理(inference);即使 `edit` 操作也只会在 JSON 中返回完整候选内容,由工具校验后写入唯一目标。每个生产 `git`/`gh`/检查命令同样在清理后的环境中 spawn,避免 pre-push 钩子的路由变量无提示地重定向维护工具。候选写入与失败回滚会针对最近一次写入内容使用尽力而为的比较并交换;回滚还会取消暂存目标,避免由适配器或检查暂存的候选项在失败运行后遗留并进入之后的 commit。
### 推广契约
@@ -52,7 +52,7 @@ flowchart TD
### 机制所在位置
工具源码、适配器二进制文件、提供方凭据和预期的每日调度器保存在维护者机器上,不会提交到本仓库。本文规定协议,参考实现属于私有基础设施。该机制只服务于由单个运维方维护的一项 skill,因此,让机制编辑持续接受仓库评审的成本高于来源可追溯性的收益。如果该机制将来移交给第二位维护者,移交工作需要一篇后续 Agent Note(agent 决策记录)来修订本决策;任何接手者都应从运维文档 [docs/cookbook/maintaining-dsh-code-review.md](../../../../docs/cookbook/maintaining-dsh-code-review.md) 入手。
工具源码、适配器二进制文件、提供方凭据和预期的每日调度器保存在维护者机器上,不会提交到本仓库。本文规定协议,参考实现属于私有基础设施。该机制只服务于由单个运维方维护的一项 skill,因此,持续通过仓库评审审查机制改动的成本高于来源可追溯性的收益。如果该机制将来移交给第二位维护者,移交工作需要一篇后续 Agent Note 来修订本决策;任何接手者都应从运维文档 [docs/cookbook/maintaining-dsh-code-review.md](../../../../docs/cookbook/maintaining-dsh-code-review.md) 入手。
## 考虑过的替代方案

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
# pnpm run verify-translation-pairing --write .agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md
2026-07-26-remove-packed-session-fixture-migrator.md: 0a29ef98828ac07d291392d637b0508937c9a9a6
2026-07-26-remove-packed-session-fixture-migrator.zh.md: 64b994855a7e92d5b0922884b6c66df1b82b6d90
2026-07-26-remove-packed-session-fixture-migrator.zh.md: aa9aece710587cef6c84ad9cf078821a2506deb1

View File

@@ -12,11 +12,11 @@ Status: proposed
## 提案
最新清单确认不再有任何开放 PR(Pull Request)需要转换会话格式 JSONL 后,移除临时 CLI `scripts/migrate-packed-session-fixtures.ts`,以及根包(package)提供的 `migrate:packed-session-fixtures` 命令。在同一变更中,移除测试政策、ACP 快照 README 和已实现打包行 Agent Note(agent 决策记录)中指向该过渡命令的链接,并将 `scripts/session-fixture-layout.snapshot.ts` 中仅适用于该命令的修复指引替换为与具体命令无关的规范布局指引。
在实时清单确认不再有任何开放 PR(Pull Request)需要转换会话格式 JSONL 后,移除临时 CLI `scripts/migrate-packed-session-fixtures.ts`,以及根包提供的 `migrate:packed-session-fixtures` 命令。在同一变更中,移除测试政策、ACP 快照 README 和已实现打包行 Agent Note 中指向该过渡命令的链接,并将 `scripts/session-fixture-layout.snapshot.ts` 中仅适用于该命令的修复指引替换为与具体命令无关的规范布局指引。
保留 `scripts/session-fixture-layout.ts`、其单元测试和 `scripts/session-fixture-layout.snapshot.ts`。它们定义并强制执行永久规范布局;只有面向分支的写入器是临时机制。
移除命令前,每个受影响分支都要合并当前 `master`,运行一次迁移器,单独提交由此产生的仅 fixture 重写,并验证仓库级快照布局检查通过。已关闭或被取代的分支无需迁移。
移除命令前,每个受影响分支都要合并当前 `master`,运行一次迁移器,将由此产生且仅包含 fixture 重写的改动单独提交,并验证仓库级快照布局检查通过。已关闭或被取代的分支无需迁移。
## 曾考虑的替代方案
@@ -24,15 +24,15 @@ Status: proposed
**随 CLI 一同移除规范布局转换模块。** 该模块不是过渡残留:快照 CI 使用它发现未来 fixture、解码混合物理记录,并与规范打包表示进行比较。移除该模块也会移除强制机制。
**打包行进入 `master` 后立即删除命令。** 较旧的开放分支在重新定向后,只能使用临时脚本或手动重新生成快照,这会增加冲突风险,也会让解码事件保真度更难评审。
**打包行进入 `master` 后立即删除命令。** 较旧的开放分支在调整目标分支后,只能使用临时脚本或手动重新生成快照,这会增加冲突风险,也会让解码事件保真度更难评审。
## 验收标准
- 最新开放 PR 清单未发现任何仍依赖临时迁移命令处理会话格式 JSONL 改动的分支。
- 实时开放 PR 清单未发现任何仍依赖临时迁移命令处理会话格式 JSONL 改动的分支。
- 临时 CLI、根包命令、所有分支收敛链接与仅适用于该命令的门禁诊断均不存在;永久规范布局转换器、单元测试和快照检查仍然保留。
- `pnpm run test:snapshot`、`pnpm run doc-sync`、lint 和空白校验在没有临时命令的情况下通过。
- 当前文档仅描述打包默认值和永久规范布局强制机制。
## 风险
若开放分支清单不完整,命令消失后,贡献者可能会受困于大规模非打包 fixture 冲突。因此,移除操作取决于实时 PR 证据,而不是经过的时间。保留命令过久的运维成本较低,但会模糊哪一种机制才是永久机制。
若开放分支清单不完整,命令消失后,贡献者可能会陷入大规模的非打包 fixture 冲突。因此,移除操作取决于实时 PR 证据,而不是经过的时间。保留命令过久的运维成本较低,但会模糊哪一种机制才是永久机制。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/process/2026-08-04-artifact-first-npm-baseline-publication.md
2026-08-04-artifact-first-npm-baseline-publication.md: e79a988531ac266d72f7d2c036bdd10f3e4520a9
2026-08-04-artifact-first-npm-baseline-publication.zh.md: d7322547c665700aeffce77ea0bb39f622220fe3

View File

@@ -0,0 +1,114 @@
# Agent Note: Artifact-first NPM baseline publication
Status: proposed
English | [中文](2026-08-04-artifact-first-npm-baseline-publication.zh.md)
## Problem
Runnable source in the monorepo does not prove that published packages are runnable. Workspace links, TypeScript paths, tsx source loading, and residual `lib/` files in the working tree can supply files or dependencies that are absent from a published tarball. Existing built-artifact tests still read `lib/` directly from the working tree, so they do not verify what `package.json#files` selects or the layout produced by package-manager installation. An execution that succeeds in development mode can therefore be published without a required bundle chunk, declaration, configuration file, or asset.
Publishing many mutually dependent `@deepseek-ai` packages also creates a set-consistency problem. If a script publishes each package immediately after packing it, a later pack or validation failure leaves the first part of an unusable baseline in the registry. The npm registry has no cross-package transaction, so “publish once” cannot promise an atomic commit. It can promise that the complete publication set is packed and validated before any remote write, then published from that immutable set by one resumable orchestration command.
The current baseline also requires a person to derive versions, authenticate, pack, publish, and retry from a local machine. A later GitHub Actions workflow must reuse the same release bundle and validation logic. It must not rebuild a different, untested set of tarballs after publication is approved.
## Proposal
The publication flow uses an immutable release bundle as its boundary. The pack phase builds every target package from one fixed Git commit, creates every tarball, validates tarball contents, and passes an installed-artifact integration test. The publish phase reads only those tarballs and their manifest and is forbidden from rebuilding or repacking.
The target set contains only `@deepseek-ai/*` workspace packages discovered from `packages/*/*/package.json` and `apps/*/package.json`. The root project, `website/`, vendor, Python, and native workspaces are outside this NPM baseline. Discovery must reject duplicate names, mixed base versions, an unexpected publication privacy state, and unknown packages in the bundle instead of relying on another hand-maintained package-name list.
The prerelease version consists of the package stable base version, a second-precision UTC timestamp captured when the command starts, and the target commit's 10-character short SHA: `<base>-<YYYYMMDDHHmmss>-<short-commit>`. The dist-tag is derived as `dev-<base>`. For example, base `0.0.1`, time `2026-08-04T00:32:00Z`, and commit `909292dd7b` produce version `0.0.1-20260804003200-909292dd7b` and tag `dev-0.0.1`. Retrying one release bundle must retain its version and manifest; repacking creates a version from the new command start time.
The pack phase runs in this order:
1. Resolve the ref to an immutable commit, capture the UTC timestamp, derive the version from that commit's root manifest, and display the commit, timestamp, version, tag, registry, and output path. Both `pack` and `release` wait for Enter at this point before expensive work; `--yes` skips this confirmation for automation.
2. Install the frozen lockfile in an isolated detached worktree and run source-manifest publication constraints before staging. Uncommitted files and old build output from the caller's working tree must not affect publication.
3. Stage every target manifest with the derived version, remove its publication-time `private` marker, and rewrite internal workspace dependencies in `dependencies`, `devDependencies`, `optionalDependencies`, and `peerDependencies` to the same exact version.
4. Build the target commit completely, then run publint and built-package invariants.
5. Pack every package in the target set without performing a registry write.
6. Inspect each tarball's package manifest, file inventory, internal dependency versions, name, and version, rejecting missing, duplicate, or extra tarballs.
7. Generate a release manifest and checksums containing the commit, version, tag, registry, and each package's tarball path, SHA-256, and npm integrity.
8. Install an isolated consumer from the local tarballs and run the installed-artifact probes available in the current implementation; expand those probes to the complete artifact-plane integration matrix defined below.
9. Print one directly executable publish command only after the complete set passes. The pack command itself always remains free of remote writes.
The local `release` command composes pack and publish. It first uses the pack confirmation above to fix the expected timestamp and version, then waits for Enter again after a successful pack before publishing the same manifest; `release --yes` skips both confirmations. Separate `pack` and `publish --manifest` operations remain the primitives used by split CI jobs and recovery.
## Current implementation boundary
The checked-in pack command implements fixed-commit staging, exact internal dependency pins, static and tarball payload checks, the immutable manifest, and an isolated npm installation with every release tarball as a local top-level dependency. It runs the installed `dsh --version` and `dsh --dump-default-config` entries under plain Node, then starts the installed default TUI in a POSIX PTY, waits for its `main-session-` ready signal, and exits through `/exit` before printing the publish command. Publish supports integrity-based resumption, separates read-only registry verification from the authenticated identity check, and finishes with a complete remote integrity and dist-tag verification pass.
Pull-request CI does not invoke the pack command; the installed-entry probes are local release checks rather than merge gates. Credential-free CI execution, package-owned probes for every other bin and public runtime entry, workflow-artifact transfer, and the protected publication job remain proposal scope.
## Publication payload contract
Published packages contain only build artifacts required by consumers. `package.json#files` must not include `src` or `lib/types/**/*.d.ts.map`; an independent tarball-content gate must also confirm that no `package/src/**` or `package/**/*.d.ts.map` entry exists, preventing manifest patterns or pack behavior from bypassing the static constraint. Runtime JavaScript, `.d.ts` declarations, configuration, assets, worker files, and dynamic bundle chunks must cover the actual entrypoint closure.
Source manifests may retain `exports["./src/*"]` for this repository's source-plane resolution. That export does not place source in the publication payload and is not a consumer contract of the published package. Static gates must check the source plane and publication payload separately: deleting the source export must not hide broken workspace resolution, and publishing `src` must not repair missing build artifacts.
Every tarball must be free of `workspace:` specifiers, and every internal dependency and peer dependency that points into the publication set must equal the exact derived version; `^`, `~`, and other semver ranges must not cross commit baselines. Except for `exports["./src/*"]`, which is explicitly source-plane-only, every consumer entry declared by the package manifest must point to a file present in the tarball. Dynamic imports, runtime-computed paths, and non-exported assets cannot be validated solely from the manifest and require installed execution.
## Artifact-plane integration test
The integration test runs after all tarballs exist and before any publish. It creates a fresh temporary project outside the monorepo, installs the declared dependency closure through local `.tgz` files from the release manifest, and executes from that installation. The test must use plain Node and package-manager-produced `node_modules`; tsx, tsconfig paths, workspace links, repository source paths, working-tree `lib/`, and the same version from the published registry are forbidden resolution inputs. The test also asserts that critical modules and bins resolve to real paths inside the temporary consumer.
Installation uses the client behavior selected for this publication. Registry uploads must use the `npm` CLI because the private registry accepts only the npm client; pnpm may still orchestrate builds. Tarball tests must neither publish these packages to the real registry first nor repack after testing.
The test covers at least these execution surfaces:
- Installed `@deepseek-ai/dsh` runs `dsh --version` and `dsh --dump-default-config` successfully under plain Node, covering the static CLI entry and one dynamic mode entry.
- Installed default `dsh` completes one keyless TUI startup in a PTY and exits under test control after reaching a defined ready signal. This path must load the real TUI dynamic chunk, so a missing publication file such as `lib/tui-*.js` fails the gate.
- Every other published `bin` defines a package-owned smoke command that neither reaches a real service nor modifies user state. Different CLIs are not forced to share `--help`; the test runs the actual installed entry and checks its agreed exit or ready signal.
- Node-compatible public runtime entries load from the installation. Browser, worker, or host-protocol-only entries use matching isolated fixtures, but their inputs must still be the current tarballs exclusively.
These tests prove executability; they do not replace unit tests, snapshots, real-API e2e, or publint. Test fixtures should reuse behavior assertions from existing built-bin and PTY scenarios while changing the entry to the tarball installation. A test that runs working-tree `lib/bin.js` directly does not satisfy this gate.
## Publication and recovery
The publish command validates the release manifest, every local checksum, the target registry, `npm ping`, and `npm whoami` before uploading tarballs in a deterministic order. It accepts only a manifest created by the pack phase, never workspace directories. The default registry is `https://registry.npm.harnessment.com/`; every publish passes the registry and derived tag explicitly so a user-level `.npmrc` cannot redirect the operation.
npm provides no multi-package atomic transaction, so uploads still occur package by package. The orchestrator reduces the failure surface through idempotent recovery: upload when remote `<name>@<version>` does not exist; skip when it exists with the release manifest's integrity; fail immediately when it exists with different content. Dist-tag inspection reads tag assignments without resolving a default tag's target, so an unrelated dangling tag cannot block recovery. At completion it must confirm every package version's integrity and every dist-tag against the release version. The workflow reports success only when the complete set passes final verification.
If pack, tarball inspection, or installed-artifact integration fails, the registry must receive zero writes. If publish fails after a partial upload, the operator reruns publish with the same release manifest and must not repack into another timestamped version instead of recovering. Only a code or build-input change that requires different tarballs reruns the complete pack and test flow.
## GitHub Actions integration
GitHub Actions separates a credential-free pack-and-test job from a protected publish job. The first checks out the exact commit, invokes the same pack entry used locally, runs tarball-consumer tests, and uploads the complete release bundle as a workflow artifact. The second depends on the first, downloads that workflow artifact, revalidates the manifest and checksums, and invokes the same publish entry. It cannot rebuild after checkout.
Pull requests and ordinary pushes may run the credential-free pack-and-test signal so payload regressions surface before merge. Actual private-registry publication starts as `workflow_dispatch` with only a target ref as input; the pack job creates the UTC timestamp, while the base version, short SHA, tag, registry, and package inventory derive from repository state or version-controlled configuration. Stable-release triggering is outside this baseline proposal.
The registry token is injected only into the publish job, which uses a protected GitHub Environment to govern human approval, allowed branches or tags, and concurrency. The pack-and-test job cannot read publication credentials. The workflow artifact may have a short retention period, but the publish job must use the bundle produced by the same workflow run instead of locating tarballs from an untrusted source by version.
## Alternatives considered
**Publish recursively from the workspace.** Rejected because it interleaves packing and registry writes, cannot prove the complete set before the first write, and allows workspace resolution and caller working-tree state to influence publication.
**Test only built `lib/` in the working tree.** Rejected because that validates the build tree rather than the tarball selected by `package.json#files`. A dynamic chunk present in the working tree but omitted from the tarball is exactly the failure this proposal must catch.
**Run only `dsh --help`.** Rejected because Commander can print help and exit before loading the TUI, Web, or headless dynamic entry. It does not prove the default production startup path is complete.
**Publish `src` and declaration maps to reduce missing-file risk.** Rejected because the source plane is not a production-runtime fallback. Expanding the payload hides bundle-closure errors and turns local debugging outputs into accidental publication contracts.
**Require truly atomic cross-package publication.** Rejected because the npm registry has no such transaction. An immutable release bundle, complete pre-publication validation, integrity comparison, and idempotent recovery provide an implementable boundary while retaining the explicit limitation that partial uploads can be briefly visible.
**Rebuild in the publish job after approval.** Rejected because the tested and uploaded tarballs would lose content identity. The workflow artifact and checksums must carry the test inputs directly into publication.
## Acceptance criteria
- One pack entry discovers every target under `packages/*/*` and `apps/*` from a fixed commit, derives and displays a version from the UTC second and short commit before waiting for Enter, generates the complete release bundle before any registry write, and prints one copyable publish command; `release` waits again after packing, while `--yes` skips both confirmations.
- Static manifest and tarball-content gates both reject published `src` and `.d.ts.map` while source manifests retain `exports["./src/*"]`.
- The release bundle records the complete package set, commit, derived version, tag, registry, and per-tarball integrity; every internal dependency is pinned exactly to that version, and publish consumes only that bundle without rebuilding.
- An isolated integration test installs from local tarballs and starts the installed default `dsh` TUI under plain Node; deleting any required dynamic chunk makes the test fail deterministically.
- Every published bin and applicable public runtime entry has post-tarball-install execution coverage, with resolution paths proving that no monorepo fallback occurred.
- Publish safely resumes from the same manifest after partial success: matching integrity is skipped, conflicting integrity is rejected, and final verification requires every version and tag to agree.
- A credential-free GitHub Actions job creates and tests the bundle, a protected job uploads the identical bundle, and the publication token exists only in the latter.
## Risks
Full packing, installation, and startup add CI time and workflow-artifact volume. The implementation should cache external dependencies and the pnpm store, but it must not cache or reuse installed workspace output for target packages. Safe consumer probes can run in parallel to reduce latency.
Installing every tarball as a temporary project's top-level dependency can hide an undeclared internal dependency. The test generator should install each tested application's declared recursive closure and retain existing dependency gates. For `@deepseek-ai/dsh`, whose dependency surface approaches the full set, package-manifest and static graph checks remain necessary to detect undeclared edges.
Platform-specific optional dependencies, native addons, PTYs, and browser entries may require platform-owned probes. The first phase must cover primary `dsh` startup on the publication Linux runner and one local macOS path, then expand the matrix with the actual publication platforms. Skipping an unstable probe must not move a production path outside the gate.
Recovery cannot remove npm's partial visibility. During a failed publication, the registry may briefly contain only some package versions from the bundle. Operators and automation must treat the final bundle verification, not one successful `npm publish`, as the baseline-availability signal.

View File

@@ -0,0 +1,114 @@
# Agent Note: 以产物为先的 NPM 基线发布
Status: proposed
[English](2026-08-04-artifact-first-npm-baseline-publication.md) | 中文
## 问题
monorepo 中可运行的源码并不能证明发布后的包可运行。workspace link、TypeScript paths、tsx 源码加载和工作树里残留的 `lib/` 都可能补上发布 tarball 中缺失的文件或依赖。即使现有构建产物测试使用普通 Node,它仍直接读取工作树中的 `lib/`,没有验证 `package.json#files` 最终选中了什么,也没有验证包管理器安装后的文件布局。一次开发模式正常的执行因此可能发布为缺少 bundle chunk、声明文件、配置或资源的包。
发布多个互相依赖的 `@deepseek-ai` 包还会产生集合一致性问题。如果脚本每 pack 一个包就立即 publish,那么后续 pack 或验证失败时,注册表中已经存在无法作为完整基线使用的前半组版本。npm 注册表没有跨包事务,因此这里的「一次性发布」不能承诺原子提交,只能承诺在任何远端写入前完整生成并验证发布集合,再由一个可恢复的编排命令发布这个不可变集合。
当前基线还需要人工在本机完成版本派生、认证、pack、发布和重试。后续 GitHub Actions 工作流必须复用同一套发布包与验证逻辑,不能在批准发布后重新构建另一组未经消费方测试的 tarball。
## 提案
发布流程以一个不可变的 release bundle(发布包集合)为边界。pack 阶段从一个确定的 Git commit 构建全部目标包、生成全部 tarball、检查 tarball 内容,并通过安装后集成测试;publish 阶段只读取这组 tarball 及其 manifest(元数据清单),禁止重建或重新 pack。
目标集合只包含 `packages/*/*/package.json` 与 `apps/*/package.json` 中命名为 `@deepseek-ai/*` 的 workspace 包。根项目、`website/`、vendor、Python 与 native workspace 不属于该 NPM 基线。发现机制必须拒绝重复包名、不同基础版本、意外的 `private` 发布状态以及集合中的未知包,而不是维护另一份手工包名列表。
预发布版本由包的稳定基础版本、命令启动时精确到秒的 UTC 时间戳和目标 commit 的 10 位短 SHA 组成:`<base>-<YYYYMMDDHHmmss>-<short-commit>`。dist-tag 由基础版本派生为 `dev-<base>`。例如,基础版本 `0.0.1`、时间 `2026-08-04T00:32:00Z` 和 commit `909292dd7b` 生成版本 `0.0.1-20260804003200-909292dd7b` 与 tag `dev-0.0.1`。同一 release bundle 的重试必须沿用原版本和 manifest;重新 pack 会按新的命令启动时间生成新版本。
pack 阶段按以下顺序执行:
1. 将 ref 解析成不可变 commit,采集 UTC 时间戳,从该 commit 的根 manifest 派生版本,并显示 commit、时间戳、版本、tag、注册表和输出路径。`pack` 与 `release` 此时都会在昂贵操作开始前等待 Enter;自动化可用 `--yes` 跳过该确认。
2. 在隔离的 detached worktree 中安装 frozen lockfile,并在暂存发布 manifest 之前运行源码 manifest 发布约束;调用方工作树中的未提交文件和旧构建输出不得参与发布。
3. 将所有目标 manifest 暂存为派生版本,移除发布时的 `private` 标记,并把 `dependencies`、`devDependencies`、`optionalDependencies` 与 `peerDependencies` 中的内部 workspace 依赖全部改写为同一精确版本。
4. 完整构建目标 commit,再运行 publint 和已构建包不变式。
5. 为目标集合中的每个包执行 pack,但不执行任何注册表写入。
6. 检查 tarball 内的 package manifest、文件清单、内部依赖版本、包名和版本,并拒绝缺失、重复或额外的 tarball。
7. 生成包含 commit、版本、tag、注册表、每个包的 tarball 路径、SHA-256 与 npm integrity 的 release manifest 和校验和文件。
8. 从本地 tarball 安装一个隔离消费方,运行当前实现已有的安装态产物探测,并将这些探测扩展为下文定义的完整产物平面集成测试矩阵。
9. 仅当整个集合通过时输出一个可直接执行的 publish 命令;pack 命令本身始终保持无远端写入。
本地 `release` 命令组合 pack 与 publish。它先通过上述 pack 确认确定预期时间戳和版本,再在 pack 成功后等待第二次 Enter,随后发布同一 manifest;`release --yes` 跳过两次确认。独立的 `pack` 与 `publish --manifest` 仍是 CI 分 job 和断点恢复使用的基础操作。
## 当前实现边界
已提交的 pack 命令实现了固定 commit 暂存、内部依赖精确固化、静态与 tarball payload 检查、不可变 manifest,以及把每个发布 tarball 都作为本地顶层依赖的隔离 npm 安装。它在输出 publish 命令前,用普通 Node 运行安装后的 `dsh --version` 与 `dsh --dump-default-config` 入口,再在 POSIX PTY 中启动安装后的默认 TUI,等待其 `main-session-` 就绪信号,并通过 `/exit` 退出。Publish 支持按 integrity 恢复,将只读注册表验证与认证身份检查分离,并以完整的远端 integrity 和 dist-tag 验证结束。
拉取请求 CI 不会调用 pack 命令;安装态入口探测属于本地发布检查,而不是合并门禁。免凭据 CI 执行、其他每个 bin 与公开运行时入口的包自有探测、workflow artifact 传递及受保护 publish job 仍属于提案范围。
## 发布 payload 契约
发布包只携带消费方需要的构建产物。`package.json#files` 禁止包含 `src` 和 `lib/types/**/*.d.ts.map`;tarball 内容门禁还要独立确认不存在任何 `package/src/**` 与 `package/**/*.d.ts.map`,避免 manifest pattern 或 pack 行为绕过静态约束。运行时 JS、声明文件 `.d.ts`、配置、资源、worker 文件和 bundle 动态 chunk 必须按实际入口闭包收齐。
源码 manifest 可以保留 `exports["./src/*"]`,供本仓库的源码平面解析使用;该 export 不代表源码会进入发布 payload,也不属于已发布包的消费方契约。静态门禁必须分别检查源码平面与发布 payload,不能通过删除 source export 来掩盖错误的 workspace 解析,也不能通过发布 `src` 来修补缺失的构建产物。
每个 tarball 必须不含 `workspace:` specifier,并且所有指向本次发布集合的内部依赖与对等依赖(peer dependency)都必须精确等于本次派生版本,禁止使用 `^`、`~` 或其他 semver 范围跨越 commit 基线。除了明确仅供源码平面使用的 `exports["./src/*"]`,package manifest 中声明的每个消费方入口都必须指向 tarball 内存在的文件;动态 import、运行时拼接路径和非 export 资源不能只靠 manifest 检查,必须由安装后执行覆盖。
## 产物平面集成测试
集成测试在全部 tarball 生成后、任何 publish 之前运行。它在 monorepo 外创建一个全新临时项目,通过本次 release manifest 中的本地 `.tgz` 文件安装声明依赖闭包,并从安装目录执行。测试必须使用普通 Node 与包管理器生成的 `node_modules`;禁止 tsx、tsconfig paths、workspace link、仓库源码路径、工作树 `lib/` 和已发布注册表中的同版本包参与解析。测试还要断言关键模块与 bin 的真实路径位于临时消费方内。
安装使用本次发布选择的客户端行为。注册表上传必须使用 `npm` CLI,以满足私有注册表只接受 npm 客户端的策略;构建编排仍可使用 pnpm。tarball 测试不得先把这些包发布到真实注册表,也不得在测试后重新 pack。
测试至少覆盖以下执行面:
- `@deepseek-ai/dsh` 安装后的 `dsh --version` 与 `dsh --dump-default-config` 在普通 Node 下成功,分别覆盖静态 CLI 入口和一个动态模式入口。
- 安装后的默认 `dsh` 在 PTY 中完成一次无密钥 TUI 启动,到达既定 ready 信号后由测试受控退出。这条路径必须加载真实 TUI 动态 chunk,因此缺少类似 `lib/tui-*.js` 的发布文件会使门禁失败。
- 每个其他已发布 `bin` 都定义一个不会访问真实服务或修改用户状态的包级冒烟命令。不同 CLI 不强制共用 `--help`;测试必须运行其真实安装入口并检查约定的退出或 ready 信号。
- Node 兼容的公开运行时入口从安装目录加载;浏览器、worker 或必须由宿主协议驱动的入口使用对应的隔离 fixture(测试前置数据),但输入仍只能是本次 tarball。
这些测试验证可执行性,不替代单元测试、快照、真实 API e2e 或 publint。测试 fixture 应复用现有 built-bin 和 PTY 场景的行为断言,但必须把入口改为 tarball 安装结果;直接运行工作树 `lib/bin.js` 的测试不能算作本门禁。
## 发布与恢复
publish 命令先验证 release manifest、所有本地校验和、目标注册表、`npm ping` 和 `npm whoami`,再按确定顺序上传 tarball。命令只接受 pack 阶段生成的 manifest,不接受 workspace 目录作为发布输入。默认注册表为 `https://registry.npm.harnessment.com/`,每次 publish 都显式传入注册表和派生 tag,避免用户级 `.npmrc` 改变目标。
npm 不提供多包原子事务,上传仍会逐包发生。编排器通过幂等恢复缩小失败面:若远端不存在 `<name>@<version>` 就上传;若已存在且 integrity 与 release manifest 相同就跳过;若已存在但内容不同就立即失败。dist-tag 检查只读取 tag 映射,不解析默认 tag 指向的版本,因此即使无关 tag 指向的版本已不存在,也不会阻断恢复。完成后必须逐包确认版本 integrity 和 dist-tag 都指向本次版本,只有整个集合通过最终验证,工作流才报告发布成功。
如果 pack、tarball 检查或安装后集成测试失败,注册表必须保持零写入。如果 publish 在部分上传后失败,操作者使用同一 release manifest 重跑 publish 命令来恢复,不得重新 pack 并生成另一个时间戳版本来代替恢复。修复代码或改变构建输入后需要不同 tarball 时,才重新执行完整 pack 与测试。
## GitHub Actions 集成
GitHub Actions 分为无凭据的 pack-and-test job 与受保护的 publish job。前者检出精确 commit,调用与本地相同的 pack 入口,运行 tarball 消费方测试,并上传完整 release bundle 作为 workflow artifact。后者依赖前者成功,从 workflow artifact 下载 bundle、重新校验 manifest 和校验和,再调用同一个 publish 入口;它不能检出后重新构建。
PR 与普通 push 可以运行无凭据的 pack-and-test 信号,从而在合并前发现 payload 回归。实际私有注册表发布先通过 `workflow_dispatch` 提供,输入只包括目标 ref;UTC 时间戳由 pack job 生成,基础版本、短 SHA、tag、注册表和包清单都由仓库状态或受版本控制的配置派生。稳定发布触发方式不在本基线提案范围内。
注册表 token 只注入 publish job,并由受保护 GitHub Environment 控制人工批准、允许的分支或 tag 以及并发。pack-and-test job 不得读取发布凭据。workflow artifact 的保留期可以较短,但 publish job 必须使用同一 workflow run 生成的 bundle,不能按版本号从不受信任的位置寻找 tarball。
## Alternatives considered
**从 workspace 直接递归 publish。** 不采用,因为命令会把 pack 与注册表写入交错,无法在第一次写入前证明整个集合完整,也容易让 workspace 解析与调用方工作树状态影响发布结果。
**只测试工作树中构建后的 `lib/`。** 不采用,因为这验证的是构建树,不是 `package.json#files` 选出的 tarball。工作树中存在而 tarball 中漏掉的动态 chunk 正是本提案必须捕获的失败。
**只运行 `dsh --help`。** 不采用,因为 Commander 可以在加载 TUI、Web 或 headless 动态入口之前输出帮助并退出。它无法证明默认生产启动路径完整。
**把 `src` 和声明映射一起发布以降低漏文件风险。** 不采用,因为源码平面不是生产运行时的后备路径;扩大 payload 会掩盖 bundle 闭包错误,并把本地调试产物变成无意的发布契约。
**要求真正的跨包原子发布。** 不采用,因为 npm 注册表没有相应事务。不可变 release bundle、发布前全量验证、integrity 比对与幂等恢复提供可实现的边界,同时明确保留部分上传短暂可见的限制。
**在批准后由发布 job 重新构建。** 不采用,因为测试通过的 tarball 与实际上传的 tarball 将不再具有内容身份。workflow artifact 与校验和必须把测试输入直接传给发布步骤。
## Acceptance criteria
- 一个 pack 入口从确定 commit 发现 `packages/*/*` 和 `apps/*` 的全部目标包,以 UTC 秒级时间戳与短 commit 生成并显示版本,再等待 Enter;它在任何注册表写入前生成完整 release bundle,并输出一个可复制的 publish 命令;`release` 在 pack 后再次等待,`--yes` 跳过两次确认。
- 静态 manifest 门禁和 tarball 内容门禁都拒绝发布 `src` 与 `.d.ts.map`,同时保留源码 manifest 中的 `exports["./src/*"]`。
- release bundle 记录完整包集合、commit、派生版本、tag、注册表和逐 tarball integrity;所有内部依赖都精确固化到该版本,publish 只消费该 bundle,绝不重建。
- 一个隔离集成测试从本地 tarball 安装消费方,并用普通 Node 启动安装后的默认 `dsh` TUI;删除任一所需动态 chunk 会使该测试稳定失败。
- 所有已发布 bin 和适用的公开运行时入口都有 tarball 安装后的执行覆盖,且解析路径证明没有回退到 monorepo。
- publish 可在部分成功后用同一 manifest 安全重跑;相同 integrity 被跳过,不同 integrity 被拒绝,最终验证要求所有版本与 tag 一致。
- GitHub Actions 的无凭据 job 生成并测试 bundle,受保护 job 上传完全相同的 bundle,发布 token 只存在于后者。
## Risks
全量 pack、安装和启动会增加 CI 时间与 workflow artifact 体积。实现应缓存外部依赖和 pnpm store,但不得缓存或复用目标包的已安装 workspace 输出;并行执行安全的消费方 probe 可以降低时延。
把所有 tarball 都安装为临时项目的顶层依赖可能掩盖未声明的内部依赖。测试生成器应按被测应用的声明式递归闭包安装,并结合现有依赖门禁;对依赖面接近全集的 `@deepseek-ai/dsh`,仍需依靠 package manifest 与静态图检查发现未声明边。
不同平台的 optional dependency、native addon、PTY 与浏览器入口可能需要平台专属 probe。第一阶段至少在发布所用 Linux runner 和一个本地 macOS 路径上覆盖主 `dsh` 启动,后续矩阵按实际发布平台扩展;不能用跳过不稳定 probe 的方式把生产路径移出门禁。
恢复机制不能消除 npm 的部分可见性。发布失败期间,注册表可能短暂含有本次版本的一部分包;操作者与自动化必须以最终 bundle 验证结果而非单个 `npm publish` 的成功作为基线可用信号。

View File

@@ -3,4 +3,4 @@
# 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-surface.md
2026-07-04-prune-dead-core-spine-surface.md: 473f655ab0944b43f9b1193413801eb7a80286d8
2026-07-04-prune-dead-core-spine-surface.zh.md: 00c6f7acca403c937ff765b6d73d3d0fa0c3153a
2026-07-04-prune-dead-core-spine-surface.zh.md: 3389c0161efa78252c9810d64f46d0cd8a7a034c

View File

@@ -6,26 +6,26 @@ Status: proposed
## 问题
若干包(package)根导出、结果字段和便利方法没有生产消费方。它们之所以存活,要么是因为测试通过公开入口导入了内部实现,要么是因为某个类型预期了一个从未出现的调用者。每一项单独看都很小,但合在一起,它们扩大了 SDK 契约、生成的 catalog、文档和回归矩阵,却没有支撑任何已交付的路径。
若干包根导出、结果字段和便利方法没有生产消费方。它们之所以存活,要么是因为测试通过公开入口导入了内部实现,要么是因为某个类型预期了一个从未出现的调用者。每一项单独看都很小,但合在一起,它们扩大了 SDK 契约、生成的 catalog、文档和回归矩阵,却没有支撑任何已交付的路径。
生产语料库是 `packages/*/*/src`、示例源码/配置和运行时脚本。测试、包 README 和 Agent Note(agent 决策记录)行文是发布的证据,但不是固定调用者。`cordis_inspect` 使 `packages/cordis/tool-cordis/src/api-catalog.ts` 对模型可见,`cordis_mount` 可以通过受保护的真实服务代理调用注入的服务,因此 catalog 中的服务方法和返回形状是真正的动态产品接口。下表因此区分「没有固定的仓库调用者」与「不可达」:涉及 catalog 词汇的行有意收缩模型编写的 mount 能发现和调用的内容,而包根实现辅助函数并不通过该服务门面可达。精确符号搜索得出以下清单:
生产语料库是 `packages/*/*/src`、示例源码/配置和运行时脚本。测试、包 README 和 Agent Note 行文是发布的证据,但不是固定调用者。`cordis_inspect` 使 `packages/cordis/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)定义为私有。 | 保留默认插件类/配置契约;移除重复的命名类导出,将协议模块保持为源码私有。 |
| `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 词汇改为源码私有。 |
| 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 测试。保持共享凭据正则公开。 |
| `depthOf`、`SubagentDepthError`、`waitForExit` 与 `exitsWithin` 根导出 | 生产 subagent 后端消费的是进程内 runner 和子进程构造/dispose(资源释放)辅助函数,而非这些强制机制和测试内部实现。`SENSITIVE_ENV_PATTERN` 不在其中,因为 SDK helper 会将它应用于调用方传入的环境。 | 保留深度与退出行为,但将剩余辅助函数和 error 改为源码私有;通过 spawn 和 dispose 测试。保持共享凭据正则公开。 |
| `PersistenceCoordinator.inits`、后端 `inits` 访问器、`seedCoversPrefix` 与 `assertSerializable` | 访问器为白盒测试而存在;`seedCoversPrefix` 没有包外生产导入者;`assertSerializable` 没有生产调用者,且与 coordinator append 边界的无损快照重复。 | 通过 `session/flush` 观察初始化,将 `seedCoversPrefix` 改为源码私有,删除 `assertSerializable`。保留两个后端、`SessionHeader` 和 SQLite 的版本契约。 |
| `LlmError.status` 与回放 status | 适配器/回放填充它,但生产分支基于稳定的错误码/消息判断,从不读取原始 status。 | 移除未读字段和回放管道,保留错误分类。 |
| `BlockAssembler.push()` 返回值 | 两个生产调用者都忽略返回的已完成块。 | 返回 `void`;保留有意公开的 `blocks()`/`message()` 契约。 |
| `compactRegion` 的独立 `session` 参数 | 固定调用者传入的对象与 `agent.session` 上已有的是同一个;模型可见的 mount API 也能调用该方法,但接受两个身份允许挂载的插件提供不一致的配对。 | 保留手动 region seam,同时有意将其收窄为以 `agent.session` 为唯一真源。 |
| `compactRegion` 的独立 `session` 参数 | 固定调用方传入的对象就是 `agent.session` 中已有的对象;模型可见的 mount API 也可以调用该方法,但同时接受两个独立对象,会让挂载的插件传入不一致的组合。 | 保留手动 region seam,同时有意将其收窄为以 `agent.session` 为唯一真源。 |
| `CompactionResult.startSeq`、`summarySeq`、`endSeq` 与 `summary` | 生产消费方只读取 shadowed range/seq/token 统计;持久日志拥有 summary 和事件标识。 | 移除四个结果回显,保留两个共享的 transcript(文本记录)渲染器。 |
| `BasicCompactService` 的 estimation/summarization 可见性 | 没有包外生产调用者调用这五个方法;已实现的 Agent Note 只将 `estimateContentTokens()` 和 `summarize()` 命名为子类钩子。 | 将这两个方法改为 `protected`,其余三个编排专用的估算器改为 private。 |
| `BasicCompactService` 的估算/摘要方法可见性 | 没有包外生产调用者调用这五个方法;已实现的 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` | 每个存储的公开值都没有生产读取者。 | 移除未读字段,保留错误消息、已解析的配置行为和任务生命周期。 |

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
# pnpm run verify-translation-pairing --write .agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.md
2026-07-19-make-jsonrpc-directional.md: 910b4988aca34dec499b2e34cb2a42042c81b0cb
2026-07-19-make-jsonrpc-directional.zh.md: acc1b433e14c65da2ed37dff7a5fe181ebee36ad
2026-07-19-make-jsonrpc-directional.zh.md: 9810cf758faa078583955ba541e2b2fe9ad9c855

View File

@@ -6,7 +6,7 @@ Status: proposed
## 问题
JSON-RPC 桥接层把两个端点都建模为对称的对等端,但实际协议具有固定方向。共享传输层(现为 `dsh-sdk-protocol`,由服务端与 TypeScript SDK 客户端共用,后者行使出站请求/入站通知方向)仍实现着没有任何端点使用的两个半边:服务端发起的请求与客户端发起的通知。Python SDK 发送请求并接收响应或通知,却还会把未使用的服务端入站请求放入队列,并公开响应辅助方法。
JSON-RPC 桥接层把两个端点都建模为对称的对等端,但实际协议具有固定方向。共享传输层(现为 `dsh-sdk-protocol`,由服务端与 TypeScript SDK 客户端共用,后者行使出站请求/入站通知方向)仍实现着没有任何端点使用的两个半边:服务端发起的请求与客户端发起的通知。Python SDK 发送请求并接收响应或通知,却还会把来自服务端、但未使用的入站请求放入队列,并公开响应辅助方法。
`session/prompt` 还会用两种协议结构报告同一个已结束轮次。服务端先发出 `session.finished`,再返回常量 `{ accepted: true }`;Python SDK 丢弃该响应,转而等待通知以取得状态。响应只有在处理函数返回后才会写入,因此在同一条有序流上,通知必然先于这个常量响应。
@@ -16,12 +16,12 @@ JSON-RPC 桥接层把两个端点都建模为对称的对等端,但实际协
按实际角色收窄两个端点。服务端保留入站请求、出站响应和出站通知;TypeScript 与 Python 客户端保留出站请求以及入站响应或通知。删除没有任何端点使用的方向——服务端发起的请求与客户端发起的通知。
在 `agent.whenIdle()` 完成后,由 `session/prompt` 直接返回 `{ status, reason }` 作为轮次结果。删除 `session.finished`、常量接纳响应以及 Python 中响应后的完成等待循环。`session.event` 与 subagent 通知仍在响应前流式发出,持久会话事件仍是最终响应重建的真源。
在 `agent.whenIdle()` 完成后,由 `session/prompt` 直接返回 `{ status, reason }` 作为轮次结果。删除 `session.finished`、常量接纳响应以及 Python 中响应后的完成等待循环。`session.event` 与 subagent 通知仍在响应前流式发出,持久化会话事件仍是最终响应重建的真源。
## 实施计划
1. 在 `packages/ui/jsonrpc/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` 不变。
2. 在 `packages/sdk/sdk-protocol/src/transport.ts` 中,把共享类收窄到有消费者的方向——入站请求/出站响应(服务端)与出站请求/入站响应加入站通知(TypeScript SDK 客户端)——只删除服务端发起的 `request()` 用法与客户端发起的通知分发,或把该类拆分为服务端与客户端两个传输。请求结果、方法不存在与处理器错误响应保持原有行为,并继续排在被等待处理器发出的通知之后。
1. 在 `packages/ui/jsonrpc/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` 不变。
2. 在 `packages/sdk/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()` 重建最终助手消息之前被收集。
5. 用按方向的覆盖替换 `packages/sdk/sdk-protocol/tests/transport.spec.ts` 中的对称传输对用例,并更新 `server.spec.ts`、`plugin-apply.spec.ts` 和 `built-scope-carrier.e2e.ts`,覆盖直接结果、顺序、重叠、关闭和收窄后的伪实现;同步更新 TypeScript SDK 客户端(`packages/sdk/sdk-client`)及其套件以采用基于响应的结束流程。更新 `python/sdk/tests/test_client.py`,覆盖基于响应的结束流程、意外请求帧处理、回调与并发行为,以及已删除的公开辅助方法。同步更新 JSON-RPC README、双语 Python SDK README、导出 JSDoc 与声明、`scripts/smoke-python-runtime.py` 和 Python 单可执行文件快照。

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
# pnpm run verify-translation-pairing --write .agents/notes/proposed/testing/2026-06-11-deterministic-and-stress-testing.md
2026-06-11-deterministic-and-stress-testing.md: d9977be835af05f9ee303b63ec6015bc9e153170
2026-06-11-deterministic-and-stress-testing.zh.md: eff9eecb699344dff388bafec270f6b6677f71ee
2026-06-11-deterministic-and-stress-testing.zh.md: 263e69f85a1cd8ee47da07210e513cab272d1a44

View File

@@ -1,4 +1,4 @@
# Agent Note: 确定性测试、回放不变式 fixture(测试前置数据)与竞态压力测试
# Agent Note: 确定性测试、回放不变式 fixture 与竞态压力测试
Status: proposed
@@ -13,8 +13,8 @@ Status: proposed
三项措施:
1. **测试中禁止挂钟睡眠。** 将 `setTimeout(N)` 等待替换为事件驱动等待(既有的 `waitForIdle` 模式,扩展为 `waitForStatus`、`waitForEvent(n)`),或在需要测试时间本身时使用 vitest 的 fake timer。通过 lint 规则禁止 `setTimeout`,适用范围是 `packages/*/tests`,白名单辅助模块除外。
2. **通用回放 fixture。** 一个共享测试辅助函数包装 agent loop harness,使每个测试结束后,agent 的会话日志被回放到一个全新的 Session 中,并自动断言 `deriveMessages()` 相等。这样该不变式在每次 CI 运行中会被套件产生的所有场景检查数百次,而非仅两次。
3. **夜间竞态压力测试。** 一个 CI job 以 `vitest --repeat=200`(加 `--shuffle`)运行 agent-loop 和 inbox 套件,以暴露调度依赖的失败;发现的任何不稳定测试都视为 bug 修复,绝不靠重试掩盖。
2. **通用回放 fixture(测试前置数据)。** 一个共享测试辅助函数包装 agent loop harness,使每个测试结束后,agent 的会话日志被回放到一个全新的 Session 中,并自动断言 `deriveMessages()` 相等。这样该不变式在每次 CI 运行中会被套件产生的所有场景检查数百次,而非仅两次。
3. **夜间竞态压力测试。** 一个 CI job 以 `vitest --repeat=200`(加 `--shuffle`)运行 agent-loop 和 inbox 套件,以暴露调度依赖的失败;发现的任何不稳定现象都视为需要修复的 bug,绝不靠重试掩盖。
## 计划
@@ -24,7 +24,7 @@ Status: proposed
- 不再使用 `setTimeout`;lint 规则在 `packages/*/tests` 中强制执行,白名单辅助模块除外。
- 共享 harness 将每个测试的会话日志回放到全新的 `Session` 中,并自动断言 `deriveMessages()` 相等,覆盖整个套件。
- 夜间 job 以 `--repeat` 和 `--shuffle` 运行 agent-loop 和 inbox 套件;发现的不稳定测试作为 bug 分诊,绝不靠重试掩盖。
- 夜间 job 以 `--repeat` 和 `--shuffle` 运行 agent-loop 和 inbox 套件;发现的不稳定现象一律按 bug 分诊,绝不通过重试消除。
## 风险

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
# pnpm run verify-translation-pairing --write .agents/notes/proposed/testing/2026-06-11-mutation-testing.md
2026-06-11-mutation-testing.md: 591d9012644a19ee2c67a916b63092d79f78db1f
2026-06-11-mutation-testing.zh.md: 9c22ed2f42e5c44e6be98f132614886bbdb188fd
2026-06-11-mutation-testing.zh.md: ebbeb561bd7bfee659882d25c808f2cb9a1aa825

View File

@@ -14,23 +14,23 @@ Status: proposed
- **PR(Pull Request)范围的增量运行**(仅变更文件),作为一个 CI job。调优后速度足以作为合并门禁。
- **每夜全量运行**,跟踪变异分数;先记录基线,再将阈值设为观测到的基线并只升不降(与覆盖率策略一致:阈值只收紧)。
- 存活的变异体是待办项:agent 选取一个存活体、编写杀死它的测试、循环往复——一个形态良好的自主循环。
- 存活的变异体是待办项:agent 选取一个存活体、编写杀死它的测试、循环往复——一个适合自主执行的闭环。
- 等价变异体(可证明不改变行为的)加注释排除并附理由,与 `/* v8 ignore */` 策略一致。
## 计划
1. 添加 Stryker 配置,范围限定在一个包(package),即 llm(最小、最具算法性),并测量运行时间。
1. 添加 Stryker 配置,范围限定在一个包,即 llm(最小、最具算法性),并测量运行时间。
2. 扩展到所有包;在配置中记录基线分数。
3. 接入每夜 job;运行时间可接受后再添加 PR 范围的增量 job。
## 验收标准
- Stryker 配置在 `packages/*/src` 上以 vitest runner 运行;每夜 job 记录变异分数,当分数低于记录的基线时,通过只升不降的阈值使运行失败。
- PR 范围的增量运行在运行时间可接受后作为合并门禁;或者明确保持仅每夜运行,并将该结论记录于此。
- Stryker 配置在 `packages/*/src` 上以 vitest runner 运行;每夜 job 记录变异分数并采用只升不降的阈值,分数低于已记录的基线时任务失败。
- PR 范围的增量运行在运行时间可接受后作为合并门禁;否则明确决定只保留每夜运行,并将该结论记录于此。
- 等价变异体带有注释排除及理由,与 `/* v8 ignore */` 策略一致。
## 风险
运行时间:变异测试开销大;逐文件 100% 覆盖率有所帮助(每个变异体至少会被执行到)。如果 PR 范围的运行始终过慢,则保持仅每夜运行,依赖分数只升不降的机制。
运行时间:变异测试开销大;逐文件 100% 覆盖率有所帮助(每个变异体至少会被执行到)。如果 PR 范围的运行始终过慢,则保持仅每夜运行,依靠变异分数阈值的只升不降机制。
<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) -->