Merge branch 'worktree/ci-native-windows-20260808' into worktree/ci-native-windows-coverage-20260808

This commit is contained in:
Tianyi Cui
2026-08-09 16:47:17 +08:00
376 changed files with 9620 additions and 1108 deletions

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/implemented/feature/2026-06-21-subagent-capability-seam.md
2026-06-21-subagent-capability-seam.md: 752e639b09ea2ac0ba19841ddfe38b15b44d22e9
2026-06-21-subagent-capability-seam.zh.md: 6009aeda6773357a4217f8956fb510c2116bbe3f
2026-06-21-subagent-capability-seam.zh.md: f0b0ba2ab8b0bb7b611e995c56e30db68b21cfa7

View File

@@ -1,37 +1,37 @@
# Agent Note: Subagent 能力 seam
# Agent Note: subagent 能力 seam
Status: implemented
[English](2026-06-21-subagent-capability-seam.md) | 中文
> 完整 seam 已交付:`dsh-subagent` 接口与 `dsh-tool-subagent` 消费方;两个进程内后端(`dsh-subagent-spawn`、`dsh-subagent-fork`);嵌套 agent 快照基础设施([逐会话快照回放](../testing/2026-06-22-subagent-snapshot-replay.md));以及进程外的 ACP、Codex 与 Claude Code 后端([ACP Agent Note](2026-06-22-acp-subagent-backend.md)、[产品提供方 Agent Note](2026-08-04-claude-code-and-codex-subagent-backends.md))。
> 完整 seam 已交付:`dsh-subagent` 接口与 `dsh-tool-subagent` 消费方;两个进程内后端(`dsh-subagent-spawn`、`dsh-subagent-fork`);嵌套 agent(智能体)快照基础设施([逐会话快照回放](../testing/2026-06-22-subagent-snapshot-replay.md));以及进程外的 ACP(Agent Client Protocol)、Codex 与 Claude Code 后端([ACP Agent Note](2026-06-22-acp-subagent-backend.md)、[产品提供方 Agent Note](2026-08-04-claude-code-and-codex-subagent-backends.md))。
## 问题
harness 有一个长期搁置的 seam 用于 **subagent**:一个 agent(智能体)将工作委派给另一个 agent。这一意图在 `Agent`/`AgentLoop` 接口中已有草案([packages/core/agent/src/types.ts](../../../../packages/core/agent/src/types.ts)、[packages/core/agent-loop/src/index.ts](../../../../packages/core/agent-loop/src/index.ts)):一个创建选项引用父 agent(fork = 用父会话的事件日志初始化子会话;spawn = 全新会话),子 agent 以 `Agent` 句柄返回,使 steering(中途引导)和事件订阅可以统一工作。本 Agent Note 实现了这个 seam;上方横幅列出了已交付的内容。
harness 有一个长期搁置的 seam 用于 **subagent**:一个 agent 将工作委派给另一个 agent。这一意图在 `Agent`/`AgentLoop` 接口中已有草案([packages/core/agent/src/types.ts](../../../../packages/core/agent/src/types.ts)、[packages/core/agent-loop/src/index.ts](../../../../packages/core/agent-loop/src/index.ts)):一个创建选项引用父 agent(fork = 用父会话的事件日志初始化子会话;spawn = 全新会话),子 agent 以 `Agent` 句柄返回,使 steering(中途引导)和事件订阅可以统一工作。本 Agent Note 实现了这个 seam;上方横幅列出了已交付的内容。
决定整体设计走向的核心需求是:**多种 subagent 实现必须在运行时共存**。一个父 agent 可能在同一个会话中既需要一个廉价的进程内子 agent 处理有限范围的子任务,又需要一个隔离的进程外子 agent(通过 ACP(Agent Client Protocol))。我们预见的传输方式:
决定整体设计走向的核心需求是:**多种 subagent 实现必须在运行时共存**。一个父 agent 可能在同一个会话中既需要一个廉价的进程内子 agent 处理有限范围的子任务,又需要一个隔离的进程外子 agent(通过 ACP)。我们预见的传输方式:
- **进程内**:在同一个 `Context` 上创建一个具体的子 `Agent`(最廉价,且鉴于现有 agent 工厂几乎零成本);
- **ACP**:作为 ACP *客户端*驱动另一个 agent 进程(可以是自身的另一个实例);
- **Codex app-server 与 Claude Code Agent SDK**:当前的一次性兄弟提供方,将同一个命名提供方 seam 应用于官方产品进程([产品提供方 Agent Note](2026-08-04-claude-code-and-codex-subagent-backends.md));
- **Codex app-server 与 Claude Code Agent SDK**:当前的一次性同类提供方,将同一个命名提供方 seam 应用于官方产品进程([产品提供方 Agent Note](2026-08-04-claude-code-and-codex-subagent-backends.md));
- 后续:**A2A**,采用同样的进程外形态:「启动子 agent、发送提示词、结算、取消」。
## 曾考虑的替代方案
### 为何不采用 bash seam 的形状
bash seam([能力 seam](../architecture/2026-06-13-capability-seams.md))在每个上下文中只注册恰好一个 `BashExecutor`;加载第二个会抛异常。这对 bash 是正确的(一台机器、一种执行命令的方式),但对这里是错误的:共存才是需求。因此 subagent 服务是一个**命名提供方注册表**——每个实现以唯一名称注册,调用方按名称选择——镜像 **LLM(大语言模型)适配器注册表**(`LlmService.registerAdapter`),而非单服务的 bash 执行器。seam 仍然是由三个包构成的结构(接口 / 实现 / 消费方);只是「一个 vs. 多个实现」这个维度不同。
bash seam([能力 seam](../architecture/2026-06-13-capability-seams.md))在每个上下文中只注册恰好一个 `BashExecutor`;加载第二个会抛异常。这对 bash 是正确的(一台机器、一种执行命令的方式),但对这里是错误的:共存才是需求。因此 subagent 服务是一个**命名提供方注册表**——每个实现以唯一名称注册,调用方按名称选择——镜像 **LLM(大语言模型)适配器注册表**(`LlmService.registerAdapter`),而非单服务的 bash 执行器。seam 仍然是由三个包构成的结构(接口、实现、消费方);只是「一个 vs. 多个实现」这个维度不同。
## 决策
### 由三个包构成的 seam
新建包(package)组 `packages/subagent/`:
新建包组 `packages/subagent/`:
| 包 | 角色 |
|---|---|
| `@deepseek-ai/dsh-subagent` | 接口:`SubagentService`(`ctx.subagents`)、`SubagentProvider`、`SubagentRun`、请求/结果/能力词汇、`subagent/*` 事件 |
| `@deepseek-ai/dsh-subagent` | 接口:`SubagentService`(`ctx.subagents`)、`SubagentProvider`、`SubagentRun`、请求、结果、能力词汇、`subagent/*` 事件 |
| `@deepseek-ai/dsh-subagent-spawn` | 实现:通过 `ctx.agents.create` 创建全新的进程内子 agent |
| `@deepseek-ai/dsh-subagent-fork` | 实现:用父 agent 日志快照初始化的进程内子 agent |
| `@deepseek-ai/dsh-subagent-acp` | 实现:作为 ACP 客户端驱动已配置的子进程 |
@@ -45,8 +45,8 @@ bash seam([能力 seam](../architecture/2026-06-13-capability-seams.md))在
### 两类可选能力,两种发现方式
- **启动时功能**(`outputSchema`、`depthLimit`、`toolFilter`、`persona`)挂在静态的 `provider.capabilities` 描述符上。服务在委派之前检查每个被请求的功能,如果提供方不支持则**大声拒绝**(`SubagentError('UNSUPPORTED_CAPABILITY')`),绝不接受后静默忽略。这些功能必须在 run 存在之前检查,因此不能是运行时方法。
- **可继续创建**使用可选的 `SubagentProvider.prepareContinuable` 方法;方法是否存在本身即为能力,TypeScript 类型收窄即为发现机制,因此不需要可能与实现失同步的独立 flag。继续执行管理器直接通过 `AgentHandle` 负责后续投递与从持久化存储恢复,而一次性 `SubagentRun` 没有 steering 或 resume 操作,具体由[可继续 subagent](2026-07-28-continuable-subagent-conversations.md)细化。
- **启动时功能**(`outputSchema`、`depthLimit`、`toolFilter`、`persona`)挂在静态的 `provider.capabilities` 描述符上。服务在委派之前检查每个被请求的功能,如果提供方不支持则**明确拒绝**(`SubagentError('UNSUPPORTED_CAPABILITY')`),绝不接受后静默忽略。这些功能必须在 run 存在之前检查,因此不能是运行时方法。
- **可继续创建**使用可选的 `SubagentProvider.prepareContinuable` 方法;方法是否存在本身即为能力,TypeScript 类型收窄即为发现机制,因此不需要可能与实现失同步的独立 flag。继续执行管理器直接通过 `AgentHandle` 负责后续投递与冷恢复,而一次性 `SubagentRun` 没有 steering 或 resume 操作,具体由[可继续 subagent](2026-07-28-continuable-subagent-conversations.md) 细化。
### Fork 与 fresh 是独立后端,而非一个 flag
@@ -58,7 +58,7 @@ bash seam([能力 seam](../architecture/2026-06-13-capability-seams.md))在
### 同步收集(首版)
`dsh-tool-subagent` 将其执行信号传给 `start()`,等待子 agent 结果,并在报告前 dispose 该 run。非完成态的结果变为错误结果,而非成功的部分输出;相互独立的结果与 dispose rejection 会保留两项 diagnostic。
`dsh-tool-subagent` 将其执行信号传给 `start()`,等待子 agent 结果,并在报告前 dispose 该 run。非完成态的结果变为错误结果,而非成功的部分输出;结果与 dispose 的拒绝相互独立,且两项诊断信息都会保留。
### 提供方选择是配置,不面向模型
@@ -66,7 +66,7 @@ bash seam([能力 seam](../architecture/2026-06-13-capability-seams.md))在
## 测试
注册表与工具测试仅用包内脚本化提供方替换非确定性的子进程边界,同时运行真实的 `SubagentService`、生命周期、任务集成和面向模型的工具。提供方与消费方的 export 形状仍保留 Loader 回归覆盖,以防止[事后分析 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md) 中描述的失败。注册表测试覆盖重载安全性、重名和启动时能力拒绝;嵌套 agent 场景通过[逐会话快照回放](../testing/2026-06-22-subagent-snapshot-replay.md)进行无密钥回放;进程内后端还有真实循环的单元测试和带密钥的 e2e 测试。
注册表与工具测试仅用包内脚本化提供方替换非确定性的子进程边界,同时运行真实的 `SubagentService`、生命周期、任务集成和面向模型的工具。提供方与消费方的 export 形状仍保留 Loader 回归覆盖,以防止[事故复盘(postmortem)0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md) 中描述的失败。注册表测试覆盖重载安全性、重名和启动时能力拒绝;嵌套 agent 场景通过[逐会话快照回放](../testing/2026-06-22-subagent-snapshot-replay.md)进行无密钥回放;进程内后端还有真实循环的单元测试和带密钥的 e2e 测试。
## 后果

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/implemented/feature/2026-07-24-model-facing-session-query-tools.md
2026-07-24-model-facing-session-query-tools.md: 863f557f11f89ff8dfc121b7da0b653852528394
2026-07-24-model-facing-session-query-tools.zh.md: 2acbf44b42aafd2eac37903e1acbefc5ba098377
2026-07-24-model-facing-session-query-tools.zh.md: 584733de7b00f22f186838b058a24c31ff035905

View File

@@ -6,11 +6,11 @@ Status: implemented
## 问题
统一的 `ctx.sessionQuery` 服务对优先使用实时数据的会话日志提供精确读取、过滤、关系追踪与全文搜索,但模型无法直接使用该服务。若把提供方请求类型交给模型,还会暴露不稳定的分页游标、受信任的语料范围、存储形态的时间值,以及更适合程序化消费者而非模型推理的结果记录。大型追踪与事件负载另有输出大小问题,但若在该消费者内部解决,就会重复 harness 的通用 spill 机制,并使会话查询工具与其他工具的行为不一致。
统一的 `ctx.sessionQuery` 服务对优先使用实时数据的会话日志提供精确读取、过滤、关系追踪与全文搜索,但模型无法直接使用该服务。若把提供方请求类型交给模型,还会暴露不稳定的分页游标、受信任的语料范围、存储形态的时间值,以及更适合程序化消费方而非模型推理的结果记录。大型追踪与事件负载另有输出大小问题,但若在该消费方内部解决,就会重复 harness 的通用 spill 机制,并使会话查询工具与其他工具的行为不一致。
## 决策
`@deepseek-ai/dsh-tool-session-query` 是 `ctx.sessionQuery` 面向模型的消费者。它注册五个职责单一的只读工具:`session_search`、`session_event_search`、`session_trace`、`session_event_trace` 和 `session_event_read`。该包依赖接口而非 SQLite 实现,负责模型参数校验与易读文本渲染,并贡献一个精简的提示词段,说明历史搜索以及从搜索转向追踪/读取的工作流。
`@deepseek-ai/dsh-tool-session-query` 是 `ctx.sessionQuery` 面向模型的消费方。它注册五个职责单一的只读工具:`session_search`、`session_event_search`、`session_trace`、`session_event_trace` 和 `session_event_read`。该包依赖接口而非 SQLite 实现,负责模型参数校验与易读文本渲染,并贡献一个精简的提示词段,说明历史搜索以及从搜索转向追踪/读取的工作流。
该包入口仅作为配置、提示词注册与工具注册的公开组合根。内部模块沿执行边界划分:`input.ts` 负责模型 schema、规范化与过滤条件构造;`service-boundary.ts` 包含提供方调用与面向模型的安全错误转换;`workspace-access.ts` 负责调用者身份、工作区授权、标题访问与谱系投影;`operations.ts` 编排五个服务工作流;`presentation.ts` 渲染工具结果与调用卡片。这样可让策略留在其所属层,同时不改变包约定。
@@ -20,7 +20,7 @@ Status: implemented
## 工作区权限
每个执行器都从不可变的 `ToolExecution.exec.agent` 身份推导调用者,绝不接受模型提供的工作区。只有当目标观测中的 `cwd` 与调用者会话的 `cwd` 完全相同时,目标才获授权。跨会话搜索始终附加该工作区过滤条件。直接操作先预检目标,然后在渲染负载前,校验与每一页事件搜索结果、事件追踪、事件读取、谱系目标或折叠标题来自同一服务观测的会话头。这样,即使实时或持久化目标在检查与使用之间被替换,也无法跨越工作区边界。谱系渲染在遇到未授权的祖先或后代子树时停止,且不泄露被隐藏的会话 id。调用者会话没有 `cwd` 时只能检查自身会话;缺少 agent 身份时按失败关闭处理。
每个执行器都从不可变的 `ToolExecution.exec.agent` 身份推导调用者,绝不接受模型提供的工作区。只有当目标观测中的 `cwd` 与调用者会话的 `cwd` 完全相同时,目标才获授权。跨会话搜索始终附加该工作区过滤条件。直接操作先预检目标,然后在渲染负载前,校验与每一页事件搜索结果、事件追踪、事件读取、谱系目标或折叠标题来自同一服务观测的会话头。这样,即使实时或持久化目标在检查与使用之间被替换,也无法跨越工作区边界。谱系渲染在遇到未授权的祖先或后代子树时停止,且不泄露被隐藏的会话 id。调用者会话没有 `cwd` 时只能检查自身会话;缺少 agent(智能体)身份时按失败关闭处理。
每个受信任的 `ctx.sessionQuery` 调用都会经过同一个模型边界净化器。它首先检查执行信号,准确保留调用者取消。对于其他失败,它会尽力把可获得的语料或提供方诊断链写入内部日志;当值无法安全检查时,则改用固定占位符。诊断格式化与错误分类各自受到保护,因此无法打印的嵌套 cause 既不会逃逸,也不会阻止对外层错误进行安全分类;分类不安全或日志记录失败时,则返回固定的通用错误码 `SESSION_QUERY_TOOL_FAILED` 及其消息。逐标题失败也会先经过同一个净化器,再转为不可用标记。工具自身的输入校验与授权错误在该服务边界之外创建,因此仍保留精确消息。
@@ -28,11 +28,11 @@ Status: implemented
## 无游标结果与 spill
两个搜索工具都不向模型公开游标、偏移量、页大小或模型可控的结果限制。一次执行会在观察到的代保持有效时持续跟随提供方游标,并收集不超过配置项 `maxSearchResults` 的结果,其默认值为 100。达到上限的结果会要求模型缩小查询或过滤范围;代发生变化时会报告必须重试完整搜索。搜索执行通过工具截止时间与服务中止信号传递可配置的 `searchTimeoutMs`,默认值为 30 秒。由于内部页面共享与代绑定的游标,两个搜索工具在 agent loop 调度器中都以独占方式执行;精确追踪与读取工具则允许和兄弟工具并行执行,因为其观测可以容忍期间发生的提交。
两个搜索工具都不向模型公开游标、偏移量、页大小或模型可控的结果限制。一次执行会在观察到的代保持有效时持续跟随提供方游标,并收集不超过配置项 `maxSearchResults` 的结果,其默认值为 100。达到上限的结果会要求模型缩小查询或过滤范围;代发生变化时会报告必须重试完整搜索。搜索执行通过工具截止时间与服务中止信号传递可配置的 `searchTimeoutMs`,默认值为 30 秒。由于内部页面共享与代绑定的游标,两个搜索工具在 agent loop(智能体循环)调度器中都以独占方式执行;精确追踪与读取工具则允许和兄弟工具并行执行,因为其观测可以容忍期间发生的提交。
追踪与读取工具同样不公开谱系分页或字符分页。规范结果采用纯文本,并在服务已有的事件窗口与搜索数量资源边界内保持完整。每个精确执行器都会将未经替换的工具执行信号传递给目标授权与服务追踪或读取。在服务解析过程中,已知实时事件追踪、事件读取与标题读取在遵循预中止的同时仍不访问持久化。会话谱系追踪会将该信号传递给全语料持久化列表;持久化事件追踪与读取则将其传递给目标列表和检查。每个已启动的后端调用都会等待清理完成后再保留准确的中止原因,即使该后端忽略了取消也不例外。通用的 `tools/post-execute` spill 策略负责行内字节保留:当已配置的部署收到过大的文本时,该策略会用有界预览、不可透明推导的定位符与读取提示替换文本,同时在 spill 存储中保留完整结果。会话查询消费者既不导入 `ctx.spillStore`,也不实现第二套截断格式。
追踪与读取工具同样不公开谱系分页或字符分页。规范结果采用纯文本,并在服务已有的事件窗口与搜索数量资源边界内保持完整。每个精确执行器都会将未经替换的工具执行信号传递给目标授权与服务追踪或读取。在服务解析过程中,已知实时事件追踪、事件读取与标题读取在遵循预中止的同时仍不访问持久化。会话谱系追踪会将该信号传递给全语料持久化列举操作;持久化事件追踪与读取则将其传递给目标列举与检查操作。每个已启动的后端调用都会等待清理完成后再保留准确的中止原因,即使该后端忽略了取消也不例外。通用的 `tools/post-execute` spill 策略负责行内字节保留:当已配置的部署收到过大的文本时,该策略会用有界预览、不透明定位符与读取提示替换文本,同时在 spill 存储中保留完整结果。会话查询消费方既不导入 `ctx.spillStore`,也不实现第二套截断格式。
会话级结果在可用时包含最新折叠标题。每次工具执行都会通过一次优先使用实时数据的语料观测批量读取唯一标题 id,最多使用服务通过 `persistedInspectConcurrency` 配置的持久化检查 worker,其默认值为 4,并将准确的工具执行信号传递给持久化列表与检查操作。实时来源会直接折叠;每个持久化 worker 都会把已完成的来源折叠为分离的会话头/标题观测,并在取出下一个 id 前释放完整日志,因此批次只保留小型投影值。对于搜索工具,该执行信号携带已配置的搜索截止时间。取消不会启动排队中的标题检查,并会在已经启动的检查全部完成后拒绝完整的工具执行;标题缺失、格式错误或发生操作性失败时,错误只影响对应 id,同时保留基础结果、渲染不可用标记并记录底层错误,而授权不匹配则按失败关闭处理。搜索结果包含最强匹配事件与提供方摘录,追踪包含完整的已授权关系,事件读取保持邻近事件表现易读,同时只为被请求的目标保留精确 JSON。
会话级结果在可用时包含最新折叠标题。每次工具执行都会通过一次优先使用实时数据的语料观测批量读取唯一标题 id,最多使用服务通过 `persistedInspectConcurrency` 配置的持久化检查 worker,其默认值为 4,并将准确的工具执行信号传递给持久化列举与检查操作。实时来源会直接折叠;每个持久化 worker 都会把已完成的来源折叠为分离的会话头/标题观测,并在取出下一个 id 前释放完整日志,因此批次只保留小型投影值。对于搜索工具,该执行信号携带已配置的搜索截止时间。取消不会启动排队中的标题检查,并会在已经启动的检查全部完成后拒绝完整的工具执行;标题缺失、格式错误或发生操作性失败时,错误只影响对应 id,同时保留基础结果、渲染不可用标记并记录底层错误,而授权不匹配则按失败关闭处理。搜索结果包含最强匹配事件与提供方摘录,追踪包含完整的已授权关系,事件读取保持邻近事件表现易读,同时只为被请求的目标保留精确 JSON。
## 宿主组合
@@ -42,14 +42,14 @@ Status: implemented
- **向模型公开提供方游标**:不予采纳,因为记录工具结果或开始下一个模型步骤会改变相关会话或全局代,导致游标通常在模型能够复用前就已过期。
- **增加工具本地截断、偏移量或 spill 文件**:不予采纳,因为执行后 spill 策略已经统一负责各工具的完整结果保留与读取。
- **允许访问所有持久化会话或由模型提供工作区过滤条件**:不予采纳,因为 `ctx.sessionQuery` 是受信任服务,面向模型的消费者必须执行调用者权限边界。
- **允许访问所有持久化会话或由模型提供工作区过滤条件**:不予采纳,因为 `ctx.sessionQuery` 是受信任服务,面向模型的消费方必须执行调用者权限边界。
- **把搜索、追踪与精确读取合并为一个带操作选择器的工具**:不予采纳,因为职责单一的名称能为模型提供更清晰的 schema、默认值、表现意图与后续选择。
- **只返回一层谱系**:不予采纳,因为 spill 已消除行内大小方面的理由,而单层输出会遗漏关系且没有继续读取路径。
## 验证
包级测试固定参数校验、过滤条件转换、时间戳规范化、精确工作区授权、父级过滤预授权与抵御预言机探测、变更观测拒绝、普通值与对抗性未知值的服务诊断脱敏、尽力记录循环 cause、日志失败隔离、身份缺失行为、隐藏边界裁剪、当前步骤排除、内部提供方翻页、搜索独占与精确读取并行分类、数量上限、精确信号传递、中止原因保留、持久化清理静止、单次扫描且并发有界的批量标题扩充、先投影再取出下一个任务的顺序、抑制排队工作、等待已启动 worker 静止、逐会话头校验、标题回退、代表性搜索/追踪/读取渲染、通用表现与可释放注册。集成覆盖使用真实 SQLite FTS 提供方查询实时与持久化会话。发布配置与 TUI/Web 组合测试证明面向模型的消费方不存在于 TUI、Web 与无头界面,而组装后的 ACP 请求头快照证明自动化界面默认不包含它。包自身的 Loader 冒烟测试与专用无密钥 ACP 快照显式挂载该消费方,并配套启用超时与 spill 支持,固定其提示词指引、schema 以及与路径无关的精确事件读取保留行为。
包级测试固定参数校验、过滤条件转换、时间戳规范化、精确工作区授权、父级过滤预授权与抵御预言机探测、变更观测拒绝、普通值与对抗性未知值的服务诊断脱敏、尽力记录循环 cause、日志失败隔离、身份缺失行为、隐藏边界裁剪、当前步骤排除、内部提供方翻页、搜索独占与精确读取并行分类、数量上限、精确信号传递、中止原因保留、持久化清理完全停稳、单次扫描且并发有界的批量标题扩充、先投影再取出下一个任务的顺序、抑制排队工作、等待已启动 worker 完全停稳、逐会话头校验、标题回退、代表性搜索/追踪/读取渲染、通用表现与可释放注册。集成覆盖使用真实 SQLite FTS 提供方查询实时与持久化会话。发布配置与 TUI/Web 组合测试证明面向模型的消费方不存在于 TUI、Web 与无头界面,而组装后的 ACP 请求头快照证明自动化界面默认不包含它。包自身的 Loader 冒烟测试与专用无密钥 ACP 快照显式挂载该消费方,并配套启用超时与 spill 支持,固定其提示词指引、schema 以及与路径无关的精确事件读取保留行为。
## 后果
模型无需获得存储权限或继续状态,即可通过与提供方无关的方式访问既往会话工作。搜索具有有限的单次调用工作边界,若要命中前 100 条以后的结果,可能需要缩小查询;搜索调用不能与兄弟工具重叠执行,而精确观测仍可并行调度。完整追踪与事件负载可能表现为 spill 引用而不是行内文本。严格的 `cwd` 字符串相等选择了保守安全边界,而不解析通过符号链接等价的路径。自定义组合可以在不挂载 spill 的情况下使用该工具,但这表示它们明确接受完整追踪与读取结果直接出现在行内。
模型无需获得存储权限或继续状态,即可通过提供方无关的方式访问既往会话工作。搜索具有有限的单次调用工作边界,若要命中前 100 条以后的结果,可能需要缩小查询;搜索调用不能与兄弟工具重叠执行,而精确观测仍可并行调度。完整追踪与事件负载可能表现为 spill 引用而不是行内文本。严格的 `cwd` 字符串相等选择了保守安全边界,而不解析通过符号链接等价的路径。自定义组合可以在不挂载 spill 的情况下使用该工具,但这表示它们明确接受完整追踪与读取结果直接出现在行内。

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/implemented/feature/2026-07-25-workspace-ui-product-flow.md
2026-07-25-workspace-ui-product-flow.md: 7a6a41e19d2930fbcbf7ba5fc9e6809d96e23166
2026-07-25-workspace-ui-product-flow.zh.md: 372339331bdf2ee5f7c63abd1cc056ae37b16f78
2026-07-25-workspace-ui-product-flow.zh.md: 14f33b0eebcf8885670e72cfb6e2c9a87b7a3432

View File

@@ -1,9 +1,9 @@
# Agent Note: Workspace UI 完整产品动线
[English](2026-07-25-workspace-ui-product-flow.md) | 中文
Status: implemented
[English](2026-07-25-workspace-ui-product-flow.md) | 中文
## Problem
[Domain KV storage 与 Workspace entity](../../proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md)定义了 Workspace 的持久实体、路径规范和有序 Session 账本,但没有定义 Host 接线、历史数据初始化或 GUI 动线。GUI 同时呈现 Workspace 和 Session;用户进入 New Session 后必须立即输入,即使此时还没有 Host Session,甚至没有 Host Workspace。
@@ -25,11 +25,11 @@ Host 在 Workspace entity 上提供以下 GUI 接线:
| `session.create({ workspaceId, sessionId? })` | 从 Workspace 解析 cwd,以可选预分配 id 幂等创建 Session 并 attach |
| `session.create({ cwd })` | 保留给非 Workspace 调用方,创建 Ungrouped Session |
`workspaceRoot` 是独立 Host 配置,未配置时回退到 Host cwd;它与保存 Workspace domain 数据的 `storageRoot` 无关。Host stream 推送 Workspace 与 Session 增量,包括 `host/workspace-removed`;Client 重连后分别刷新 `workspace.list` 与 `session.list` 基线。删除注册记录的所有权与安全边界由 [Workspace 注册记录删除 Agent Note](2026-07-27-workspace-registration-deletion.md)定义。
`workspaceRoot` 是独立 Host 配置,未配置时回退到 Host cwd;它与保存 Workspace domain 数据的 `storageRoot` 无关。Host 流推送 Workspace 与 Session 增量,包括 `host/workspace-removed`;Client 重连后分别刷新 `workspace.list` 与 `session.list` 基线。删除注册记录的所有权与安全边界由 [Workspace 注册记录删除 Agent Note](2026-07-27-workspace-registration-deletion.md)定义。
Workspace 的 `sessionIds` 是有序候选索引。成员投影同时要求 id 位于索引且对应 `SessionHeader.cwd` canonical 后等于 Workspace path;SessionHeader 不增加 `workspaceId`。cwd 匹配但未入索引的 Session 保持 Ungrouped,索引命中但 header 缺失、cwd 无效或 cwd 不匹配的 id 被过滤。同一 Session 被两个 Workspace 索引占用属于损坏状态并 fail loud。
Workspace 的 `sessionIds` 是有序候选索引。成员投影同时要求 id 位于索引且对应 `SessionHeader.cwd` canonical 后等于 Workspace path;SessionHeader 不增加 `workspaceId`。cwd 匹配但未入索引的 Session 保持 Ungrouped,索引命中但 header 缺失、cwd 无效或 cwd 不匹配的 id 被过滤。同一 Session 被两个 Workspace 索引占用属于损坏状态并明确报错。
Workspace domain 以 durable marker 区分“从未初始化”和“已初始化但为空”。marker 未设置时,Registry 只调用 `SessionPersistence.list()` 读取 header 元数据,不调用 `load`、`inspect`、history 或解析事件正文;有效 cwd 按 canonical path 分组,组内 Session 与 Workspace 组均按 header `createdAt` 降序初始化。Bootstrap 可重入,最后才写 marker;marker 写入后,绕过 `workspaceId` 的新 Session 不再被自动收编。
Workspace domain 以 durable marker 区分「从未初始化」和「已初始化但为空」。marker 未设置时,注册表只调用 `SessionPersistence.list()` 读取 header 元数据,既不调用 `load` 或 `inspect`,也不读取历史数据或解析事件正文;有效 cwd 按 canonical path 分组,组内 Session 与 Workspace 组均按 header `createdAt` 降序初始化。Bootstrap 可重入,最后才写 marker;marker 写入后,绕过 `workspaceId` 的新 Session 不再被自动收编。
### Client 对象模型
@@ -56,15 +56,15 @@ Session 自己持有首条输入并驱动一条内部流水线:必要时以预
### 首次发送与恢复
前端 Session 的 `pendingPrompt` 在 Host 接受消息前始终保留原文。首次发送按 Workspace materialize、Session attach、prompt send 顺序推进:
前端 Session 的 `pendingPrompt` 在 Host 接受消息前始终保留原文。首次发送按 Workspace materialize、Session attach、提示词发送顺序推进:
1. Workspace 创建失败时,Workspace Intent 保留输入与错误,Session 仍指向该对象。
2. Session 创建在发布前失败时,Session Intent 回到可编辑状态,以同一预分配 SessionId 重试。
3. `workspace-attach-failed` 证明 Session 已发布;同一 Session 对象进入真实列表并保留 prompt,后续重试 attach。
4. prompt 失败时,Session 保留 prompt 并只重试 send,不重复创建 Workspace 或 Session。
5. Session 创建期间若页面切换到另一个 Intent,旧 Session 即使随后发布也不自动发送;它保留原 prompt 和可见错误。
3. `workspace-attach-failed` 证明 Session 已发布;同一 Session 对象进入真实列表并保留提示词,后续重试 attach。
4. 提示词发送失败时,Session 保留提示词并只重试发送,不重复创建 Workspace 或 Session。
5. Session 创建期间若页面切换到另一个 Intent,旧 Session 即使随后发布也不自动发送;它保留原提示词和可见错误。
RPC lost response、Host frame 先于 completion 和 completion 先于 Host frame 都通过预分配 SessionId 与对象身份收敛。Manager 对 Host view 做有序 upsert,本地 materialize 时优先保留原对象身份,不生成同 id 的临时第二行。
RPC 响应丢失、Host frame 先于 completion 和 completion 先于 Host frame 都通过预分配 SessionId 与对象身份收敛。Manager 对 Host view 做有序 upsert,本地 materialize 时优先保留原对象身份,不生成同 id 的临时第二行。
### Sidebar 与排序
@@ -72,7 +72,7 @@ Workspace 组严格使用 Host 返回的持久顺序。Bootstrap 一次性确定
组内严格使用 `Workspace.sessionIds`。新 attach 的 Session 放在首位,后续某个 Session 活跃时 Host 只前移该 id 并持久化。Client 不在 Session list 到达后按时间整体重排,因此不会先显示一套 Workspace 顺序再因 hydration 瞬间跳动。
前端 Session Intent 只有在目标是真实 Workspace 时才作为 “New session” 行显示,并临时计入该组 Session 数量;目标是 Workspace Intent 时,Workspace 与 Session 都不进入 sidebar。Intent 发布后由同一预分配 id 对应的真实行接替,刷新后 Intent 行和临时计数一起消失。搜索模式不保存或筛选 Intent 行。
前端 Session Intent 只有在目标是真实 Workspace 时才作为 「New session」 行显示,并临时计入该组 Session 数量;目标是 Workspace Intent 时,Workspace 与 Session 都不进入 sidebar。Intent 发布后由同一预分配 id 对应的真实行接替,刷新后 Intent 行和临时计数一起消失。搜索模式不保存或筛选 Intent 行。
无法归入任何 Workspace 的真实 Session 进入 Ungrouped。Host `session-added` 与 `workspace-changed` 可以任意顺序到达,列表合并不依赖 frame 顺序。
@@ -80,19 +80,19 @@ Workspace 组严格使用 Host 返回的持久顺序。Bootstrap 一次性确定
### React 与 slot 边界
React 组件只消费 `useSessions`、`useWorkspaces` 与 session-scoped hooks,不拥有实体生命周期。Zustand store 只保留布局、当前 view、普通真实 Session 的 composer 文本和其他纯呈现状态;Session/Workspace Intent、materialize phase、错误和 retained prompt 位于 React-free runtime 对象层。
React 组件只消费 `useSessions`、`useWorkspaces` 与 session-scoped 钩子,不拥有实体生命周期。Zustand store 只保留布局、当前 view、普通真实 Session 的 composer 文本和其他纯呈现状态;Session/Workspace Intent、materialize phase、错误和 retained prompt 位于 React-free 运行时对象层。
Sidebar 与 conversation empty hero 通过 slot 获得标准化动作:`startSession`、`updateSessionPrompt`、`sendSession`、`open` 与 `toggleSidebar`。Workspace picker 复用同一组件与 `createWorkspace` seam;owner 只提供 popover 开关、锚点和选中回调。呈现层不直接发送 `host/workspace-changed`,Host event 只由 Host mutation 与 stream adapter 产生。
Sidebar 与 conversation empty hero 通过 slot 获得标准化动作:`startSession`、`updateSessionPrompt`、`sendSession`、`open` 与 `toggleSidebar`。Workspace picker 复用同一组件与 `createWorkspace` seam;owner 只提供 popover 开关、锚点和选中回调。呈现层不直接发送 `host/workspace-changed`,Host 事件只由 Host mutation 与流适配器产生。
## Alternatives considered
**为待创建 Workspace 与 Session 保存独立页面记录。** 该方案在 materialize 后需要替换身份并转交输入、错误、焦点和 sidebar 行;对象自身的 Intent 状态可以保持身份连续。
**由呈现层或 root Zustand store 编排对象生命周期。** 该方案会重复 Manager/Service 的职责,并把领域状态带回 React。标准化动作由 runtime service 提供,slot 只注入呈现所需的窄接口。
**由呈现层或 root Zustand store 编排对象生命周期。** 该方案会重复 Manager 与服务的职责,并把领域状态带回 React。标准化动作由 runtime service 提供,slot 只注入呈现所需的窄接口。
**零态立即创建 Host Session 或 Host persistence intent。** 未输入页面会进入 Host 生命周期,并改变刷新语义;前端 Session 在首次发送前只保留 page-local Intent。
**零态立即创建 Host Session 或 Host 持久化 Intent。** 未输入页面会进入 Host 生命周期,并改变刷新语义;前端 Session 在首次发送前只保留 page-local Intent。
**显式 Create Workspace 延迟到首次发送。** 用户确认后 sidebar 仍看不到真实空 Workspace,“创建 Workspace”与“准备 Session”语义混合;只有系统自动产生的零 Workspace Intent 延迟 materialize。
**显式 Create Workspace 延迟到首次发送。** 用户确认后 sidebar 仍看不到真实空 Workspace,「创建 Workspace」与「准备 Session」语义混合;只有系统自动产生的零 Workspace Intent 延迟 materialize。
**持续按 cwd 动态派生 Workspace。** 该方案无法表达空 Workspace、稳定显示名和显式顺序,也会自动收编非 Workspace 调用方;cwd 只用于一次历史 bootstrap 与成员双向校验。
@@ -104,7 +104,7 @@ Sidebar 与 conversation empty hero 通过 slot 获得标准化动作:`startSe
- 完全无 Workspace 的零态不写 Host 且允许输入;显式 Create Workspace 立即创建并显示空 Workspace。
- 前端 Session 与 Workspace 在 materialize 前后保持对象身份,输入、错误、焦点和 sidebar 投影始终来自对象层。
- 首发按 Workspace、Session、prompt 顺序推进,各成功阶段不回滚,输入在 prompt 接受前不丢失,创建重试使用同一 SessionId。
- 首发按 Workspace、Session、提示词顺序推进,各成功阶段不回滚,输入在提示词被接受前不丢失,创建重试使用同一 SessionId。
- Workspace list 只读取 header 完成一次可重入 bootstrap;initialized 的空 registry 重启不重复初始化,成员读取同时校验索引与 canonical cwd。
- 初始默认目标只在两份基线 ready 后确定一次;Workspace 组不因 hydration 或 Session 活跃整体重排,单个活跃 Session 只前移自身。
- 真实 Workspace 下的前端 Session 临时计入 sidebar 数量,Workspace Intent 保持隐藏,发布与刷新都不会留下重复行或重复计数。

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/implemented/feature/2026-07-26-todo-parallel-in-progress.md
2026-07-26-todo-parallel-in-progress.md: 8d047f33ab1aebd8c0de2a0a5e90e7efb1b28154
2026-07-26-todo-parallel-in-progress.zh.md: 3d86a32fbd0d17d802a9f6d69ab49176a808c72f
2026-07-26-todo-parallel-in-progress.zh.md: ebd4b1567eba637493956993b43b1ec56df14c39

View File

@@ -12,7 +12,7 @@ Status: implemented
把单一 `in_progress` 上限从固定规则改为部署策略,并要求每个组合都作出选择:
- `packages/todo/tool-todo/src/index.ts` 新增必填的 `Config.allowParallelInProgress` 字段。为 `true` 时,`execute` 接受任意数量的活跃条目,描述指示模型把每个正在处理的任务标记为 `in_progress`(并行工作时可以有多个,顺序工作时只有一个),并在仍有工作未完成时至少保留一个;为 `false` 时,描述要求恰好一个,`execute` 拒绝标记更多的调用。
- `packages/todo/tool-todo/src/index.ts` 新增必填的 `Config.allowParallelInProgress` 字段。为 `true` 时,`execute` 接受任意数量的活跃条目,描述指示模型把每个正在处理的任务标记为 `in_progress`(并行工作时可以有多个,顺序工作时只有一个),并在仍有工作未完成时至少保留一个;为 `false` 时,描述要求恰好一个,`execute` 拒绝标记多个活跃条目的调用。
- `packages/todo/tool-todo/src/invariant.ts` 中的持久日志不变式不再拒绝含多个活跃条目的快照,且不跟随该配置,因此此前持久化的日志不受影响,并行快照在任一策略下都能干净回放。
其余编码的不变式保持不变:`content` 去除首尾空白后非空且唯一、status 为合法枚举值。本决定取代[原始设计的校验决策](2026-06-29-todo-write-tool.md)中「至多一个活跃」的条款;该 Agent Note 的其余部分(整列表替换、日志支撑的状态、单一所有者)依然成立。
@@ -23,7 +23,7 @@ Status: implemented
## 该策略是部署层的选择
并发的活跃任务是否合理,取决于工具无法观测的运行时并发情况——但一个部署的 agent 是否会并发展开工作,在组装期就是可知的。因此该策略是必填的 `Config` 字段,而非常量或默认值:每个 cordis.yml 组合都会有意设置 `allowParallelInProgress`,为可能并行展开工作的 agent 选择 `true`,或为单活跃项纪律选择 `false`。
并发的活跃任务是否合理,取决于工具无法观测的运行时并发情况——但一个部署的 agent(智能体)是否会并发展开工作,在组装期就是可知的。因此该策略是必填的 `Config` 字段,而非常量或默认值:每个 cordis.yml 组合都会有意设置 `allowParallelInProgress`,为可能并行展开工作的 agent 选择 `true`,或为单活跃项纪律选择 `false`。
该开关会同时改变面向模型的指令与接受的输入。把两者拆开才是 bug:描述要求只保留一个活跃任务、而 `execute` 却接受多个,等于教给模型一条工具并不遵守的规则;反过来则会拒绝描述所邀请的调用。描述中只有活跃状态那一句会变化,因为这是该策略唯一改变的指令。
@@ -37,13 +37,13 @@ Status: implemented
## 展示面是本次改动的一部分
解除上限使一种此前任何渲染器都不曾收到的列表形状变得可达,因此本分支 stack(栈叠)在 [web todo 展示](2026-07-23-web-todo-display.md)之上,而不是与之并行落地:两者都改 `tool-todo`,而 GUI 正是并行计划变得可见的地方。web 有两处用 `todos.find(t => t.status === 'in_progress')` 推导单行摘要——折叠态的计划横条表头与 `todo_write` 工具行——在旧上限下这个 `find` 是完备的,因为最多只能有一个条目匹配。一旦有多个活跃项,它会静默丢掉除第一个之外的全部活跃条目:一个四条目、三个任务在跑的计划折叠后只显示其中一个的名字,工具行读作 `1/4 已完成 · <一个任务>`,而另外两个仍在进行。展开态的列表始终正确(它遍历每个条目),这也是两个 PR 的测试都没抓到它的原因——只有折叠表头与工具行丢失了信息。面板重做把折叠表头的具名提示换成以 `·` 连接的各状态计数(本地化后形如 `1 已完成 · 2 进行中 · 1 待处理`,计数为零的段落省略),它能正确报告并行工作,且不需要任何可被截断的名字;工具行才是本分支仍需修的那一处。
解除上限使一种此前任何渲染器都不曾收到的列表形状变得可达,因此本分支 stack(栈叠)在 [web todo 展示](2026-07-23-web-todo-display.md)之上,而不是与之并行落地:两者都改 `tool-todo`,而 GUI 正是并行计划变得可见的地方。web 有两处用 `todos.find(t => t.status === 'in_progress')` 推导单行摘要——折叠态的计划横条表头与 `todo_write` 工具行——在旧上限下这个 `find` 是完备的,因为最多只能有一个条目匹配。一旦有多个活跃项,它会静默丢掉除第一个之外的全部活跃条目:一个四条目、三个任务在跑的计划折叠后只显示其中一个的名字,工具行读作 `1/4 已完成 · <一个任务>`,而另外两个仍在进行。展开态的列表始终正确(它遍历每个条目),这也是两个 PR(Pull Request)的测试都没抓到它的原因——只有折叠表头与工具行丢失了信息。面板重做把折叠表头的具名提示换成以 `·` 连接的各状态计数(本地化后形如 `1 已完成 · 2 进行中 · 1 待处理`,计数为零的段落省略),它能正确报告并行工作,且不需要任何可被截断的名字;工具行才是本分支仍需修的那一处。
工具行改用 `toolviews/plan-summary.ts` 中的 `planSummary`。它给出第一个活跃条目,并计数其余活跃项,因此工具行报告的是有多少任务在跑,而不是暗示只有一个。列出全部活跃条目被否决了:工具行是单行,无上界的拼接会溢出——在列表做不到的地方,计数能够可预测地降级。该推导放在 toolviews 域内而非 `contract/`(域间共享面):面板自行内联计算其计数,与工具行不共享任何东西,因此放进 contract 会声明一种已不存在的共享关系。
`planSummary` 把任务名与计数作为两个独立字段返回,而不是一个拼好的字符串,因为工具行用 `overflow: hidden` / `text-overflow: ellipsis` 截断其摘要文本。计数接在任务名之后时位于可截断文本的末端,于是恰恰是让计数变得有意义的那些场景——窄视口、长任务名——会把它裁掉,让并行计划看起来与顺序计划无异。因此工具行把计数交给共享的 `ToolRow`,作为 `summarySuffix`——一个紧邻被省略号截断的摘要文本、且不会收缩的槽位;一个预先拼好的字符串无法表达这个切分,而把计数放到任务名之前也被否决了:读者首先要找的是任务名。
`planSummary` 把任务名与计数作为两个独立字段返回,而不是一个拼好的字符串,因为工具行用 `overflow: hidden` / `text-overflow: ellipsis` 截断其摘要文本。计数接在任务名之后时位于可截断文本的末端,于是恰恰是让计数变得有意义的那些场景——窄视口、长任务名——会把它裁掉,让并行计划看起来与顺序计划无异。因此工具行把计数交给共享的 `ToolRow`,作为 `summarySuffix`——一个紧邻被省略号截断的摘要文本、且不会收缩的 slot;一个预先拼好的字符串无法表达这个切分,而把计数放到任务名之前也被否决了:读者首先要找的是任务名。
`summarySuffix` 是 `ToolRow` 上的槽位,而不是 todo 工具行自有的标记:每个 toolview 都经由这个共享组件渲染,而它的 `summary` 是一个会被省略号截断的普通字符串,容不下一个必须挺过截断的片段。该后缀落在 `.summary` 规则之外,因此重复了该规则的 `font-size` 与 `line-height`——Web 外壳把正文字号留在浏览器默认值而非该行的 14px,所以未加样式的 span 会明显大于同一 24px 行内与之并列的文本。错误行会丢弃该后缀,因为它折叠态的摘要是失败行,而非任何由调用 args 推导出的内容。
`summarySuffix` 是 `ToolRow` 上的 slot,而不是 todo 工具行自有的标记:每个 toolview 都经由这个共享组件渲染,而它的 `summary` 是一个会被省略号截断的普通字符串,容不下一个必须挺过截断的片段。该后缀落在 `.summary` 规则之外,因此重复了该规则的 `font-size` 与 `line-height`——Web 外壳把正文字号留在浏览器默认值而非该行的 14px,所以未加样式的 span 会明显大于同一 24px 行内与之并列的文本。错误行会丢弃该后缀,因为它折叠态的摘要是失败行,而非任何由调用 args 推导出的内容。
## 暂缓项
@@ -51,4 +51,4 @@ Status: implemented
## 后果
现在 todo 列表可以忠实反映并行执行,并且每个展示面都能一次渲染多个活跃标记:计划横条的表头会计数活跃条目,工具行则需要上述推导。设置 `allowParallelInProgress: true` 的组合不再拒绝一种此前无效的快照形状;设置为 `false` 的组合仍保留旧的拒绝行为,而持久日志不变式两者都接受。面向模型的描述发生了变化,这重新记录了 tool-catalog 页面以及每个带有 todo schema 的快照 sidecar。此处不记录数量:该集合会随每个新落地的 pin 场景增长,而本 Note 先前记过的两次点时刻计数都在几天内失实。有效规则是:改动工具描述的分支必须刷新它分叉之后落地的那些 sidecar —— 包括固定 subagent 类工具的编号文件 `tool-schemas.<n>.expected.json`,其 schema 不被父场景覆盖 —— `pnpm run test:snapshot:refresh` 可以无 key 地对整个语料完成刷新。web fixture 的 todo 样本现在有两个条目处于 `in_progress`,因此两个由 fixture 驱动的展示面渲染的都是并行计划。`packages/client/ui-conversation/tests/todo-panel.spec.tsx` 在 src 上固定工具行摘要与计划横条,ACP `todo-write` 场景录制的是三条目、两个活跃的计划,而 `apps/web/tests/todo-row.snapshot.ts` 在组装后的应用中固定这两个面——它从构建产物 `packages/client/*/lib/client.js` 启动,因此是唯一覆盖 keyed 注册与打包接线的地方。该文件把 `summary`、`suffix` 与横条表头记录为独立字段,因此即便拼接后的文本读起来一样,把 `+N` 计数折回摘要字符串也会改变预期输出。
现在 todo 列表可以忠实反映并行执行,并且每个展示面都能一次渲染多个活跃标记:计划横条的表头会计数活跃条目,工具行则需要上述推导。设置 `allowParallelInProgress: true` 的组合不再拒绝一种此前无效的快照形状;设置为 `false` 的组合仍保留旧的拒绝行为,而持久日志不变式两者都接受。面向模型的描述发生了变化,这重新记录了 tool-catalog 页面以及每个带有 todo schema 的快照伴随文件。此处不记录数量:该集合会随每个新落地的 pin 场景增长,而本 Agent Note 先前记过的两次时点统计都在几天内失实。有效规则是:改动工具描述的分支必须刷新它分叉之后落地的那些伴随文件——包括固定 subagent 类工具的编号文件 `tool-schemas.<n>.expected.json`,其 schema 不被父场景覆盖——`pnpm run test:snapshot:refresh` 可以无 key 地对整个语料完成刷新。web fixture(测试前置数据)的 todo 样本现在有两个条目处于 `in_progress`,因此两个由 fixture 驱动的展示面渲染的都是并行计划。`packages/client/ui-conversation/tests/todo-panel.spec.tsx` 在 src 上固定工具行摘要与计划横条,ACP(Agent Client Protocol)`todo-write` 场景录制的是三条目、两个活跃的计划,而 `apps/web/tests/todo-row.snapshot.ts` 在组装后的应用中固定这两个面——它从构建产物 `packages/client/*/lib/client.js` 启动,因此是唯一覆盖 keyed 注册与打包接线的地方。该文件把 `summary`、`suffix` 与横条表头记录为独立字段,因此即便拼接后的文本读起来一样,把 `+N` 计数折回摘要字符串也会改变预期输出。

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/implemented/feature/2026-07-27-native-workspace-directory-picker.md
2026-07-27-native-workspace-directory-picker.md: a36f7b239a9115fe5eb33472ec5084818a66e9f2
2026-07-27-native-workspace-directory-picker.zh.md: cf30b4810fe5dc4d0ecfe30cd5eaf4b75ce3c23c
2026-07-27-native-workspace-directory-picker.zh.md: e45d45a95b27b19d573d6bd709357d08a2e2f3fc

View File

@@ -27,7 +27,7 @@ Status: implemented
只有来自回环套接字、且携带同源浏览器元数据的请求才能调用原生对话框 RPC。该 RPC 不使用默认的 30 秒请求超时,因为系统对话框可能无限期保持打开;调用方中止或连接中止仍会传递至平台进程。
平台适配器不经 shell 打开对话框——POSIX 上 spawn 原生工具,Windows 上是子进程 COM 会话:
平台适配器不经 shell 打开对话框——POSIX 上 spawn 原生工具,Windows 上进行进程内 COM 交互:
- macOS:`osascript` 和系统文件夹选择器。
- Windows:koffi `IFileOpenDialog` 子进程,使用宿主接受的最佳线程 DPI 感知(可用时为 per-monitor-v2;不支持 PMv2 的主机级联到 per-monitor 或 system-aware)(见[进程内对话框 Note](2026-08-02-win32-in-process-folder-dialog.md));该层无回退——失败原样上报(见[PowerShell 链删除](../simplification/2026-08-04-drop-windows-powershell-picker-fallback.md))。

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/implemented/feature/2026-07-27-trajectory-inspection-ledger.md
2026-07-27-trajectory-inspection-ledger.md: e3fc22234c1df449c99eac90af27de7da0b6f202
2026-07-27-trajectory-inspection-ledger.zh.md: f167d818281c39a06e658c294f45e47d3ba40642
2026-07-27-trajectory-inspection-ledger.zh.md: 736523ebb02b8ea3c6e4cdbf9bc27f67779c7426

View File

@@ -1,4 +1,4 @@
# Agent Note:轨迹检查记录表
# Agent Note: 轨迹检查记录表
Status: implemented
@@ -16,13 +16,13 @@ Status: implemented
- 事件类型与内容构成两个稳定列。角色标签朝内容侧对齐,嵌套子工具略微缩进,内容预览使用 CSS 截断以适应可用宽度。token 用量和耗时留在检查器中。
- 产品正文使用现有无衬线字体栈。轮次 id、token 数、耗时、工具调用、原始载荷和其他机器数据使用现有代码字体栈。
- 现有主题 token 同时负责亮色和暗色渲染。中性边框与表面构成整体结构;区分度较低的角色色帮助扫读而不表达成功或失败语义,业务蓝色则标识选择状态、链接和焦点。
- 客户端 runtime 提供独立于 Session 和 SessionManager 的只读历史数据源。每个已激活的数据源自行拥有原始条目、分页、实时缺口修复和重连重建;普通会话快照仍然只是 Chat 所需的折叠投影。Trajectory 在挂载期间打开该数据源的尾部,当用户到达已加载范围顶部时请求一页更早的历史,再按需派生事件顺序、上下文谱系、schema 索引和请求,避免让所有会话消费者承担这些结构。
- 普通生成调用与压缩调用形成一条按时间排序的请求投影,以 purpose 区分而不是放入不同集合。生效的提示词状态及其变化附着在引入它们的请求上;压缩和提示词变化都不是独立检查实体。请求编号和累计用量覆盖已加载的历史窗口,并随更早页面到达而扩展。
- 客户端运行时提供独立于 Session 和 SessionManager 的只读历史数据源。每个已激活的数据源自行拥有原始条目、分页、实时缺口修复和重连重建;普通会话快照仍然只是 Chat 所需的折叠投影。Trajectory 在挂载期间打开该数据源的尾部,当用户到达已加载范围顶部时请求一页更早的历史,再按需派生事件顺序、上下文谱系、schema 索引和请求,避免让所有会话消费方承担这些结构。
- 普通生成调用与压缩调用形成一条按时间排序的请求投影,以用途区分而不是放入不同集合。生效的提示词状态及其变化附着在引入它们的请求上;压缩和提示词变化都不是独立检查实体。请求编号和累计用量覆盖已加载的历史窗口,并随更早页面到达而扩展。
- 调用 schema 来自当前生效且已记录的请求头。无密钥快照 fixture(测试前置数据)有意将该目录替换为非数组 token `{{tools}}`,持久化检查边界会将其视为不可用,而不是尝试投影或虚构 schema。
- 选择记录或请求后,轨迹视图内部会打开检查器,其标签页和概览区域随实体类型变化:Markdown 消息提供渲染、源码、来源和层级视图;工具提供 JSON 载荷/结果和 schema 视图;请求提供选项、用量、计时和结果跳转。可滚动的概述区域默认保持滚动条滑块透明,直到悬停或 `focus-within` 时才显示,同时保留滚动条预留空间和滚动行为。图片以媒体形式渲染,而不是显示为序列化数据。
- 折叠轮次时保留其第一条记录,将后续行替换为紧凑的步骤和工具调用数量;折叠助手时对其工具调用后代应用相同交互。全局控件可以分别折叠或展开这两个层级。
- 选择记录或请求后,轨迹视图内部会打开检查器,其标签页和概述区域随实体类型变化:Markdown 消息提供渲染、源码、来源和层级视图;工具提供 JSON 载荷/结果和 schema 视图;请求提供选项、用量、计时和结果跳转。可滚动的概述区域默认保持滚动条滑块透明,直到悬停或 `focus-within` 时才显示,同时保留滚动条预留空间和滚动行为。图片以媒体形式渲染,而不是显示为序列化数据。
- 折叠轮次时保留其第一条记录,将后续行替换为紧凑的步骤数和工具调用数;折叠助手时对其工具调用后代应用相同交互。全局控件可同时折叠或展开这两个层级。
- 长记录表初始时将已加载尾部置于底部,只挂载视口对应的行窗口及有界的额外缓冲行。仅含请求的分隔行并入下一个具备可测高度的虚拟项,末尾分隔行则保留固定留白,因此虚拟化器不会管理零高度项。可安全用于 DOM 的语义行键与 ARIA 索引使标识不依赖挂载位置。只要已知尾部之前仍有更早历史,即使当前已加载投影低于常规行数阈值,也会立即启用虚拟化。基于稳定键的虚拟化器锚定会在向前补页和尾部追加时保留当前可见项;只有分页完成导致虚拟化停用时,才使用手动滚动高度兜底。选择、时间线聚焦、折叠、搜索和末尾跟随均按稳定的事件或工具调用标识定位,不要求对应 DOM 行已存在。初始定位完成前以及更早页面仍在等待时,明确的加载行会遮住真实记录。原始窗口的基准序号即使在一页未增加任何 surface 可见节点时,也能检测到这次向前补页。
- 移除独立的 waterfall(瀑布式事件)标签页。固定在记录表上方的 Overview 区域将所有 `startedAt` 已知的已加载记录按各自耗时投影到三条语义计时轨道。仍有更早前缀尚未加载且 viewport 包含已加载时间域起点时,中性的省略号控件会遮住截断边缘并加载一页更早历史,而不会为未知历史虚构耗时;悬停在该控件上会隐藏普通的时间线光标。已完成的助手时间条以首个非空 token 增量为分界,用不同颜色按真实比例表示 TTFT 与解码时间;计时不完整时退化为单一助手色。悬停 500 ms 后会显示精确起止时刻、总耗时、TTFT 和解码时间,而不依赖浏览器原生 tooltip 的延迟。向左或向右拖动会提交包含边界的区间筛选:任何活动区间与所选区间相交的记录都会保留,计时未知的记录会从聚焦后的记录表中移除,清除选择则恢复完整分支。滚轮手势用于缩放时间域。右键单击会清除区间选择;右键拖动则只会平移已放大的 viewport,不会改变该选区。聚焦后,Overview 区域仍保留完整时间范围,以便在不失去方位的情况下调整或清除选择。
- 移除独立的 waterfall(瀑布式事件)标签页。固定在记录表上方的 Overview 区域将所有 `startedAt` 已知的已加载记录按各自耗时投影到三条语义计时轨道。仍有更早前缀尚未加载且 viewport 包含已加载时间域起点时,中性的省略号控件会遮住截断边缘并加载一页更早历史,而不会为未知历史虚构耗时;悬停在该控件上会隐藏普通的时间线光标。已完成的助手时间条以首个非空 token 增量为分界,用不同颜色按真实比例表示 TTFT 与解码时间;计时不完整时退化为单一助手色。悬停 500 ms 后会显示精确起止时刻、总耗时、TTFT 和解码时间,而不依赖浏览器原生 tooltip 的延迟。向左或向右拖动会提交包含边界的区间筛选:任何活动区间与所选区间任一边界重叠的记录都会保留,计时未知的记录会从聚焦后的记录表中移除,清除选择则恢复完整分支。滚轮手势用于缩放时间域。右键单击会清除区间选择;右键拖动则只会平移已放大的 viewport,不会改变该选区。聚焦后,Overview 区域仍保留完整时间范围,以便在不失去方位的情况下调整或清除选择。
- 实时历史更新仅在用户已经跟随记录表末尾时保留底部位置。向上滚动会清除跟随状态,因此流式分块和新追加的记录不会打断对旧记录的检查。末尾跟随与虚拟化器测量仅响应行键和高度,而非内容标识,因此仅含文本的流式帧既不会丢弃测量缓存,也不会重复执行 DOM 滚动写入。
- token 流式输出会复用已完成历史的检查结果、布局、请求编号、Overview 投影和搜索结果。每个帧只追加当前未完成助手的单元格,并在查询处于激活状态时搜索这部分内容;文本与推理(reasoning)增量不会重新折叠或扫描已加载前缀,而消息完成、工具生命周期、压缩、`rewrite` 及其他结构事件会重建受影响的投影。在这些投影重建前,检查记录表会丢弃已完成步骤中没有任何投影读取的 token 载荷,但会保留首个可见 token 用于计时、保留所有用量分片用于核算,并保留未完成或中断步骤的所有分片;独立历史数据源仍保留原始条目。
- 历史折叠只把已加载的 surface 事件重新编号为紧凑连续的输入并交给规范 surface manager,再将其节点映射回会话绝对序号。因此,结构事件会保留规范的替换校验,而无需重放 token 分片,也不会为未加载的序号实体化合成事件。
@@ -45,7 +45,7 @@ Status: implemented
**复用全局 Chat 详情栏。** 不予采纳:这会让局部检查与会话导航耦合,还会使行点击意外改变另一个视图的状态。
**由 Trajectory CSS 覆盖 composer seat。** 不予采纳:跨包(package)选择器会依赖生成类选择器的优先级和样式表顺序。显式视图标记让 seat 几何形状和活跃阶段优先级留在 `ConversationRoot` 中,而 Trajectory 只负责自身内部的避让空间。
**由 Trajectory CSS 覆盖 composer seat。** 不予采纳:跨包选择器会依赖生成类选择器的优先级和样式表顺序。显式视图标记让 seat 几何形状和活跃阶段优先级留在 `ConversationRoot` 中,而 Trajectory 只负责自身内部的避让空间。
**将计时保留在独立的 waterfall 标签页中。** 不予采纳:占位实现汇总的是节点数而非记录计时,并迫使用户离开想要聚焦的记录。保留完整时间范围的 Overview 区域让计时和筛选后的记录处于同一视觉上下文中。

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/implemented/feature/2026-07-28-cross-workspace-resume.md
2026-07-28-cross-workspace-resume.md: e8a8636911015a5b300b45a30e268ee1be5d47d1
2026-07-28-cross-workspace-resume.zh.md: 9e5e126f4b204927d7fb134a196dd0d0a98468d4
2026-07-28-cross-workspace-resume.zh.md: c56641be35bb37fb8aafb7613bdf8b8158e198d9

View File

@@ -16,15 +16,15 @@ Status: implemented
## Decision
共享 CLI 配置提供 Harness home 下的同一个会话根目录,选择器获得 workspace 范围,交接过程携带目标目录。
共享 CLI(命令行界面)配置提供 Harness home 下的同一个会话根目录,选择器获得 workspace 范围,交接过程携带目标目录。
**存储。** 共享 base 在 `apps/cli/config/base.cordis.yml` 中拥有默认值:其 `session-persistence-jsonl` 配置项调用由 app-boot 提供的 `dshHomePath('sessions')`,该函数使用规范的 `DSH_HOME` 解析器及其标准的 `~/.dsh` 回退值。因此 TUI、Web 与 headless 使用同一个默认值,无需针对会话的启动器补丁或槽位。若 overlay 或个人 patch 显式声明根目录,它会整体替换该配置项的 `config`,并继续作为部署的权威选择。
**存储。** 共享 base 在 `apps/cli/config/base.cordis.yml` 中拥有默认值:其 `session-persistence-jsonl` 配置项调用由 app-boot 提供的 `dshHomePath('sessions')`,该函数使用规范的 `DSH_HOME` 解析器及其标准的 `~/.dsh` 回退值。因此 TUI、Web 与 headless 使用同一个默认值,无需针对会话的启动器补丁或 slot。若 overlay 或个人 patch 显式声明根目录,它会整体替换该配置项的 `config`,并继续作为部署的权威选择。
**是范围,不是排除。** 当前 workspace 之外的 workspace 是一种展示范围,而不是禁用理由。`showResume()` 汇总每一条记录,`ResumePicker` 持有一个 `'workspace' | 'all'` 的 `scope`,默认为当前 workspace,因此常见场景毫无变化。Tab 切换范围;范围行会说明当前生效的范围,以及另一个范围下的数量;在全 workspace 范围中每一行都报告自己的 workspace,而该标签只在展示它的范围里才加入可搜索文本。切换范围会清空查询和选中项,使高亮行始终属于可见列表;而逐行的 workspace 行会让该范围下的每一行在终端里多占一行,可见条数预算已经把这一点计入。
因此 `summarizeResumeCandidate` 去掉了 `'different workspace'`,并新增 `'session has no recorded workspace'`。这是一条真正新增的拒绝理由,而不是改名:没有 `cwd` 的头部没有指明任何目录供宿主进入,所以即便它的日志完好也无法完成交接。
**交接。** `TuiResumeHost.handoff` 在 `SessionId` 之外还接收目标 `cwd`。`preflightResume` 把两者一起解析并一起返回,因此调用方无法从它展示过的那一行里重新推导出一个陈旧目录——在列表展示与预检之间 `cwd` 发生了移动的记录,会在*重新读取到的*目录中恢复,这也是原先「拒绝已移动的 cwd」的行为如今变成携带新路径完成交接的原因。已交付的宿主在释放应用之前切换目录:不可达的目录必须在调用方还能恢复终端时就拒绝,因为拆卸之后已经没有任何所有者可供汇报。恢复始终使用默认的 `dsh --resume` 界面,因为 `meta` 会拒绝父级选项;交接过程已经进入持久化保存的目标目录。
**交接。** `TuiResumeHost.handoff` 在 `SessionId` 之外还接收目标 `cwd`。`preflightResume` 把两者一起解析并一起返回,因此调用方无法从它展示过的那一行里重新推导出一个陈旧目录——在列表展示与预检之间 `cwd` 发生了变化的记录,会在*重新读取到的*目录中恢复,这也是原先「拒绝发生变化的 cwd」的行为如今变成携带新路径完成交接的原因。已交付的宿主在 dispose(资源释放)应用之前切换目录:不可达的目录必须在调用方还能恢复终端时就拒绝,因为拆卸之后已经没有任何所有者可供汇报。恢复始终使用默认的 `dsh --resume` 接口,因为 `meta` 会拒绝父级选项;交接过程已经进入持久化保存的目标目录。
## Alternatives considered
@@ -36,7 +36,7 @@ Status: implemented
**把所有 workspace 铺成一个扁平列表。** 否决:这会丢掉绝大多数场景想要的「本项目」默认值,而在一个繁忙的 home 目录里,当前项目的会话会和无关会话争夺注意力。
**让宿主从还原后的会话头部推断目录。** 否决:会话头部是面向模型与提示词的状态,在启动*之后*才还原,而目录必须在 `execve` *之前*进入。显式传递它能让这个顺序在边界处保持可见。
**让宿主从还原后的会话头部推断目录。** 否决:会话头部是面向模型与提示词的状态,在启动*之后*才还原,而目录必须在 `execve` *之前*进入。显式传递它能让这个顺序在 seam 处保持可见。
## Consequences
@@ -46,4 +46,4 @@ Status: implemented
## Testing
TUI 测试覆盖默认范围隐藏其他 workspace 但报告其数量、Tab 显示它们并带上逐行 workspace 标签、再按 Tab 返回时清空查询与选中项、按 workspace 标签搜索、无 cwd 的记录仍可见但不可选,以及交接同时收到 id 和在预检时重新读取到的 workspace。原先「拒绝已移动的 cwd」的用例现在断言交接携带新目录。构建后的 CLI PTY 测试会运行共享配置默认值与每进程派生的查询索引。无密钥 TUI 快照固定选择器的两个范围,包括范围行、逐行 workspace 行,以及页脚中的 Tab 提示。手动执行的一次跨 workspace 恢复在进程层面验证了替换后进程的工作目录变为目标 workspace。
TUI 测试覆盖默认范围隐藏其他 workspace 但报告其数量、Tab 显示它们并带上逐行 workspace 标签、再按 Tab 返回时清空查询与选中项、按 workspace 标签搜索、无 cwd 的记录仍可见但不可选,以及交接同时收到 id 和在预检时重新读取到的 workspace。原先「拒绝发生变化的 cwd」的用例现在断言交接携带新目录。构建后的 CLI PTY 测试会检验共享配置默认值与每进程派生的查询索引。无密钥 TUI 快照固定选择器的两个范围,包括范围行、逐行 workspace 行,以及页脚中的 Tab 提示。手动执行的一次跨 workspace 恢复在进程层面验证了替换后进程的工作目录变为目标 workspace。

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/implemented/feature/2026-07-28-feedback-command.md
2026-07-28-feedback-command.md: 78cc9e89a5811b3f1520bae2bd971cbcf2522ede
2026-07-28-feedback-command.zh.md: c8870ca2988501d7899ae826e1bbcbad0bef5678
2026-07-28-feedback-command.zh.md: b0bba25ec3331123cb86066fc6c89186f2297cc9

View File

@@ -14,11 +14,11 @@ Status: implemented
位于 `packages/feedback/command-feedback/` 的 `@deepseek-ai/dsh-command-feedback` 通过 `ctx.commands` 注册一个全局 `feedback` 命令。`/feedback <text>` 给出确认;空输入或仅含空白的输入返回直接用法错误。处理器是同步的,只注入 `commands`,且没有任何配置。
本包(package)声明仅写入日志的 `feedback/record { text }` 会话事件,并导出 `recordFeedback(session, text)`,作为不依赖命令的生产方。该生产方丢弃前后空白,拒绝空结果,并且恰好追加一个事件。`/feedback` 委托给它,因此其他 UI、钩子或 host 集成无需构造斜杠命令也能记录同一个领域事实。
本包声明仅写入日志的 `feedback/record { text }` 会话事件,并导出 `recordFeedback(session, text)`,作为不依赖命令的生产方。该生产方丢弃前后空白,拒绝空结果,并且恰好追加一个事件。`/feedback` 委托给它,因此其他 UI、钩子或 host 集成无需构造斜杠命令也能记录同一个领域事实。
`dsh-commands` 仍会围绕 `/feedback` 写入 `command/run` / `command/done` 生命周期配对,但该命令设置了 `recordInput: false`。因此,它的 `command/run` 携带命令标识与来源,但不携带 `args`;反馈文本只存在于 `feedback/record` 中,而 `command/done` 携带确认结果。三个记录都仅写入日志且非 surface。它们的追加会进入持久化的常规有界写入路径;没有任何环节强制 flush,因此确认文本报告的是反馈已进入日志,而非已经落盘。
采集对正在运行的 agent 与模型仍不产生后续动作。可选的 OTel 遥测包后续增加了一个基础设施消费方:它在 `FEEDBACK_ONLY` 模式下将 `feedback/record` 作为释放触发器,在 `DISABLED` 模式下将其作为本地警告触发器,且不改变反馈事件或命令路径。见[反馈门控的会话遥测](2026-08-05-feedback-gated-session-telemetry.md)。
采集对正在运行的 agent(智能体)与模型仍不产生后续动作。可选的 OTel 遥测包后续增加了一个基础设施消费方:它在 `FEEDBACK_ONLY` 模式下将 `feedback/record` 作为释放触发器,在 `DISABLED` 模式下将其作为仅限本地的警告触发器,且不改变反馈事件或命令路径。见[反馈门控的会话遥测](2026-08-05-feedback-gated-session-telemetry.md)。
### 为何反馈拥有自己的事件
@@ -34,7 +34,7 @@ Status: implemented
### 一个新的分组
`packages/feedback/` 是新分组,因为现有分组都不拥有此职责:`goal/` 负责目标状态,`session-title/` 负责标题,`core/` 是产品主干。该分组只包含一个生产方包;跨领域的消费方留在各自所属的分组,而不是迫使这个包不断膨胀。
`packages/feedback/` 是新分组,因为现有分组都不拥有此职责:`goal/` 负责目标状态,`session-title/` 负责标题,`core/` 是产品主干。该分组只包含一个生产方包;跨领域的消费方留在各自所属的分组,而不是迫使这个分组不断膨胀。
## 考虑过的替代方案

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/implemented/feature/2026-07-30-config-only-repository-plugins.md
2026-07-30-config-only-repository-plugins.md: 35327a30e03c51311f634e05ade209ab93ae0155
2026-07-30-config-only-repository-plugins.zh.md: 4f4ea6eb55bf278d869df6b2917754af72b76ef0
2026-07-30-config-only-repository-plugins.zh.md: 5755045560da761b59f7c65e99d551f599c2b5b3

View File

@@ -16,7 +16,7 @@ Status: implemented
## 实时更新与失败
`dsh-app-boot` 通过一个辅助函数挂载根 Include,并保留其确切的 Loader `Entry`。TUI 和 Web 通过 Cordis HMR(热模块替换)注册 `$DSH_HOME/config.yaml`;无头界面在启动时读取同一文件,但不保留监视器。监视器更新会重新构建 Include 补丁列表,先放置不可变的应用自有补丁,再放置新解析的个人补丁。因此,Web 生成的端口、会话根目录、信任和前端值会在每次个人编辑后保留,除非后续个人补丁有意替换相应配置项。
`dsh-app-boot` 通过一个辅助函数挂载根 Include,并保留其确切的 Loader `Entry`。TUI 和 Web 通过 Cordis HMR(热模块替换)注册 `$DSH_HOME/config.yaml`;无头模式在启动时读取同一文件,但不保留监视器。监视器更新会重新构建 Include 补丁列表,先放置不可变的应用自有补丁,再放置新解析的个人补丁。因此,Web 生成的端口、会话根目录、信任和前端值会在每次个人编辑后保留,除非后续个人补丁有意替换相应配置项。
Cordis 会串行处理并合并该确切路径上的变更。Include 与 Loader 以事务方式协调候选配置:成功时提交新源列表;拉取、准备、包装模块导入、格式或子插件失败时拒绝候选配置,并保留或恢复最后一个可用树。HMR 会把捕获的值规范化为 `Error`,记录错误,并广播并行的 `hmr/config-update-failed(filename, error)` 事件;观察者失败不会中断刷新处理。Repository MCP 服务器采用严格启动,因此初始连接、发现或工具注册失败会拒绝候选配置,并构成配置更新失败;非严格的独立 MCP 客户端仍保留其所收束的「插件成功加载但无工具」行为。
@@ -47,4 +47,4 @@ Cordis 会串行处理并合并该确切路径上的变更。Include 与 Loader
## 测试
仓库包测试固定源规范化、默认和嵌套 `.dsh-plugin` 路径、缓存根解析、重复项拒绝、已准备包装模块加载及资源释放。App-boot 测试通过真实 HMR/Include/Loader 路径驱动确切路径的新增、两类失败、恢复、移除、失败事件及生成补丁保留。一个无密钥 PTY 冒烟测试仅通过个人配置启动已交付的 `dsh` 组合,并从预置的不可变缓存版本中调用一个 skill。
仓库包测试固定源规范化、默认和嵌套 `.dsh-plugin` 路径、缓存根解析、重复项拒绝、已准备包装层加载及资源释放。App-boot 测试通过真实 HMR/Include/Loader 路径驱动确切路径的新增、两类失败、恢复、移除、失败事件及生成补丁保留。一个无密钥 PTY 冒烟测试仅通过个人配置启动已交付的 `dsh` 组合,并从预置的不可变缓存版本中调用一个 skill。

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/implemented/feature/2026-07-30-continuable-subagent-report-tool.md
2026-07-30-continuable-subagent-report-tool.md: 8324f1fa08f7dace6153712575e7e70a07ee9344
2026-07-30-continuable-subagent-report-tool.zh.md: 6b7472717e724af9bf598fada6d7e82070d06f3c
2026-07-30-continuable-subagent-report-tool.zh.md: 7adc4acc470bd701eee756e7d9ef89d287852a57

View File

@@ -1,4 +1,4 @@
# Agent Note(agent 决策记录):可继续 subagent 报告工具
# Agent Note: 可继续 subagent 报告工具
Status: implemented
@@ -12,7 +12,7 @@ Status: implemented
## 决策
新增可独立安装的 `@deepseek-ai/dsh-tool-subagent-report` 包(package)。它会向每个可继续进程内 child Activation 贡献一个普通的面向模型 `report` 工具。child 在一个轮次中可调用零次或多次。调用成功既不会结束该轮次或结算 Activation,也不会阻止 parent 之后继续 follow-up;完成轮次也绝不会自动报告。
新增可独立安装的 `@deepseek-ai/dsh-tool-subagent-report` 包。它会向每个可继续进程内 child Activation 贡献一个普通的面向模型 `report` 工具。child 在一个轮次中可调用零次或多次。调用成功既不会结束该轮次或结算 Activation,也不会阻止 parent 之后继续 follow-up;完成轮次也绝不会自动报告。
该功能是协作控制,不是承载结果的执行包装层。它不新增 Task、`SubagentRun`、结果 promise、Activation 状态、投递队列或回放路径。
@@ -24,7 +24,7 @@ Status: implemented
工具描述会明确报告操作是显式、可重复、仅限直接 parent 且不会结束轮次的。它还会警告:发送被接受后,后续 `tools/post-execute` 失败可能替换工具结果,因此工具结果失败时内容仍可能已经送达。没有幂等键时,更强的表述会诱导调用方在结果不明确的失败后重复重试。
该工具使用不带 location 的通用渲染,其确认中包含 `messageId`。作用域局部注册使呈现与执行保持一致:root、one-shot child、远程提供方、同级作用域和无 agent 执行既不能看到,也不能执行 `report`。它会在 child 的全局 `toolFilter` 之后安装,因此委派 allow-list 不会意外移除这条结构性返回通道;不需要返回通道的部署不安装该包。
该工具使用不带 location 的通用渲染,其确认中包含 `messageId`。作用域局部注册使呈现与执行保持一致:root、one-shot child、远程提供方、同级作用域和无 agent(智能体)执行既不能看到,也不能执行 `report`。它会在 child 的全局 `toolFilter` 之后安装,因此委派 allow-list 不会意外移除这条结构性返回通道;不需要返回通道的部署不安装该包。
### 服务权限
@@ -40,7 +40,7 @@ root、one-shot child、伪造对象、陈旧 Agent 和同 id 替换对象都以
静默投递调用 `parent.inject()`。它会添加模型可见上下文,但不启动 parent 模型请求:若 parent 空闲,则在调用返回前追加消息;若 parent 正在准入或运行,则暂存报告,留到下一个安全日志位置。该模式不创建 inbox 条目实例,因此也不会产生虚构的继续执行管理器接受记录。
唤醒投递调用 `parent.followup()`。它会创建一个普通的 FIFO parent 轮次,唤醒已驻留的 parent driver,且绝不 steering 已开始的轮次。当该 parent 本身也是可继续 Activation 时,发送会使用管理器现有的准入计数,防止 parent 在同步入队与准入微任务之间结算。
唤醒投递调用 `parent.followup()`。它会创建一个普通的 FIFO parent 轮次,唤醒已停驻的 parent driver,且绝不 steering(中途引导)已开始的轮次。当该 parent 本身也是可继续 Activation 时,发送会使用管理器现有的准入计数,防止 parent 在同步入队与准入微任务之间结算。
两种模式都会将一条用户角色消息封装为 `Background subagent <child-id> reported:`,后面跟随完全原样的 `output`。持久化来源信息为 `{ kind: 'subagent-report', senderSessionId: child.id }`。并发发送的顺序由 Agent 的常规规则决定;subagent 层不会创建第二条队列。
@@ -48,13 +48,13 @@ root、one-shot child、伪造对象、陈旧 Agent 和同 id 替换对象都以
成功表示确切的在线 parent 已同步接受该消息。空闲 parent 在接受静默注入时已经完成追加,而暂存的静默上下文只有到达正常日志边界后才可重建。唤醒投递包含一个 inbox 条目实例,其 id 与返回的稳定消息 id 保持分离。
首个版本不提供持久化邮箱、幂等键、投递回执、重试协议或恰好一次保证。进程故障可能让调用方无法确定结果,在结果未知时重试则可能重复报告。parent 不可用时,持久化 child transcript 仍是恢复来源。
首个版本不提供持久化邮箱、幂等键、投递回执、重试协议或恰好一次保证。进程故障可能让调用方无法确定结果,在结果未知时重试则可能重复报告。parent 不可用时,持久化 child transcript(文本记录)仍是恢复来源。
### 组合与生命周期
subagent seam 新增 `registerContinuableSetup(contribution): () => void`,由 `SubagentActivationSetupRegistry` 支撑。每个同步贡献都会接收尚未发布的 child 上下文,并返回其安装的 disposer。继续执行管理器首先应用基础 child 组合,然后通过同一个用于首次创建与冷恢复的设置闭包,按注册顺序应用当前贡献。
注册表负责注册、每个 child 的安装记录、设置回滚、child 作用域清理和立即撤销。应用一个批次会返回 Agent setup 提交对象,用于在所有 setup 的 await 均结算后、紧邻 Agent 发布前重新校验配置状态。因此,某项贡献抛出异常或被并发撤销时,会在 Agent 与 Session 发布前拒绝操作并回滚该批次。新注册项只会在驻留 child 的下一个 Activation 生效;移除注册项时,会先将它对新设置关闭,再立即撤销为正在配置或驻留的每个 child 安装的实例。注册 dispose 与 child 上下文 dispose 都是幂等的,两者都会先尝试每项释放,再聚合失败。
注册表负责注册、每个 child 的安装记录、设置回滚、child 作用域清理和立即撤销。应用一个批次会返回 Agent setup 提交对象,用于在所有 setup 的 await 均结算后、紧邻 Agent 发布前重新校验配置状态。因此,某项贡献抛出异常或被并发撤销时,会在 Agent 与会话发布前拒绝操作并回滚该批次。新注册项只会在驻留 child 的下一个 Activation 生效;移除注册项时,会先将它对新设置关闭,再立即撤销为正在配置或驻留的每个 child 安装的实例。注册 dispose(资源释放)与 child 上下文 dispose 都是幂等的,两者都会先尝试每项释放,再聚合失败。
该 seam 使继续执行管理器无需知道工具名。report 包只安装 `report`;`@deepseek-ai/dsh-tool-subagent-control` 则独立安装 parent 侧的 `send_message` 和 `list_agents`。部署时可安装任一方向、同时安装两者或两者均不安装。提供方仍只负责数据,持久化描述符不会对 report 可用性或投递模式建立快照,冷恢复则使用部署当前的贡献与策略。
@@ -96,7 +96,7 @@ ACP(Agent Client Protocol)快照 harness 新增 `waitForSubagentTurnEnd`,
### 在 Agent 创建后校验 setup
创建完成后的撤销检查只能在 Agent 与 Session 均已发布后拒绝 Activation。对返回的 handle 执行 dispose 会移除实时对象,但当前 seam 无法删除持久化内容,因此会留下一个仍可恢复的 child,而继续执行管理器却判定它从未建立。改为返回 `AgentSetupCommit`,Agent 工厂便可在自身的发布边界同步执行同一项可变状态检查。
创建完成后的撤销检查只能在 Agent 与会话均已发布后拒绝 Activation。对返回的 handle 执行 dispose 会移除实时对象,但当前 seam 无法删除持久化内容,因此会留下一个仍可恢复的 child,而继续执行管理器却判定它从未建立。改为返回 `AgentSetupCommit`,Agent 工厂便可在自身的发布边界同步执行同一项可变状态检查。
## 影响
@@ -115,4 +115,4 @@ ACP(Agent Client Protocol)快照 harness 新增 `waitForSubagentTurnEnd`,
wakeup 模式可能在嵌套 child 频繁报告时放大模型工作量。由部署所有者控制并默认静默,可以限制该风险,但无法完全消除。
注册表中的存在性就是 parent 在线信号。宿主拥有的 parent 如果已开始 `AgentHandle.dispose()` 但尚未展开其作用域,仍可能接受并追加一条本进程不会再处理的报告。要弥合这个缺口,需要 Agent 层面的 dispose 开始信号,不能由 subagent 层推断。
注册表中的存在性就是 parent 在线信号。宿主拥有的 parent 如果已开始 `AgentHandle.dispose()` 但尚未完成其作用域清理,仍可能接受并追加一条本进程不会再处理的报告。要弥合这个缺口,需要 Agent 层面的 dispose 开始信号,不能由 subagent 层推断。

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/implemented/feature/2026-07-30-current-sandbox-policy-context.md
2026-07-30-current-sandbox-policy-context.md: 093f9a8a0b58256bb4f2301cc62fa83629de4fbd
2026-07-30-current-sandbox-policy-context.zh.md: 6189df12ae2cd9c9a040cd40636596cda23548c9
2026-07-30-current-sandbox-policy-context.zh.md: ce7ff2d0cff0fc1068995fde174a4eb1df5343b7

View File

@@ -18,7 +18,7 @@ Status: implemented
现有 `dsh-system-prompt` 组装在稳定系统段与工具 schema 之外,还包含有序的动态上下文。组装一个步骤后,agent loop(智能体循环)会将所有活动上下文渲染成一份带显式取代声明的完整快照。仅当不存在保留快照、字节发生变化、压缩(compaction)移除了保留消息,或最后一项贡献消失而需要一份清除快照时,它才会追加一条带来源的 `user/message`。快照追加在现有历史之后、`step/start` 之前,因此策略变化时仍会保留此前的系统与对话缓存前缀。会话事件本身可以重建确切的模型输入;只有策略上下文变化时,`request/header` 仍逐字节相同。
归属范围保持收敛。批准策略会将其完整的当前 `ask` 或 `never` 事实贡献给同一份完整快照;只迁移沙箱无法保留缓存,因为 `/permission` 会同时改变两方。计划模式仍由 `plan:policy` 负责,工具插件也继续负责各自的 schema,以及尝试、拒绝与升级引导。上下文负责说明常驻策略;文件系统、一次性 bash 与终端后端仍是强制执行边界。
归属范围仍然有限。批准策略会将其完整的当前 `ask` 或 `never` 事实贡献给同一份完整快照;只迁移沙箱无法保留缓存,因为 `/permission` 会同时改变两方。计划模式仍由 `plan:policy` 负责,工具插件也继续负责各自的 schema,以及尝试、拒绝与升权引导。上下文负责说明常驻策略;文件系统、一次性 bash 与终端后端仍是强制执行边界。
缓存决策依据当前源码,而不只依靠类比。Codex 将权限建模为 developer 角色的 `WorldState` 段并保存其指纹;只有状态变化或保留的历史丢失该片段时才发出它,同时记录快照转换。Hermes 在会话期间保持系统提示词不变,并明确将不断变化的 skill(技能)、模型与语音通知前置到下一条用户消息,以免提示词缓存失效。Pi 没有可比的内置沙箱状态,Claude Code 当前的原生实现也无法公开检视;不过,Anthropic 的公开缓存指南仍将不断变化的逐请求上下文放在稳定缓存前缀之后。
@@ -26,7 +26,7 @@ Status: implemented
## 措辞证据
措辞实验预先登记「预防性拒绝」为主要终点,并要求旧常驻句子在十二个 fresh session 中至少产生一次拒绝,之后才能评判任何替代措辞。2026-07-30,commit `2bf41990401b194bd8637f07bbd90c67a9eeac75` 通过已交付的 Web 组合运行 `deepseek-v4-flash`,使用精确的阳性对照句子 `Bash commands run under the "read-only" file sandbox.` 与当前工具归属方的尝试引导。对照组产生零次预防性拒绝和零次推测性升级;十二个会话全部先发起普通 bash 调用、观察到拒绝、在同一轮次升级、获得批准,并让所请求文件实际落盘。没有样本被排除。
措辞实验预先登记「预防性拒绝」为主要终点,并要求旧常驻句子在十二个 fresh session 中至少产生一次拒绝,之后才能评判任何替代措辞。2026-07-30,commit `2bf41990401b194bd8637f07bbd90c67a9eeac75` 通过已交付的 Web 组合运行 `deepseek-v4-flash`,使用精确的阳性对照句子 `Bash commands run under the "read-only" file sandbox.` 与当前工具归属方的尝试引导。对照组产生零次预防性拒绝和零次推测性升权;十二个会话全部先发起普通 bash 调用、观察到拒绝、在同一轮次升权、获得批准,并让所请求文件实际落盘。没有样本被排除。
缓存安全交付变更后,commit `10d4e0ff7b68d38fc4403403b644aac442b97a00` 通过新的尾部上下文通道重复了同一项十二会话阳性对照。结果再次为零次预防性拒绝和零次推测性升权;十二个会话的首次调用均为普通调用,随后观察到拒绝、在同一轮次升权并获得批准。其中八个会话让所请求文件按确切要求落盘,没有样本被排除。
@@ -42,7 +42,7 @@ Status: implemented
**把当前策略放入动态系统段。** 不予采用,因为真实提供方证据显示,首次权限切换后缓存读取降至 256 个 token,而约 14.7k 个输入 token 未命中缓存。DeepSeek 匹配完整前缀;改变第一条 wire 消息会阻止复用更长的系统与历史前缀。
**由每个策略归属方独立调用 `agent.inject()`。** 不予采用,因为同级监听器的顺序会决定模型所见顺序,分开的消息可能暴露不匹配的中间快照,并且每个归属方都需要各自扫描压缩后的保留状态。现有组装归属方可以对贡献排序,并具体化一份原子化的完整快照。
**由每个策略归属方独立调用 `agent.inject()`。** 不予采用,因为同级监听器的顺序会决定模型所见顺序,分开的消息可能暴露不匹配的中间快照,并且每个归属方都需要各自扫描压缩后的保留状态。现有组装归属方可以对贡献排序,并生成一份原子化的完整快照。
**通用运行时事实包。** 不予采用,因为现有系统提示词组装已经拥有段、schema、变量、作用域和权威的逐步骤 waterfall(瀑布式事件)。为该归属方增加有序上下文,无需新增包或第二个注册表服务。
@@ -60,4 +60,4 @@ Status: implemented
模型在试探工具前就会收到常驻文件策略,且 `/permission` 后的下一个请求会反映已提交的模式。稳定的系统提示词不再随沙箱或批准状态变化;变化后的完整上下文快照会在保留的历史之后仅追加,状态不变时不增加消息。较旧的快照仍保留在历史中,但最新的完整快照会明确取代它们。该声明是引导,而不是强制执行护栏:运行时安全仍来自文件系统、一次性 bash 与终端后端消费同一项解析完成的策略。
聚焦测试固定了所有模式、规范化根目录、切换时机、服务释放、上下文顺序、清除、稳定的请求 header、恢复,以及不同 `TMPDIR` 值下的字节稳定性。无密钥的组装快照通过真实 Loader 组合固定持久上下文消息。无密钥回放负责固定中性的拒绝到升级轨迹;它是结构回归证明,而不是措辞选型证据。
聚焦测试固定了所有模式、规范化根目录、切换时机、服务释放、上下文顺序、清除、稳定的请求 header、恢复,以及不同 `TMPDIR` 值下的字节稳定性。无密钥的组装快照通过真实 Loader 组合固定持久上下文消息。无密钥回放负责固定中性的拒绝到升权轨迹;它是结构回归证明,而不是措辞选型证据。

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/implemented/feature/2026-07-30-queued-manual-compaction.md
2026-07-30-queued-manual-compaction.md: 5100808ada4b7b284228113584e577d46ff91101
2026-07-30-queued-manual-compaction.zh.md: fe3b0ff5e6b8cab7cb332f958adc6ab8d94f23b8
2026-07-30-queued-manual-compaction.zh.md: c995fc59fda83de639b6e1a603448243ba12c505

View File

@@ -30,7 +30,7 @@ Status: implemented
维护阶段不会创建第二个队列。之后发送的项保留其 `MessageId`、位置、FIFO 顺序与唤醒信息。会唤醒的输入会保持排队,直至维护任务结算,再启动既有驱动器路径;`inject()` 仍然不会唤醒驱动器。
`whenIdle()` 会把维护任务及其结算后释放的所有唤醒工作视为尚未完成的活动。取消会中止 agent 自有的维护信号,生命周期 teardown 则会在 dispose(资源释放)完成前排空同一个活动边界。
`whenIdle()` 会把维护任务及其结算后释放的所有唤醒工作视为尚未完成的活动。取消会中止 agent(智能体)自有的维护信号,生命周期 teardown 则会在 dispose(资源释放)完成前排空同一个活动边界。
### 一个参数化事务拥有每一对标记
@@ -97,9 +97,9 @@ DSH 有意在调用摘要器前记录 `compact/start`。缓慢或崩溃的尝试
## 验证
Agent loop 测试覆盖同一 tick 内的优先权、保留 ID 与 FIFO 生命周期、会唤醒和静默的排队工作、幂等释放、`whenIdle()`、取消与 teardown。压缩测试覆盖独立与数字形式的不变量 owner、end-seed 回放、活动与陈旧未匹配标记、listener 重入、所选 span 漂移、commit 与闭合失败、flush 顺序、原始取消原因、raw output 与 usage 保留,以及自动/手动互斥。
agent loop(智能体循环)测试覆盖同一 tick 内的优先权、保留 ID 与 FIFO 生命周期、会唤醒和静默的排队工作、幂等释放、`whenIdle()`、取消与 teardown。压缩测试覆盖独立与数字形式的不变量 owner、end-seed 回放、活动与陈旧未匹配标记、listener 重入、所选 span 漂移、commit 与闭合失败、flush 顺序、原始取消原因、raw output 与 usage 保留,以及自动/手动互斥。
命令包固定注册行为、Loader 组合、参数拒绝、精确的成功/失败文本、取消、不进入模型历史的保证,以及处置操作在中止使执行器停止等待处理器后,仍会跨越相互独立的闭合与 flush 边界等待该处理器结算。客户端运行时投影测试固定 end-seed 中断,以及随后一次独立尝试的完成。`queued-manual-compact` 终端快照通过已组装 TUI 驱动真实按键:`/help` 可发现该命令;被暂停的摘要会接纳一个排队提示词和即时注入;`turn: null` 标记与 flush 先于排队提示词轮次;命令生命周期保持纯日志;派生顺序固定为检查点 → 注入 → 排队提示词。
命令包固定注册行为、Loader 组合、参数拒绝、精确的成功/失败文本、取消、不进入模型历史的保证,以及 dispose 在中止使执行器停止等待处理器后,仍会跨越相互独立的闭合与 flush 边界等待该处理器结算。客户端运行时投影测试固定 end-seed 中断,以及随后一次独立尝试的完成。`queued-manual-compact` 终端快照通过已组装 TUI 驱动真实按键:`/help` 可发现该命令;被暂停的摘要会接纳一个排队提示词和即时注入;`turn: null` 标记与 flush 先于排队提示词轮次;命令生命周期保持纯日志;派生顺序固定为检查点 → 注入 → 排队提示词。
## 后果

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/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md
2026-07-30-versioned-gui-welcome-onboarding.md: 4707769d4fa9fbf184e09a2e73087dfd326070be
2026-07-30-versioned-gui-welcome-onboarding.zh.md: c9d2e6274c476c59abc077aa3255ffe57a8a48bc
2026-07-30-versioned-gui-welcome-onboarding.zh.md: 002a04b1fc6acc4fc7cfee053bdbbb2abaef047e

View File

@@ -14,11 +14,11 @@ GUI 的凭据引导从 DeepSeek 专用的就绪状态检查开始,但内部测
**不属于单一功能的产品引导由 `ui-settings-general` 持有。** `src/onboarding-copy.ts` 是完整通知、「继续」按钮文案和 `WELCOME_NOTICE_VERSION` 的唯一可编辑来源;GUI 支持的两种 locale 都有意渲染同一份中文所有者文案。运行时 locale 字典从该文件派生欢迎文案,测试也导入同一个所有者,而不重复段落文本。该通知只存在于浏览器 UI:它不会创建会话事件,也不会贡献任何模型可见内容。通知明确以 `DSH_TELEMETRY_DISABLED=1` 作为遥测关闭方式。
**loopback 确认状态按 Harness profile 持久化。** 宿主端在 user-settings seam 中注册 `ui-onboarding` 分节,并存入当前 `$DSH_HOME/settings.yaml`。connection 插件通过 `ctx.connection.isLoopback` 统一发布当前页面是否使用 loopback authority;hostname 判定函数留在 connection 包内,其他客户端插件只消费服务状态,不跨插件导入实现函数。除非 `welcomeNoticeVersion` 与文案所有者文件中的常量精确相等,否则 loopback 浏览器会显示通知。「继续」会以当前版本执行一次路径变更,并且仅在宿主端提交成功后调用 `complete()`;写入失败时通知保持打开,关闭页面或进程则不会写入任何内容。提升该常量会有意要求每个 profile 对修订后的文案重新确认一次。非 loopback 浏览器不能调用仅限 loopback 的 settings API;它仍显示同一通知,但显式点击「继续」只会在当前浏览器进程中完成该步骤,重新加载或新进程会再次显示通知。
**loopback 确认状态按 Harness profile 持久化。** 宿主端在 user-settings seam 中注册 `ui-onboarding` 分节,并存入当前 `$DSH_HOME/settings.yaml`。connection 插件通过 `ctx.connection.isLoopback` 统一发布当前页面是否使用 loopback authority;hostname 判定留在 connection 包内,其他客户端插件只消费服务状态,而不导入其实现。除非 `welcomeNoticeVersion` 与文案所有者文件中的常量精确相等,否则 loopback 浏览器会显示通知。「继续」会以当前版本执行一次路径变更,并且仅在宿主端提交成功后调用 `complete()`;写入失败时通知保持打开,关闭页面或进程则不会写入任何内容。更新该常量会有意要求每个 profile 对修订后的文案重新确认一次。非 loopback 浏览器不能调用仅限 loopback 的 settings API;它仍显示同一通知,但显式点击「继续」只会在当前浏览器进程中完成该步骤,重新加载或新进程会再次显示通知。
**并发 loopback 视图无需陈旧的整体替换即可收敛。** 确认写入有意省略 `expectedRevision`:每个 loopback 标签页都向同一路径写入相同版本,因此该操作是幂等的,并会保留同级字段,而不是重建整个分节。`settings/document-updated` 会转为 `host/settings-changed`;另一个标签页或外部编辑器提交当前版本后,已挂载的 loopback 标签页会重新拉取状态并推进。API 网关在可配置提供方 namespace 之外,通过封闭的允许列表暴露这一个产品 namespace,同时不会把它的变更视为模型目录失效事件。
**并发 loopback 视图无需陈旧的整体替换即可收敛。** 确认写入有意省略 `expectedRevision`:每个 loopback 标签页都向同一路径写入相同版本,因此该操作是幂等的,并会保留同级字段,而不是重建整个分节。`settings/document-updated` 会转为 `host/settings-changed`;另一个标签页或外部编辑器提交当前版本后,已挂载的 loopback 标签页会重新拉取状态并推进。API Proxy(`@deepseek-ai/dsh-host-apiproxy`)在可配置提供方 namespace 之外,通过封闭的允许列表暴露这一个产品 namespace,同时不会把它的变更视为模型目录失效事件。
**引导流程会暂时接管视口,形成一个连续阶段。** 纯色产品界面通过挂载到 `body` 的 portal 取代完整的应用视图,并将底层应用根节点标记为 inert;严格符合要求的遮罩仍挂载在该界面后方,并保留 `position:absolute`、left/right/bottom 偏移量为零、`top:80px`、`rgba(0, 0, 0, 0.24)` 和 `backdrop-filter: blur(2px)`。欢迎页和按条件显示的凭据设置页在这一阶段中依次呈现,而不是各自作为独立的模态窗口。两个页面都复用 Web UI 的黑色 `BrandWordmark`。欢迎页在 `内测声明` 标题下逐字保留既定的四段文案;所有段落统一采用 16/28 的正文字号与行高,只有最后一段中指定的行动语句使用较为克制的 500 字重。短暂的错落式透明度与纵向位移动画营造出舒缓节奏,但不会阻碍交互,并会在用户启用减少动态效果时禁用。初始焦点落在标题上,「继续」是唯一按钮,且不存在关闭、Escape 或点击遮罩的退出路径。
**引导流程会暂时接管视口,形成一个连续阶段。** 纯色产品界面通过挂载到 `body` 的 portal 取代完整的应用视图,并将底层应用根节点标记为 inert;严格符合要求的遮罩仍挂载在该界面后方,并保留 `position:absolute`、left/right/bottom 偏移量为零、`top:80px`、`rgba(0, 0, 0, 0.24)` 和 `backdrop-filter: blur(2px)`。欢迎页和按条件显示的凭据设置页在这一阶段中依次呈现,而不是各自作为独立的模态窗口。两个页面都复用 Web UI 的黑色 `BrandWordmark`。欢迎页在 `内测声明` 标题下逐字保留既定的四段文案;所有段落统一采用 16/28 的正文字号与行高,只有最后一段中指定的行动语句使用较为克制的 500 字重。短暂的错落式透明度与纵向位移动画营造出节奏感,但不会阻碍交互,并会在用户启用减少动态效果时禁用。初始焦点落在标题上,「继续」是唯一按钮,且不存在关闭、Escape 或点击遮罩的退出路径。
## 曾考虑的替代方案
@@ -28,7 +28,7 @@ GUI 的凭据引导从 DeepSeek 专用的就绪状态检查开始,但内部测
**在渲染或窗口关闭时持久化**:不予采用,因为看见通知不等于确认,窗口关闭事件也无法可靠送达。只有显式提交「继续」才能阻止通知在下次启动时再次显示。
**通用的公开设置暴露标志**:不予采用,因为一个产品 namespace 不足以证明应当扩大每个 settings 注册方的公开配置面。网关保留显式的封闭允许列表。
**通用的公开设置暴露标志**:不予采用,因为一个产品 namespace 不足以证明应当扩大每个 settings 注册方的公开配置面。该 API Proxy 保留显式的封闭允许列表。
## 后果

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/implemented/feature/2026-07-30-web-queue-steer-action.md
2026-07-30-web-queue-steer-action.md: b04095b81f499982c8680a2d3627d8e98a70d8ac
2026-07-30-web-queue-steer-action.zh.md: af13292574b7f130fe858369c746ff2b8911984e
2026-07-30-web-queue-steer-action.zh.md: 1b46ec90a27fbc3c8f3660a7cbdccb78a21a2cfa

View File

@@ -1,4 +1,4 @@
# Agent Note(agent 决策记录):将 Web 已排队消息转为活动轮次的 steering(中途引导)
# Agent Note: 将 Web 已排队消息转为活动轮次的 steering(中途引导)
Status: implemented
@@ -6,7 +6,7 @@ Status: implemented
## 问题
Web composer 原本会在 agent 运行期间把所有 Enter 提交作为 Queue 入队。QueueDock 已经为每条待处理消息提供可寻址的行,持久 transcript(文本记录)也已能把消费后的 steer 事件渲染为用户样式气泡,但 Web 既没有连接这两个界面的操作,也没有让用户从 composer 直接选择当前轮次 steering 的手势。
Web composer 原本会在 agent(智能体)运行期间把所有 Enter 提交作为 Queue 入队。QueueDock 已经为每条待处理消息提供可寻址的行,持久 transcript(文本记录)也已能把消费后的 steer 事件渲染为用户样式气泡,但 Web 既没有连接这两个界面的操作,也没有让用户从 composer 直接选择当前轮次 steering 的手势。
如果 Web 先在客户端删除该行,再调用 `session.prompt(mode: 'steer')`,就会把用户的一次意图拆分到两个 RPC 中。驱动器可能在两次调用之间先认领该项,steering 投递也可能在删除后失败;现有尽力而为的 `agent.steer()` 回退还可能在原单次入队项被移除后,静默追加一个新的 Queue 项。因此,立即发送操作必须区分当前轮次 steering 与 Queue 前移,并在 steering 已不可用时保留原行。
@@ -14,7 +14,7 @@ Web composer 原本会在 agent 运行期间把所有 Enter 提交作为 Queue
### 产品约定
普通会话中每个非编辑态的 QueueDock 行都会提供名为“插话发送”的向上箭头操作。仅当会话报告 agent 正在运行时,该操作才会启用;包含混合内容的消息仍可使用,因为 steering 会转发完整且不可变的 `UserMessage`,而非该行的文本投影。已寻址 subagent 的 Queue 投影保持只读,因为其继续执行传输不提供 Queue 变更。
普通会话中每个非编辑态的 QueueDock 行都会提供名为「插话发送」的向上箭头操作。仅当会话报告 agent 正在运行时,该操作才会启用;包含混合内容的消息仍可使用,因为 steering 会转发完整且不可变的 `UserMessage`,而非该行的文本投影。已寻址 subagent 的 Queue 投影保持只读,因为其继续执行传输不提供 Queue 变更。
触发该操作会针对对应的 `InboxItemId` 请求严格的当前轮次 steering。操作成功后,权威 Host 快照会移除 Queue 行,并在 `Deep diving...` 运行状态行之后立即投影同一条待处理 steering;该气泡提供复制,但消息尚无持久事件序号,因此不提供 fork。AgentLoop 排空该项后,现有持久 `user/message` 事件会接管同一个用户样式气泡,并恢复时钟、复制和 fork,无需另建持久展示路径。
@@ -34,9 +34,9 @@ Composer 对新输入采用另一套尽力而为约定。所寻址会话空闲
`session.updateQueue` 会携带 `steer` 操作,并把两种负面结果映射为类型化 RPC 错误。这项转换是一次同步 Agent 操作;Host 绝不会通过组合移除和提示词调用来重建它。
Host 仍以现有 `queuedMirror` 作为唯一的瞬态 inbox 权威。`session/queue` 快照会携带所有存活单次入队项及其 `placement: 'queued' | 'steering'`:QueueDock 只渲染 queued 行,ChatView 则在会话流末尾、`Deep diving...` 运行状态行之后渲染待处理 steering,提供复制操作,但不提供 fork、编辑或删除操作。重连会重放同一份快照,因此这项可见性既不依赖客户端乐观展示,也不需要第二个 registry。
Host 仍以现有 `queuedMirror` 作为唯一的瞬态 inbox 权威。`session/queue` 快照会携带所有存活单次入队项及其 `placement: 'queued' | 'steering'`:QueueDock 只渲染 queued 行,ChatView 则在会话流末尾、`Deep diving...` 运行状态行之后渲染待处理 steering,提供复制操作,但不提供 fork、编辑或删除操作。重连会重放同一份快照,因此这项可见性既不依赖客户端乐观展示,也不需要第二个注册表。
AgentLoop 认领待处理 steering 时,会在同步追加持久 `user/message` 之前立即发出 `agent/inbox/dequeue`。Host 会等到下一个微任务才退役该 steering 行,让持久 session 事件先进入线性 mux 流。客户端 Session 接纳该实时事件时,会在发布快照前退役第一个匹配的当前 steering 单次入队项;历史回放不会消费后来复用同一 `MessageId` 的单次入队项。因此,ChatView 无需扫描持久历史就能每次只渲染一份权威,持久投影则会根据已记录的事件时间与序号恢复时钟、复制与 fork 操作。追加失败时,已认领行仍会退役。
AgentLoop 认领待处理 steering 时,会在同步追加持久 `user/message` 之前立即发出 `agent/inbox/dequeue`。Host 会等到下一个微任务才退役该 steering 行,让持久会话事件先进入线性 mux 流。客户端 Session 接纳该实时事件时,会在发布快照前退役第一个匹配的当前 steering 单次入队项;历史回放不会消费后来复用同一 `MessageId` 的单次入队项。因此,ChatView 无需扫描持久历史就能每次只渲染一份权威,持久投影则会根据已记录的事件时间与序号恢复时钟、复制与 fork 操作。追加失败时,已认领行仍会退役。
现有 `session.prompt(mode: 'steer')` 对主会话新输入仍采用尽力而为的约定:在 next-step 窗口之外,它会变为唤醒 agent 的后续轮次。Composer 会让显式 `queue | steer` 模式经过 slash 裁决与引用序列化,再调用该约定。浏览器本地的提交策略拥有持久化的繁忙态 Enter 偏好,并且只为支持 steering 的会话把普通 Enter 与加速 Enter 解析为互补手势;Settings 行和 InputBar 共享该策略,不重复实现存储或投递窗口权威。只有 Queue 行操作采用严格语义,因为任一种负面结果都会经原 Queue 单次入队项收敛。

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/implemented/feature/2026-07-30-web-tool-row-unified-expand-and-inspect.md
2026-07-30-web-tool-row-unified-expand-and-inspect.md: 4fcf1e9a567b73cd04f1bbed170c391aaedd9e73
2026-07-30-web-tool-row-unified-expand-and-inspect.zh.md: aa81bb584cb794110454cf97520f82c1421e3e41
2026-07-30-web-tool-row-unified-expand-and-inspect.zh.md: 765cd058c99e53e5dea81f83c203a6d75d56f104

View File

@@ -1,29 +1,29 @@
# Agent Note:Web 工具行统一展开交互与 trajectory Inspect
# Agent Note: Web 工具行统一展开交互与 trajectory Inspect
状态:已实现
Status: implemented
[English](2026-07-30-web-tool-row-unified-expand-and-inspect.md) | 中文
## 问题
聊天视图的工具行交互已经分裂成多种方言:ToolRow 通过前导图标切换展开、且仅限有 args body 的调用,bash 示例有自己的一套展开方式,todo / ask-question 行只能展开原始 args,单文件工具完全不可展开,而调用的 OUTPUT 只能通过右侧详情面板查看。失败的 bash 命令(exit≠0 但结算为 `isError:false`)在折叠行上没有任何失败信号。此外聊天行没有跳转到 trajectory 记录的入口,且 chat → trajectory → chat 切换会丢失阅读位置(标签环会卸载非活跃视图)。
聊天视图的工具行交互已经分裂成多种方言:ToolRow 通过前导图标切换展开、且仅限有 args body 的调用,bash 示例有自己的一套展开方式,todo / ask-question 行只能展开原始 args,单文件工具完全不可展开,而调用的 OUTPUT 只能通过详情面板查看。失败的 bash 命令(exit≠0 但结算为 `isError:false`)在折叠行上没有任何失败信号。此外聊天行没有跳转到 trajectory 记录的入口,且 chat → trajectory → chat 切换会丢失阅读位置(标签环会卸载非活跃视图)。
## 决定
**所有可展开工具行共享同一交互——整行即开关(点击 / Enter / 空格),图标 hover 时渐变为 chevron 预览——以及同一展开体:带 IN/OUT 侧栏标签的卡片,各分区独立滚动上限;hover 显示的 Inspect 胶囊通过 store 的一次性交接跳到该调用的 trajectory 记录;聊天视图用内存态的按会话 Map 在视图切换间保留语义阅读位置。**
- `toolRowModel` 在 args 之外同时派生结果材料:`output`(`resultText` 拍平逻辑从 DetailsPanel 移入 contract)和 `errorSummary`(失败首行,以错误色作为折叠摘要)。有 body、output 或 terminal 材料的行即可展开;行本身是开关(`role="button"`、`aria-expanded`),文件路径摘要通过 `stopPropagation` 保持独立链接。
- `toolRowModel` 在 args 之外同时派生结果材料:`output`(`resultText` 拍平逻辑从 DetailsPanel 移入 contract)和 `errorSummary`(失败首行,作为折叠摘要并以错误色显示)。有 body、output 或 terminal 材料的行即可展开;行本身是开关(`role="button"`、`aria-expanded`),文件路径摘要通过 `stopPropagation` 保持独立链接。
- 展开卡片(figma 1249:35657)是 IN/OUT 分区列:每个分区是独立滚动区(max-height 150px),侧栏标签 sticky 固定,l2 分割线横贯整卡宽度。Think 的推理文本和 run_code 的 CodeBlock 保持非卡片体;上下文注入复用此行并以无标签的 `plainBody` 卡片展开。
- `terminalFailed` 读取已结算 terminal 卡片的退出状态,让 BashRow 和 GenericToolCard 把失败命令显示为行的红色状态点——这是折叠行唯一的失败信号,因为调用本身结算为 `isError:false`。
- TerminalBlock 的横幅并入同一阅读模型:与卡片共用同一表面(不再用 banner token),与正文之间是 l2 细线,命令列上限 150px 内部滚动,复制/状态控件 sticky 且顶对齐第一行提示符。
- Inspect:`ToolCallOwnerProps.inspect`(无调用身份的行不提供)在展开体左下角以真实布局位置渲染胶囊,hover 整个 tool call 任意位置显示。点击将 `{ callId }` 写入 chat store 的一次性 `inspect` 字段并切换到 trajectory 视图;TrajectoryTable 找到记录、打开其摘要,并通过清空字段确认。
- 滚动保留:每次非贴底滚动时,聊天视图把 `{ anchorKey, anchorTop, scrollTop }` 保存到 apply 作用域的按会话 Map,并经注入 props 的 `chatScroll` 暴露;重挂载时先用 `scrollTop` 到达近似窗口,再按稳定 node/call 锚点的矩形差值校正,因此宽度重排后仍把同一阅读行保持在原位。包括「回到底部」在内的每条贴底路径都会在切换 tab 或会话前同步清除该项。Map 仍刻意不持久化——新页面加载保持打开即贴底的默认行为。
- Inspect:`ToolCallOwnerProps.inspect`(无调用身份的行不提供)在展开体左下角的正常布局流中渲染胶囊,hover 到工具调用的任意位置时显示。点击将 `{ callId }` 写入 chat store 的一次性 `inspect` 字段并切换到 trajectory 视图;TrajectoryTable 找到记录、打开其摘要,并通过清空字段确认。
- 滚动保留:每次非贴底滚动时,聊天视图把 `{ anchorKey, anchorTop, scrollTop }` 保存到 apply 作用域的按会话 Map,并以 `chatScroll` 暴露;重挂载时先用 `scrollTop` 到达近似窗口,再按稳定 node/call 锚点的矩形差值校正,因此宽度重排后仍把同一阅读行保持在原位。包括「回到底部」在内的每条贴底路径都会在切换 tab 或会话前同步清除该项。Map 仍刻意不持久化——新页面加载保持打开即贴底的默认行为。
## 曾考虑的替代方案
**保留前导图标开关和各注册方自有的展开方式。** 否决:三个表面已经分化;注册方姿态(bash 示例本地复刻 CSS)意味着除非交互约定本身统一且足够小——整行开关加 hover 预览——否则漂移会永久存在。
**通过 URL 或 trajectory 视图 prop 传递 Inspect。** 否决:视图环经由 slot 注册表渲染,两个视图没有可携带 prop 的共同父级;chat store 本就跨越该边界,一次性字段让交接可安全重放(字段出现之前的持久化快照以 `?? null` 复水)。
**通过 URL 或 trajectory 视图 prop 传递 Inspect。** 否决:视图环经由 slot 注册表渲染,两个视图没有可携带 prop 的共同父级;chat store 本就跨越该边界,一次性字段让交接具备回放安全性(字段出现之前的持久化快照以 `?? null` 复水)。
**持久化聊天滚动偏移。** 否决:把几天前的偏移恢复到已经增长的会话里读起来像 bug;内存 Map 把记忆精确限定在会丢位置的视图切换场景。

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/implemented/feature/2026-07-31-browser-derived-initial-locale.md
2026-07-31-browser-derived-initial-locale.md: 0c49a6bbfec0ab33a5aa3ce53dde0cac747f3816
2026-07-31-browser-derived-initial-locale.zh.md: c013d24dcd3bb49d176eaddd42ff41dde320ff1f
2026-07-31-browser-derived-initial-locale.zh.md: ca8eeceae58e25836bb42f7c1cec0e61a232d921

View File

@@ -16,21 +16,21 @@ Status: implemented
**判定浏览器用的是 `window` 而非 `navigator`。** Node ≥ 21 暴露全局 `navigator` 并报告机器自身语言(CI runner 上是 `en-US`),因此以 `navigator` 把关会让 node 启动客户端树时解析成 `en`,而非文档约定的回落值。以 `window` 把关可使所有非浏览器运行都停留在 `FALLBACK_LOCALE`。
**显式选择是永久的。** `setLocale` 的持久化未作改动,且持久化值最先被查询,因此选过语言的用户即便在不同浏览器配置或系统语言之间辗转也保留原选择。没有任何代码把探测到的 locale 写回存储:探测在每次启动时重新推导,对"用户是否做过选择"这一问题始终不可见。
**显式选择是永久的。** `setLocale` 的持久化未作改动,且持久化值最先被查询,因此选过语言的用户即便在不同浏览器配置文件或系统语言之间辗转也保留原选择。没有任何代码把探测到的 locale 写回存储:探测在每次启动时重新推导,对「用户是否做过选择」这一问题始终不可见。
**浏览器 e2e 车道现在钉住浏览器语言,而不只是存储项。** 断言中文文案的场景(`access-confirmation`、`models-settings`、`onboarding-deepseek-config`、`settings-chrome`)以 `apps/web/tests/support.ts` 的 `locale: ZH_BROWSER_LOCALE` 打开页面;`newEnglishPage` 仍然钉 `dsh.locale=en`,它依旧压过任何浏览器语言。`settings-chrome.e2e.ts` 新增一个场景:另开一个存储项为空的 `en-US` 页面,断言设置界面以英文呈现——这是本功能在组装后应用中的证据。
## Alternatives considered
- **`Intl.DateTimeFormat().resolvedOptions().locale` 或单读 `navigator.language`**:两者都把用户的有序偏好列表塌缩成一个标签,于是 `['de', 'en', 'zh']` 的读者拿到的是 zh 而非 en。列表恰恰是浏览器这份声明里最值得读的部分。
- **首次启动即持久化探测结果**:那会把探测变成一次性事件,让一次陈旧的首访凌驾于此后改变的浏览器语言之上,也摧毁了整个解析顺序所依赖的区分——存储值将不再意味着"用户选了它"。
- **首次启动即持久化探测结果**:那会把探测变成一次性事件,让一次陈旧的首访凌驾于此后改变的浏览器语言之上,也摧毁了整个解析顺序所依赖的区分——存储值将不再意味着「用户选了它」。
- **完整的 BCP 47 协商(`Intl.LocaleMatcher` 式查找、地区与文字权重)**:在只提供两个语言互异的 locale 时,主子标签匹配就是正确答案的全部;协商层只会带来无行为支撑、也无从测试的表面积。
- **为默认 locale 增加一个 cordis config key**:此处部署之间并无差异——回落值是产品对"完全没有信号"给出的答案,不是旋钮。仓库策略把 `Config` 字段留给有当前消费者、且随部署变化的选择。
- **为默认 locale 增加一个 Cordis 配置键**:此处部署之间并无差异——回落值是产品对「完全没有信号」给出的答案,不是旋钮。仓库策略把 `Config` 字段留给有当前消费方、且随部署变化的选择。
- **让 e2e 车道的中文场景继续钉存储项(`dsh.locale=zh`)**:那会让套件保持绿色,却抹掉浏览器推导路径在组装后应用中唯一的运行处;改钉浏览器语言才能端到端地演练新的解析过程。
## Consequences
- 来自英文浏览器的首访落在英文界面,而语言行依然呈现同样两个以自身语言自述的选项,两个方向的脱身通道都未改变。
- `FALLBACK_LOCALE` 收窄回它真正的职责——字典回落与无信号时的答案——不再兼职充当"用户尚未选择"。
- `FALLBACK_LOCALE` 收窄回它真正的职责——字典回落与无信号时的答案——不再兼职充当「用户尚未选择」。
- 在 jsdom 下构造 `LocaleService` 的测试现在依赖环境的 `navigator`:断言本地化文案的用例以一行套件级 `usePinnedBrowserLanguages('zh-CN')`(dsh-client-test-runtime)声明其浏览器,今后任何断言默认值的用例同样如此。本包自己的用例直接给全局打桩,因为它们需要该 helper 刻意不表达的形状(`languages` 缺失、列表与 `language` 解耦、完全没有 `window`)。
- 探测的代价是每次服务构造遍历一次数组,且不写存储,因此启动行为与持久化状态面均无变化。

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/implemented/feature/2026-07-31-even-out-shipped-tool-rosters.md
2026-07-31-even-out-shipped-tool-rosters.md: 0195620055da5e570d2f54792d950a88bab8d652
2026-07-31-even-out-shipped-tool-rosters.zh.md: cc67f24720f650283daa4ee85a0a1af5358756a8
2026-07-31-even-out-shipped-tool-rosters.zh.md: ab6982e33c4a0a25cbc2fde386456840ce99d9c5

View File

@@ -6,19 +6,19 @@ Status: implemented
## 问题
两个交付的 `dsh` surface 提供着不同的工具,而没有任何记录说明为什么。会话检查点、工具结果裁剪、goal 工具和 Ralph 在 `tui.cordis.yml`;`tool-todo` 以及后来的 web 搜索在 `web.cordis.yml`。两个 surface 都没有会话搜索、字符串替换编辑器和重复工具守卫,尽管这三者都已成包存在,且没有一个是 surface 专属的。
两个交付的 `dsh` surface 提供着不同的工具,而没有任何记录说明为什么。会话检查点、工具结果裁剪、goal 工具和 Ralph 在 `tui.cordis.yml`;`tool-todo` 以及后来的 web 搜索在 `web.cordis.yml`。两个 surface 都没有会话搜索、字符串替换编辑器和重复工具守卫,尽管这三者都已成包存在,且没有一个是 surface 专属的。
结果是一处没人做过决定的用户可见差异:同一个模型、同一个请求,在终端上能定目标而在浏览器里不能,在浏览器里能搜网页而在终端上不能。
结果是一处没人做过决定的用户可见差异:同一个模型、同一个请求,在终端上能定目标而在浏览器里不能,在浏览器里能搜网页而在终端上不能。
## 决策
那些并非 surface 专属的行移入 [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml),另有三行加入:`tool-session-query`、`tool-str-replace-editor` 和 `repeat-tool-guard`。Web 搜索也一并移入;其[部署决策](2026-07-31-web-default-search.md)负责安全边界,共享 base 则负责与 surface 无关的挂载。两个 surface 组装同一份清单:每台宿主上都有二十二个工具——二十个共享行加上 `glob` 和 `grep`,它们成为固定成员,因为 `dsh-tool-fs-search` 直接 spawn [打包的 ripgrep 二进制](../architecture/2026-08-01-packaged-ripgrep-search.md)。`tool-session-query` 加入后又退出了——[session-search-not-shipped-default 决策](2026-08-02-session-search-not-shipped-default.md)让面向模型的消费方保持需显式启用——而这份清单的其余部分保持不变。
那些并非 surface 专属的行移入 [`base.cordis.yml`](../../../../packages/bundle/base/cordis.patch.yml),另有三行加入:`tool-session-query`、`tool-str-replace-editor` 和 `repeat-tool-guard`。Web 搜索也一并移入;其[部署决策](2026-07-31-web-default-search.md)负责安全边界,共享 base 则负责与 surface 无关的挂载。两个 surface 组装同一份清单:每台宿主上都有二十二个工具——二十个共享行加上 `glob` 和 `grep`,它们成为固定成员,因为 `dsh-tool-fs-search` 直接 spawn [打包的 ripgrep 二进制](../architecture/2026-08-01-packaged-ripgrep-search.md)。`tool-session-query` 加入后又退出了——[session-search-not-shipped-default 决策](2026-08-02-session-search-not-shipped-default.md)让面向模型的消费方保持需显式启用——而这份清单的其余部分保持不变。
有两行仍是 surface 专属。`tmux-context` 只在 TUI,因为浏览器 surface 没有终端复用器可描述。`session-reference` 只在 TUI,因为它以 launcher 的进程本地路径驱动共享的 session-query 索引,而浏览器侧边栏会在自己的首次搜索里重建该索引。
有两行仍是 surface 专属。`tmux-context` 只在 TUI,因为浏览器 surface 没有终端复用器可描述。`session-reference` 只在 TUI,因为它以 launcher 的进程本地路径驱动共享的 session-query 索引,而浏览器侧边栏会在自己的首次搜索里重建该索引。
**本次工具清单决策当时只做加法。** 落地时两个 surface 均未移除任何工具行,目录对比只发现了新增,别无其他。这些新增中的一项 `tool-session-query` 随后被[session-search-not-shipped-default 决策](2026-08-02-session-search-not-shipped-default.md)移除。共享执行器、沙箱组合与访问默认值独立归属[workspace-write 默认值决策](2026-07-31-workspace-write-surface-default.md)。
### 什么保持不挂,以及为什么
### 什么保持不挂,以及为什么
有三项能力基于其自身包所记录的证据保持在外,列在这里是为了让「我们忘了」和「我们决定不要」保持可区分。
@@ -26,13 +26,13 @@ Status: implemented
**`dsh-web-fetch-local`** 保持不挂,`dsh-tool-web` 保持 `fetch: false`。SSRF 防护在实现中是 deferred 状态([`policy.ts`](../../../../packages/web/web-fetch-local/src/policy.ts) 只校验协议、凭据与长度),包里也直说了:「this provider is an SSRF primitive and **must not be enabled** in a deployment that can reach sensitive internal network targets」([README](../../../../packages/web/web-fetch-local/README.md))。目标由模型选择,其中包括 harness 自己跑在环回地址上的网关、内网段和云元数据端点。
弃用它收窄的是接触面而非可达性:`bash` 是挂着的,`curl` 照样能拿到同一个页面——一次真实运行确认了这点。这个缺席买到的是去掉一个无需 shell、以参数成形的请求原语,以及随之而来的那条意外路径:一次「帮我总结这个页面」悄悄打到环回地址。真要收住出站流量的部署需要的是网络层管控。
不挂载它收窄的是接触面而非可达性:`bash` 是挂着的,`curl` 照样能拿到同一个页面——一次真实运行确认了这点。这个缺席买到的是去掉一个无需 shell、以参数成形的请求原语,以及随之而来的那条意外路径:一次「帮我总结这个页面」悄悄打到环回地址。真要收住出站流量的部署需要的是网络层管控。
**LSP 三件套**留在外面是运维原因而非安全原因:`command` 在插件加载时从 `PATH` 解析,因此缺少语言服务器会让整次启动失败,而不只是失去一个工具。等到「缺失」退化为「跳过注册」之后,它就可以挂了。
### MCP 是依赖,不是配置行
`@deepseek-ai/dsh-mcp-client` 成为本 CLI 的运行时依赖,但在任何交付配置里都没有对应的行。该插件每个实例只挂载一台服务器,且 `command` 是必填,因此一个默认值必须点名一台第三方服务器,并在每次启动时把它作为子进程 spawn——不经 `ctx.bash`,因而也在 Web surface 所组合的沙箱策略之外。
`@deepseek-ai/dsh-mcp-client` 成为本 CLI(命令行界面)的运行时依赖,但在任何交付配置里都没有对应的行。该插件每个实例只挂载一台服务器,且 `command` 是必填,因此一个默认值必须点名一台第三方服务器,并在每次启动时把它作为子进程 spawn——不经 `ctx.bash`,因而也在 Web surface 所组合的沙箱策略之外。
真正能让 MCP 成为默认的那一层,恰恰是本仓库尚未拥有的:一个读取用户服务器清单、按条目逐台挂载客户端的桥接,形态与 [`dsh-hooks-claude`](../../../../packages/hooks/hooks-claude/README.md) 读取 Claude Code 的 `hooks.json` 完全相同。交付这个依赖意味着已安装的 `dsh` 今天就能从 `$DSH_HOME/config.yaml` 挂载服务器;CLI README 里给了那段 YAML。
@@ -54,11 +54,11 @@ Status: implemented
**把共享的行复制进两份 overlay,而不是提升到 base。** 基于「一处归属」原则否决:新增行里有三行会存在两份,而这些副本没有任何理由发生分歧,下一次改工具清单还得记着改两处。
**在同一次改动里给 TUI 加沙箱。** 不予采纳,因为这是一个不属于工具清单改动的独立决定:TUI 挂的是不受限执行器,替换它们会改变一个既有 surface 做什么,而非它提供什么。这个决定需要自己的证据——尤其因为 TUI 没有 `approval/request` 的应答方,升级请求在那里会 fail-closed,而不是弹出提示。
**在同一次改动里给 TUI 加沙箱。** 不予采纳,因为这是一个不属于工具清单改动的独立决定:TUI 挂的是不受限执行器,替换它们会改变一个既有 surface 做什么,而非它提供什么。这个决定需要自己的证据——尤其因为 TUI 没有 `approval/request` 的应答方,升权请求在那里会 fail-closed,而不是弹出提示。
**开启 Code Mode。** 它的信任立场按设计与 bash 同级,工具调用要过与 bash 相同的 `tools/pre-execute` 闸门,所以它与上面那些模型写码工具不是同一个判断。在这里仍被否决:`both` 会改变两个 surface 上每一个模型可见请求,而 `code` 是把线路替换而非加一个——两者都是呈现方式的决定,不是工具清单的决定。
**默认挂一台 MCP 服务器。** 否决,因为交付默认值必须点名一台,而任何选择都会在每个用户的机器上、在沙箱之外 spawn 一个第三方子进程。改为交付依赖。
**默认挂一台 MCP 服务器。**否决,因为交付默认值必须点名一台,而任何选择都会在每个用户的机器上、在沙箱之外 spawn 一个第三方子进程。改为交付依赖。
## 后果

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/implemented/feature/2026-07-31-gui-full-access-confirmation.md
2026-07-31-gui-full-access-confirmation.md: ca89ed23fb1b5c6ea438dd22fdf20d2b82af754c
2026-07-31-gui-full-access-confirmation.zh.md: db5f4bc58e05e2441bcaa3da160de3bc9e707a6a
2026-07-31-gui-full-access-confirmation.zh.md: b4ab92271f6b4a6ebe3b7e7c3f82f859c53933bf

View File

@@ -4,28 +4,28 @@ Status: implemented
[English](2026-07-31-gui-full-access-confirmation.md) | 中文
## Problem
## 问题
在 Web 客户端的权限选择器中切换到 `danger-full-access` 只需一次点击,且预设以 Title Case 机器名 `Danger Full Access` 展示。Full access 会减少确认步骤,允许智能体执行敏感操作、修改文件或运行外部命令,误点即在毫无刻意确认环节的情况下启用了最危险的预设。
在 Web 客户端的权限选择器中切换到 `danger-full-access` 只需一次点击,且预设以 Title Case 机器名 `Danger Full Access` 展示。Full access 会减少确认步骤,允许 agent(智能体)执行敏感操作、修改文件或运行外部命令,误点即在毫无刻意确认环节的情况下启用了最危险的预设。
## Decision
## 决策
**每个权限选择器都把 `danger-full-access` 关进共享的页面内 `RiskConfirmation` 对话框:启用按钮在用户勾选明确的风险确认复选框前保持禁用;预设以产品标签 `Full access` 展示;所有取消路径都不作任何提交。**
- `RiskConfirmation`(ui-primitives)是受控的 Modal 组合:标题、说明、确认复选框、取消,以及 `acknowledged` 勾选前禁用的确认按钮。它始终是页面内对话框——Modal portal 到本文档 body,绝不打开可能落在另一块显示器上的原生或独立浏览器窗口。`Modal` 新增 `contentClassName` 座位,令警示正文在受限的移动端/横屏视口内滚动,动作行保持固定。
- 编辑器 chip(ui-conversation 的 `PermissionSelect`)在 `/permission` 提交前拦截 Full-access 选择:`confirmation`/`acknowledged` 组件状态打开对话框,确认后经与其他选择完全相同的注入 `command` 通道提交 `/permission danger-full-access`;取消、Escape、关闭与遮罩点击均保持当前预设不变并重置复选框。会话锁定时确认自行撤销(`locked`/值缺席 effect),切换任务时随 `key={sessionId}` 重挂载而重置。文案经标准 `conversation` locale 座位以 `access.confirm.*` 键供给。
- `/permission` popup(ui-permission 骑在 ui-command 外壳上)以数据而非第二套对话框实现完成把关:`SelectOption` 新增可选的 `confirmation` 载荷,popup 控制器拥有 `confirming`/`acknowledged` 状态迁移,`PopupSelectView` 在门控选项未决期间把选择卡换成同一个 `RiskConfirmation`。
- `RiskConfirmation`(ui-primitives)是受控的 Modal 组合:标题、说明、确认复选框、取消,以及 `acknowledged` 勾选前禁用的确认按钮。它始终是页面内对话框——Modal portal 到本文档 body,绝不打开可能落在另一块显示器上的原生或独立浏览器窗口。`Modal` 新增 `contentClassName` slot,令警示正文在受限的移动端/横屏视口内滚动,动作行保持固定。
- composer chip(ui-conversation 的 `PermissionSelect`)在 `/permission` 提交前拦截 Full-access 选择:`confirmation`/`acknowledged` 组件状态打开对话框,确认后经与其他选择完全相同的注入 `command` 通道提交 `/permission danger-full-access`;取消、Escape、关闭与遮罩点击均保持当前预设不变并重置复选框。会话锁定时确认自行撤销(`locked`/值缺席 effect),切换任务时随 `key={sessionId}` 重挂载而重置。文案经标准 `conversation` locale slot 以 `access.confirm.*` 键供给。
- `/permission` popup(ui-permission 构建于 ui-command 外壳之上)以数据而非第二套对话框实现完成把关:`SelectOption` 新增可选的 `confirmation` 载荷,popup 控制器拥有 `confirming`/`acknowledged` 状态迁移,`PopupSelectView` 在门控选项未决期间把选择卡换成同一个 `RiskConfirmation`。
- 「通用」设置中的「权限」行在把 Full access 持久化为后续会话的默认值前,也使用同一个受控 `RiskConfirmation`。警示会明确说明该设置只影响后续会话;取消、Escape、关闭与点击遮罩均不会改动已存默认值。
- `Full access` 在每个选择器中都有意覆盖 kebab 转 Title Case 的显示变换;命令与 Settings 写入在 wire 上保留机器名,每份警示正文都保持中英文 locale 感知。
## Alternatives considered
## 考虑过的替代方案
**原生/操作系统或独立窗口确认。** 已拒:对话框必须留在当前 WebUI 窗口内;第二个窗口可能出现在另一块显示器上,使决策脱离其守护的页面状态。
**每个面的安全文案共享一个 locale namespace。** 已拒:ui-permission bundle 与 ui-conversation 可独立加载,而 Settings 警示说明的是另一种只影响后续会话的生效周期。每个 bundle 各自拥有文案,ui-permission 也将 popup 与 Settings 词典分开,而非跨 bundle 边界 import。
**每个界面的安全文案共享一个 locale namespace。**不予采用:ui-permission bundle 与 ui-conversation 可独立加载,而 Settings 警示说明的是另一种只影响后续会话的生效周期。每个 bundle 各自拥有文案,ui-permission 也将 popup 与 Settings 词典分开,而非跨 bundle 边界 import。
**在 host/权限后端把关。** 设计上即出界:本变更只涉浏览器客户端确认流;后端权限语义、默认值与更安全预设的一键行为均不变。
## Consequences
## 后果
进入 Full access 的每条可见 GUI 路径现在都要求刻意且知情的确认,代价是真想启用该预设的用户多一步对话框。新的选择器通过各自拥有的状态机复用共享对话框,或在 popup 路径挂 `confirmation` 载荷。验收:`input-bar.spec.tsx` 中编辑器流的门控用例、`popup-view.spec.tsx` 与 `popup.spec.ts` 的 popup 门、`permission-row.spec.tsx` 的默认设置门控、`atoms.spec.tsx` 的 Modal/RiskConfirmation 约定,以及组装态 Web 回放。

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/implemented/feature/2026-07-31-permission-default-for-new-sessions.md
2026-07-31-permission-default-for-new-sessions.md: ffa4c8a07bdd08ca52edbc14fe10372ad76e8cf8
2026-07-31-permission-default-for-new-sessions.zh.md: 0b994a3bf35d02ecc581c82591df624e50c00b2e
2026-07-31-permission-default-for-new-sessions.zh.md: c70d431ec9bbe8cdeabfdc8a0052a9c436844f02

View File

@@ -6,7 +6,7 @@ Status: implemented
## 问题
Web「通用」设置页将「权限」显示为禁用的骨架控件,尽管 `dsh-permission` 已经拥有 preset 表和当前会话的切换路径。Settings seam 可以持久化由插件拥有的值,但 Web Settings API 只暴露可配置 LLM 提供方的 namespace。更重要的是,如果把用户偏好当成实时生效的全局权限,现有会话的执行策略就会在其持久日志之外发生变化。
Web「通用」设置页将「权限」显示为禁用的骨架控件,尽管 `dsh-permission` 已经拥有 preset 表和当前会话的切换路径。Settings seam 可以持久化由插件拥有的值,但 Web Settings API 只暴露可配置 LLM(大语言模型)提供方的 namespace。更重要的是,如果把用户偏好当成实时生效的全局权限,现有会话的执行策略就会在其持久日志之外发生变化。
## 决策
@@ -26,7 +26,7 @@ ApiProxy 在可配置提供方 namespace 之外,将 `permission` 显式加入
## 曾考虑的替代方案
**将 Settings 值实时应用于每个会话。** 不予采纳,因为执行策略会在没有会话事件的情况下改变,重放也无法重建先前工具调用采用了哪种权限。
**将 Settings 值实时应用于每个会话。** 不予采纳,因为执行策略会在没有会话事件的情况下改变,回放也无法重建先前工具调用采用了哪种权限。
**创建时只记录 `permission/preset`。** 不予采纳,因为沙箱和审批是由不同组件独立拥有的全量值调节项;固定全部三个事实,可以让其消费方不依赖未来的组合默认值变化。

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/implemented/feature/2026-07-31-session-archive-global-set.md
2026-07-31-session-archive-global-set.md: fab99a405a6f8264c36453473327e32905bac9c8
2026-07-31-session-archive-global-set.zh.md: e33f3b5272a6d8fc90cfad247ba21d4d10afb045
2026-07-31-session-archive-global-set.zh.md: 4789c11a6d6883966b2218a16644177408e3df1f

View File

@@ -1,26 +1,26 @@
# Agent Note: Session 归档(注册表级全局集合)
# Agent Note: 会话归档(注册表级全局集合)
状态:implemented
Status: implemented
[English](2026-07-31-session-archive-global-set.md) | 中文
## 问题
Sidebar workspace 浏览区的 session 行菜单里,「Delete session」一直是纯视觉占位(无 handler)。产品口径定为**归档**而非删除:session 日志与 workspace 记账都不动,只把该 session 从所有分组视图(workspace 分组、Ungrouped、搜索、平铺列表)里隐藏。归档记录需要一个落点:Ungrouped 的 session 不属于任何 workspace 实体,per-workspace 字段放不下它。
Sidebar workspace 浏览区的会话行菜单里,「Delete session」一直是纯视觉占位(无 handler)。产品口径定为**归档**而非删除:会话日志与 workspace 记账都不动,只把该会话从所有分组视图(workspace 分组、Ungrouped、搜索、平铺列表)里隐藏。归档记录需要一个落点:Ungrouped 的会话不属于任何 workspace 实体,per-workspace 字段放不下它。
## 决策
**归档集合是 workspace domain 全局单例(`workspaceDomainState.archivedSessionIds`)上的一个新字段,覆盖在 workspace 记账之上;显示过滤全部收敛在 client 的 `tree.ts` 派生层;wire 面走全快照姿态。**
- 存储:`archivedSessionIds: z.array(sessionId).default([])`,domain version 保持 2——纯增量字段,旧介质经 schema default 解析为空集合,无迁移代码。被归档的 session 保留其 `sessionIds` 席位(未来取消归档恢复原位置),因此与「一个 session 只被一个 workspace 记账」不变式零纠缠。
- Registry:`ctx.workspace.archiveSession(id)` 走 `enqueueOperation` 与 create/delete 串行;未知 session(实时与持久化都查不到)抛 `WorkspaceUnknownSessionError`;已归档 id 不写盘不发事件。`archivedSessionIds` getter 暴露只读集合。
- RPC:`workspace.archiveSession({sessionId}) → {archivedSessionIds}`(应答完整更新后集合);`workspace.list` 响应携带集合作为重连基线;新 host 帧 `host/archived-sessions-changed` 在每次持久变更后推完整快照(与 `host/workspace-changed` 同姿态,从 `domain/changed` 的 global put 分支比对推帧)。未知 session 复用错误码 `session-not-found`。
- client runtime:`WorkspaceListState.archivedSessionIds`(按 Host 顺序的 `readonly SessionId[]`,成员不变不换引用——公有快照状态保持 store 引擎的纯数据词汇:immer draft 不开 MapSet 插件就不接受 Set;membership 查询在派生函数内自建临时 Set,与 expandedProjects 同款);list 基线、unary 回声、changed 帧三路都整体替换安装。投影层在当前 selection 落入归档集合时统一清空回 New Session 视图(用户拍板:归档当前打开的 session 主视图回 hero)——一条规则同时覆盖本地 unary 回声、其他标签页的 changed 帧、以及重连基线恢复出一个离线期间被归档的 selection;帧/回声落在 in-flight `workspace.list` 期间时还会屏蔽旧基线对新集合的回滚。
- 存储:`archivedSessionIds: z.array(sessionId).default([])`,domain version 保持 2——纯新增字段,旧介质经 schema default 解析为空集合,无迁移代码。被归档的会话保留其 `sessionIds` slot(未来取消归档恢复原位置),因此与「一个会话只被一个 workspace 记账」不变式零纠缠。
- 注册表:`ctx.workspace.archiveSession(id)` 走 `enqueueOperation` 与 create/delete 串行;未知会话(实时与持久化都查不到)抛 `WorkspaceUnknownSessionError`;已归档 id 不写盘不发事件。`archivedSessionIds` getter 暴露只读集合。
- RPC:`workspace.archiveSession({sessionId}) → {archivedSessionIds}`(应答更新后的完整集合);`workspace.list` 响应携带集合作为重连基线;新 host 帧 `host/archived-sessions-changed` 在每次持久变更后推完整快照(与 `host/workspace-changed` 同姿态,从 `domain/changed` 的 global put 分支比对推帧)。未知会话复用错误码 `session-not-found`。
- client 运行时:`WorkspaceListState.archivedSessionIds`(按 Host 顺序的 `readonly SessionId[]`,成员不变不换引用——公有快照状态保持 store 引擎的纯数据词汇:immer draft 不开 MapSet 插件就不接受 Set;membership 查询在派生函数内自建临时 Set,与 expandedProjects 同款);list 基线、unary 回声、changed 帧三路都会用完整集合整体替换现有值。投影层在当前 selection 落入归档集合时统一清空回 New Session 视图(用户拍板:归档当前打开的会话会使主视图回到 hero)——一条规则同时覆盖本地 unary 回声、其他标签页的 changed 帧、以及重连基线发现当前 selection 已在此 client 离线期间被归档的情形;帧/回声落在 in-flight `workspace.list` 期间时还会屏蔽旧基线对新集合的回滚。
- UI:菜单项 `delete`(visual-only)改为 `archive`(label「Archive session」,非 danger 样式,无确认对话框——非破坏性操作,误触后果只是列表隐藏);过滤实现为 `tree.ts` 的 `sessionVisible` 判据加一档,`deriveGroups`/`deriveFlat` 增加 `archived` 集合入参,四个视图(分组循环、stray 兜底、搜索、平铺)同源生效。
## 已考虑的替代方案
**per-workspace archivedSessionIds(最初表述)。** 否决:Ungrouped session 无落点;用户改口全局。
**per-workspace archivedSessionIds(最初表述)。** 否决:Ungrouped 会话无落点;用户改口全局。
**SessionSummary 打 archived 标(session.list 层)。** 否决:要把 workspace domain 事实 join 进 sessions domain 投影,summary 无增量帧还得另发通知,跨域耦合大于收益。
@@ -30,4 +30,4 @@ Sidebar workspace 浏览区的 session 行菜单里,「Delete session」一直
## 后果
归档后 UI 无查看/取消归档入口(本期口径,README Known Limitation 记账);数据与席位完好,后续加恢复面只是 UI + 一个逆向 RPC。`workspace.list` 响应形状变化是 pre-release 直改(无兼容层)。e2e(workspace-management)钉住了「归档→行消失→reload 后仍隐藏、日志仍在」的全链路;domain 层测试钉住幂等、未知 id 拒绝、跨重启恢复与旧介质默认升级。
归档后 UI 无查看/取消归档入口(本期口径,记录在 README 的 Known Limitation 中);数据与 slot 完好,后续加恢复面只是 UI + 一个逆向 RPC。`workspace.list` 响应形状变化是 pre-release 直改(无兼容层)。e2e(workspace-management)钉住了「归档→行消失→reload 后仍隐藏、日志仍在」的全链路;domain 层测试钉住幂等、未知 id 拒绝、跨重启恢复与旧介质默认升级。

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/implemented/feature/2026-07-31-web-telemetry-default-mount.md
2026-07-31-web-telemetry-default-mount.md: c1525a44196991d5059969ea70af34501b199323
2026-07-31-web-telemetry-default-mount.zh.md: f203ea1943387beda445bd81ac78fa2cc0471d45
2026-07-31-web-telemetry-default-mount.zh.md: ef5f7e4500a1776e049ac27e0e64756e78725a61

View File

@@ -4,11 +4,11 @@ Status: implemented
[English](2026-07-31-web-telemetry-default-mount.md) | 中文
## Problem
## 问题
遥测 seam 与 OTel backend([revival Note](2026-07-23-session-telemetry-otel-revival.md))自完成以来从未接入任何部署组合:没有 roster 行、没有开关、没有节奏口径,内部部署对用户会话零可观测。需要一个部署决策:哪些 surface 上报、报到哪、什么节奏、怎么关、CI 怎么隔离。
遥测 seam 与 OTel 后端([revival Note](2026-07-23-session-telemetry-otel-revival.md))自完成以来从未接入任何部署组合:没有 roster 行、没有开关、没有节奏口径,内部部署对用户会话的可观测性为零。需要一个部署决策:哪些 surface 上报、报到哪、什么节奏、怎么关、CI 怎么隔离。
## Decision
## 决策
`dsh` 共享 base(`apps/cli/config/base.cordis.yml`)默认挂载 `telemetry-otel` 行,内置生产 endpoint,因此 Web 与 headless 都会上报;原始配置命令也会先挂载该行,再应用其必需的部署 overlay。这是**内部测试期的部署立场**——有 endpoint 就上报,用户可通过环境变量退出。Web 与 headless 在 SIGINT/SIGTERM 时使用[有界、可升级的进程关闭控制器](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md),在启动器 5 秒上限到期前,先给后端 3 秒关闭截止时间完成排空。
@@ -17,23 +17,23 @@ Status: implemented
| 挂载面 | base.cordis.yml(原始配置 + Web + headless) | 所有加载共享 base 的配置树采用同一个部署立场;原始配置 overlay 决定该部署是否创建会话 |
| endpoint | `DSH_TELEMETRY_OTLP_URL`,缺省 `https://harness-telemetry.deepseeksvc.com/v1/logs` | 内部 collector;env 覆盖供本地/联调 |
| 退出开关 | `DSH_TELEMETRY_DISABLED` 非空(含 `0`/`false`)即关 | 隐私向开关取「宁关勿误开」;行级 disable 只能在 AppCLIEntry 的 patch 层做(config 无 disable 语义,且必须先于 `exporter.url` 的加载期校验生效) |
| 上报节奏 | `processor.scheduledDelayMillis: 10000`(10s/批) | 流式回流,非退出才报;崩溃至多丢最后一个未导出间隔 |
| 退出 drain 上界 | `exporter.timeoutMillis: 1000` + `maxExportBatchSize: 2048(== maxQueueSize)` + `exportTimeoutMillis: 1500` + `shutdownTimeoutMillis: 3000` | collector 不可达的常规故障会在约 1s 内放行:timeoutMillis 是单次 socket 超时与重试 deadline,使用与队列等大的单批可避免依次排空导致耗时倍增。由 DSH 管理的 3s 外层上限覆盖 SDK 先执行的无界 `forceFlush()` 等待,即传输 Promise 始终无法取得 socket 的情况。 |
| 上报节奏 | `processor.scheduledDelayMillis: 10000`(10s/批) | 流式回流,非退出才报;崩溃至多丢失最后一个尚未导出间隔内的数据 |
| 退出 drain 上界 | `exporter.timeoutMillis: 1000` + `maxExportBatchSize: 2048`(与 maxQueueSize 相等) + `exportTimeoutMillis: 1500` + `shutdownTimeoutMillis: 3000` | collector 不可达的常规故障会在约 1s 内放行:timeoutMillis 是单次 socket 超时与重试 deadline,使用与队列等大的单批可避免依次排空导致耗时倍增。由 DSH 管理的 3s 外层上限覆盖 SDK 先执行的无界 `forceFlush()` 等待,即传输 Promise 始终无法取得 socket 的情况。 |
| 压缩 | `compression: gzip` | 事件 body 含全文,跨机房带宽 |
| CI 隔离 | 全部 8 个 GitHub workflow 顶层 `env: DSH_TELEMETRY_DISABLED: '1'` | CI 启动 web 组合的所有通道(e2e/snapshot/built smoke)不得向生产 endpoint 泄测试会话 |
| CI 隔离 | 全部 8 个 GitHub 工作流顶层 `env: DSH_TELEMETRY_DISABLED: '1'` | CI 启动 web 组合的所有通道(e2e/快照/已构建版本的冒烟测试)不得向生产 endpoint 泄测试会话 |
集成测试 `apps/cli/tests/telemetry-web.e2e.ts`(keyless)钉住部署级行为:测试内 OTLP collector + mock LLM,真启动 `dsh web`,断言 ledger 覆盖、seq 单调、chunk 首条投影、以及 SIGINT drain 后 ops `shutdown` 标记到达。
集成测试 `apps/cli/tests/telemetry-web.e2e.ts`(keyless)钉住部署级行为:测试内 OTLP collector + mock LLM(大语言模型)服务器,真启动 `dsh web`,断言 ledger 覆盖、seq 单调、步骤首个分片投影、以及 SIGINT drain 后 ops `shutdown` 标记到达。
## Alternatives considered
## 考虑过的替代方案
**默认不挂载,部署方自行加行(SDK 立场的延续)。** 否决于当前阶段:本仓的 web/headless 组合就是内部部署本身,「上报默认开」是这个部署的产品要求;SDK 立场仍由 seam 包保持(不挂 = 零外发)。
**默认不挂载,部署方自行加行(SDK 立场的延续)。** 当前阶段不采用:本仓的 web/headless 组合就是内部部署本身,「上报默认开」是这个部署的产品要求;SDK 立场仍由 seam 包保持(不挂 = 零外发)。
**开关做成 config 字段而非 env patch。** 不可行:cordis 行没有 config 层的 disable 语义,且 `exporter.url` 校验在插件构造期 fail-loud,开关必须在 Loader 之前生效——AppCLIEntry patch 层是唯一落点。
**退出时 `Promise.race` 兜底超时。** 最初暂缓,是因为 SDK 参数看似已经将后端排空耗时限制在约 1.5-3s(通常 <100ms),实测 SIGINT 到退出耗时 110ms-1.1s。后来在 Linux 沙箱中复现并证明,`BatchLogRecordProcessor.shutdown()` 可能在 `exporter.forceFlush()` 中永久等待,无法进入受 `exportTimeoutMillis` 限制的完成 Promise。因此,[CLI 关闭修复](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md) 既为这一特定缺口增加 3 秒后端上限,也为整棵插件树增加 5 秒进程级上限和重复信号退出途径。
## Consequences
## 后果
- 无本地 collector 的开发者跑 `dsh web` 会对生产 endpoint 每 10s 发一次 POST(联不通则静默失败,OTel diag logger 未注册);本地开发设 `DSH_TELEMETRY_DISABLED=1` 或 `DSH_TELEMETRY_OTLP_URL` 指本地。
- **当前零脱敏规则挂载**:导出即原始捕获副本(用户/助手消息全文、工具参数与结果、system prompt、`session.cwd` 本地路径)。跨信任边界前必须挂 `telemetry/record` 规则——脱敏规则、其余身份 Resource 维度(hostname/surface;匿名 user id 已由[匿名用户 id Note](2026-07-31-telemetry-anonymous-user-id.md)落地)、使用数据 metrics 轨是本决策明确的后续工作。
- 复用这棵树的测试载具(如 `apps/web/tests/scaffold.ts`)须显式关停该行,否则 fixture 会话会流向 env 里碰巧存在的 collector。
- **当前尚未挂载任何脱敏规则**:导出即原始捕获副本(用户/助手消息全文、工具参数与工具结果、系统提示词、`session.cwd` 本地路径)。跨信任边界前必须挂 `telemetry/record` 规则——脱敏规则、其余身份 Resource 属性(hostname/surface;匿名 user id 已由[匿名用户 id Note](2026-07-31-telemetry-anonymous-user-id.md)落地)、使用数据 metrics 轨是本决策明确的后续工作。
- 复用这棵树的测试载具(如 `apps/web/tests/scaffold.ts`)须显式关停该行,否则 fixture(测试前置数据)会话会流向 env 里碰巧存在的 collector。

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/implemented/feature/2026-07-31-workspace-write-surface-default.md
2026-07-31-workspace-write-surface-default.md: e6a36ad4ee7179cabf958114681728c3ac7340b4
2026-07-31-workspace-write-surface-default.zh.md: 5ced928b167892d9abe1f4423da72a0243590735
2026-07-31-workspace-write-surface-default.zh.md: fd18f2b69eb40df8392afb50892c7740d0e30af0

View File

@@ -14,7 +14,7 @@ Status: implemented
真正的新会话会在执行前固定 `permission/preset: workspace-write`、`sandbox/mode: workspace-write` 和 `approval/policy: ask`。现有会话和恢复的会话保留日志中记录的权限,更改「通用」设置中的默认值只影响之后创建的会话。浏览器保留 Access 选择器、可应答的审批卡片,以及选择 Full access 时的风险确认。共享 Permission 服务在 TUI 中激活其命令子件,因此 TUI 会获得现有的 `/permission` 命令。
该模式只管辖文件效果。受沙箱约束的 bash 与文件系统修改只允许写入会话工作区和平台临时根目录;读取、网络访问与进程可见性仍不受该策略约束。若没有平台 runner 能强制执行受限的 bash 调用,执行会以拒绝方式关闭,不会退回不受限命令。
该模式只管辖文件效果。受沙箱约束的 bash 与文件系统修改只允许写入会话工作区和平台临时根目录;读取、网络访问与进程可见性仍不受该策略约束。若没有平台 runner 能强制执行受限的 bash 调用,执行会以拒绝告终,不会退回不受限命令。
## 测试
@@ -26,10 +26,10 @@ Status: implemented
**保留不受限的 TUI,只更改浏览器回退值。** 不予采纳,因为这会保留无法解释的界面差异,并让全新的终端会话继续拥有本决策要移除的权限。
**在同一次变更中添加终端审批对话框。** 不予采纳,因为这是另一个交互与生命周期决策。TUI 没有 `approval/request` 应答者,因此一次性自动升权当前会落定为不可用并以拒绝方式关闭;需要更宽权限的用户可以通过 `/permission` 主动选择其他 preset。
**在同一次变更中添加终端审批对话框。** 不予采纳,因为这是另一个交互与生命周期决策。TUI 没有 `approval/request` 应答者,因此一次性自动升权当前会落定为不可用并以拒绝告终;需要更宽权限的用户可以通过 `/permission` 主动选择其他 preset。
## 后果
全新的会话无需额外提示即可修改当前工作区与临时根目录,尝试修改其他位置则会在触及目标前被拒绝。Full access 仍可通过显式选择获得,浏览器选择时也仍会显示确认对话框。系统不会重写已存储的用户默认值和会话日志中记录的权限。
由浏览器支撑的无头入口继承 Web 组合,因此默认值相同。TUI 缺少审批应答者是本次变更的明确限制:自动请求更宽权限的重试会在那里以拒绝方式关闭,而不会显示权限询问。
由浏览器支撑的无头入口继承 Web 组合,因此默认值相同。TUI 缺少审批应答者是本次变更的明确限制:自动请求更宽权限的重试会在那里以拒绝告终,而不会显示权限询问。

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/implemented/feature/2026-08-02-session-search-not-shipped-default.md
2026-08-02-session-search-not-shipped-default.md: 65bd72fff76210b726e7562fb8e88e5f8802434a
2026-08-02-session-search-not-shipped-default.zh.md: 5e42a2c2323904117f9322b5c4a53c43c6ed3f2a
2026-08-02-session-search-not-shipped-default.zh.md: 4eb0851c1e584b84847b6bb5118c8bb2f3156845

View File

@@ -6,20 +6,20 @@ Status: implemented
## 问题
[交付清单决策](2026-07-31-even-out-shipped-tool-rosters.md)把 `tool-session-query` 设为共享 [`cordis.patch.yml`](../../../../packages/bundle/base/cordis.patch.yml) 的默认行,于是交付的 TUI 与 Web surface 把这五个会话搜索工具(`session_search`、`session_event_search`、`session_trace`、`session_event_trace`、`session_event_read`)呈现给了模型。这与[面向模型的会话查询工具决策](2026-07-24-model-facing-session-query-tools.md)相抵触,该决策持需显式启用的立场,包 README 将其记录为「shipped host compositions do not mount it by default」。这份默认还交付了一个提示词段,向模型讲授一套既往工作搜索工作流,而没有任何用户要求过。
[交付清单决策](2026-07-31-even-out-shipped-tool-rosters.md)把 `tool-session-query` 设为共享 [`cordis.patch.yml`](../../../../packages/bundle/base/cordis.patch.yml) 的默认行,于是交付的 TUI 与 Web surface 把这五个会话搜索工具(`session_search`、`session_event_search`、`session_trace`、`session_event_trace`、`session_event_read`)呈现给了模型。这与[面向模型的会话查询工具决策](2026-07-24-model-facing-session-query-tools.md)相抵触,该决策持需显式启用的立场,包 README 将其记录为「shipped host compositions do not mount it by default」。这项默认设置还交付了一个提示词段,向模型讲授一套既往工作搜索工作流,而没有任何用户要求过。
## 决策
交付的 TUI、Web 与无头 surface 不再挂载 `@deepseek-ai/dsh-tool-session-query`:该行从共享的 `cordis.patch.yml` 移除,opt-in 的 [`core-web.cordis.yml`](../../../../apps/cli/config/core-web.cordis.yml) profile 中那条已悬空的 `disabled` patch 也随之删除,workspace 依赖也从 `apps/cli/package.json` 中移除。该消费方仍保持 opt-in,与面向模型的会话查询工具决策所述完全一致:ACP 示例的 [`session-query.cordis.yml`](../../../../examples/acp-agent/session-query.cordis.yml) 及其快照对侧文件仍是挂载参考,自定义组合也可以连同超时与 spill 策略一起挂载该包。
交付的 TUI、Web 与无头 surface 不再挂载 `@deepseek-ai/dsh-tool-session-query`:该行从共享的 `cordis.patch.yml` 移除,opt-in 的 [`core-web.cordis.yml`](../../../../apps/cli/config/core-web.cordis.yml) profile 中那条已悬空的 `disabled` patch 也随之删除,workspace 依赖也从 `apps/cli/package.json` 中移除。该消费方仍保持 opt-in,与面向模型的会话查询工具决策所述完全一致:ACP(Agent Client Protocol)示例的 [`session-query.cordis.yml`](../../../../examples/acp-agent/session-query.cordis.yml) 及其快照对侧文件仍是挂载参考,自定义组合也可以连同超时与 spill 策略一起挂载该包。
`ctx.sessionQuery` 服务本身保持挂载。`session-query-sqlite` 仍是 base 的一行,TUI 的 `session-reference` 消费它来实现 `/resume`,Web overlay 也继续把它 patch 成内存索引,供浏览器内容搜索使用。被移除的只有面向模型的消费方。
`ctx.sessionQuery` 服务本身保持挂载。`session-query-sqlite` 仍是 base 的一行,TUI 的 `session-reference` 消费它来实现 `/resume`,Web overlay 也继续把它 patch 成内存索引,供浏览器内容搜索使用。被移除的只有面向模型的消费方。
## 曾考虑的替代方案
- **把 `session-query-sqlite` 索引也一并移除**——否决,因为 `/resume` 和 Web 内容搜索框直接消费 `ctx.sessionQuery`;它们是宿主功能,不是模型工具,移除提供方会破坏它们。
- **保留该行,但在每个 overlay 中禁用它**——否决,因为一条被禁用的 base 行仍会交付依赖,而且一行就能轻易重新启用;已记录的 opt-in 立场要求消费方不出现在交付的 surface 上,以 ACP 示例作为挂载参考。
- **只在 TUI 上挂载**——否决,因为共享 base 是所有 surface 共用的一组行;surface 专属挂载会重新引入交付清单决策所消除的清单分裂。
- **把 `session-query-sqlite` 索引也一并移除**——否决,因为 `/resume` 和 Web 内容搜索框直接消费 `ctx.sessionQuery`;它们是宿主功能,不是模型工具,移除提供方会破坏它们。
- **保留该行,但在每个 overlay 中禁用它**——否决,因为一条被禁用的 base 行仍会交付依赖,而且一行就能轻易重新启用;已记录的 opt-in 立场要求消费方不出现在交付的 surface 上,以 ACP 示例作为挂载参考。
- **只在 TUI 上挂载**——否决,因为共享 base 是所有 surface 共用的一组行;surface 专属挂载会重新引入交付清单决策所消除的清单分裂。
## 后果
两个 surface 都回到同样的二十个无条件工具(ripgrep 可用时再加上 `glob`/`grep`),五个会话搜索 schema 及其提示词段也一并退出默认请求。两个 surface 上的交付组合测试都固定这份更小的目录,因此把会话搜索重新作为默认加回会触及同样的测试。想要会话搜索的用户从个人 overlay 或 ACP 示例挂载该消费方,并在挂载处添加依赖。
两个 surface 都回到同样的二十个无条件工具(ripgrep 可用时再加上 `glob`/`grep`),五个会话搜索 schema 及其提示词段也一并退出默认请求。两个 surface 上的交付组合测试都固定这份更小的目录,因此把会话搜索重新作为默认加回会触及同样的测试。想要会话搜索的用户从个人 overlay 或 ACP 示例挂载该消费方,并在挂载处添加依赖。

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/implemented/feature/2026-08-02-win32-in-process-folder-dialog.md
2026-08-02-win32-in-process-folder-dialog.md: 5389293605169ce5ca269a127de5f609b8b7dd11
2026-08-02-win32-in-process-folder-dialog.zh.md: ef81bc1f65b2859de1eb60f74f79e4869da4746b
2026-08-02-win32-in-process-folder-dialog.zh.md: db4a6503f37f4b9cc95ce79ab8f476aeedfe5a60

View File

@@ -1,4 +1,4 @@
# Agent Note:Win32 文件夹选择器迁至 koffi 子进程
# Agent Note: Win32 文件夹选择器迁至 koffi 子进程
Status: implemented
@@ -6,22 +6,22 @@ Status: implemented
## 问题
Windows 目录选择器的主层此前是围绕 WinForms `FolderBrowserDialog` 的外部 PowerShell 脚本:只有恰好安装了 PowerShell 7 的机器才有现代对话框;review 指出的回归——PowerShell 6 可解析却没有 WinForms(退出码 1 而非 `ENOENT`,5.1 回退永远不会触发);`SetProcessDPIAware` 只有系统 DPI 的上限;选择器的行为取决于机器装了哪些 shell,而不是取决于 Windows 本身。
Windows 目录选择器的主层此前是围绕 WinForms `FolderBrowserDialog` spawn 出的 PowerShell 脚本:只有恰好安装了 PowerShell 7 的机器才有现代对话框;review 指出的回归——PowerShell 6 可解析却没有 WinForms(退出码 1 而非 `ENOENT`,5.1 回退永远不会触发);`SetProcessDPIAware` 只有系统 DPI 的上限;选择器的行为取决于机器装了哪些 shell,而不是取决于 Windows 本身。
## 决策
`packages/host/directory-picker-native` 现在经 koffi——它已是仓库其他 `win32.ts` 面的工作区依赖——在进程内打开 `IFileOpenDialog`(`FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_NOCHANGEDIR`),作为 win32 主层。COM 会话运行在 spawn 出的子进程中,模态 `Show` 永不阻塞宿主事件循环;子进程在阻塞前上报其原生线程 id,driver 通过向该线程的窗口反复投递 `WM_CLOSE`(`EnumThreadWindows`)来服务中止,关闭预算耗尽时 kill 子进程。对话框是子进程的第一个窗口,Windows 会自动激活它,无需手动前台调用。子进程线程启用宿主接受的最佳线程 DPI 感知(`SetThreadDpiAwarenessContext`,按 per-monitor-v2 → per-monitor → system-aware 级联并检查返回值),严格优于脚本的系统 DPI 上限;DPI 保持为纯外观的 best-effort——全部不被接受的宿主仍得到现代对话框,而不会降级。模块切分让覆盖率在任何主机上都诚实:`win32-dialog-logic.ts`(纯时序)与 `win32-dialog.ts`(driver)在任何平台对假件测试;`win32-dialog-bindings.ts` 对 mock 的 `koffi` COM 世界测试(`dsh-session-persistence-jsonl` 的技法);POSIX 主机把真实 spawn 管道跑到 koffi 加载失败的拒绝;win32 主机跑真实的"打开并中止关闭"冒烟。先于本层存在的 PowerShell 链已被删除(见[链删除](../simplification/2026-08-04-drop-windows-powershell-picker-fallback.md)):该层无回退。
`packages/host/directory-picker-native` 现在经 koffi——它已是仓库其他 `win32.ts` 代码的工作区依赖——在进程内打开 `IFileOpenDialog`(`FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_NOCHANGEDIR`),作为 win32 主层。COM 会话运行在 spawn 出的子进程中,模态 `Show` 永不阻塞宿主事件循环;子进程在阻塞前上报其原生线程 id,driver 通过向该线程的窗口反复投递 `WM_CLOSE`(`EnumThreadWindows`)来处理中止请求,关闭预算耗尽时 kill 子进程。对话框是子进程的第一个窗口,Windows 会自动激活它,无需手动前台调用。子进程线程启用宿主接受的最佳线程 DPI 感知(`SetThreadDpiAwarenessContext`,按 per-monitor-v2 → per-monitor → system-aware 级联并检查返回值),严格优于脚本的系统 DPI 上限;DPI 保持为纯外观的 best-effort——不接受其中任何一种的宿主仍得到现代对话框,而不会降级。模块切分让覆盖率在任何主机上都诚实:`win32-dialog-logic.ts`(纯时序)与 `win32-dialog.ts`(driver)在任何平台对假件测试;`win32-dialog-bindings.ts` 对 mock 的 `koffi` COM 世界测试(`dsh-session-persistence-jsonl` 的技法);POSIX 主机运行真实的 spawn 管道,并验证其因 koffi 加载失败而拒绝;win32 主机跑真实的「打开并中止关闭」冒烟测试。先于本层存在的 PowerShell 链已被删除(见[链删除](../simplification/2026-08-04-drop-windows-powershell-picker-fallback.md)):该层无回退。
## 考虑过的替代方案
- **预编译原生助手(`native/` 家族,如 `@deepseek-ai/node-addon-landlock-run`)。** 否决:再增加一个 npm 包家族、MSVC 供给和 Windows 构建/发布通道——只为交付约 150 行目前无法在 CI 中执行的 C(现有 CI 没有真 Windows 通道);koffi 以零新增供应链提供同一 COM 面。
- **N-API 进程内插件。** 否决:同样的 CI/工具链原因,另加需要自有 C++ 处理 STA 线程与消息泵,而子进程 + koffi 用 TypeScript 就能表达。
- **保留 PowerShell 为主层并探测版本。** 否决:选择器仍被 shell 打包形态挟持(6 与 7、Store 别名、profile),且没有 pwsh 的机器地板仍是 5.1 的旧版对话框;仅把回退触发条件的拓宽吸收进回退层。
- **预编译原生辅助程序(`native/` 家族,如 `@deepseek-ai/node-addon-landlock-run`)。** 否决:再增加一个 npm 包家族、MSVC 环境配置和 Windows 构建/发布通道——只为交付约 150 行仓库目前无法通过 CI 检验的 C 代码(现有 CI 没有真 Windows 通道);koffi 以零新增供应链提供同一 COM 接口。
- **N-API 进程内插件。** 否决:同样的 CI/工具链原因,还需自行维护处理 STA 线程与消息泵的 C++ 代码,而子进程 + koffi 用 TypeScript 就能表达。
- **保留 PowerShell 为主层并探测版本。** 否决:选择器仍被 shell 打包形态挟持(6 与 7、Store 别名、profile),且没有 pwsh 的机器仍只能使用 5.1 的旧版对话框;只有拓宽回退触发条件这一项改动被纳入了回退层。
- **在主线程上阻塞模态调用。** 直接否决:对话框打开期间 web 宿主必须继续服务 RPC。
## 后果
- 每台 Windows 机器都得到带其所支持的最佳 DPI 感知(1703+ 为 per-monitor-v2)的现代对话框,无论是否安装 PowerShell。
- 真实对话框渲染与选中路径仍是手动 Windows 检查(自动关闭冒烟证明打开/中止/收尾)。
- 所用 COM vtable 槽位与 GUID 是冻结的 Windows ABI(Vista 起);koffi 签名错误可能引发原生访问冲突,但被限制在对话框子进程内——宿主 Node 进程存活,失败原样上报(无回退层;见[链删除](../simplification/2026-08-04-drop-windows-powershell-picker-fallback.md))。mocked-koffi 的 ABI 钉与真实 win32 冒烟正是为了在交付前捕获这类错误。
- 打包二进制的臂——打包后的可执行文件以对话框入口形式自我 spawn——不受任何自动化测试覆盖:源码平面与普通 node 下构建出的 `lib/worker.cjs` 已被覆盖,打包 spawn 推迟到 Windows CI 路线图。
- 真实对话框渲染与选中路径仍是手动 Windows 检查(自动关闭冒烟测试证明打开/中止/收尾)。
- 所用 COM vtable 槽位与 GUID 是冻结的 Windows ABI(Vista 起);koffi 签名错误可能引发原生访问冲突,但被限制在对话框子进程内——宿主 Node 进程存活,失败原样上报(无回退层;见[链删除](../simplification/2026-08-04-drop-windows-powershell-picker-fallback.md))。mocked-koffi 的 ABI 钉与真实 win32 冒烟测试正是为了在交付前捕获这类错误。
- 打包二进制路径——打包后的可执行文件以对话框入口形式自我 spawn——不受任何自动化测试覆盖:源码侧与普通 node 下构建出的 `lib/worker.cjs` 已被覆盖,打包 spawn 推迟到 Windows CI 路线图。

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/implemented/feature/2026-08-03-fs-tool-error-remedy.md
2026-08-03-fs-tool-error-remedy.md: f227c31365725652b130e097d70c79d3daab3684
2026-08-03-fs-tool-error-remedy.zh.md: 11acd0cf48924833ced91591d5ea1424735969cd
2026-08-03-fs-tool-error-remedy.zh.md: 6bb0d0b6ffcda282db67dd1a54f0074b3be5c6e1

View File

@@ -1,32 +1,32 @@
# Agent Note: Guarded-mutation errors append the recovery instruction at the model boundary
# Agent Note: 受防护变更错误在模型边界追加恢复指令
Status: implemented
[English](2026-08-03-fs-tool-error-remedy.md) | 中文
## Problem
## 问题
受防护的 `write` 与 `edit` 失败以只陈述条件、不给出唯一正确恢复方式的消息到达模型:`FS_STALE_VERSION`("file changed since it was read")与 `FS_NOT_OBSERVED`("edit requires reading … first")。模型必须自行猜测恢复方式是重新读取(或首次读取)后重试,而基于结构化错误码路由的重试/权限/UI 层看到的也是同一段消息文本。提供方拥有的消息属于存储接缝的面向机器词汇([filesystem capability seam](../architecture/2026-06-17-filesystem-capability-seam.md)),因此恢复指令不能放在那里,否则会把面向模型的措辞泄漏给 `FsError` 的每个消费者。
受防护的 `write` 与 `edit` 失败以只陈述条件、不给出唯一正确恢复方式的消息到达模型:`FS_STALE_VERSION`("file changed since it was read")与 `FS_NOT_OBSERVED`("edit requires reading … first")。模型必须自行猜测恢复方式是重新读取(或首次读取)后重试,而基于结构化错误码路由的重试/权限/UI 层看到的也是同一段消息文本。提供方拥有的消息属于存储 seam 的面向机器词汇([文件系统能力 seam](../architecture/2026-06-17-filesystem-capability-seam.md)),因此恢复指令不能放在那里,否则会把面向模型的措辞泄漏给 `FsError` 的每个消费方。
## Decision
## 决策
`dsh-tool-fs` 拥有一个面向模型的错误包装 `remediateFsError`(位于 `src/error.ts`),在 `write.ts` 与 `edit.ts` 中于沙箱拒绝映射之后应用。它为两个受防护变更错误码追加恢复指令,其余错误原样透传:
`dsh-tool-fs` 拥有一个面向模型的错误包装层 `remediateFsError`(位于 `src/error.ts`),在 `write.ts` 与 `edit.ts` 中于沙箱拒绝映射之后应用。它为两个受防护变更错误码追加恢复指令,其余错误原样透传:
- `FS_STALE_VERSION`(包括缺失的编辑目标——它与陈旧错误共用同一错误码)追加 `— re-read the file, then retry`。
- `FS_NOT_OBSERVED` 追加 `— read the file, then retry`。
结构化 `FsError` 错误码保持不变,使重试/权限/UI 层继续基于它路由;原始错误作为 `cause` 链入。提供方消息保持面向机器且不变。
在 `edit.ts` 中,`fs/edit-intent` waterfall 现在与提供方变更位于同一个 `try` 内,因此策略插件从 intent 槽抛出的 `FS_NOT_OBSERVED` 拒绝也会获得恢复指令——两条拒绝路径都以相同的恢复措辞到达模型。
在 `edit.ts` 中,`fs/edit-intent` waterfall(瀑布式事件)现在与提供方变更位于同一个 `try` 内,因此策略插件从 intent slot 抛出的 `FS_NOT_OBSERVED` 拒绝也会获得恢复指令——两条拒绝路径都以相同的恢复措辞到达模型。
## Alternatives considered
## 考虑过的替代方案
- **在 `dsh-fs` / `dsh-fs-local` 的提供方消息中追加恢复指令。** 被拒绝:这些消息是面向机器的接缝词汇,除模型表面外还被重试、权限与 UI 层消费;面向模型的措辞应位于模型边界,即 `dsh-tool-fs` 已经拥有结果格式化之处([filesystem capability seam](../architecture/2026-06-17-filesystem-capability-seam.md))。
- **在 `dsh-fs` / `dsh-fs-local` 的提供方消息中追加恢复指令。** 被拒绝:这些消息是面向机器的 seam 词汇,除面向模型的接口外还被重试、权限与 UI 层消费;面向模型的措辞应位于模型边界,即 `dsh-tool-fs` 已经拥有结果格式化之处([文件系统能力 seam](../architecture/2026-06-17-filesystem-capability-seam.md))。
- **改为在提示词引导中加入恢复方式。** 被拒绝:失败发生在任务中途;静态指令无法可靠地影响重试决策,而错误消息恰好在模型必须行动时出现。
- **用新的 `FsError` 错误码表达恢复指令。** 被拒绝:这两种失败本就是重试层已处理的相同条件;拆分错误码会让语义相同的路由分叉。
- **用新的 `FsError` 错误码表达恢复指令。** 被拒绝:这两种失败对应的条件,重试层本就已经处理;拆分错误码会使相同语义采用不同路由。
## Consequences
## 后果
两个错误码的模型可见文本发生变化;`fs-policy-reject` 无密钥快照被重新录制,`dsh-tool-fs` 与 `dsh-fs-policy` 的 README 逐字固定追加后的文本。单元测试直接覆盖包装器(恢复指令文本、错误码保留、cause 链、其他错误码与非 `FsError` 值的透传),组装后的工具路径断言两个错误码的恢复指令都到达模型。
两个错误码的模型可见文本发生变化;`fs-policy-reject` 无密钥快照被重新录制,`dsh-tool-fs` 与 `dsh-fs-policy` 的 README 逐字固定追加后的文本。单元测试直接覆盖包装层(恢复指令文本、错误码保留、cause 链、其他错误码与非 `FsError` 值的透传),组装后的工具路径断言两个错误码的恢复指令都到达模型。
恢复指令不是承诺:已删除的观察目标无法被解除阻塞,因为重新读取缺失文件会以 `FS_NOT_FOUND` 失败且不记录观察。这一死胡同在集成测试中以 fail-closed 方式固定——在目标重新存在并被新鲜观察之前,重试的变更以相同方式失败。
恢复指令不是承诺:已观察但被删除的目标无法被解除阻塞,因为重新读取缺失文件会以 `FS_NOT_FOUND` 失败且不记录观察。这一死胡同在集成测试中以 fail-closed 方式固定——在目标重新存在并被重新观察之前,重试的变更以相同方式失败。

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/implemented/feature/2026-08-03-web-search-source-scroll.md
2026-08-03-web-search-source-scroll.md: 3215302b2f6c7a4e6d6cd7440a0c7f5048b72a80
2026-08-03-web-search-source-scroll.zh.md: 88f2ef9869ef5de7483eb0d7e2a76f8c14e8356b
2026-08-03-web-search-source-scroll.zh.md: 7d644b9e3950906b97d382e997eb3b204a577326

View File

@@ -4,13 +4,13 @@ Status: implemented
[English](2026-08-03-web-search-source-scroll.md) | 中文
## Problem
## 问题
`web_search` 结果卡片(`WebBlock`,`packages/client/ui-primitives/src/WebBlock.tsx`)此前用首尾折叠渲染它的来源列表:超过 `maxSources` 数量(详情面板为 16,聊天行经由 `CHAT_WEB_MAX_SOURCES` 为 8)时,它画出前 `ceil(max/2)` 条来源、一个 `… 其余 N 条来源` 展开按钮,再画出末尾 `max - ceil(max/2)` 条,与 `TerminalBlock` 的输出上限一致。用户阅读该卡片时看到 `来源列表已截断`,会以为前端丢弃了它正持有的来源。
`web_search` 结果卡片(`WebBlock`,`packages/client/ui-primitives/src/WebBlock.tsx`)此前用首尾折叠渲染它的来源列表:超过 `maxSources` 数量(详情面板为 16,聊天行经由 `CHAT_WEB_MAX_SOURCES` 为 8)时,它画出前 `ceil(max/2)` 条来源、一个 `… 其余 N 条来源` 展开按钮,再画出末尾 `max - ceil(max/2)` 条,仿照 `TerminalBlock` 的输出上限机制。用户阅读该卡片时看到 `来源列表已截断`,会以为前端丢弃了它正持有的来源。
其实并没有。seam(`capSources`,`packages/web/web/src/index.ts`)把 provider 的来源裁剪到工具的 `searchMaxResults` 上限(默认 8)并置位 `truncated`,而这一份被裁剪过一次的列表同时喂给面向模型的 render 文本与卡片的 `presentationMeta`。卡片持有的来源绝不会多于这一次裁剪的产物。因此这个折叠隐藏的正是用户本有权完整查看的来源——并且在默认上限为 8、面板上限为 16 时,它几乎从不触发,只留下 `truncated` 提示,却无从展开任何内容。
## Decision
## 决策
`WebBlock` 的 search 分支把它收到的每一条来源都渲染进单个 `<ol className={css.sources}>`,不做首尾切片、不设展开按钮、也不带 `maxSources` prop。`.sources`(`WebBlock.module.css`)获得一个固定的 `max-height` 与 `overflow-y: auto`,因此长于卡片高度的列表在原地滚动,而非撑大卡片或隐藏行。该高度是卡片几何形状的一个设计常量,因此放在 CSS 里,而非插件配置字段。
@@ -22,25 +22,25 @@ Status: implemented
把列表变成滚动容器,也把它的 `padding-left` 从间距变成了正确性约束。滚动容器裁掉 inline-start 方向的溢出且无从滚回,而 `::marker` 右对齐到内容边缘,因此宽于 padding 的序号会静默丢掉前导数字——在列表原本的 20px 下,两位数序号被画成 `0.` 与 `1.`,而本该是 `10.` 与 `11.`。`searchMaxResults` 是无上界的正整数,因此该 padding 以 `em` 计量——相对列表自身的字体,也就是序号所继承的那个——装得下三位数序号(`999. ` 在应用字体栈下量得 2.35em),并保留一位数情形原有的间隙。
## Alternatives considered
## 考虑过的替代方案
**提高 `searchMaxResults`(或让它无上限),使更多来源同时抵达模型与卡片。** 被用户否决:它改变了模型侧行为(每个请求的上下文纳入更多来源、更多 token),并拉大模型读到的内容与卡片画出的内容之间的差距。指令很明确——保留上限与截断,加一个滚动条。
**保留首尾折叠,仅对展开区域加滚动。** 否决:一个关注点上两套重叠机制。一旦整份列表始终渲染,折叠的算术、展开/折叠状态与那个按钮都是死重;仅靠滚动即可约束高度。
**保留首尾折叠,仅对展开区域加滚动。** 否决:一个关注点上两套重叠机制。一旦整份列表始终渲染,折叠的算术、展开/折叠状态与那个按钮都是累赘;仅靠滚动即可约束高度。
**把滚动高度做成插件配置字段。** 否决:该高度约束的是卡片在屏幕上的几何形状,而非部署策略,因此它属于 `WebBlock.module.css`,与 [Web result 卡片前端笔记](2026-07-30-web-result-card-frontend.md) 已作为本卡片几何固定在那里的圆角、表面与外边距并列。
## Consequences
## 后果
工具返回的每一条来源始终存在于 DOM 中,因此 view 携带的来源没有一条被藏在交互之后。无论来源数量多少,卡片高度都受限;高于容器的列表在原地滚动。代价是滚动提示依赖平台的滚动条渲染:overlay 滚动条系统(macOS 默认)在指针离开时不显示常驻滚动条,因此被裁剪的列表依靠 `来源列表已截断` 提示加上被裁切的最后一行来表明还有更多内容。`WebSearchBlockProps`/`WebFetchBlockProps` 失去 `maxSources` prop,primitive 失去 `DEFAULT_WEB_MAX_SOURCES`,因此未来任何调用方都从构造上渲染完整列表,而不是靠传入一个很大的上限值。
工具返回的每一条来源始终存在于 DOM 中,因此 view 携带的来源没有一条被藏在交互之后。无论来源数量多少,卡片高度都受限;高于容器的列表在原地滚动。代价是滚动提示依赖平台的滚动条渲染:overlay 滚动条系统(macOS 默认)在指针离开时不显示常驻滚动条,因此受高度限制的列表依靠 `来源列表已截断` 提示加上被裁切的最后一行来表明还有更多内容。`WebSearchBlockProps`/`WebFetchBlockProps` 失去 `maxSources` prop,primitive 失去 `DEFAULT_WEB_MAX_SOURCES`,因此未来任何调用方都从构造上渲染完整列表,而不是靠传入一个很大的上限值。
## Testing
## 测试
`packages/client/ui-primitives/tests/web-block.spec.tsx` 删去折叠相关用例(首尾切片、点击展开、折叠尾部编号、展开器不计入编号、仅首部、默认上限),并新增:一个 30 条来源的卡片渲染出全部 30 个 `<li>`,无 `[aria-expanded]`、无 `<button>`,每个 `<ol>` 子元素都是一条来源 `<li>`,且 `<li value>` 从 1 到 N 连续编号。`packages/client/ui-tool/tests/web-card.spec.tsx` 删去 `CHAT_WEB_MAX_SOURCES` 上限断言;WebRow 展开测试仍断言卡片展示每一个来源字段。`packages/web/tool-web` 的测试不变——模型侧未曾移动。
`packages/client/ui-primitives/tests/web-block.spec.tsx` 删去折叠相关用例(首尾切片、点击展开、折叠尾部编号、展开器不计入编号、仅首部、默认上限),并新增:一张含 30 条来源的卡片渲染出全部 30 个 `<li>`,无 `[aria-expanded]`、无 `<button>`,每个 `<ol>` 子元素都是一条来源 `<li>`,且 `<li value>` 从 1 到 N 连续编号。`packages/client/ui-tool/tests/web-card.spec.tsx` 删去 `CHAT_WEB_MAX_SOURCES` 上限断言;WebRow 展开测试仍断言卡片展示每一个来源字段。`packages/web/tool-web` 的测试不变——模型侧没有改动。
jsdom 不解析 CSS Modules 布局,对任何元素都报 `scrollHeight === clientHeight`,因此它根本无从见证这次滚动。几何改由组装态浏览器钉住,位于 `apps/web/tests/web-search-round.e2e.ts`:其确定性 search double 返回 12 条 provider 结果,每条带标题、引用摘录与日期。这首先在真实组合里端到端钉住 seam 的裁剪——出厂 `searchMaxResults` 保留 8 条,面向模型的 render 文本含这 8 条标题、不含被丢弃的 4 条 URL,并含 `(Showing the first 8 sources. Refine the query for more.)`,`meta.truncated` 为 true。随后位于 aria golden 之后的一个用例展开 `web_search` 行,对卡片的 `<ol>` 断言:8 个 `<li>`、卡片内任何位置都没有 `<button>`、`来源列表已截断` 指示可见,以及计算样式 `max-height: 320px` 与 `overflow-y: auto`,`scrollHeight` 为 574、`clientHeight` 为 320。再后一个用例在列表自身继承的字体下量出 `999. ` 序号的宽度,要求计算后的 `padding-left` 不小于该宽度,从而把滚动容器无从滚回的那段序号空间钉在最宽序号上,而非钉在某一份 fixture 的来源条数上。录制的模型流与 aria golden 都未变动:replay 是对 fixture 中 `assistant/chunk` 条目的位置游标,而 search double 是 provider 经 `fetch` 抵达的另一个本地端点;捕获时卡片处于折叠状态,其 `<ol>` 不在 DOM 中,摘要行也不携带来源数量。
jsdom 不解析 CSS Modules 布局,对任何元素都报 `scrollHeight === clientHeight`,因此它根本无从见证这次滚动。几何改由组装态浏览器钉住,位于 `apps/web/tests/web-search-round.e2e.ts`:其确定性 search double 返回 12 条提供方结果,每条带标题、引用摘录与日期。这首先在真实组合里端到端钉住 seam 的裁剪——出厂 `searchMaxResults` 保留 8 条,面向模型的 render 文本含这 8 条标题、不含被丢弃的 4 条 URL,并含 `(Showing the first 8 sources. Refine the query for more.)`,`meta.truncated` 为 true。随后位于 aria golden 之后的一个用例展开 `web_search` 行,对卡片的 `<ol>` 断言:8 个 `<li>`、卡片内任何位置都没有 `<button>`、`来源列表已截断` 指示可见,以及计算样式 `max-height: 320px` 与 `overflow-y: auto`,`scrollHeight` 为 574、`clientHeight` 为 320。再后一个用例在列表自身继承的字体下量出 `999. ` 序号的宽度,要求计算后的 `padding-left` 不小于该宽度,从而把滚动容器无从滚回的那段序号空间钉在最宽序号上,而非钉在某一份 fixture(测试前置数据)的来源条数上。录制的模型流与 aria golden 都未变动:回放是对 fixture 中 `assistant/chunk` 条目的位置游标,而 search double 是提供方经 `fetch` 抵达的另一个本地端点;捕获时卡片处于折叠状态,其 `<ol>` 不在 DOM 中,摘要行也不携带来源数量。
## Related
## 相关文档
- [Web result card](2026-07-30-web-result-card.md) —— 本卡片消费的 `card: 'web'` 渲染意图分支与 `presentationMeta` 路由;那份裁剪过一次的列表的来源。
- [Web result 卡片前端](2026-07-30-web-result-card-frontend.md) —— `WebBlock`、唯一的 `web-card-model` 派生,以及绘制该卡片的各渲染点由它拥有;本笔记替换掉它所规定的来源列表折叠,它的其余决策(一个组件绘制两种 kind、http(s) 链接 allowlist、单一派生、常驻姿态)依然成立。

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/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
2026-08-04-claude-code-and-codex-subagent-backends.md: 2063b99a7b0ca34f434b3e56628f3ce765d90d81
2026-08-04-claude-code-and-codex-subagent-backends.zh.md: ee3ef4e130a8c9066e30ed59a58e063192f47ed0
2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 71d4a8e9fc060f8c96483b22641fd67ee1f71c08

View File

@@ -8,7 +8,7 @@ Status: implemented
命名的 [`ctx.subagents`](2026-06-21-subagent-capability-seam.md) 注册表让父 agent(智能体)无需了解子级的运行方式即可委派工作,但 harness 需要通往真实 Codex 与 Claude Code 产品的第一方路径。每条路径都必须向产品交付一项自包含任务,让它在父会话的工作区中执行,返回最终回答或明确的失败或取消结果,并且不留下任何受管的产品进程。
产品集成不得成为任务文本、cwd、取消、结果结算或进程树的第二责任方。因此,所需证据要区分三个事实:无密钥真实产品测试证明官方集成、原生身份验证形态、确定性答案与资源清理;Loader 组合测试证明公开包(package)和文档所示的工具配置无需启动产品即可加载;带密钥 e2e 证明生产提供方与真实产品能够从真实 DeepSeek 服务取得唯一答案。直接发起模型 HTTP 请求或使用产品替身无法取代上述任一产品运行层级;手工挂载插件无法取代 Loader 层级。
产品集成不得成为任务文本、cwd、取消、结果结算或进程树的第二责任方。因此,所需证据要区分三个事实:无密钥真实产品测试证明官方集成、原生身份验证形态、确定性答案与资源清理;Loader 组合测试证明公开包和文档所示的工具配置无需启动产品即可加载;带密钥 e2e 证明生产提供方与真实产品能够从真实 DeepSeek 服务取得唯一答案。直接发起模型 HTTP 请求或使用产品替身无法取代上述任一产品运行层级;手工挂载插件无法取代 Loader 层级。
## 决策

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/implemented/feature/2026-08-04-pointer-revealed-sidebar-scrollbars.md
2026-08-04-pointer-revealed-sidebar-scrollbars.md: eb86f710e44cd8f1fb2cef41db99504af42413bb
2026-08-04-pointer-revealed-sidebar-scrollbars.zh.md: 4f871a70298396b8ed75b39c2a4d199b69061472
2026-08-04-pointer-revealed-sidebar-scrollbars.zh.md: f3a92d6645c7f98a60fd3c4431cdfb84babfd51b

View File

@@ -1,38 +1,38 @@
# Agent Note:侧边栏的滚动条跟随指针
# Agent Note: 侧边栏的滚动条跟随指针
Status: implemented
[English](2026-08-04-pointer-revealed-sidebar-scrollbars.md) | 中文
## Problem
## 问题
侧边栏的会话列表只要有十来个会话就会溢出,从那一刻起它的滚动条就一直画在那里——所处的这一列大部分时间都是静止的,而列表行自己的操作按钮只在悬停时才出现。它是侧边栏里唯一始终常驻的构件,而在有人真的伸手去拖它之前,它不提供任何可操作性。产品诉求(2026-08-04)是只在指针位于侧边栏内时才绘制它,并留一小段拖尾,避免指针路过时它一闪而灭。
侧边栏的会话列表只要有几个会话就会溢出,从那一刻起它的滚动条就一直画在那里——所处的这一列大部分时间都是静止的,而列表行自己的操作按钮只在悬停时才出现。它是侧边栏里唯一始终常驻的构件,而在有人真的伸手去操作它之前,它不提供任何可操作性。产品诉求(2026-08-04)是只在指针位于侧边栏内时才绘制它,并留一小段拖尾,避免指针路过时它一闪而灭。
## Decision
## 决策
`SidebarRoot` 跟踪整列上的指针,只要指针不在列内就给根元素挂上 `quietBars` 类。该类选中的规则把 ui-theme 的那组间接变量——`--dsh-scrollbar-thumb` 与 `--dsh-scrollbar-thumb-hover`——重新绑定为 `transparent`,于是嵌套在这一列下的每个滚动区域都不绘制滑块。今天这样的区域只有会话列表;将来新增的区域会直接继承这一行为,而不需要逐个接入。
拖尾是 `SCROLLBAR_LINGER_MS = 2000`:离开会启动一个定时器,进入会取消尚未触发的定时器,只有定时器真正触发才会把类加回去。指针越过列边界又折返时——绕过一个 portal 菜单,或是奔向某一行时冲过了头——不会看到滑块闪动。
进入用的是列自身的 `pointerenter`;离开则按列的盒子判定,由一个只在滚动条可见期间存在的 `pointermove` 监听完成。DOM 包含关系无法判定离开:ui-settings 把整屏的设置面板渲染为这一列的 fixed 定位**后代**,指针移到该面板上——或在面板关闭后移到对话区——都不会在这里触发 `pointerleave`,滚动条就会继续画在一个没人指向的列上。元素自身的 leave 仍然保留,用于几何判定看不到的那一种情况:指针移出窗口后不再产生任何移动事件。
进入用的是列自身的 `pointerenter`;离开则按列的盒子判定,由一个只在滚动条可见期间存在的 `pointermove` 监听完成。DOM 包含关系无法判定离开:ui-settings 把整屏的设置面板渲染为这一列的 fixed 定位*后代*,指针移到该面板上——或在面板关闭后移到对话区——都不会在这里触发 `pointerleave`,滚动条就会继续画在一个没人指向的列上。元素自身的 leave 仍然保留,用于几何判定看不到的那一种情况:指针移出窗口后不再产生任何移动事件。
承载指针的是整列,而不是列表。奔向滚动条的指针会先经过 logo 行、New Session 胶囊和搜索框,所以只在列表上显示,会让滚动条等到指针已经落在行中间时才出现。
`transparent` 正是让这次显示不触发任何布局的原因。列表上的 `scrollbar-gutter: stable` 存在的意义就是让行永不移动(见[空槽 Agent Note(agent 决策记录)](../bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md));重新绑定的只是颜色,那份预留始终有效,所以滑块出现在列表本就为它留出的空间里。
`transparent` 正是让这次显示不触发任何布局的原因。列表上的 `scrollbar-gutter: stable` 存在的意义就是让行永不移动(见[空槽 Agent Note](../bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md));重新绑定的只是颜色,那份预留始终有效,所以滑块出现在列表本就为它留出的空间里。
选择这组间接变量而不是给列表加规则,是因为这组变量正是 ui-theme 写明的重新绑定接缝:一次声明同时作用于两条渲染路径(WebKit 伪元素与 Firefox 的 `scrollbar-color`),而自定义属性会继承——这正是让整列、而不是列内每个滚动区域,成为该状态所有者的原因。
选择这组间接变量而不是给列表加规则,是因为这组变量正是 ui-theme 写明的重新绑定 seam:一次声明同时作用于两条渲染路径(WebKit 伪元素与 Firefox 的 `scrollbar-color`),而自定义属性会继承——这正是让整列、而不是列内每个滚动区域,成为该状态所有者的原因。
这拓宽了重新绑定约定,因此它的门禁把新的形态明写出来,而不是默许通过:`ui-theme/tests/scrollbar-styles.spec.ts` 只接受两种重新绑定目标,即 l2 那一组或 `transparent`,并且判定的是**整条规则**而不是逐条声明——混合规则(`thumb: transparent` 与 l2 的 hover 并列)会在指针一碰到滚动条时重新上色,却能通过逐条检查。抬升那一半按整个值与这组变量的规范写法比对,这同时也拒绝了交叉绑定和被包在字面表达式里的 token;绑回 l1 与裸颜色本来就在门外。
隐藏不再算作抬升:只有 l2 重绑才能让一张样式表免于「任何既滚动又绘制抬升表面的样式表都必须重新绑定」。既隐藏滚动条又在抬升表面上滚动的样式表,仍然欠着那里真正绘制滑块时所需的 l2。
隐藏不再算作抬升:只有 l2 重绑才能让一张样式表免于「任何在抬升表面上滚动的样式表都必须重新绑定」。既隐藏滚动条又在抬升表面上滚动的样式表,仍然欠着那里真正绘制滑块时所需的 l2。
## Alternatives considered
## 考虑过的替代方案
**只用列上的 CSS `:hover`,不引入 JavaScript 状态。** 整套机制只需一条规则,但它表达不出拖尾:指针越过边界的那一帧滑块就会消失,而那恰好是指针正奔向对话区或绕行 portal 菜单的时刻。诉求本身点名了拖尾,只有 hover 的版本读起来就是闪烁。
**留在 CSS 里、用过渡拿到这段延迟**,即通过 `@property` 注册 `--dsh-scrollbar-thumb` 让该自定义属性可动画,再用 `transition-delay` 把颜色按住。因代价与作用范围被否决:这项注册对每个读取这组变量的表面都是全局的,却只为一列的时序服务;而且这套调色板实际渲染所走的 WebKit 滚动条伪元素并不可靠地支持过渡——延迟会被声明在观察不到它的地方。
**直接把滚动条藏掉**——`scrollbar-width: none`,或对 `::-webkit-scrollbar` 用 `display: none`。被否决,因为这会连带取消那段预留:滚动条重新出现时要重新占走 8px,会把每一行都在显示它的那个指针底下横向推移,而这正是当初加入空槽预留所修掉的回归。
**直接把滚动条藏掉**——`scrollbar-width: none`,或对 `::-webkit-scrollbar` 用 `display: none`。被否决,因为这会连带取消那段预留:滚动条重新出现时会重新占走 8px,使每一行都在触发其显示的指针下方横向移动,而这正是当初加入空槽预留所修掉的回归。
**在应用内自绘一个覆盖式滑块**,并彻底隐藏原生滚动条,这是完全自定义淡入淡出所需要的做法。它换来任意样式,代价是命中测试、拖拽、滚轮、惯性以及两套调色板下的 hover 状态——在一个滚动条已由 token 统一主题化的客户端里,为观感付出的是一大片自持表面。
@@ -40,7 +40,7 @@ Status: implemented
**滚动事件也触发显示**,让键盘或触摸驱动的滚动同样显示滚动条。被否决,因为那是在为触发它的输入方式画一个它用不上的可供性;行本身已经说明列表移动过了。
## Consequences
## 后果
- 用键盘或触摸拖动滚动的列表,在拖尾结束后不显示滑块,因为这两种方式都不会把指针留在列上。e2e 会钉住这一点,而不只是把它写下来。
- 拖动滑块本身移出列不会在拖动中途把它隐藏:滚动条会接管指针捕获,按住按键期间页面收不到 `pointermove`。已在 Chromium 实测——指针拖到列右侧 900px 处、超过拖尾窗口后,滚动条依然绘制并继续滚动。
@@ -48,15 +48,15 @@ Status: implemented
- 嵌套在列内、为自身抬升层级把这组变量重新绑定到 l2 的抬升表面,会覆盖静默状态并继续绘制自己的滚动条。今天侧边栏内没有这样的表面。
- 外壳的 DOM 现在带有一个状态类,因此 ui-sidebar 的外壳快照会钉住 `quietBars`,默认状态出现回归时表现为快照 diff,而不是需要有人从截图里看出来的东西。
## Testing
## 测试
`packages/client/ui-sidebar/tests/pointer-scrollbars.spec.tsx` 用假定时器把这个类走过各次跃迁:进入时显示,拖尾结束前 1 毫秒仍然显示,结束后 1 毫秒转为静默,以及窗口内折返会取消隐藏。另有两条覆盖几何判定的离开:落在列盒子之外的 `pointermove` 会在没有任何 DOM leave 的情况下隐藏滚动条(即设置面板那种形态),落回盒子之内的则取消待触发的隐藏。它还在拖尾进行中卸载组件并断言没有定时器存活——待触发的隐藏落到已销毁的组件上,正是这种写法容易犯的错。事件用的是带 `relatedTarget` 的 `pointerover`/`pointerout`,因为 React 由它们合成 enter 与 leave,而会忽略原生的那两个事件。
`packages/client/ui-sidebar/tests/scrollbar-quiet-styles.spec.ts` 直接读样式表:该规则必须写出这组变量的两半——只重新绑定静止态滑块,会让指针一碰到滚动条就露出 hover 颜色——并且不得出现 `scrollbar-gutter`,那属于滚动区域自己。
`apps/web/tests/sidebar-scrollbar.e2e.ts` 是两半在真实引擎里汇合的地方。它在每次读取颜色前先把指针停在列表上,因为一个从不移动鼠标的场景全程测到的都是静默状态,会变成空洞的绿。随后它自己的用例把指针移开,断言在 leave 当下滑块仍在绘制,轮询直到它解析为 `rgba(0, 0, 0, 0)`,在该状态下重新测量几何以证明滚动条隐藏期间那份预留依然生效,并以编程方式滚动列表——键盘或触摸拖动所做的事——来钉住无指针滚动不绘制任何滑块。提交的 golden 记录了两套调色板下、两个指针位置上的滑块颜色。
`apps/web/tests/sidebar-scrollbar.e2e.ts` 是两半在真实引擎里汇合的地方。它在每次读取颜色前先把指针停在列表上,因为一个从不移动鼠标的场景全程测到的都是静默状态,会在未实际验证目标行为的情况下通过。随后它自己的用例把指针移开,断言在 leave 当下滑块仍在绘制,轮询直到它解析为 `rgba(0, 0, 0, 0)`,在该状态下重新测量几何以证明滚动条隐藏期间那份预留依然生效,并以编程方式滚动列表——键盘或触摸拖动所做的事——来钉住无指针滚动不绘制任何滑块。提交的 golden 记录了两套调色板下、两个指针位置上的滑块颜色。
这条 e2e 的对照是一次 mutation,而它需要插件自己的产物:把 `quietBars` 从外壳中去掉,先重新构建 `@deepseek-ai/dsh-client-ui-sidebar`、之后再跑 `build:web`,该用例会因为滑块解析为 `rgb(229, 229, 229)`、而期望 `rgba(0, 0, 0, 0)` 而变红。只重跑 `build:web` 用的是过期产物,即使改动已被删除也照样通过,这正是[空槽 Agent Note(agent 决策记录)](../bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md)记录过的陷阱。
这条 e2e 的对照是一次 mutation,而它需要插件自己的产物:把 `quietBars` 从外壳中去掉,先重新构建 `@deepseek-ai/dsh-client-ui-sidebar`、之后再跑 `build:web`,该用例会因为滑块解析为 `rgb(229, 229, 229)`、而期望 `rgba(0, 0, 0, 0)` 而变红。只重跑 `build:web` 用的是陈旧产物,即使改动已被删除也照样通过,这正是[空槽 Agent Note](../bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md)记录过的陷阱。
拓宽后的门禁也有自己的对照,每个都是对真实样式表的一处声明改动:把 `transparent` 与 l2 的 hover 混用,以及把 l2 token 包进 `color-mix(…)`,都会让这条成对断言变红。

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/implemented/feature/2026-08-04-web-composer-shared-width-axis.md
2026-08-04-web-composer-shared-width-axis.md: 96cddda25bf79e9f2df7a0298039c27f316befca
2026-08-04-web-composer-shared-width-axis.zh.md: 9a9f5a513bbce8f3e97698d58ab48b0abdefb142
2026-08-04-web-composer-shared-width-axis.zh.md: b5a455f9e8c561ae89faa9baf526fdbb263bc89a

View File

@@ -4,19 +4,19 @@ Status: implemented
[English](2026-08-04-web-composer-shared-width-axis.md) | 中文
## Problem
## 问题
Web 会话列的各个界面各自独立设定尺寸:转录列、输入卡片、todo/goal/queue 停靠卡片、ask-question/approval/plan-review 接管卡片各自硬编码 max-width(736/752/776/800px 等变体)与各自的侧边内边距。这些界面在全宽下彼此漂移几个像素,在窄视口下偏差更大——有的面板保留了到屏幕边缘的间隙,有的却贴边。另外,输入卡片的控制行没有自适应行为——窄卡片下权限触发器的文字标签会挤压整行;锚定在卡片上的浮层菜单也可能渲染得比卡片更宽,越过其右边缘。
Web 会话列的各个界面各自独立设定尺寸:transcript(文本记录)列、输入卡片、todo/goal/queue 停靠卡片、ask-question/approval/plan-review 接管卡片各自硬编码 max-width(736/752/776/800px 等变体)与各自的侧边内边距。这些界面在全宽下彼此漂移几个像素,在窄视口下偏差更大——有的面板保留了到屏幕边缘的间隙,有的却贴边。另外,输入卡片的控制行没有自适应行为——窄卡片下权限触发器的文字标签会挤压整行;锚定在卡片上的浮层菜单也可能渲染得比卡片更宽,越过其右边缘。
## Decision
## 决策
一个内容宽度变量拥有整列。`--dsh-chat-content-width`(748px)声明在 ConversationRoot 的 `.root` 上——转录与 composer 座位是兄弟子树,声明必须放在共同祖先上,CSS 自定义属性才能通过继承同时到达两者。其他几何全部由它推导:输入卡片上限为 `content + 32px`(`--dsh-composer-card-max-width`),停靠卡片从卡片宽度中减去四个停靠 inset(4 × 8px)正好回到内容宽度,接管卡片直接使用内容宽度。窄视口不变式以结构而非数值表达:内容宽度的界面每侧 pad `calc(var(--dsh-composer-side-clearance) + 16px)`,而输入卡片只留裸 clearance(16px),因此"输入卡片 = 内容 + 32px"在任意视口宽度下都成立,而不只是在上限处。
一个内容宽度变量控制整列。`--dsh-chat-content-width`(748px)声明在 ConversationRoot 的 `.root` 上——transcript 与 composer 座位是兄弟子树,声明必须放在共同祖先上,CSS 自定义属性才能通过继承同时到达两者。其他几何全部由它推导:输入卡片上限为 `content + 32px`(`--dsh-composer-card-max-width`),停靠卡片从卡片宽度中减去四个停靠 inset(4 × 8px)正好回到内容宽度,接管卡片直接使用内容宽度。窄视口不变式以结构而非数值表达:内容宽度的界面每侧 pad `calc(var(--dsh-composer-side-clearance) + 16px)`,而输入卡片只留裸 clearance(16px),因此「输入卡片 = 内容 + 32px」在任意视口宽度下都成立,而不只是在上限处。
卡片内的控制行是一个 `container-type: inline-size` 容器,权限触发器在 460px 容器查询下收起文字标签(保留图标 + 下拉箭头)。查询刻意匿名:CSS modules 按模块哈希 `container-name`,InputBar 样式表里声明的名字永远无法匹配 PermissionSelect 样式表里写的查询——两个哈希后的名字悄然不同,查询永不触发。只有带模式图标的触发器才收起(`:has(.triggerIcon)`);没有图标的宿主自定义模式保留文字作为其唯一标识。
锚定在卡片上的浮层菜单(slash 菜单、command popupSelect)钳制到锚点宽度(`max-width: min(<设计上限>, 100%)`),过长的行以省略号截断而不是溢出卡片。Tooltip 气泡在钳制中保留 12px 的视口边缘安全距离(ui-primitives Tooltip)。
锚定在卡片上的浮层菜单(slash 菜单、command popupSelect)钳制到锚点宽度(`max-width: min(<design cap>, 100%)`),过长的行以省略号截断而不是溢出卡片。Tooltip 气泡在钳制中保留 12px 的视口边缘安全距离(ui-primitives Tooltip)。
## Alternatives considered
## 考虑过的替代方案
**保留各界面独立宽度,手工对齐数值。** 否决:本次改动消除的漂移正是手工对齐常量的残留;未来任何宽度调整都需要五处协同编辑,且没有任何机制强制这组关系。
@@ -26,6 +26,6 @@ Web 会话列的各个界面各自独立设定尺寸:转录列、输入卡片
**用 JS ResizeObserver 实现标签收起。** 否决:容器查询是声明式的,无需监听器生命周期,而 460px 阈值无论哪种方案都是设计选择。
## Consequences
## 后果
修改列宽现在是一行编辑,比例关系由构造保证——评审期间 736 → 748 的重调已经验证了这一点。代价是间接性:五个界面的宽度不再能从各自的样式表直接读出,需要沿变量链追到 ConversationRoot。容器查询收起增加了一个约束:InputBar 的行必须保持为尺寸容器;删掉那条声明会静默禁用权限触发器的自适应行为。匿名查询也意味着未来若在行与触发器之间出现第二个容器,它会截获该查询——届时查询必须迁移,或避免中间容器。

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/implemented/feature/2026-08-04-web-context-source-and-steer-marks.md
2026-08-04-web-context-source-and-steer-marks.md: ca44702cf637c4250d141396ac22d206a16acc15
2026-08-04-web-context-source-and-steer-marks.zh.md: 09cefd07e3b417b63f05ce430cfed7b30597452f
2026-08-04-web-context-source-and-steer-marks.zh.md: da8b629da262ed462f84083030c4fdfb1083eb77

View File

@@ -1,16 +1,16 @@
# Agent Note:Web transcript 标出上下文来源、召回与 steering
# Agent Note: Web transcript 标出上下文来源、召回与 steering
Status: implemented
[English](2026-08-04-web-context-source-and-steer-marks.md) | 中文
## Problem
## 问题
生产方向模型侧对话补充的一切内容,进入 Web transcript(文本记录)后只剩两种匿名形态。每一条已记录的非用户 `user/message`——skill 目录、运行时快照、经过对账的 `AGENTS.md` 指令、guard 提示、子 agent 汇报、跨会话快照——都塌缩成同一行 `上下文注入`,读者不逐行展开去读原始 JSON 就无从知道究竟注入了什么。steering(中途引导)的情况更糟:它渲染成与开轮提示完全相同的气泡,于是 transcript 无法说明哪一条消息打断了正在运行的轮次。
生产方向模型侧对话补充的一切内容,进入 Web transcript(文本记录)后只剩两种匿名形态。每一条已记录的非用户 `user/message`——skill(技能)目录、运行时快照、经过对账的 `AGENTS.md` 指令、guard 提示、subagent 汇报、跨会话快照——都塌缩成同一行 `上下文注入`,读者不逐行展开去读原始 JSON 就无从知道究竟注入了什么。steering(中途引导)的情况更糟:它渲染成与开轮提示完全相同的气泡,于是 transcript 无法说明哪一条消息打断了正在运行的轮次。
这些区分本来就是持久事实。`user/message.source` 是每个生产方都必须提供的可合并扩展来源,`agent/inbox/spliced` 则记录有身份的消息是从 `next-turn` 还是 `next-step` 进入和离开;把这些事实丢掉的只有呈现层。被这套 Web UI 取代的终端 transcript 本来会写出每张卡片的生产者,因此面对同一份日志,Web 侧是一次倒退。
## Decision
## 决策
transcript 为非提示消息可能承担的三种角色分别命名:注入上下文、召回会话、steering。
@@ -22,7 +22,7 @@ transcript 为非提示消息可能承担的三种角色分别命名:注入上
`MessageItem` 为持久与待处理的 steering 气泡加上 `插话` 标注。runtime 会重放持久 `agent/inbox/spliced` 事件;如果一条用户来源的消息以相同身份从 `next-step` 被领取,后续 `user/message` 就投影为 `SteeringMessageNode`。从排队轮次领取的消息仍是 `UserMessageNode`,非用户来源的 next-step 消息仍是上下文。这推翻了[已归档的取消 steer 入口与插话装饰决策](../../archived/simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md)中的一条结论。当时移除徽章,是因为 composer 无法 steer,标签指向了用户做不到的动作。此后 composer 获得了 Steer 手势,却没有同步修订那份 note;本决策提供了它在「重新引入」条款中要求的产品决策,并订正了其中留下的过时事实。标注是这里唯一的 steering 装饰:composer 模式、Queue dock 的严格 steer 操作、待处理 steering 的生命周期仍归各自的所有者。
## Alternatives considered
## 考虑过的替代方案
**在客户端本地化生产者名称。** 以插件 id 为键的字典读起来确实比 `@deepseek-ai/dsh-system-prompt` 好,但它会在每次重命名时悄悄失准,每新增一个生产者都要改客户端,而且对来自外部的日志根本无法命名。日志已经承载的来源,比客户端自己编出来的措辞更有价值。
@@ -34,13 +34,13 @@ transcript 为非提示消息可能承担的三种角色分别命名:注入上
**把同一套名称扩展到 trajectory 表格。** 不在本次范围内:该表格的上下文单元格有自己的文本推导,而 issue 要求的是对话面。
## Testing
## 测试
- `packages/client/runtime` 单元覆盖钉住每个来源分支、名称字段缺失/为空/类型不符时的回退、来源没有可读 kind 时的无名降级,以及 reset 和实时 append 路径上的 steering 重建。
- `packages/client/ui-conversation` 的 jsdom 覆盖钉住角色标题、标题旁的生产者名称、展开后该名称的留存、无名时的标题形态,以及持久与待处理气泡上的 steering 标注。
- 无密钥的组装 Web 黄金基线携带带名称的标题栏与 steering 标注,因此证明这些标识的是组装后的 transcript,而不只是组件测试。
## Consequences
## 后果
- 读者一眼即可归因 transcript 中每一条非提示消息;即便面对本客户端版本从未见过其生产者的日志,标题栏依然如实。
- 只要来源仅携带插件 id,UI 中的生产者名称就呈现为包名形态(`dsh-tool-skill`、`@deepseek-ai/dsh-system-prompt`)。这是拒绝客户端名称表的代价;想要更好名称的生产者应当记录更好的来源。

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/implemented/feature/2026-08-04-web-latency-throughput-metrics.md
2026-08-04-web-latency-throughput-metrics.md: 4d7627a9a38127259f1ba07121cea7282f794f2e
2026-08-04-web-latency-throughput-metrics.zh.md: 09e59242b799a3f4d03b9e0259f1a5a0d85f6a53
2026-08-04-web-latency-throughput-metrics.zh.md: f0089cb45775f2dc9701a7256119f04e4709dd76

View File

@@ -6,11 +6,11 @@ Status: implemented
## 问题
Web 聊天已经记录了逐步骤的 LLM 计时(`stepStartTime`/`firstTokenTime`/`completedTime`)和逐步骤 usage,trajectory 视图也按步骤展示它们,但聊天界面既回答不了「这一轮响应有多快」,也回答不了「这个会话跑得有多快」:assistant 页脚只显示轮次实际耗时,统计行也只折算墙钟时间总量。
Web 聊天已经记录了逐步骤的 LLM(大语言模型)计时(`stepStartTime`/`firstTokenTime`/`completedTime`)和逐步骤 usage,trajectory 视图也按步骤展示它们,但聊天界面既回答不了「这一轮响应有多快」,也回答不了「这个会话跑得有多快」:assistant 页脚只显示轮次实际耗时,统计行也只折算墙钟时间总量。
## 决策
包内折算 `ui-conversation` 的 `chat/turn-metrics.ts` 是从 assistant 节点推导延迟/吞吐读数的唯一位置。`assistantStepReading` 把一个节点转成一次步骤读数:TTFT(首 token 延迟)需要 `stepStartTime` 与 `firstTokenTime` 同时存在,解码时长需要 `firstTokenTime`,负时长收敛为零,输出 token 数只在不可信的 `usage` 值有限且非负时才采纳。`deriveTurnMetrics` 按轮次折算读数:编号最小的步骤拥有该轮次的 TTFT 槽位,吞吐用「同时携带两者的那些步骤」的输出 token 总和除以解码时长总和,因此缺采样的步骤直接退出而不是让比值失真;两个数字都没有的轮次不产生条目。
`ui-conversation` 包内的折算逻辑 `chat/turn-metrics.ts` 是从 assistant 节点推导延迟/吞吐读数的唯一位置。`assistantStepReading` 把一个节点转成一次步骤读数:TTFT(首 token 延迟)需要 `stepStartTime` 与 `firstTokenTime` 同时存在,解码时长需要 `firstTokenTime`,负时长钳制为零,输出 token 数只在不可信的 `usage` 值有限且非负时才采纳。`deriveTurnMetrics` 按轮次折算读数:编号最小的步骤拥有该轮次的 TTFT 槽位,吞吐用「同时携带两者的那些步骤」的输出 token 总和除以解码时长总和,因此缺采样的步骤直接退出而不是让比值失真;两个数字都没有的轮次不产生条目。
assistant 页脚把读数追加到既有 hover 显示的时间附属元素中、`用时` 之后,形如 `首 token {s}秒 · {tps} tok/s`,未记录的数字各自省略。ChatView 仅在该轮次的 `turnTimings` 条目带有 `endTime` 时才显示读数:已加载窗口是日志的连续后缀,因此窗口内已结算的轮次必然带着它的全部步骤,首步 TTFT 是真实值而非窗口截断的产物。`formatLatencySeconds` 不带单位,各语言模板各自拥有秒后缀(`TTFT {seconds}s`/`首 token {seconds}秒`)。
@@ -18,7 +18,7 @@ assistant 页脚把读数追加到既有 hover 显示的时间附属元素中、
## 考虑过的替代方案
**持久的会话投影(token-meter 形态)。** 在 host 侧用 `ProjectionDefinition` 折算步骤计时可以跨越压缩与窗口分页、覆盖整个日志。是暂缓而非否决:投影状态必须保持 O(1)(只能均值,不能分位数),它需要 host 改动加 schema,而聊天统计行的耗时事实本就被记录为窗口作用域——新分组沿用该作用域。后续 PR 可以在不挪动这些读数的情况下补上持久投影。
**持久的会话投影(token-meter 形态)。** 在 host 侧用 `ProjectionDefinition` 折算步骤计时可以跨越压缩(compaction)与窗口分页、覆盖整个日志。是暂缓而非否决:投影状态必须保持 O(1)(只能均值,不能分位数),它需要 host 改动加 schema,而聊天统计行的耗时事实本就被记录为窗口作用域——新分组沿用该作用域。后续 PR(Pull Request)可以在不挪动这些读数的情况下补上持久投影。
**逐步骤页脚附属元素。** 让每条 assistant 消息显示自己的 TTFT,会给轮次中段的叙述节点挂上附属元素,而页脚设计刻意让它们保持无 chrome;trajectory 视图已经暴露逐步骤计时细节。
@@ -28,4 +28,4 @@ assistant 页脚把读数追加到既有 hover 显示的时间附属元素中、
窗口内已结算轮次的页脚在 hover 时于实际耗时之后显示 `首 token`/`tok/s`,统计行在墙钟时间旁以本地化标签显示窗口平均延迟与吞吐,全程不新增会话事件、不改 host。指标以省略的方式退化:没有计时或 usage 采样的提供方或步骤只是丢掉对应数字,而不会渲染成零。已加载窗口之外的更早历史仍不计入,已记录在包 README 的统计行限制中。
两个读数都以实测墙钟时间作分母,因此都不可复现:同一个回放场景在本机连续两次跑出 69 与 70 tok/s,而一段 3 毫秒的回放流会读成 26333 tok/s。因此 Web aria golden 在既有的 `{{duration}}` 之外,把吞吐归一化为 `{{throughput}}`;页脚的装饰性分隔符也补上了两侧空格——没有它们,这些读数会连成一整串无障碍文本(`Ran for 13sTTFT 0.2s12 tok/s`),既让屏幕阅读器失去读数之间的边界,也让 `{{duration}}` 失去它赖以匹配的词边界。
两个读数都来自实测墙钟时间,因此都不可复现:TTFT 是 `firstTokenTime - stepStartTime` 的时间差,吞吐则以 `completedTime - firstTokenTime` 的解码墙钟时长为分母。同一个回放场景在本机连续两次跑出 69 与 70 tok/s,而一段 3 毫秒的回放流会读成 26333 tok/s。因此 Web aria golden 在既有的 `{{duration}}` 之外,把吞吐归一化为 `{{throughput}}`;页脚的装饰性分隔符也补上了两侧空格——没有它们,这些读数会连成一整串无障碍文本(`Ran for 13sTTFT 0.2s12 tok/s`),既让屏幕阅读器失去读数之间的边界,也让 `{{duration}}` 失去它赖以匹配的词边界。

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/implemented/feature/2026-08-04-web-slash-command-fuzzy-discovery.md
2026-08-04-web-slash-command-fuzzy-discovery.md: 8d7fe88f8d19a6edc7b51e63578c468df085c238
2026-08-04-web-slash-command-fuzzy-discovery.zh.md: d3efdc23351a1b50853ee76fb731aee046004750
2026-08-04-web-slash-command-fuzzy-discovery.zh.md: a96f9c984e32dd777950b9f9d8594b3a9c8b7c17

View File

@@ -4,17 +4,17 @@ Status: implemented
[English](2026-08-04-web-slash-command-fuzzy-discovery.md) | 中文
## Problem
## 问题
Web 命令菜单要求按命令名前缀匹配,因此用户只记得关键字母却不记得其准确位置时,就无法发现命令。扩大菜单的匹配范围可使命令更易发现,但命令执行仍必须保持精确匹配和确定性:近似输入行绝不能执行相近命令。
## Decision
## 决策
`/` 命令 source 将键入的查询作为不区分大小写的有序子序列,与命令名进行模糊匹配。精确前缀构成排名最高的一类匹配。在每类匹配中,对齐分数越高越优先:分隔符边界和相邻字符会提高分数,前导字符和间隔会降低分数;分数相同则保持 host 目录和 client contribution 的顺序。位置过滤仍会在排名前从行内菜单中移除接收参数的命令。
评分器对每个候选项使用动态规划,时间复杂度为 `O(query length × name length)`,空间复杂度为 `O(name length)`。候选项评分只在客户端进行且只检查命令名;命令描述不影响匹配。菜单选择仍派发所选的精确名称,而 space 与 Enter 裁决继续要求命令 token 精确匹配。
评分器对每个候选项使用动态规划,时间复杂度为 `O(query length × name length)`,空间复杂度为 `O(name length)`。候选项评分只在客户端进行且只检查命令名;命令描述不影响匹配。菜单选择仍派发所选的精确名称,而空格键和 Enter 键的判定逻辑仍要求命令 token 精确匹配。
## Alternatives considered
## 考虑过的替代方案
**保留仅前缀匹配。** 否决,因为本功能要解决的用户无法准确回忆前缀的问题依然存在:`/cpt` 无法发现 `/compact`。
@@ -22,6 +22,6 @@ Web 命令菜单要求按命令名前缀匹配,因此用户只记得关键字
**使用通用模糊搜索依赖。** 否决,因为该界面只需对小型命令目录使用一种受限的子序列规则;可配置搜索索引会增加 bundle 体积,并引入产品未使用的排名行为。
## Consequences
## 后果
用户可以凭按顺序记得的字母发现命令;只要目录相同,排名就保持稳定。评分刻意采用启发式规则:与分隔符对齐的匹配可能排在原始跨度更短的匹配之前。包(package)测试固定各项排名因素以及同分时的稳定顺序,组装后的 Web 回放快照固定 `/cpt` 解析为 `/compact` 的行为。精确执行语义保持不变。
用户可以凭按顺序记得的字母发现命令;只要目录相同,排名就保持稳定。评分刻意采用启发式规则:与分隔符对齐的匹配可能排在原始跨度更短的匹配之前。包测试固定各项排名因素以及同分时的稳定顺序,组装后的 Web 回放快照固定 `/cpt` 解析为 `/compact` 的行为。精确执行语义保持不变。

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/implemented/feature/2026-08-05-composer-context-meter-breakdown.md
2026-08-05-composer-context-meter-breakdown.md: a757bcbc8bc57f4c3a16f663a9c922155b8bb575
2026-08-05-composer-context-meter-breakdown.zh.md: 441fd3013f2db721527838955b5ce227865615f1
2026-08-05-composer-context-meter-breakdown.zh.md: a6db86b437dedb848cb9bc9bf3bd1be23f2f355c

View File

@@ -6,7 +6,7 @@ Status: implemented
## 问题
Web 聊天的统计行把上下文占用率作为一个行内数字(`Context N% of X`)挤在计费分组之间。它回答了「有多满」,却回答不了「被什么占满」:没有任何地方展示窗口在系统提示词、工具 schema 与对话之间如何分配,而单行统计行也容纳不下这种明细。可用的数字还分属两套口径——来自 `contextPressure` 的提供方精确计费 prompt 规模,与 token-meter 的固定字符启发式——没有任何既有界面能在不混淆两者的前提下展示组成。
Web 聊天的统计行把上下文占用率作为一个行内数字(`Context N% of X`)挤在计费分组之间。它回答了「有多满」,却回答不了「被什么占满」:没有任何地方展示窗口在系统提示词、工具 schema 与对话之间如何分配,而单行统计行也容纳不下这种明细。可用的数字还分属两套口径——来自 `contextPressure` 的提供方精确计费的提示词规模,与 token-meter 的固定字符启发式——没有任何既有界面能在不混淆两者的前提下展示组成。
## 决定
@@ -14,17 +14,17 @@ Web 聊天的统计行把上下文占用率作为一个行内数字(`Context N
`dsh-session` 导出纯函数 `deriveEventMessage(event)`(此前只能通过 `Session` 方法访问,该方法现在委托给它),使 host 侧 fold 无需 `Session` 实例即可为表层节点计价。
`dsh-token-meter` 把计价启发式抽取到 `src/estimate.ts`、把位置表层折叠抽取到 `src/surface-fold.ts`(两者都与测量服务逐字共享),并注册第三个会话投影 `contextBreakdown`,携带 `systemTokens` / `toolsTokens` / `messageTokens`。envelope 数字在每条 `request/header` 上经 `canonicalHeader` 按后者胜重新计价;消息数字在逐节点 `{seq, tokens}` 列表上重放 `foldSurfaceTokens`,因此它在每个事件边界上按构造等于 `measure().surfaceTokens`,压缩会像缩小下一个请求那样缩小它。这份共享折叠是全函数且总是新建数组——返回下一个表层而不是原地改写——从而保留了服务侧「先校验再提交」的重放事务:抛出时重放游标不前进,同一条畸形事件在重试时报同样的错。折叠表层中不存在的替换范围会直接抛出:已提交日志在追加时就经过表层校验,无法解析的范围是日志损坏,而不是可跳过的事件。
`dsh-token-meter` 把计价启发式抽取到 `src/estimate.ts`、把位置表层折叠抽取到 `src/surface-fold.ts`(两者都与测量服务逐字共享),并注册第三个会话投影 `contextBreakdown`,携带 `systemTokens` / `toolsTokens` / `messageTokens`。envelope 数字在每条 `request/header` 上经 `canonicalHeader` 按后者胜重新计价;消息数字在逐节点 `{seq, tokens}` 列表上重放 `foldSurfaceTokens`,因此它在每个事件边界上按构造等于 `measure().surfaceTokens`,压缩(compaction)会像缩小下一个请求那样缩小它。这份共享折叠是全函数且总是新建数组——返回下一个表层而不是原地改写——从而保留了服务侧「先校验再提交」的重放事务:抛出时重放游标不前进,同一条畸形事件在重试时报同样的错。折叠表层中不存在的替换范围会直接抛出:已提交日志在追加时就经过表层校验,无法解析的范围是日志损坏,而不是可跳过的事件。
`ui-conversation` 把上下文占用率从统计行移走(一个事实一个家),放到 composer 尾部的 `ContextMeter`:模型座位之后的一枚 14px 占用圆环,由 `contextPressure` 供数,点击弹出的面板把提供方精确的百分比与 `~已用 / 容量` 标题,与 4px 分色分段进度条及带 `~` 前缀的组成明细行并列。两套口径刻意永不对账——启发式占比只用于切分进度条的彩色分段与明细行,且每个启发式数字都标 `~`,因为固定的「4 字符≈1 token」启发式会系统性低估 CJK 文本与代码。(本记录落地时,圆环、标题与进度条总长取的是提供方精确值;它们现在改读锚定在提供方读数上的 `projectedTokens`,因为裸样本看不见压缩——见[仪表对压缩的失明](../bug-fix/2026-08-05-context-meter-blind-to-compaction.md)。)标题是一整句本地化文案(`context.aria`,与圆环的无障碍名共用),在 `{percent}` 槽位处切开渲染,于是读数的位置由各语言自己决定——英文在前、中文在后——同时读数保留自己的字重;宽度算出为零的分段直接不渲染,否则 `.segment` 的 min-width 会在 0% 占用时画出一段填充色。
`ui-conversation` 把上下文占用率从统计行移走(一个事实一个家),放到 composer 尾部的 `ContextMeter`:模型座位之后的一枚 14px 占用圆环,由 `contextPressure` 供数,点击弹出的面板把提供方精确的百分比与 `~已用 / 容量` 标题与 4px 分色分段进度条及带 `~` 前缀的组成明细行并列。两套口径刻意永不对账——启发式数字只决定进度条各彩色分段之间的相对比例,并原样显示在明细行中;每个数字都标有 `~`,因为固定的「4 字符≈1 token」启发式会系统性低估 CJK 文本与代码。(本记录落地时,圆环、标题与进度条总长取的是提供方精确值;它们现在改读锚定在提供方读数上的 `projectedTokens`,因为裸样本看不见压缩——见[仪表对压缩的失明](../bug-fix/2026-08-05-context-meter-blind-to-compaction.md)。)标题是一整句本地化文案(`context.aria`,与圆环的无障碍名共用),在 `{percent}` 槽位处切开渲染,于是读数的位置由各语言自己决定——英文在前、中文在后——同时读数保留自身独立的强调样式;宽度算出为零的分段直接不渲染,否则 `.segment` 的 min-width 会在 0% 占用时画出一段填充色。
## 备选方案
**在客户端从已加载窗口推导组成。** 窗口是日志的连续后缀:携带系统提示词与工具 schema 的 `request/header` 事件可能在窗口之外,翻页还会让数字悄悄变化。只有持久的 host 侧投影能在翻页与压缩后幸存,这正是数据以第三个投影而非聊天窗口 fold 的形式过线的原因。
**把启发式明细行按比例缩放到与 `pressureTokens` 相加一致。** 强行对账是在捏造精度:压力滞后一个请求,还包含估算器从不建模的提供方封装开销,会让明细行在组成毫无变化时也跟着变动。最终选择以显式 `~` 展示估算器的真实口径。
**把启发式明细行按比例缩放,使其总和等于 `pressureTokens`。** 强行对账是在捏造精度:压力滞后一个请求,还包含估算器从不建模的提供方封装开销,会让明细行在组成毫无变化时也跟着变动。最终选择以显式 `~` 展示估算器的真实口径。
**更细的类别(rules、skills、MCP 工具,如 Claude Code 的 `/context`)。** 在这里不可分:harness 在请求标头存在之前就把这些贡献折入系统文本与工具列表,因此三个类别是诚实的分辨率。
**更细的类别(rules、skill(技能)、MCP 工具,如 Claude Code 的 `/context`)。** 在这里不可分:harness 在请求标头存在之前就把这些贡献折入系统文本与工具列表,因此三个类别是诚实的分辨率。
## 后果

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/implemented/feature/2026-08-05-context-form-vocabulary.md
2026-08-05-context-form-vocabulary.md: 618c11b925208d09a62d10101656e3358e327ddb
2026-08-05-context-form-vocabulary.zh.md: 66278b261f23601461c96f08e87ea58766b2bfc2
2026-08-05-context-form-vocabulary.zh.md: b68672cd3338a11ea7b924198ce81380f99296fa

View File

@@ -1,41 +1,41 @@
# Agent Note:由生产方声明的上下文形态
# Agent Note: 由生产方声明的上下文形态
Status: implemented
[English](2026-08-05-context-form-vocabulary.md) | 中文
## Problem
## 问题
每一条已记录的非用户 `user/message` 都通过同一个内容区渲染:把整条消息序列化成内联 JSON。读者展开一行,看到的是 `{ "content": [ { "type": "text", "text": "…\n\n…" } ], "source": { … } }`——转义把唯一值得读的东西(面向模型的散文)压成了一行,而来源信息又和它挤在同一坨里。
在标题栏写出生产者([来源与 steer 标识决策](2026-08-04-web-context-source-and-steer-marks.md))解决了「这是谁加的」。它解决不了「加进来的是什么东西」,因为日志里根本没有这句话。注入上下文不是一种形状:对账后的 `AGENTS.md`、可用 skill 的目录、运行时策略快照、子 agent 的汇报,彼此之间的差别不亚于终端卡片与 diff 卡片,然而这四者呈现出来是同一堵转义 JSON 的墙。
在标题栏写出生产者([来源与 steer 标识决策](2026-08-04-web-context-source-and-steer-marks.md))解决了「这是谁加的」。它解决不了「加进来的是什么东西」,因为日志里根本没有这句话。注入上下文不是一种形状:对账后的 `AGENTS.md`、可用 skill(技能)的目录、运行时策略快照、subagent 的汇报,彼此之间的差别不亚于终端卡片与 diff 卡片,然而这四者呈现出来是同一堵转义 JSON 的墙。
工具面早就解决过同一个形状问题。`ToolCallView` 只有三种卡片,而不是每个工具一种,由工具自己声明本次调用属于哪一种。上下文没有对应物:既没有形状词汇表,生产方也无从声明自己发出的是哪一种。
## Decision
## 决策
`MessageSource` 新增一个可选、由生产方声明的 `form: ContextForm`——一份关于信息**形状**的小型 tagged 词汇表,与 `kind` 相互独立:
- `kind` 回答**由谁产生**,保持纯粹的溯源语义。
- `form` 回答**这是何种形态的信息**。多个生产方可以共用一种形态,一个生产方在一次会话中也可以发出多种。
该词汇表是语义的,绝不涉及视觉。取值只陈述「内容是某个文件的指令」或「是一份可用项目录」;颜色、图标、排序、默认折叠状态归消费方管,不得进入这个联合类型。它随生产方补齐各自形态所需的结构化字段而逐个增长。本次声明两个:
该词汇表是语义的,绝不涉及视觉。取值只陈述「内容是某个文件的指令」或「是一份可用项目录」;颜色、图标、排序、默认折叠状态归消费方管,不得进入这个联合类型。它随生产方补齐各自形态所需的结构化字段而逐个增长。当前词汇表声明六种形态:
**`instructions`**——从工作区文件中读出的指令。`workspace-context` 在启动基线与后续增量上都声明它;其既有的 `changes[]` 已经携带了呈现所需的路径、动作与 digest,因此没有新增字段。内容区在正文之上列出对账过的文件,并原样保留 `<system-reminder>` 包装:那层包装本就是模型读到的一部分,隐藏它会歪曲这次请求。
**`catalog`**——本会话可用项的目录,随变化重新发布。`dsh-tool-skill` 从共享的 `plugin` kind 迁到自有的 `skill-catalog` 来源,携带 `entries`(本次发布的 `name`/`description` 对)与替换目录上的 `update`,后者由内容区渲染成替换提示。内容区直接列出这些条目,不再从散文里反解 `<available_skills>` 块。
**`catalog`**——本会话可用项的目录,随变化重新发布。`dsh-tool-skill` 从共享的 `plugin` kind 迁到自有的 `skill-catalog` 来源,携带 `entries`(本次发布的 `name`/`description` 对)以及替换时的 `update`,后者由内容区渲染成替换提示。内容区直接列出这些条目,不再从散文里反解 `<available_skills>` 块。
条目记录的是**未转义**的发布事实。伪 XML 转义属于 `<available_skills>` 这层为模型而设的框架,因此只在渲染该框架时施加、从不存储;否则消费方要正确展示含 `<` 的描述就得知道框架的编码方式,本决策刚移除的框架知识会换一种形式泄漏回来。`escapeText` 确定且单射,故对未转义条目取 digest 与此前完全等价,重新发布语义不变,面向模型的文本逐字节不变。
这次迁移同时挪动了目录的**身份**:重新发布用的 digest 现在覆盖持久条目而非渲染文本,于是面向模型的包装再也无法左右是否需要重新发布,那段从已记录消息里切出条目的文本切分逻辑也随之删除。若恢复的会话中最新目录早于本次改动,会重新发布一次——发布前阶段的姿态允许这样做。有一种情形不会自愈:当那份旧格式目录是唯一的一份、且当前视图没有任何 skill 时,插件看不到已发布目录,也就不会发出 tombstone,模型手里会留着一份无人替换的过期目录。发布前阶段的姿态(「后端拒绝旧的磁盘格式」)允许这一点;此处如实记录,而不是只写乐观路径。
这次迁移同时挪动了目录的**身份**:重新发布用的 digest 现在覆盖持久条目而非渲染文本,于是面向模型的包装再也无法左右是否需要重新发布,那段从已记录消息里切出条目的文本切分逻辑也随之删除。若恢复的会话中最新目录早于本次改动,会重新发布一次——发布前阶段的姿态允许这样做。有一种情形不会自愈:当那份旧格式目录是唯一的一份、且当前视图没有任何 skill 时,插件看不到已发布目录,也就不会发出 tombstone,模型手里会留着一份无人替换的陈旧目录。发布前阶段的姿态(「后端拒绝旧的磁盘格式」)允许这一点;此处如实记录,而不是只写乐观路径。
**`snapshot`**——会被同一生产方后续快照取代的当前状态。运行时快照、`time-context`、`tmux-context` 声明它。`renderContextSections()` 暴露出装配时的具名贡献——`renderContextSnapshot()` 本来就是把它们拼给模型的——因此内容区能把每一段归属到产生它的子系统,而不必去切分已经拼好的散文。两个单贡献生产方各记录一段。运行时快照的「已清空」标记没有任何贡献可归属,因此不声明形态。
**`notice`**——刚刚发生了什么的一次性说明。`tool-tasks`、`tool-goal` 收尾、`plan-mode` 切换与 `repeat-tool-guard` 提醒都带 `summary` 声明它,而该摘要出现在**折叠态**行上:notice 的全部意义就是不展开也能读完。摘要在其输入是调用方文本时自行封顶(任务的 label 与状态 detail 本身没有长度约束)。Goal 状态变更仍是由领域层持有的 `goal/change` 事件,而非模型上下文,因此不声明 form。
**`relay`**——另一个 agent 发给本 agent 的消息。两个子 agent 定向来源都声明它;发送方以来源已记录的不透明会话 id 呈现,因为本客户端无法把它解析成标题。
**`relay`**——另一个 agent(智能体)发给本 agent 的消息。两个 subagent 定向来源都声明它;发送方以来源已记录的不透明会话 id 呈现,因为本客户端无法把它解析成标题。
**`recall`**——从另一个会话日志搬来的材料。`session-reference` 声明它,且不需要新增字段:其 references 已经记录了标题、保留与省略条数、截断标记,内容区把这些放在最前面——召回上下文在进入时是有界的,隐藏省略条数的卡片会夸大模型实际收到的内容。
**`recall`**——从另一个会话日志搬来的材料。`session-reference` 声明它,且不需要新增字段:其 references 已经记录了标签、保留与省略条数、截断标记,内容区把这些放在最前面——召回上下文在进入时是有界的,隐藏省略条数的卡片会夸大模型实际收到的内容。
两个读取器都是**全有或全无**:一条不可读的条目即判定整条记录不可用,而不是把它丢掉——会替换掉面向模型文本的内容区,不得给出自信但残缺的「模型读到了什么」。行上的形态标记报告的是实际渲染出的形态,而非声明的形态。
@@ -45,9 +45,9 @@ Status: implemented
## 为什么不做 presenter 注册表
工具接缝把它的词汇表与 `presentCall(args)` 配对,那是每个工具在 host 侧实现的纯函数。上下文刻意不设对应物,因为输入的归属不同:工具的 `args` 由**模型**按面向模型的 schema 生成,翻译步骤无法回避;而上下文的 `source` 由**生产方插件**自己构造,不受任何外部约束,完全可以直接记录呈现所需的事实。加一层注册表买到的是一次没人需要的翻译,代价却是一个 host 计算点、每条上下文消息一个 wire 字段、以及每个生产方包都要出浏览器 bundle(客户端纯度门禁禁止 host 包贡献组件)。
工具 seam 把它的词汇表与 `presentCall(args)` 配对,那是每个工具在 host 侧实现的纯函数。上下文刻意不设对应物,因为输入的归属不同:工具的 `args` 由**模型**按面向模型的 schema 生成,翻译步骤无法回避;而上下文的 `source` 由**生产方插件**自己构造,不受任何外部约束,完全可以直接记录呈现所需的事实。加一层注册表买到的是一次没人需要的翻译,代价却是一个 host 计算点、每条上下文消息一个 wire 字段、以及每个生产方包都要出浏览器 bundle(客户端纯度门禁禁止 host 包贡献组件)。
## Alternatives considered
## 考虑过的替代方案
**在客户端把来源 kind 映射到渲染器。** 写起来最省,也不用改格式,但它把生产方知识放回了客户端:此后每新增一个 kind 都要客户端发版才能渲染成 opaque 以外的东西,而外部日志根本无法分类。它还会重新引入[来源与 steer 标识决策](2026-08-04-web-context-source-and-steer-marks.md)刚为名称去掉的那种耦合。
@@ -57,17 +57,17 @@ Status: implemented
**把 instructions 渲染成 Markdown。** 正文本来就是 Markdown 文件,渲染出来更好读。但文本同时携带 `<system-reminder>` 包装,Markdown 渲染器会把它当原始 HTML 丢弃,于是 Markdown 内容区会悄悄隐藏模型读到的一部分。推迟到生产方按文件结构化记录内容之后再做。
## Testing
## 测试
- `packages/client/runtime` 钉住形态投影,包括必须降级为 opaque 的未知值、空值、类型不符与缺失。
- `packages/client/ui-conversation` 逐个钉住内容区:opaque 的换行留存与来源字段、instructions 的文件列表与原样包装、catalog 的条目列表,以及条目不可用的 catalog 回落到 opaque。
- `packages/skill/tool-skill` 钉住首次发布与替换时的新来源、由持久条目驱动的重新发布行为,以及畸形持久目录不打断步骤观察。
- 无密钥的组装 Web seeded-history 场景在 Chromium 中展开一条真实的 `instructions` 上下文,断言其文件列表、原样包装与未改动的展开项几何。`catalog` 没有组装态覆盖:隔离脚手架不发布任何 skill,因此没有目录能进入浏览器场景。
## Consequences
## 后果
- 读者不展开就能知道加进来的是什么,展开之后读到的也不再是转义 JSON。
- 持久 `MessageSource` 现在在溯源之外还承载一个语义分类。这条边界是承重的:只放事实与形状,绝不放呈现。想要更好卡片的生产方应当记录更好的事实。
- 目录身份不再依赖面向模型的散文,删掉了那条可能把「改了措辞」误判为「改了内容」的文本切分路径。
- 除两个 hook 桥接外,每个已发布的生产方现在都声明了形态。桥接按设计保持 opaque:其内容是外部程序打印出来的任意文本,无法承诺任何形状。未知 kind 与不可读记录同样落在这里。
- 除两个钩子桥接外,每个已发布的生产方现在都声明了形态。桥接按设计保持 opaque:其内容是外部程序打印出来的任意文本,无法承诺任何形状。未知 kind 与不可读记录同样落在这里。
- `ContextFormed` 按 `form` 判别,因此生产方无法在缺少该形态所需事实的情况下声明它——没有 summary 的 `notice`、没有 sections 的 `snapshot`,都会编译失败。

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/implemented/feature/2026-08-05-feedback-gated-session-telemetry.md
2026-08-05-feedback-gated-session-telemetry.md: 7d923a7e4cf61e8d1119187564b87e4cbb2065b7
2026-08-05-feedback-gated-session-telemetry.zh.md: 2862162c0c36e5194846c7e1c7bbc24230ce90aa
2026-08-05-feedback-gated-session-telemetry.zh.md: e2fa6f9561de6e7295cbf860527c7d7915fec0bf

View File

@@ -1,4 +1,4 @@
# Agent Note:反馈门控的会话遥测
# Agent Note: 反馈门控的会话遥测
Status: implemented

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/implemented/feature/2026-08-05-web-preview-product-badge.md
2026-08-05-web-preview-product-badge.md: c20dedf8caa497d17a577cf46261c6a24c09a1ce
2026-08-05-web-preview-product-badge.zh.md: c428dabf2a9a2ece2f90b0c3a527835a494c7cb7
2026-08-05-web-preview-product-badge.zh.md: 415a7cb3eb02030ef5481f4eba9110b56547ce64

View File

@@ -1,6 +1,6 @@
# Agent Note:Web 预览版产品徽标
# Agent Note: Web 预览版产品徽标
状态:已实现
Status: implemented
[English](2026-08-05-web-preview-product-badge.md) | 中文

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/implemented/feature/2026-08-06-session-completed-done-dot.md
2026-08-06-session-completed-done-dot.md: bd6911ce137f1272090c86c029710c9f4054ee6d
2026-08-06-session-completed-done-dot.zh.md: 9ec2199a29d1307c3ebd0238e84d5f90d36fe21c
2026-08-06-session-completed-done-dot.zh.md: 41da7d126a29a5f9341ca8b87da343fdbff7601c

View File

@@ -6,20 +6,20 @@ Status: implemented
## Problem
操作者派发任务后切换到其他会话,原会话完成时没有任何信号。运行指示停止后,该行与普通空闲会话看起来完全一样,操作者只能反复查看列表或很晚才发现工作已完成。等待交互的琥珀点只覆盖需要操作者输入的会话,不覆盖"只是干完了活"的会话。
操作者派发任务后切换到其他会话,原会话完成时没有任何信号。运行指示停止后,该行与普通空闲会话看起来完全一样,操作者只能反复查看列表或很晚才发现工作已完成。等待交互的琥珀点只覆盖需要操作者输入的会话,不覆盖「只是干完了活」的会话。
## Decision
`SessionManager` 持有客户端侧的完成提醒集合,与待交互位并列:非当前会话发生 running→idle 边沿时点亮其提醒;`select()`/`selectSubagent()` 消费掉提醒;重新开始一轮运行会熄灭提醒并在再次完成时重新点亮;会话被移除时清理提醒。该位经 `SessionListEntry` → `SessionSummary`(可选字段,缺省 = 无提醒)进入工作区浏览区,其会话行与搜索结果行渲染现有的 `StateDot` `done` 状态——运行中仍显示转圈,无提醒的空闲会话不显示任何点——悬停卡片将该提醒标注为"已完成 / Completed"。
`SessionManager` 持有客户端侧的完成提醒集合,与待交互位并列:非当前会话发生 running→idle 边沿时点亮其提醒;`select()`/`selectSubagent()` 消费掉提醒;重新开始一轮运行会熄灭提醒并在再次完成时重新点亮;会话被移除时清理提醒。该位经 `SessionListEntry` → `SessionSummary`(可选字段,缺省 = 无提醒)进入工作区浏览区,其会话行与搜索结果行渲染现有的 `StateDot` `done` 状态——运行中仍显示转圈,无提醒的空闲会话不显示任何点——悬停卡片将该提醒标注为「已完成 / Completed」。
提醒仅存在于内存中且按浏览器实例隔离。它跨连接代存活——传输抖动不会使"你还没回来看"失效——但页面刷新后重置。
提醒仅存在于内存中且按浏览器实例隔离。它跨连接代存活——传输抖动不会使「你还没回来看」失效——但页面刷新后重置。
## Consequences
侧边栏行状态成为三个互斥信号:绿 = 已完成且未查看,琥珀 = 等待操作者输入,蓝 = 运行中。无 wire、磁盘或配置格式变更:`SessionSummary.completed` 为可选字段,现有消费者与测试 fixture 保持有效,只有工作区浏览区读取它。完成边沿在每次列表变更与拉取时即时检测(仅在建快照时检测会把连续两个状态帧折叠为一次观察,从而漏掉完成事件)。
侧边栏行状态成为三个互斥信号:绿 = 已完成且未查看,琥珀 = 等待操作者输入,蓝 = 运行中。协议格式(wire format)、磁盘格式或配置格式均无变更:`SessionSummary.completed` 为可选字段,现有消费方与 fixture(测试前置数据) 保持有效,只有工作区浏览区读取它。完成边沿在每次列表变更与拉取时即时检测(仅在建快照时检测会把连续两个状态帧折叠为一次观察,从而漏掉完成事件)。
## Alternatives considered
- **组件本地 UI 状态。** 已拒绝:侧边栏折叠时会卸载,且多个界面(分组树、单列表、搜索)需要同一状态位;manager 本就持有运行状态迁移与选中状态,manager 持有的集合是所有界面都能投影的唯一事实源。
- **组件本地 UI 状态。** 已拒绝:侧边栏折叠时会卸载,且多个界面(分组树、扁平列表、搜索)需要同一状态位;manager 本就持有运行状态迁移与选中状态,manager 持有的集合是所有界面都能投影的唯一真源。
- **仅从状态帧做事件驱动点亮。** 已拒绝:列表拉取本身也可能携带 running→idle 迁移(刷新在途时会话已完成);提醒需对每次变更与拉取做对账。
- **持久化提醒。** 已拒绝:提醒的含义是"此浏览器里你还没查看该会话";刷新会恢复选中状态且用户正看着列表,持久化位只会过期。
- **持久化提醒。** 已拒绝:提醒的含义是「此浏览器里你还没查看该会话」;刷新会恢复选中状态且用户正看着列表,持久化位只会变得陈旧。

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/implemented/feature/2026-08-07-default-model-follows-the-picker.md
2026-08-07-default-model-follows-the-picker.md: ed7e7a424d2cacadea890506fd9150ffdf7a993c
2026-08-07-default-model-follows-the-picker.zh.md: 52308518f06a140a9f79440e48229356bc61dca5
2026-08-07-default-model-follows-the-picker.zh.md: 523c6f917dedf21c726ce5631226ba545126c757

View File

@@ -8,7 +8,7 @@ Status: implemented
会话模型选择器与部署默认值是同一项偏好的两个层次。如果选择器只影响其所在会话,下一个空白会话可能选择不同模型,用户却没有途径使默认值与选择器一致。如果默认值位于 Host 网关内部,直接创建 Agent 的前门只有依赖 Host 或复制状态才能共享它。
推理强度使持久化形态成为契约的一部分:不含强度的模型选择必须清除已存强度,否则下一个 Agent 可能会采用所选模型不接受的强度。
推理强度使持久化形态成为约定的一部分:不含强度的模型选择必须清除已存强度,否则下一个 Agent 可能会采用所选模型不接受的强度。
## 决定
@@ -38,7 +38,7 @@ Status: implemented
## 考虑过的替代方案
| 替代方案 | 契约不匹配之处 |
| 替代方案 | 约定不匹配之处 |
|---|---|
| 已存提供方不可用时回落到组合条目 | 产品会静默切离用户选择。 |
| 根据目录成员关系校验已存选择 | 目录仅供参考,可能省略仍可请求的模型。 |

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/implemented/feature/2026-08-08-user-explicit-skill-invocation.md
2026-08-08-user-explicit-skill-invocation.md: d925938279923282170dc99934f4fa44d8ecf2b4
2026-08-08-user-explicit-skill-invocation.zh.md: 64e23be0b42519fb9681adefcd0f05074d3aa35e
2026-08-08-user-explicit-skill-invocation.zh.md: 2d6d7130a1e80e3e9dba2681815668b3510356ba

View File

@@ -12,16 +12,16 @@ Status: implemented
用户显式调用是一次宿主侧的 pre-step 注入,对每一个用户可调用的 skill 和每一种前端一致:
- `dsh-tool-skill` 注册第二个 `agent/pre-step` 监听器(与其目录监听器并列,也是 `workspace-instructions` 与运行时上下文快照搭乘的同一 seam):它在该步骤已认领的消息中扫描以空白为界的 `/name` token——文本中任意位置均可,与 transcript(文本记录)chip 装饰所用的词边界形状相同——收集按首见去重的名称,逐个经 `ctx.skills.get` 加载,在已加载定义上检查 `isUserInvocable`(产生注入内容的正是这同一次查找),用共享的 `renderSkillContent` 渲染,并把注入追加在该步骤所有其他注入之后:背景在前(工作区规则、运行时策略、目录),模型必须着手处理的材料在最后、最贴近它的回答。注册顺序钉住了这一位置——手势监听器先于目录监听器注册,因此 waterfall 会把携带目录的列表交给它来扩展。
- `dsh-tool-skill` 注册第二个 `agent/pre-step` 监听器(与其目录监听器并列,也是 `workspace-instructions` 与运行时上下文快照搭乘的同一 seam):它在该步骤已认领的消息中扫描以空白为界的 `/name` token——文本中任意位置均可,与 transcript(文本记录)chip 装饰所用的词边界形状相同——收集按首见去重的名称,逐个经 `ctx.skills.get` 加载,在已加载定义上检查 `isUserInvocable`(产生注入内容的正是这同一次查找),用共享的 `renderSkillContent` 渲染,并把注入追加在该步骤所有其他注入之后:背景在前(工作区规则、运行时策略、目录),模型必须着手处理的材料在最后、最贴近它的回答。注册顺序钉住了这一位置——手势监听器先于目录监听器注册,因此 waterfall(瀑布式事件)会把携带目录的列表交给它来扩展。
- 精确性来自封闭集合匹配,与斜杠命令完全一致:`/goal` 对照命令注册表解析,`/name` 对照工作区的用户可调用 skill 目录解析;未命中即保持为普通行文,因此绝不猜测。只扫描 `source.kind === 'user'` 的消息——外部文本无法伪造手势。路径(`/usr/bin`)、分数(`5/8`)与带前缀的 token(`foo/name`)都会破坏该边界。
- 客户端停留在决策 21:菜单 pick 落下字面文本 `/name `,提示词将其原样发出;ui-skill 不实现任何裁决钩子,也没有引用 codec。`skill.list`(现在是该领域唯一的 RPC)提供每一个用户可调用的 skill 并携带 `modelInvocable`,供菜单标出仅限用户的条目。与宿主命令同名的名称解析为命令——裁决在客户端把该行认领走,它尚未成为提示词。
- 注入是一条携带 `skill-invocation` 来源(`{ name, form: 'instructions' }`)的 `user` 角色消息,因此 `user/message` 落账、上下文注入的 transcript 行(以 skill 名称标注)与回放全部免费获得;`renderSkillContent` 位于 `dsh-skill` seam,与 `skill` 工具结果逐字共享,目录的结尾一句会告诉模型遵循注入块而不是重新加载。
- 客户端停留在决策 21:菜单 pick 落下字面文本 `/name `,该文本随提示词原样提交;ui-skill 不实现任何裁决钩子,也没有引用 codec。`skill.list`(现在是该领域唯一的 RPC)提供每一个用户可调用的 skill 并携带 `modelInvocable`,供菜单标出仅限用户的条目。与宿主命令同名的名称解析为命令——客户端会在该行成为提示词之前完成裁决并将其认领。
- 注入是一条携带 `skill-invocation` 来源(`{ name, form: 'instructions' }`)的 `user` 角色消息,因此 `user/message` 落账、上下文注入的 transcript 行(以 skill 名称标注)与回放全部免费获得;`renderSkillContent` 位于 `dsh-skill` seam,由注入和 `skill` 工具结果共用,二者内容逐字相同,目录的结尾一句会告诉模型遵循注入块而不是重新加载。
同类产品调研(Pi、OpenCode、Claude Code、Kimi Code、Codex、DeepSeek-Reasonix——本地检出)一致表明:用户显式触发都是模型零参与的程序化注入;最终形态最接近 Codex 核心侧的 `$name` mention 扫描——它同样让每一种前端免于自行实现识别。
## 考虑过的替代方案
- **`skill.invoke` RPC(宿主注入、客户端认领)**——最先实现,共两轮迭代:先是单条混合消息(用户文本折进正文),后是经 inbox 原语投递的手势提示词加注入两条消息。经真实会话测试后否决:混合消息让用户行文污染了注入;两条消息的形态依赖唤醒顺序的微妙之处(`followup` 在第一个唤醒调用内同步认领整个 next-turn 队列,把之后的消息滞留到下一轮次——已实际复现),而专设 RPC 复制了 `session.prompt` 已提供的路径,还让 TUI/ACP 不得不各自重新实现识别。pre-step seam 把 RPC、认领机制与顺序隐患一并干净移除。
- **`skill.invoke` RPC(宿主注入、客户端认领)**——最先实现,共两轮迭代:先是单条混合消息(用户文本折进正文),后是经 inbox 原语投递的手势提示词加注入两条消息。经真实会话测试后否决:混合消息让用户行文污染了注入;两条消息的形态依赖唤醒顺序的微妙之处(`followup` 在第一个唤醒调用内同步认领整个 next-turn 队列,把之后的消息滞留到下一轮次——已实际复现),而专设 RPC 复制了 `session.prompt` 已提供的路径,还让 TUI/ACP(Agent Client Protocol)不得不各自重新实现识别。pre-step seam 把 RPC、认领机制与顺序隐患一并干净移除。
- **从 RPC 处理器调用 `agent.inject()`**——inject 队列(`next-step`,不唤醒)会在 next-turn 提示词之前被认领,使注入在日志中排到手势之上;而与会唤醒的 `followup` 搭配又会重新引入同样的顺序耦合。pre-step 监听器在步骤组装内部注入,那里的顺序是显式的。
- **宿主 `/skill <name>` 命令**(命令注册表,plan 模式先例)——两 token 的 UX、没有名称补全、仅限用户的 skill 在菜单里仍不可发现;按 cwd 的 skill 目录也与静态命令注册表格格不入。否决。
- **客户端展开**(拉取正文、拼进提示词)——授权沦为可被绕过的客户端善意,日志失去调用语义,而且 Codex 已删除其等价机制(custom prompts)转向核心注入。否决。
@@ -30,7 +30,7 @@ Status: implemented
## 后果
- 决策 21 的纯文本引用如今就是客户端的全部故事:草稿承载纯文本,chip 视觉由 lexicon 派生,发出的文本由宿主边界评判——手动键入的手势、菜单 pick 与 TUI 提示词无从区分,也同等确定。
- 决策 21 的纯文本引用如今就是客户端的全部故事:草稿承载纯文本,chip 视觉由 lexicon 派生,发出的文本由宿主边界评判——手动键入的手势、菜单 pick 与 TUI 提示词无从区分,也同样具有确定性。
- 每一次用户可调用 skill 的调用都无条件付出其完整渲染正文的成本——这是确定性的代价,同类调研表明所有产品都在支付。在句子中间提到一个已知 skill 名称也会加载它;这就是 Codex 的 mention 语义,属于有意接受。
- `skill-invocation` 来源搭乘 `user/message`,因此「模型可见 ⟺ 已记录」在不新增事件类型的情况下继续成立,回放与 UI 读取的是元数据而非文本标记。
- 放弃逐次注入前导语后被接受的残余:no-reload framing 只搭乘目录,而 skill 全部为仅用户的工作区永远不会发布首个目录——注入可能在完全没有 framing 的情况下到达,模型可能多余地调用一次 `skill` 工具(替换目录的空臂携带该句;从未发布的情形没有)。仅为 framing 而发布目录被判定比这一次可恢复的错误更糟。
- 放弃逐次注入前导语后被接受的残余:no-reload framing 只搭乘目录,而 skill 全部为仅用户的工作区永远不会发布首个目录——注入可能在完全没有 framing 的情况下到达,模型可能多余地调用一次 `skill` 工具(替换目录的空分支携带该句;从未发布的情形没有)。仅为 framing 而发布目录被判定比这一次可恢复的错误更糟。