Merge remote-tracking branch 'origin/master' into fix/stats

This commit is contained in:
07akioni
2026-08-12 21:29:00 +08:00
1021 changed files with 3194 additions and 3311 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/bug-fix/2026-07-20-error-cause-chain-diagnostics.md
2026-07-20-error-cause-chain-diagnostics.md: d1944b3b435f2e1c64e1612adf0e61dd8e7401ff
2026-07-20-error-cause-chain-diagnostics.zh.md: 914e983d7ea81eef8bca8fd5aa3791f2f44d4080
2026-07-20-error-cause-chain-diagnostics.zh.md: cd493f99388282189c807353b32fec15f1007f87

View File

@@ -13,10 +13,10 @@ TUI 连接不可达的 DeepSeek 端点时,失败只显示一条 `fetch failed`
## 决策
- `dsh-llm` 导出 `errorChain(value)`:渲染抛出值及其完整 `cause` 链(`outer: inner: …`)与 AggregateError 成员(`msg [m1; m2]`),并容错循环 cause 和恶意强制转换。它只是用于诊断界面的渲染器;路由仍然基于 `HarnessError.code`
- `dsh-llm` 导出 `errorChain(value)`:渲染抛出值及其完整 `cause` 链(`outer: inner: …`)与 AggregateError 成员(`msg [m1; m2]`),并容错循环 cause 和恶意强制转换。它只是用于诊断输出的渲染器;路由仍然基于 `HarnessError.code`
- DeepSeek 适配器把拿到响应之前的传输失败包装成 `LlmError('TRANSPORT')`,写明配置的 `baseURL` 并将原始拒绝值作为 `cause` 串入错误链。被中止的请求变为 `LlmError('ABORTED')`由于轮次信号已处于中止状态agent loop智能体循环仍将该轮次归类为取消而非恢复。
- 每个诊断边界改用 `errorChain` 而非 `error.message`/`String(error)`agent-loop 的持久化 `turn/end` 错误消息(`errorData`、其日志警告、TUI 的 `agent/error` 通知与启动失败行、以及 `dsh-stdio` 的启动失败日志行。实时 `agent/error` 事件与 `SettleReason``unknown` 原样保留抛出值;各诊断 Consumer 自行渲染,而不是由循环把它包装成另一个错误。`dsh-agent-loop``dsh-stdio``dsh-tui` 里各自的 `renderThrown` 副本被删除,统一使用这一个共享渲染器。
- `dsh-stdio` 渲染失败的 `turn/end` reason`[turn failed <code>] <message>``[turn aborted] <reason>``[turn rejected] <reason>``[turn interrupted by a previous process exit]` 以及输出 token 上限通知。通过声明合并扩展出的未知 kind 按普通 turn 结束处理。
- 每个诊断边界改用 `errorChain` 而非 `error.message`/`String(error)`agent-loop 的持久化 `turn/end` 错误消息(`errorData`、其日志警告、TUI 的 `agent/error` 通知与启动失败行、以及 `dsh-stdio` 的启动失败日志行。实时 `agent/error` 事件与 `SettleReason``unknown` 原样保留抛出值;各诊断消费方自行渲染,而不是由循环把它包装成另一个错误。`dsh-agent-loop``dsh-stdio``dsh-tui` 里各自的 `renderThrown` 副本被删除,统一使用这一个共享渲染器。
- `dsh-stdio` 渲染失败的 `turn/end` reason`[turn failed <code>] <message>``[turn aborted] <reason>``[turn rejected] <reason>``[turn interrupted by a previous process exit]` 以及输出 token 上限通知。通过声明合并扩展出的未知 kind 按普通轮次结束处理。
`errorChain``HarnessError` 一样放在 `dsh-llm` 里,理由相同:它是每个消费方都已导入的叶子包,共享不增加新的依赖边。
@@ -24,7 +24,7 @@ TUI 连接不可达的 DeepSeek 端点时,失败只显示一条 `fetch failed`
**在每个错误的构造函数里渲染链(把 cause 写入 `message`)。** 否决:当消费方同时遍历 `cause` 时会双重渲染(适配器修复的第一版产出了 `… fetch failed: bad port: fetch failed: bad port`),并且破坏了想按内层错误路由的消费方所需的结构化链。
**只做一个感知 `cause` 的日志导出器。** 否决:持久化的 `turn/end` reason 和 TUI 通知不是日志行;被掩盖的消息会留在会话日志——轮次内失败的唯一持久记录——以及主要 UI 表面里
**只做一个感知 `cause` 的日志导出器。** 否决:持久化的 `turn/end` reason 和 TUI 通知不是日志行;被掩盖的消息会留在会话日志——轮次内失败的唯一持久记录——以及主要 UI
**逐包升级 `renderThrown`。** 否决:三个包已经各自持有几乎相同的私有副本;分别升级只会固化共享渲染器所要消除的重复。

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/bug-fix/2026-07-24-empty-model-response-is-retryable.md
2026-07-24-empty-model-response-is-retryable.md: 16e4a79ec64a1abb5489690f5e064ccb104ec5bc
2026-07-24-empty-model-response-is-retryable.zh.md: 2573ef66d9ca1e6c0610686d0051c738774af588
2026-07-24-empty-model-response-is-retryable.zh.md: a6bd3ed5a5f98a1f617be984cea1c9b1a847cfb4

View File

@@ -31,6 +31,6 @@ Status: implemented
## 后果
- 一个偶发异常的提供方会消耗一次有界重试,而不是一个没有输出的轮次;一个持续返回空内容的模型则会暴露为用户可据以行动的 `EMPTY_RESPONSE` 轮次失败。
- 一个偶发异常的提供方会消耗一次有界重试,而不是一个没有输出的轮次;一个持续返回空内容的模型则会产生用户可据以行动的 `EMPTY_RESPONSE` 轮次失败。
- 一个确实打算什么都不说的模型(罕见,但在一次工具结果之后有可能出现)会被重试,若始终为空,则该轮次失败。这个取舍是经过审慎权衡后接受的:一条空的 assistant 消息与提供方缺陷无法区分,且对用户毫无价值。
- `empty-response-retry` ACPAgent Client Protocol快照一个人工编写的无密钥场景配有确定性的 1 ms 零抖动重试 overlay`examples/acp-agent/retry.cordis.yml`)钉住了产品可见的行为:持久的 `llm/retry` 事件、被丢弃的尝试不产生任何 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/bug-fix/2026-07-29-human-transcript-append-origin.md
2026-07-29-human-transcript-append-origin.md: fe60ab6e745fdc08d84deb4a6e2e4fe8168a23e8
2026-07-29-human-transcript-append-origin.zh.md: b8fe6f6e4b9b5298f9d07bdf395e33e33b4a200d
2026-07-29-human-transcript-append-origin.zh.md: 02a0d714d509e5057eee616b1721c360f60d110d

View File

@@ -4,50 +4,50 @@ Status: implemented
[English](2026-07-29-human-transcript-append-origin.md) | 中文
## Problem
## 问题
终端与宿主历史网关都把模型可见的 surface 当作人类可读记录(transcript。一次成功的压缩compaction会用一个检查点节点替换一段 surface 范围,因此该替换一落地,终端就丢弃了它所遮蔽的每条消息——那些是用户已经读过的对话——并在此后任何替换到来时重新执行这次破坏性重建。同样的混淆也波及分页:`maxMessages` 统计窗口内的每个 `user/message``assistant/message`,于是仅供模型使用的替换副本占用了一个人类从未填充的页面额度,而切分点还可能落在压缩的仅日志 `compact/summary` 事件与引用它的替换之间。
终端与宿主历史网关都把模型可见的 surface 当作 transcript(文本记录。一次成功的压缩compaction会用一个检查点节点替换一段 surface 范围,因此该替换一落地,终端就丢弃了它所遮蔽的每条消息——那些是用户已经读过的对话——并在此后任何替换到来时重新执行这次破坏性重建。同样的混淆也波及分页:`maxMessages` 统计窗口内的每个 `user/message``assistant/message`,于是仅供模型使用的替换副本占用了一个人类从未填充的页面额度,而切分点还可能落在压缩的仅日志 `compact/summary` 事件与引用它的替换之间。
日志本身没有丢失任何内容。`Session.events` 仍保存着每条原始消息和完整的工具结果surface 只决定接下来发送给模型的内容。缺陷完全在投影层。
## Decision
## 决策
模型投影与人类投影是分开的,而事件属于哪一种由事件自身的标记决定。`dsh-session` 在浏览器安全的 `surface` 模块中导出按两种 `SurfaceOp` 变体划分的谓词 `isAppendSurfaceEvent(event)``isReplacementSurfaceEvent(event)`。追加来源的事件是记录的持久来源,替换副本仅供模型使用。凡是必须准确发送模型所见内容的部分——`deriveMessages`、token 记账、压缩后端、工具配对、注入上下文的存活判断、跨会话引用投影——都继续读取 `session.surface`
模型投影与人类投影是分开的,而事件属于哪一种由事件自身的标记决定。`dsh-session` 在浏览器安全的 `surface` 模块中导出按两种 `SurfaceOp` 变体划分的谓词 `isAppendSurfaceEvent(event)``isReplacementSurfaceEvent(event)`。追加来源的事件是 transcript 的持久来源,替换副本仅供模型使用。凡是必须准确发送模型所见内容的部分——`deriveMessages`、token 记账、压缩后端、工具配对、注入上下文的存活判断、跨会话引用投影——都继续读取 `session.surface`
终端从追加来源的 surface 事件回放记录,并通过 `transcriptToolCallIds` 让被遮蔽步骤的工具卡片保持配对:该函数读取追加来源的 `assistant/message`,而不是 surface 成员关系。已落地的压缩会在其自身日志位置贡献一行暗色 `… earlier context was compacted …`:这行标记报告模型从何处起不再看到那段历史,而不是把它抹掉。带框的检查点载荷从不渲染,且两条路径都按同一个标记对 surface 事件分类,因此实时到达的压缩与恢复后回放同一份日志会产生相同的记录。只有回放会重新推导 `tool/call` 的配对关系:调用事件自身不携带标记,其归属继承自公布它的 `assistant/message`,而实时监听器必然刚刚渲染过后者。
终端从追加来源的 surface 事件回放 transcript,并通过 `transcriptToolCallIds` 让被遮蔽步骤的工具卡片保持配对:该函数读取追加来源的 `assistant/message`,而不是 surface 成员关系。已落地的压缩会在其自身日志位置贡献一行暗色 `… earlier context was compacted …`:这行标记报告模型从何处起不再看到那段历史,而不是把它抹掉。带框的检查点载荷从不渲染,且两条路径都按同一个标记对 surface 事件分类,因此实时到达的压缩与恢复后回放同一份日志会产生相同的 transcript。只有回放会重新推导 `tool/call` 的配对关系:调用事件自身不携带标记,其归属继承自公布它的 `assistant/message`,而实时监听器必然刚刚渲染过后者。
检查点通过压缩接缝自身的约定来识别——`isCompactCheckpointSource`,即 `CompactService` 要求替换用户消息携带的、与后端无关的标记——因此终端依赖的是已声明的词汇,而不是替换的形态。`dsh-session-reference` 已经在用该谓词投影另一个会话的日志;这里只是另一个读者提出同样的问题。其他替换保持静默:被裁剪的 `tool/result` 与重新生成的 `assistant/message` 只是为模型重写一个节点,并不在对话中标出边界。
检查点通过压缩 seam 自身的约定来识别——`isCompactCheckpointSource`,即 `CompactService` 要求替换用户消息携带的、与后端无关的标记——因此终端依赖的是已声明的词汇,而不是替换的形态。`dsh-session-reference` 已经在用该谓词投影另一个会话的日志;这里只是另一个读者提出同样的问题。其他替换保持静默:被裁剪的 `tool/result` 与重新生成的 `assistant/message` 只是为模型重写一个节点,并不在对话中标出边界。
`session.history` 只把追加来源的消息计入 `maxMessages`。每一页仍是一段连续的原始事件区间,因此压缩的 `compact/summary` 事件会与引用它的替换留在同一页。
持久事件、RPC 信封、压缩事务与模型可见的 surface 都没有变化,也不需要迁移。
## Deferred
## 延后事项
浏览器客户端在[Web 记录投影笔记](2026-07-30-web-transcript-log-ordered-projection.md)中单独修复:它按日志顺序投影同一份 append 来源记录并渲染一个标记组件,同时闭合本次变更打开的分页缺口——因为 `session.history` 不再为检查点消耗额度,它永远不会在检查点与检查点引用的来源事件这个整体内切分,于是一页可以携带一个引用了窗口之外 `surfaceOp.start` 的检查点,而浏览器的 surface fold 会拒绝该范围。这个缺口早于本次变更(此前计数就可能越过检查点进入它所遮蔽的范围),但当检查点是最旧的被计数消息时,旧分页规则会把整段被遮蔽的范围放在同一页。
浏览器客户端在[Web transcript 投影笔记](2026-07-30-web-transcript-log-ordered-projection.md)中单独修复:它按日志顺序投影同一份追加来源 transcript 并渲染一个标记组件,同时闭合本次变更打开的分页缺口——因为 `session.history` 不再为检查点消耗额度,它永远不会在检查点与检查点引用的来源事件这个整体内切分,于是一页可以携带一个引用了窗口之外 `surfaceOp.start` 的检查点,而浏览器的 surface fold 会拒绝该范围。这个缺口早于本次变更(此前计数就可能越过检查点进入它所遮蔽的范围),但当检查点是最旧的被计数消息时,旧分页规则会把整段被遮蔽的范围放在同一页。
终端的[已归档实时压缩进度决策](../../archived/feature/2026-07-30-compaction-progress-visibility.md)使用独立标记对中的事件驱动现有的单格指示器。它既不改变本文所负责的完成标记,也不添加规模信息:检查点的 `sourceEventSeqs` 仍可供经另行论证的计数或区间使用。因此,进度显示既不需要修改标记内容,也不以提取 `renderReplacement(event)` 为前置条件。
## Alternatives considered
## 曾考虑的替代方案
**按形态识别检查点(一个替换型 `user/message`)。** 被否决:那读取的是当前生产者的巧合而非已声明的约定,而未来任何用用户消息替换一段范围的生产者都会静默地继承压缩标记。接缝已经发布 `COMPACT_CHECKPOINT_SOURCE`,正是为了让消费方与后端无关地识别检查点。
**按形态识别检查点(一个替换型 `user/message`)。** 被否决:那读取的是当前生产者的巧合而非已声明的约定,而未来任何用用户消息替换一段范围的生产者都会静默地继承压缩标记。seam 已经发布 `COMPACT_CHECKPOINT_SOURCE`,正是为了让消费方与后端无关地识别检查点。
**继续把检查点渲染为注入上下文卡片。** 被否决:带框的检查点是为模型撰写的指令信封,不是人类对话内容。展示它却隐藏它替换掉的历史,正好颠倒了读者的需要。
**持久化第二份展示用记录** 被否决:仅追加的日志已经包含权威源材料,平行记录换不来任何东西,反而增加迁移与一致性工作。
**持久化第二份展示用 transcript** 被否决:仅追加的日志已经包含权威源材料,平行 transcript 换不来任何东西,反而增加迁移与一致性工作。
**用 `compact/*` 括号而不是检查点来推导标记。**记录而言被否决:括号是围绕一次操作的一对时间点标记,而记录需要的是 surface 真正发生变化的位置。括号适合作为进度与耗时的来源,而本次变更并不渲染这些。
**用 `compact/*` 括号而不是检查点来推导标记。** transcript 而言被否决:括号是围绕一次操作的一对时间点标记,而 transcript 需要的是 surface 真正发生变化的位置。括号适合作为进度与耗时的来源,而本次变更并不渲染这些。
**像 `session-query` 为搜索所做的那样重新折叠日志来分类事件(`current``shadowed``log-only`)。** 被否决:折叠回答的是整份日志的问题,而投影问的是逐事件的问题,事件自身的标记已能以常数时间给出答案。
## Consequences
## 后果
压缩不再抹掉终端历史;被压缩多次的会话会按日志顺序显示每次落地压缩对应的一行标记。分页的每一页可以携带比以前更多的原始事件,因为额度只花在人类或模型真正产生的消息上。
`rebuildTranscript` 现在会为整份日志中的每个追加来源事件物化一个组件,并在挂载时、终端配色方案变化时以及每次切换 reasoning 时运行。压缩此前正好为压缩所服务的那些长会话限制了这项工作量,因此这份开销现在随会话长度增长,而不再随 surface 增长。这正是本次修复要做的取舍——保留历史才是目的——但窗口化或复用策略属于第一个真正测到重建变慢的人,而不属于日后某个疑惑工作量为何增长的性能分析者。
`rebuildTranscript` 现在会为整份日志中的每个追加来源事件物化一个组件,并在挂载时、终端配色方案变化时以及每次切换推理(reasoning时运行。压缩此前正好为压缩所服务的那些长会话限制了这项工作量,因此这份开销现在随会话长度增长,而不再随 surface 增长。这正是本次修复要做的取舍——保留历史才是目的——但窗口化或复用策略属于第一个真正测到重建变慢的人,而不属于日后某个疑惑工作量为何增长的性能分析者。
`dsh-tui` 为一个纯谓词新增了对 `dsh-compact` 接缝的依赖,与 `dsh-session-reference` 现有用法一致。终端在运行时仍然不需要任何压缩后端。
`dsh-tui` 为一个纯谓词新增了对 `dsh-compact` seam 的依赖,与 `dsh-session-reference` 现有用法一致。终端在运行时仍然不需要任何压缩后端。
两项行为随其测试一起改变。表层替换的终端测试此前钉住的是抹除(隐藏被遮蔽的工具调用),现在钉住的是保留加恰好一行标记,其中被裁剪的结果副本、重新生成的 assistant 消息以及来自其他插件的替换都不渲染任何内容。压缩快照场景此前声称钉住压缩,却写入了 `workspace-context` 来源;现在它写入真实的检查点来源,并重新录制三份 fixture以显示被保留的提示、完整的工具卡片和那行标记。
两项行为随其测试一起改变。表层替换的终端测试此前钉住的是抹除(隐藏被遮蔽的工具调用),现在钉住的是保留加恰好一行标记,其中被裁剪的结果副本、重新生成的 assistant 消息以及来自其他插件的替换都不渲染任何内容。压缩快照场景此前声称钉住压缩,却写入了 `workspace-context` 来源;现在它写入真实的检查点来源,并重新录制三份 fixture(测试前置数据),以显示被保留的提示、完整的工具卡片和那行标记。
上文的实时/回放等价性由 fixture 钉住,而不只是在此断言:`surface-replayed-compaction` 在挂载时替换已经存在,其录制结果与实时路径的 `surface-after-compaction-wide` 逐字节一致。改动任一路径都会破坏这项相等——这正是要点:回放投影才是当初对用户造成回归的部分,两份 fixture 必须一起变动。

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/bug-fix/2026-07-30-approval-panel-command-cap.md
2026-07-30-approval-panel-command-cap.md: 1d74d790299522a0f1e6b23f342cd10b35192d27
2026-07-30-approval-panel-command-cap.zh.md: d31db819144cd4ce2099bf5d5f025b2e8df0e0e1
2026-07-30-approval-panel-command-cap.zh.md: ff58e0e43ddabb71318f1ca28d321c10cb2a0d86

View File

@@ -14,7 +14,7 @@ Status: implemented
面板的理由与命令移入同一个滚动区域(`data-approval-scroll`),其高度上限与 composer 的草稿区完全相同;琥珀色状态条与操作按钮行位于该区域之外,因此无论内容多长,两个按钮都留在卡片内。
这个上限是一个值、两个消费,以 `--dsh-composer-text-max-height: 336px` 声明在 `ConversationRoot``.composerSeat` 上——它是 composer 链唯一的共同祖先,因为兜底的 InputBar 与被选中的接管面板是兄弟节点。`InputBar` 的草稿滚动容器与面板的滚动区域都读取它,于是同一个容器不可能给它的两种状态设出不同上限:设计同学要求的「可以跟输入框最大高度统一」,如今是样式表中的一个事实,而不是抄在两个文件里的一个数字。该区域取 `box-sizing: border-box`,因此上限指的是它的外框高度,与 composer 草稿区占据的是同一个盒子。
这个上限是一个值、两个消费,以 `--dsh-composer-text-max-height: 336px` 声明在 `ConversationRoot``.composerSeat` 上——它是 composer 链唯一的共同祖先,因为兜底的 InputBar 与被选中的接管面板是兄弟节点。`InputBar` 的草稿滚动容器与面板的滚动区域都读取它,于是同一个容器不可能给它的两种状态设出不同上限:设计同学要求的「可以跟输入框最大高度统一」,如今是样式表中的一个事实,而不是抄在两个文件里的一个数字。该区域取 `box-sizing: border-box`,因此上限指的是它的外框高度,与 composer 草稿区占据的是同一个盒子。
该区域自身是一个 Tab 停靠点(`tabIndex={0}`,带名称的 `role="group"`)。提问 composer 的滚动体不需要这样做——它的选项行本身可聚焦,会把容器一起带过去;而这里除文本之外别无内容:没有自己的停靠点,仅用键盘的用户能走到按钮却走不到命令尾部,于是可能批准了自己没读完的东西。
@@ -47,6 +47,6 @@ Status: implemented
几何断言块与 golden 仅在回放模式下执行,这样录制模式才能走到写入 fixture测试前置数据那一步而不是在布局检查处中断。
该场景只保留一份 golden——等待中的面板回应之后的状态改为对世界作断言决策结果、越权命令写出的那个文件、`DONE`、面板消失、输入框重新可用)。一份"已回应 transcript"的 golden 立不住第一次被拒绝的尝试渲染的是操作系统自己的拒绝文本而这段文本因平台而异macOS 为 `bash: notes.txt: Operation not permitted`Linux 为 `bash: line 1: notes.txt: Read-only file system`)。任何 transcript 中含有被沙箱拒绝命令的场景都会继承这一点,因此这类拒绝只能进断言,绝不能进 golden。
该场景只保留一份 golden——等待中的面板回应之后的状态改为对世界作断言决策结果、越权命令写出的那个文件、`DONE`、面板消失、输入框重新可用)。一份已回应 transcript的 golden 立不住第一次被拒绝的尝试渲染的是操作系统自己的拒绝文本而这段文本因平台而异macOS 为 `bash: notes.txt: Operation not permitted`Linux 为 `bash: line 1: notes.txt: Read-only file system`)。任何 transcript 中含有被沙箱拒绝命令的场景都会继承这一点,因此这类拒绝只能进断言,绝不能进 golden。
该面板以客户端模包的形式发布:单跑 `pnpm run build:web` 不会带上对 `ApprovalPanel.module.css` 的改动,也不会带上 `ApprovalPanel.tsx` 中新增的 `data-` 钩子——必须先执行包构建,否则浏览器测试通道会对着一个比工作树更旧的客户端做断言。
该面板以客户端模包的形式发布:单跑 `pnpm run build:web` 不会带上对 `ApprovalPanel.module.css` 的改动,也不会带上 `ApprovalPanel.tsx` 中新增的 `data-` 钩子——必须先执行包构建,否则浏览器测试通道会对着一个比工作树更旧的客户端做断言。

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/bug-fix/2026-07-30-bounded-overwrite-diff-basis.md
2026-07-30-bounded-overwrite-diff-basis.md: 7a09934bd1798059de43a092f338d37aa9ccbd9a
2026-07-30-bounded-overwrite-diff-basis.zh.md: 1d6bdd1068d119aae859132d9f8216ca29d0dc11
2026-07-30-bounded-overwrite-diff-basis.zh.md: 761c0e6e5591ba8d31f7a43ac555d4b10b97659a

View File

@@ -4,11 +4,11 @@ Status: implemented
[English](2026-07-30-bounded-overwrite-diff-basis.md) | 中文
## Problem
## 问题
`dsh-fs-local` 会在 `FsWriteOutcome.before` 中返回完整旧文件,供消费方生成覆写上下文 diff。这个仅用于展示的预读没有上限大文件覆写可能分配整个旧文件而仅检查较早的路径 stat 也无法真正实施上限,因为外部进程可以在 stat 与读取之间替换文件或扩大文件。即使旧文件很小,大替换内容也会使上下文 hunk 接近替换内容本身的大小。本改动关闭了 [result-time applied-hunk diff](../../archived/architecture/2026-07-02-result-time-applied-hunk-diffs.md) 中记录的暂缓上限事项。
## Decision
## 决策
`LocalFileSystem.Config.diffBasisMaxBytes` 是一个不超过运行时 Buffer 分配和字符串解码上限的正安全整数部署配置,默认 10 MiB。只有当 UTF-8 替换内容严格低于该上限,且为生成基础而打开的旧文件最终也低于该上限时,覆写才提供 `before`。旧文件读取会打开文件描述符、检查该描述符,并按可响应取消的分块最多读取配置的字节数;一旦到达边界便返回 `null`。描述符 stat 后发生大小变化时同样返回 `null`,即使最终大小仍低于上限,因为部分前缀会成为错误的 diff 基础。旧内容为二进制或无效 UTF-8 时也返回 `null`;描述符阶段的任何 errno 同样如此——旧文件在调用方预检之后、基础读取打开之前被删除或变得不可读,不能让调用方已经提交的写入失败;只有取消和非 errno 故障会继续向上传播。这些结果都不会阻止原子写入。
@@ -16,7 +16,7 @@ Status: implemented
`before: null` 要求消费方使用既有的整文件回退。该上限只限制额外获取旧内容的成本,以及上下文内容对是否合格;它不限制调用方持有的替换内容、返回的 `after` 值或消费方的回退渲染。
## Alternatives considered
## 考虑过的替代方案
**保留一个与读取工具流式阈值相等的硬编码阈值。** 否决,因为读取阈值可由部署配置,且归消费方所有。两个同值常量会形成无法强制的一致性耦合,而覆写基础本身也是部署层面的内存与展示选择。
@@ -26,6 +26,6 @@ Status: implemented
**为任意大的内容对流式生成上下文 diff。** 本次缺陷修复不采用,因为当前文件系统 seam 返回完整的 `before`/`after` 字符串,当前 diff 实现也消费这两个字符串。流式 diff 需要独立的跨包协议与展示设计。
## Consequences
## 后果
部署可以调整额外的覆写基础成本,而不改变读取路由。达到或超过排他上限时,覆写仍会成功,并通过整文件回退保持可见,但不再提供上下文 hunk。低于上限时除调用方的替换内容外提供方仍可能持有接近 `diffBasisMaxBytes` 的旧文本。对于合格覆写,有上限的描述符读取会增加一次 open/stat/read 序列,同时防止陈旧路径探测把该序列变成无上限分配。

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/bug-fix/2026-07-30-web-transcript-log-ordered-projection.md
2026-07-30-web-transcript-log-ordered-projection.md: f2eda6983ab88a420a4506be7766320dc5da0fa2
2026-07-30-web-transcript-log-ordered-projection.zh.md: 4c8bb5d0b9b49762d12aba837cef9c0818740cb0
2026-07-30-web-transcript-log-ordered-projection.zh.md: 84b01c606debcce36fa626898417b0992d54f1a8

View File

@@ -6,7 +6,7 @@ Status: implemented
## Problem
浏览器客户端从模型可见的 surface 构建会话:`FoldAdapter` 在历史窗口上运行核心 `SurfaceManager` 并读取 `surface.nodes`。一次成功的压缩会用一个检查点节点替换一段 surface 范围因此该替换一落地Web 流就把它所遮蔽的每条消息折叠成一行灰暗的上下文——那是用户已经读过的对话。日志中什么都没丢失;缺陷完全在投影层,而[终端与宿主历史网关已按同一方式修复](2026-07-29-human-transcript-append-origin.md),浏览器留给了本次变更。
浏览器客户端从模型可见的 surface 构建会话:`FoldAdapter` 在历史窗口上运行核心 `SurfaceManager` 并读取 `surface.nodes`。一次成功的压缩compaction会用一个检查点节点替换一段 surface 范围因此该替换一落地Web 流就把它所遮蔽的每条消息折叠成一行灰暗的上下文——那是用户已经读过的对话。日志中什么都没丢失;缺陷完全在投影层,而[终端与宿主网关已按同一方式修复](2026-07-29-human-transcript-append-origin.md),浏览器留给了本次变更。
surface 顺序还让另外两个问题成为结构性的。一次替换之后它并非按 seq 升序——`SurfaceManager` 把高 seq 的检查点拼接到它所遮蔽范围的位置上——因此按数值 seq 归并进该数组的仅日志节点(斜杠命令行、被打断的冻结节点)可能被冲刷到检查点之前,再也无法交错回保留下来的尾部。而且由于分页不再为 replacement 副本消耗 `maxMessages` 额度,一页现在可以携带一个 `surfaceOp.start` 落在窗口之外的检查点;核心 fold 拒绝该范围,于是 `nodes()` 退回到一次宽容的线性扫描、打印一条 `console.error`,并发布一个描述该失败的 `foldDegraded` 标志。
@@ -49,7 +49,7 @@ const COMPACT_PLUGIN: CompactCheckpointSource['plugin'] = 'compact'
## Alternatives considered
**从新叶子值导入该谓词**,并把 `dsh-compact` 加入客户端 `INLINE_SAFE` 白名单。已拒绝:客户端需要的是插件 id不是谓词——一个类型就够了而被擦除的导入根本不会抵达纯度门禁因此无需向它放行任何东西。白名单只在值导入时才有意义而在那里它是笔糟糕的交换`INLINE_SAFE`标识符*前缀*匹配,因此放行该包会连它那个会导入 cordis 的根部一起放行。
**从新叶子值导入该谓词**,并把 `dsh-compact` 加入客户端 `INLINE_SAFE` 白名单。已拒绝:客户端需要的是插件 id不是谓词——一个类型就够了而被擦除的导入根本不会抵达纯度门禁因此无需向它放行任何东西。白名单只在值导入时才有意义而在那里它是笔糟糕的交换`INLINE_SAFE`模块说明符*前缀*匹配,因此放行该包会连它那个会导入 cordis 的根部一起放行。
**一条纯形状规则**——任何 replacement `user/message` 都是压缩。已拒绝:它今天正确只因为压缩是 replacement `user/message` 的唯一生产者,一旦这点改变便无任何机制能捕获。那个 pin 测试只花一个文件,就精确消除了这一风险。

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/bug-fix/2026-07-31-fail-loud-releases-the-terminal.md
2026-07-31-fail-loud-releases-the-terminal.md: d5a97d73ec60897a3a40f3c99417e43de190af9c
2026-07-31-fail-loud-releases-the-terminal.zh.md: 271769d32ea1dd66c698a65b35461f51042b7efb
2026-07-31-fail-loud-releases-the-terminal.zh.md: 3de0a038fd644d13a6953333e772878858ae6875

View File

@@ -17,7 +17,7 @@ zsh: command not found: 4cecho
Loader 并发挂载各个条目,因此条目失败的顺序并不等于启动顺序。`ui-tui` 会先激活并调用 pi-tui 的 `ProcessTerminal.start()`,它把 stdin 置为 raw 模式、启用 bracketed paste并写出 Kitty 键盘协议探测序列——该序列以一个 Device Attributes 查询(`ESC [ c`)结尾。随后某个同级条目(这里是 `llm-pi-ai`)因自身配置而 rejection。
在当时,该 rejection 以未处理 rejection 的形式浮现,而 `installFailLoud` 只写一行 stderr 就立即调用 `process.exit(1)`。(事务化 Loader 现在让配置树失败经 `boot()` 结算,由它自行 dispose资源释放部分构建的上下文release 回调仍然守护 `boot()` 看不到的 rejection——插件游离的异步工作在挂载期间或挂载之后失败。没有任何环节 dispose 这棵树,因此 `ProcessTerminal.stop()` 从未执行raw 模式、bracketed paste 和键盘协议都残留在比进程活得更久的 shell 上。终端对 Device Attributes 查询的回应(`1;2;4c`)在进程退出之后才到达,被 shell 当作用户输入读入——也就是上面那段字面文本。
在当时,该 rejection 以未处理 rejection 的形式浮现,而 `installFailLoud` 只写一行 stderr 就立即调用 `process.exit(1)`。(事务化 Loader 现在让配置树失败经 `boot()` 结算,由它自行 dispose资源释放部分构建的上下文release 钩子仍然守护 `boot()` 看不到的 rejection——插件游离的异步工作在挂载期间或挂载之后失败。没有任何环节 dispose 这棵树,因此 `ProcessTerminal.stop()` 从未执行raw 模式、bracketed paste 和键盘协议都残留在比进程活得更久的 shell 上。终端对 Device Attributes 查询的回应(`1;2;4c`)在进程退出之后才到达,被 shell 当作用户输入读入——也就是上面那段字面文本。
`/exit` 路径从不受影响,因为它会 dispose 整棵树,从而进入 TUI 自身的 `shutdown()`:先 `drainInput()`(吸收尚未返回的响应),再 `ui.stop()`。缺陷在于**启动失败**没有通往这同一套拆卸流程的路径。
@@ -32,11 +32,11 @@ Loader 并发挂载各个条目,因此条目失败的顺序并不等于启动
`dsh` 的 TUI 启动器传入的 release 会释放根上下文,从而执行 TUI 已有的 `shutdown()` 并把终端交还。
启动器在 `boot()``prepare` 回调中捕获根上下文而不是取其返回值。rejection 到达时 `boot()` 尚未结算,因此在 `await` 之后赋值的 `app.current` 恰好在回调需要它的那一刻仍是 `undefined``prepare` 在 Loader 安装之后、任何配置树条目挂载之前运行,覆盖了条目可能 rejection 的整个窗口。
启动器在 `boot()``prepare` 钩子中捕获根上下文而不是取其返回值。rejection 到达时 `boot()` 尚未结算,因此在 `await` 之后赋值的 `app.current` 恰好在回调需要它的那一刻仍是 `undefined``prepare` 在 Loader 安装之后、任何配置树条目挂载之前运行,覆盖了条目可能 rejection 的整个窗口。
## 考虑过的替代方案
**在 fail-loud 处理函数里直接重置终端**(写 `ESC [ ? 2004 l`、弹出键盘协议、清除 raw 模式)。这会在一个并不拥有终端的包里重复 pi-tui 的拆卸逻辑,并随 pi-tui 启动序列的变化而漂移。它同样无法吸收尚未返回的 Device Attributes 响应——而这正是弄乱下一个提示符的原因,只有在 stdin 仍处于 raw 模式时排空它才能解决。
**在响亮失败处理函数里直接重置终端**(写 `ESC [ ? 2004 l`、弹出键盘协议、清除 raw 模式)。这会在一个并不拥有终端的包里重复 pi-tui 的拆卸逻辑,并随 pi-tui 启动序列的变化而漂移。它同样无法吸收尚未返回的 Device Attributes 响应——而这正是弄乱下一个提示符的原因,只有在 stdin 仍处于 raw 模式时排空它才能解决。
**在 TUI 中注册 `process.on('exit')` 终端重置。** exit 处理函数是同步的,无法等待 `drainInput()`,残留响应依旧会落到 shell而且这把拆卸挂到全局钩子上而非已经存在的释放路径。
@@ -52,7 +52,7 @@ Loader 并发挂载各个条目,因此条目失败的顺序并不等于启动
## 测试
`packages/boot/app-boot/tests/app-boot.spec.ts` 覆盖 release 约定:退出提交前会等待该回调;回调 rejection 时仍退出 1永不结算的回调会在 `FAIL_LOUD_RELEASE_TIMEOUT_MS` 后退出;以及一连串 rejection 只报告第一个,同时 release 仍能跑完。
`packages/boot/app-boot/tests/app-boot.spec.ts` 覆盖 release 约定:退出提交前会等待该钩子;钩子 rejection 时仍退出 1永不结算的钩子会在 `FAIL_LOUD_RELEASE_TIMEOUT_MS` 后退出;以及一连串 rejection 只报告第一个,同时 release 仍能跑完。
这些基于假进程的测试无法观测到最关键的两种失败形态——真实事件循环下的进程退出码,以及退出之后的终端状态——因此回归用例放在 `apps/cli/tests/tui-keyless-smoke.e2e.ts`。它在真实 PTY 中以 `fixtures/tui-invalid-provider.cordis.yml``providers` 为列表形状,正是用户真实会犯的错误)启动出厂配置树,期望退出码为 1并断言捕获到的字节流同时包含带标签的启动 rejection`dsh: plugin tree failed to load:`)与 `ESC[?2004l`。同一用例端到端钉住了启动路径:正是它发现了以 13 静默退出、终端状态未被恢复的 [HMR热模块替换初始扫描启动死锁](2026-08-03-hmr-initial-scan-boot-deadlock.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/bug-fix/2026-07-31-resume-selector-batch-projection.md
2026-07-31-resume-selector-batch-projection.md: 5b0b33c34137efb95f31e07e7fce7622aeb2dbce
2026-07-31-resume-selector-batch-projection.zh.md: a12df91ba75b7bdc2d9f48dc633ad1689d163180
2026-07-31-resume-selector-batch-projection.zh.md: a8e89c2e989680a1d1bf98f0cd2817b5900d36d0

View File

@@ -4,32 +4,32 @@ Status: implemented
[English](2026-07-31-resume-selector-batch-projection.md) | 中文
## Problem
## 问题
打开 TUI `/resume` 选择器时,会在一个无界 `Promise.all` 中对每个列出的会话调用一次 `sessionQuery.readSession()`。每次调用都会在 `SessionCorpus.load()` 内部重新列出整个持久化存储O(N²) 次列表查询)、读取并解压完整日志、通过 `Session` 构造函数对每个事件做回放验证,并将 header 和事件深克隆多达三次——而这一切只为推导一行选择器条目的标题、最近活动时间、最后一个 `turn/end` 标签、提供方/模型路由和目标阶段。在真实存储上185 个会话、压缩后 87 MB、约 35.3 万个事件),选择器需要数十秒才能打开,且开销随日志总大小而非会话数量增长。
## Decision
## 决策
选择器行除标题外不折叠任何内容,行内其余信息全部来自元数据:
- 标题来自投影系统:`session-title` 已注册 `title` 投影单元,因此实时行读取注册表快照,持久化行读取持久 checkpoint 行(`sessionProjectionCache.cachedSnapshot`,零 I/O只有没有可用 checkpoint 的行才付出一次 `coldSnapshot`——checkpoint 加 `readFrom` 尾部折叠,并写回使下次扫描零 I/O。冷读取受 TUI `resumeScanConcurrency` 配置约束。未挂载缓存的组合回退到一次对日志的有界 `readTitleSnapshots` 批量读取;两条路径都把单行失败隔离为禁用的"Unreadable session"回退。
- 标题来自投影系统:`session-title` 已注册 `title` 投影单元,因此实时行读取注册表快照,持久化行读取持久 checkpoint 行(`sessionProjectionCache.cachedSnapshot`,零 I/O只有没有可用 checkpoint 的行才付出一次 `coldSnapshot`——checkpoint 加 `readFrom` 尾部折叠,并写回使下次扫描零 I/O。冷读取受 TUI `resumeScanConcurrency` 配置约束。未挂载缓存的组合回退到一次对日志的有界 `readTitleSnapshots` 批量读取;两条路径都把单行失败隔离为禁用的Unreadable session回退。
- 活动时间戳从不读取日志:实时会话取内存中最后一个事件的时间;持久化会话对可选 `sessionPersistence.locate()` 命名的产物做 statmtime当后端定位不到按会话的产物SQLite或 stat 失败时回退到 header 的创建时间。任何追加都会移动 mtime因此仅仅一次 pickup 边界也会让浏览过的会话上浮——这是元数据时间戳的代价,予以接受。
- 行内不再有最后轮次标签、提供方/模型路由和目标阶段列。路由可用性改由 Enter 时的预检强制:预检通过 `readSession` 完整读取并回放验证选中的那一份日志后才移交。
选择器 overlay 在 `/resume` 分发时同步打开,早于扫描结算:`undefined` 候选集渲染"Loading sessions…"加载占位符选择器从第一帧起就拥有终端输入Enter 提示会话仍在加载Escape 取消。关闭 overlay 会通过查询方法接受的 `AbortSignal` 中止扫描;忽略信号的后端的迟到结算由过期检查丢弃。扫描完成后通过 `setCandidates`(同时清除过期的仍在加载错误)换入行数据,不替换 overlay排在正在关闭的前任之后的排队激活会在构造时直接收到已扫描的集合列表查询、标题与 mtime 共用同一个 catch因此任何扫描失败都会关闭 overlay 并报告通知,而不会让加载占位符悬置。
选择器 overlay 在 `/resume` 分发时同步打开,早于扫描结算:`undefined` 候选集渲染Loading sessions…加载占位符选择器从第一帧起就拥有终端输入Enter 提示会话仍在加载Escape 取消。关闭 overlay 会通过查询方法接受的 `AbortSignal` 中止扫描;忽略信号的后端的迟到结算由陈旧性检查丢弃。扫描完成后通过 `setCandidates`(同时清除陈旧的仍在加载错误)换入行数据,不替换 overlay排在正在关闭的前任之后的排队激活会在构造时直接收到已扫描的集合列表查询、标题与 mtime 共用同一个 catch因此任何扫描失败都会关闭 overlay 并报告通知,而不会让加载占位符悬置。
session-query 与 session-persistence 的任何表面都未改变。随附的 TUI 组合新增投影注册表、storage 与投影缓存行(镜像 web overlay共用同一 `storages` 根,因此任一面写的 checkpoint 都服务两者);对既有存储的首次扫描仍会各读取一次日志以播种 checkpoint之后的每次扫描都只读元数据。
session-query 与 session-persistence 的任何接口都未改变。随附的 TUI 组合新增投影注册表、storage 与投影缓存行(镜像 web overlay共用同一 `storages` 根,因此任一面写的 checkpoint 都服务两者);对既有存储的首次扫描仍会各读取一次日志以播种 checkpoint之后的每次扫描都只读元数据。
## Alternatives considered
## 备选方案
**通过通用批量投影(`projectSessions`)保留每行的路由/轮次/目标列。** 先实现后否决:它仍在每次 `/resume` 时解压并解析全部日志,浏览开销依旧是 O(日志总字节数),且为单一消费扩大了 session-query 公开 API。该公开约定已回退`readTitleSnapshots` 继续使用内部 `projectMany`,保持不变。
**通过通用批量投影(`projectSessions`)保留每行的路由/轮次/目标列。** 先实现后否决:它仍在每次 `/resume` 时解压并解析全部日志,浏览开销依旧是 O(日志总字节数),且为单一消费扩大了 session-query 公开 API。该公开约定已回退`readTitleSnapshots` 继续使用内部 `projectMany`,保持不变。
**只修复 `SessionCorpus.load()` 内部的 O(N²) 列表查询。** 作为主要修复被否决:在大日志上,按候选行执行的完整解压、回放验证和三重克隆才是主要开销。`load()` 中的冗余预列表查询仍是一个候选清理项,但涉及错误语义。
**通过 `listSnapshots`/`SessionRecord` 暴露最后修改时间。** 从 seam 角度最干净,但要触碰持久化约定、两个后端和查询记录形状,而 TUI 已能用 `locate()` 加一次 stat 得到同样的信息。若出现第二个需要元数据活动时间的消费再引入。
**通过 `listSnapshots`/`SessionRecord` 暴露最后修改时间。** 从 seam 角度最干净,但要触碰持久化约定、两个后端和查询记录形状,而 TUI 已能用 `locate()` 加一次 stat 得到同样的信息。若出现第二个需要元数据活动时间的消费再引入。
**专门的持久化标题索引或 TUI 本地标题缓存。** 否决session-projection 缓存本身就是自有的持久 checkpoint 系统,并已带失效约定(`stateVersion`、身份绑定、日志收缩锚定);挂载它优于再造一套并行缓存。
## Consequences
## 后果
打开 `/resume` 只执行一次列表查询、每个持久化行一次 stat标题读取在 checkpoint 就绪后只触碰 checkpoint 行和日志尾部——O(会话数) 的元数据开销,而非 O(日志总字节数);无缓存的回退路径仍是一次有界标题扫描。行内只显示标题、时间戳、状态和 id路由问题以 Enter 时预检错误的形式出现,而不再是禁用行;回放会失败的会话由预检而非列表阶段拦截。浏览后放弃的会话会因 pickup 的 mtime 上浮。TUI 测试中的伪造 `sessionQuery` 服务在 `listSessions`/`readSession` 之外提供 `readTitleSnapshots`,测试 harness 会转发可选的 `locate`。由于选择器立即接管焦点,启动第二次扫描需要先关闭当前 overlay——扫描期间输入的第二个 `/resume` 会落入搜索字段,这正是预期的输入捕获行为。

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/bug-fix/2026-07-31-same-basename-workspace-adoption.md
2026-07-31-same-basename-workspace-adoption.md: 1192558632fdc8bd732ea59f0eca5051f49f5d2c
2026-07-31-same-basename-workspace-adoption.zh.md: 9c1f1ffd221936e24300b223ad395e1f44552810
2026-07-31-same-basename-workspace-adoption.zh.md: 722e747e0bd03da42a5f2555ee2fb3e46522cca2

View File

@@ -20,7 +20,7 @@ Host 的 `workspace.create({ path })` 接纳入口沿用该规则。Workspace
## 验证
Workspace 注册表与 Host API 测试会在不同父目录下创建两个末级名称相同的真实目录,并断言其 id 和路径互不相同,且持久顺序正确。选择器组件将相同标签渲染为按 id 区分的独立条目。无密钥 Web 浏览器场景通过组合而成的目录流程接纳这两个目录,并观察到两个 Workspace 均已注册且完成渲染。
Workspace 注册表与 Host API 测试会在不同父目录下创建两个末级名称相同的真实目录,并断言其 id 和路径互不相同,且持久顺序正确。选择器组件将相同标签渲染为按 id 区分的独立条目。无密钥 Web 浏览器场景通过组合而成的目录流程接纳这两个目录,并观察到两个 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/bug-fix/2026-08-03-cli-signal-shutdown-escalation.md
2026-08-03-cli-signal-shutdown-escalation.md: 173d06482cd8a1fcbb985763cc313e3f9b170bc6
2026-08-03-cli-signal-shutdown-escalation.zh.md: efa52524199906cf636cb2b55cb4249857dc1348
2026-08-03-cli-signal-shutdown-escalation.zh.md: 88ca7cad25444afc908e83c6aa82bdd21c46d627

View File

@@ -8,7 +8,7 @@ Status: implemented
默认挂载遥测后,`dsh web` 与 headless 命令(现为 `dsh --profile headless`)新增了 SIGINT/SIGTERM 处理器,使进程退出时可以排空 Cordis 插件树而不是丢弃排队中的遥测数据。每个处理器都使用单向布尔闩锁latch并且只有在 `ctx.fiber.dispose()` 结算后才退出。headless 正常完成时同样会无界等待整棵树执行 dispose资源释放
随后有用户复现headless 命令在打印观察 URL 后立即卡死,重复按 `Ctrl+C` 也没有反应;设置 `DSH_TELEMETRY_DISABLED=1` 后不再卡死,而同一 Linux 沙箱中的独立 Node 信号处理器能够收到 SIGINT。这将待结算的 disposer 定位到遥测而非终端信号转发。OTel 的 `BatchLogRecordProcessor.shutdown()` 会先等待 `exporter.forceFlush()`,再进入`exportTimeoutMillis` 限制的完成 promiseOTLP 导出器的 `forceFlush()` 则直接等待正在进行的 HTTP Promise。因此代理沙箱连接始终无法取得 socket 时,即使已经配置两项 SDK 超时,也会让提供方关闭一直待结算。
随后有用户复现headless 命令在打印观察 URL 后立即卡死,重复按 `Ctrl+C` 也没有反应;设置 `DSH_TELEMETRY_DISABLED=1` 后不再卡死,而同一 Linux 沙箱中的独立 Node 信号处理器能够收到 SIGINT。这将待结算的 disposer 定位到遥测而非终端信号转发。OTel 的 `BatchLogRecordProcessor.shutdown()` 会先等待 `exporter.forceFlush()`,再等待`exportTimeoutMillis` 限制的完成 PromiseOTLP 导出器的 `forceFlush()` 则直接等待正在进行的 HTTP Promise。因此代理沙箱连接始终无法取得 socket 时,即使已经配置两项 SDK 超时,也会让提供方关闭一直待结算。
闩锁随后把这个遥测缺陷变成无法终止的 CLI命令行界面正常完成流程已经在等待单次根级 dispose第一次 SIGINT 会加入同一个待结算的 dispose并设置信号闩锁后续 SIGINT 在闩锁处直接返回因此进程再无退出途径。正常完成之前收到信号时同样会陷入无界等待。Web 使用的闩锁结构与此相同。
@@ -16,7 +16,7 @@ Status: implemented
## 决策
修复分为两层归属。OTel 后端围绕 SDK 提供方的完整关闭 Promise 增加 `shutdownTimeoutMillis`(默认值和交付值均为 3 秒)。超过该截止时间时会 reject,并进入遥测协调器现有的失败隔离路径,使 Cordis 插件树能够完成 dispose由于 OTel 未公开取消传输 Promise 的能力,待处理记录可能丢失。
修复分为两层归属。OTel 后端围绕 SDK 提供方的完整关闭 Promise 增加 `shutdownTimeoutMillis`(默认值和交付值均为 3 秒)。超过该截止时间时会以拒绝状态结算,并进入遥测协调器现有的失败隔离路径,使 Cordis 插件树能够完成 dispose由于 OTel 未公开取消传输 Promise 的能力,待处理记录可能丢失。
Web 与 headless 共用 `createProcessShutdown`,它是围绕根级 dispose 建立的进程级控制器:
@@ -49,7 +49,7 @@ headless 对完成的轮次仍以 0 退出,对其他轮次结束原因或 API
## 测试
`apps/cli/tests/process-shutdown.spec.ts` 固定了 dispose 成功后的自然完成、dispose 失败后的强制退出、5 秒退出兜底、正常调用汇合、信号拥有的 dispose、信号中断正常 dispose 或 dispose 后句柄排空,以及第二次信号强制退出的行为。
`apps/cli/tests/process-shutdown.spec.ts` 固定了 dispose 成功后的自然完成、dispose 失败后的强制退出、5 秒退出兜底、正常调用汇合、信号发起的 dispose、信号中断正常 dispose 或 dispose 后句柄排空,以及第二次信号强制退出的行为。
`apps/cli/tests/headless-shutdown.e2e.ts` 在 PTY 中启动真实交付的 Web/headless Loader 插件树,并挂载一个仅用于测试的插件;该插件的 disposer 会声明已经进入清理流程,但永不结算。测试在观察地址出现后发送 SIGINT等待 dispose 已启动的证据,再次发送 SIGINT并要求进程以 130 退出。源码/产物启动解析器使两个执行平面都覆盖同一项回归。该 PTY 用例覆盖用户可见的进程状态;模型输出快照没有变化。

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/bug-fix/2026-08-04-load-pre-react-loop-sessions.md
2026-08-04-load-pre-react-loop-sessions.md: 277a481f366f1182fd3948caf858607efd550e5e
2026-08-04-load-pre-react-loop-sessions.zh.md: 30e482cbb1c761f7296dbcb083dc0a76f3fa8f85
2026-08-04-load-pre-react-loop-sessions.zh.md: d49c03ad36db7e71fb351e5de91450225976df16

View File

@@ -6,7 +6,7 @@ Status: implemented
## 问题
react-loop 简化在保持 `SESSION_FORMAT_VERSION` 为 0 的同时更改了持久事件。该变更基线所存储的会话包含 steering中途引导事件 `steering/message` `turn/start.trigger`;其终止原因还使用粗粒度 `aborted`、独立的 `disposed` 和两种旧版错误载荷。当前 surface 和轮次不变量无法直接回放这些记录。
react-loop 简化在保持 `SESSION_FORMAT_VERSION` 为 0 的同时更改了持久事件。该变更基线所存储的会话包含 steering中途引导事件 `steering/message`,以及 `turn/start.trigger` 字段;其终止原因还使用粗粒度 `aborted`、独立的 `disposed` 和两种旧版错误载荷。当前表层和轮次不变量无法直接回放这些记录。
新的持久 inbox 不属于此兼容性问题。该基线会发出进程本地 inbox 通知,但不会产生 `agent/inbox/*` 会话事件,因此将旧历史回放为待处理工作会让已经领取或丢弃的提示词再次执行。

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/bug-fix/2026-08-06-api-key-format-validation.md
2026-08-06-api-key-format-validation.md: 356891c01c003f26d8edd092ebde8887e17d2448
2026-08-06-api-key-format-validation.zh.md: 156e3141549c94f609d68f1937f30202c40751c6
2026-08-06-api-key-format-validation.zh.md: e996b6bf407b1849f8ffcd8fec31b060e38f9223

View File

@@ -8,7 +8,7 @@ Status: implemented
一个含有 HTTP header value 无法承载的字符的 API Key曾被每个配置入口接受直到构造请求时才失败——离引发它的那个字段已经很远。
把含 emoji、中或全角标点的 Key 粘进 Web 模型设置页,保存会报成功。第一轮对话随即失败于 `Cannot convert argument to a ByteString because the character at index 7 has a value of 55357 which is greater than 255`——其中的下标与码点是 UTF-16 内部细节,不附带任何可执行动作,却泄露了 Key 中某一个字符的码点。`llm-deepseek` 之所以产出这句,是因为 `fetch` 在 [adapter.ts](../../../../packages/llm/llm-deepseek/src/adapter.ts) 的 `try` 内部构造 `Bearer` header而那个 `catch` 把一切失败都标为 `TRANSPORT`;该标签又在 `DEFAULT_RETRYABLE_CODES` 之中,于是一个永久且确定的故障还会被重试三次。
把含 emoji、中日韩文字或全角标点的 Key 粘进 Web 模型设置页,保存会报成功。首个轮次随即失败,报错为 `Cannot convert argument to a ByteString because the character at index 7 has a value of 55357 which is greater than 255`——其中的下标与码点是 UTF-16 内部细节,不附带任何可执行动作,却泄露了 Key 中某一个字符的码点。`llm-deepseek` 之所以产出这句,是因为 `fetch` 在 [adapter.ts](../../../../packages/llm/llm-deepseek/src/adapter.ts) 的 `try` 内部构造 `Bearer` header而那个 `catch` 把一切失败都标为 `TRANSPORT`;该标签又在 `DEFAULT_RETRYABLE_CODES` 之中,于是一个永久且确定的故障还会被重试三次。
同样的输入在 `llm-pi-ai` 上更糟。它的探测路径在 [discovery.ts](../../../../packages/llm/llm-pi-ai/src/discovery.ts) 里用裸 `fetch` 构造同一个 header并把一切失败包装成 `could not reach <url>`,于是一个本地的 Key 故障被报成网络不可达。这条探测在保存之前就够得着:`ProviderEditor` 把用户输入的 `keyDraft` 直接放进探测请求,所以「获取模型列表」按钮会在任何东西落盘之前就把非法 Key 发出去。
@@ -18,7 +18,7 @@ Status: implemented
一条规则定义什么是合法 Key**trim 之后非空,且每个字符都落在 `[\x21-\x7E]`**——可打印 ASCII不含空格。
这一个断言覆盖了所有已报告的输入空值、首尾空白、中间空白、C0 控制字符、emoji、中、全角标点。它同时正是造成 ByteString 失败的那条约束,所以这些故障收敛于同一个定义,而不是两个恰好相关的修复。
这一个断言覆盖了所有已报告的输入空值、首尾空白、中间空白、C0 控制字符、emoji、中日韩文字、全角标点。它同时正是造成 ByteString 失败的那条约束,所以这些故障收敛于同一个定义,而不是两个恰好相关的修复。
第二条更窄的规则用于识别整行粘贴的环境变量:匹配 `^[A-Z][A-Z0-9_]*=[^=]` 或首尾成对引号的输入会被拒绝。把前缀限定为全大写可以让真实 Key 与之绝缘——`sk-` 这类形态会在连字符处中断标识符匹配——而要求分隔符之后必须是非 `=` 字符,则让 base64 的 padding 也与之绝缘。它报出的是与非法字符相同的那条格式失败,而不是自己的一句:读到它的人下一步动作完全一样,因此单列一句只会点出一个原因,却不改变该怎么做。
@@ -32,9 +32,9 @@ Status: implemented
规则作用于*已提供*的值;至于究竟有没有提供,由各个调用方自行判断。
**未点名凭据。** 省略 `apiKeyEnv` 的 pi-ai profile 可以在 harness 持有的凭据路径之外鉴权。[provider.ts](../../../../packages/llm/llm-pi-ai/src/provider.ts) 中的 `routeAuth` 保留内置 catalog 提供方自身的鉴权,正是为了让 provider 原生的 ambient 发现继续工作;而该 catalog 附带的 `openai-codex` 通过 OAuth 鉴权。`namesCredential` 承载这一区分;省略不是需要校验的值。
**未点名凭据。** 省略 `apiKeyEnv` 的 pi-ai profile 可以在 harness 持有的凭据路径之外鉴权。[provider.ts](../../../../packages/llm/llm-pi-ai/src/provider.ts) 中的 `routeAuth` 保留内置 catalog 提供方自身的鉴权,正是为了让提供方原生的 ambient 发现继续工作;而该 catalog 附带的 `openai-codex` 通过 OAuth 鉴权。`namesCredential` 承载这一区分;省略不是需要校验的值。
**Web UI 中留空的输入框。** 即便某个 provider 的 Key 已经存好,该输入框也是空着打开的——`keyStored` 的文案写的是「已配置——输入新值以替换」——所以留空意味着*保持已存储的值*。`ProviderEditor` 在草稿为空时完全跳过 `credentials.set`,这一点保持不变:留空绝不拦截提交,否则改一个 base URL 都得重新输一遍 Key。
**Web UI 中留空的输入框。** 即便某个提供方的 Key 已经存好,该输入框也是空着打开的——`keyStored` 的文案写的是「已配置——输入新值以替换」——所以留空意味着*保持已存储的值*。`ProviderEditor` 在草稿为空时完全跳过 `credentials.set`,这一点保持不变:留空绝不拦截提交,否则改一个 base URL 都得重新输一遍 Key。
**解析得到的值只含空白。** 两个适配器都将其视为非法,因为它无法为请求鉴权。在浏览器中,这同样是字段级失败:字段是人刚刚敲过字的地方,静默丢弃他敲进去的内容永远不是正确答案。
@@ -70,11 +70,11 @@ Status: implemented
**在适配器的 `catch` 中嗅探 `TypeError`。** 这只是事后归类 ByteString 失败header 构造本身仍无防护。它依赖 Node 错误消息的措辞,因而会随运行时版本静默失效;它也帮不到 `llm-pi-ai`——后者的请求 header 构造在 pi-ai SDK 内部。在交出 Key 之前就拒绝,则对两个适配器与探测路径同时有效。
**在 `credentials-local.set` 中执行。** 它能一次性拦住所有写入方,包括手工编辑的文件。它落败于该 provider 存储各种类型的凭据,而一条源自 HTTP header 编码的规则并不属于它。
**在 `credentials-local.set` 中执行。** 它能一次性拦住所有写入方,包括手工编辑的文件。它落败于该提供方存储各种类型的凭据,而一条源自 HTTP header 编码的规则并不属于它。
**让形状启发式也在 resolver 中运行。** 更对称,且能拦住直接写进 `.env` 的整行环境变量。因上文所述的锁死风险而否决resolver 中的一次误判会让用户无路可走,浏览器中的一次误判则仍留有环境变量这条路。
**在保存时探测 provider 以证明 Key 可用。** 它能关掉最初报告的那件事——保存报成功、第一轮才失败。因超出范围而否决,且在当时的代码上无法建成:对 pi-ai 恰好自带 catalog 的那些提供方,`discoverModels` 会在任何网络调用之前短路到内置 catalog因而对 Key 什么都验证不了;而 DeepSeek 卡片根本没有探测。验证器的价值在于分清「Key 被拒」与「无法连通」,而这正是本次改动让其变得可靠的区分;先建验证器只会得到一个分不清自身结果的验证器。同类产品也不在保存时验证,因此保存时的阻断式网络调用会是一个意外行为,而非一处缺失。
**在保存时探测提供方以证明 Key 可用。** 它能关掉最初报告的那件事——保存报成功、首个轮次才失败。因超出范围而否决,且在当时的代码上无法建成:对 pi-ai 恰好自带 catalog 的那些提供方,`discoverModels` 会在任何网络调用之前短路到内置 catalog因而对 Key 什么都验证不了;而 DeepSeek 卡片根本没有探测。验证器的价值在于分清「Key 被拒」与「无法连通」,而这正是本次改动让其变得可靠的区分;先建验证器只会得到一个分不清自身结果的验证器。同类产品也不在保存时验证,因此保存时的阻断式网络调用会是一个意外行为,而非一处缺失。
## 后果
@@ -92,10 +92,10 @@ Status: implemented
## 测试
`packages/llm/llm/tests/api-key.spec.ts` 以整张输入表驱动 `normalizeApiKey``assertUsableApiKey`——空值、纯空白、带首尾空白、含中间空格、C0 控制字符、emoji、中、全角、latin-1以及可打印 ASCII 的边界字符——并钉住一次拒绝携带 `INVALID_CREDENTIAL` 且不含 Key 的任何部分。
`packages/llm/llm/tests/api-key.spec.ts` 以整张输入表驱动 `normalizeApiKey``assertUsableApiKey`——空值、纯空白、带首尾空白、含中间空格、C0 控制字符、emoji、中日韩文字、全角、latin-1以及可打印 ASCII 的边界字符——并钉住一次拒绝携带 `INVALID_CREDENTIAL` 且不含 Key 的任何部分。
`packages/llm/llm-deepseek/tests/``dynamic-config.spec.ts` 中经真实凭据 seam而非 stub端到端覆盖已存储凭据路径。`packages/llm/llm-pi-ai/tests/` 覆盖探测路径,包括不带 Key 的探测不会发出 `authorization` 标头。
`packages/client/ui-models/tests/` 以同一张表加上形状用例钉住 `apiKeyFailure`,并驱动两张卡片:留空的输入框可提交且不写入凭据、只含空白的输入框在字段上失败、非法或被包裹的 Key 同时拦截提交与探测、带首尾空白的 Key 在 `credentials.set` 与探测之前被 trim以及手工声明的路由可以完全不带 Key 创建。
用户可见的终态则钉在它真正被组装的位置:`examples/headless-agent/tests/headless.snapshot.ts` 让 one-shot 应用在一个 HTTP 标头无法承载的已存密钥下运行,复用其 missing-credential 兄弟场景的同一套无密钥 composition并记录该轮以 `INVALID_CREDENTIAL` 结束、消息可操作且既不含密钥也不含 `ByteString` 字样。包级测试无法证明这一点,而 web e2e 只覆盖了浏览器那一半。
用户可见的终态则钉在它真正被组装的位置:`examples/headless-agent/tests/headless.snapshot.ts` 让 one-shot 应用在一个 HTTP 标头无法承载的已存密钥下运行,复用其 missing-credential 兄弟场景的同一套无密钥 composition并记录该轮`INVALID_CREDENTIAL` 结束、消息可操作且既不含密钥也不含 `ByteString` 字样。包级测试无法证明这一点,而 web 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/bug-fix/2026-08-06-list-agents-residency-vocabulary.md
2026-08-06-list-agents-residency-vocabulary.md: 01fff958921465909f5cd300dc355fdac6ec0772
2026-08-06-list-agents-residency-vocabulary.zh.md: d68b9857de23ccb0b68f0bfafba153fde63303a7
2026-08-06-list-agents-residency-vocabulary.zh.md: ba528ade01a5adf6302511735168d292fe3aee89

View File

@@ -4,13 +4,13 @@ Status: implemented
[English](2026-08-06-list-agents-residency-vocabulary.md) | 中文
## Problem
## 问题
`list_agents` 把可继续 child 的进程驻留状态投影为 `running | idle | complete``complete` 读起来像一项终态工作,且结果就在某处,但底层事实只表示没有驻留的 Activation对话完好无损`send_message` 可以继续它,而且它对 child 的结果不作任何断言。读到 `complete` 的模型会合理地寻找可收集结果,或向一个它以为已经结束的对话发送替代工作。
这个词与[由管理器负责的结算投递](../feature/2026-08-06-manager-owned-subagent-settlement-delivery.md)同时出现时尤其容易误导。完成会以通知到达 parent列表用于回忆持久化对话而不是轮询该通知。
## Decision
## 决策
面向模型的投影报告 `running | idle | ready`
@@ -22,7 +22,7 @@ Status: implemented
服务层不变。`SubagentListEntry.activity` 保留 `'running' | 'inactive'`,对 UI 等消费方而言,这准确描述了语料驻留状态。面向模型的适配器把 `inactive` 映射为 `ready`,因为这个词表达了模型可执行的操作,而没有虚构结果。
## Alternatives considered
## 考虑过的替代方案
**保留 `complete`,并在描述中限定它。** 一段解释 `complete` 不代表完成的描述,每次被读取时都在与渲染状态对抗。模型扫读的那一行必须自身表达正确区别。
@@ -32,7 +32,7 @@ Status: implemented
**重命名服务活动值。** `running | inactive` 在服务层是正确的,并且有非模型消费方。为了修复一个适配器的呈现而搅动通用契约并不合理;[持久化目录 Agent Note](../feature/2026-07-22-durable-subagent-catalog-and-list-agents.md) 继续拥有该服务词汇。
## Consequences
## 后果
- 渲染行使用 `<id> [running] — <label>``<id> [idle] — <label>``<id> [ready] — <label>`
- 输出 schema 的 `status` 枚举与渲染契约一同变化。生成的工具目录会带上新描述;它只渲染每个工具的 `parameters`,从来不收录输出 schema。

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/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.md
2026-08-06-onboarding-step-owned-takeover-chrome.md: 97f5335412f256bf7e60f36731c9e0c4475ee99e
2026-08-06-onboarding-step-owned-takeover-chrome.zh.md: 054072764a7b9a25586899a93596fdc922e71dcb
2026-08-06-onboarding-step-owned-takeover-chrome.zh.md: 10adb8e5bd5e90cfc7c18fb68e5e5fbf5214994a

View File

@@ -22,14 +22,14 @@
**把 `settings.onboarding` 改成 chain 并把完成集合外置为 store。** composer takeover 的版型做过原型后回退。selector 只能判定 owner props私有就绪事实仍然只能在组件内部解析——chain 买来的是当前两个步骤并不需要的路由通用性,代价却是跨三个包的约定变更。
**在渲染点探测 slot 输出为空。** `renderSlot` 无条件返回 outlet 元素owner 无法据步骤的 `null` 分支;探测已渲染 DOM 是否为空需要先提交再撤回的手法,其动态翻转会失去 paint 前的保证。
**在渲染点探测 slot 输出为空。** `renderSlot` 无条件返回 outlet 元素owner 无法据步骤的 `null` 进行分支判断;探测已渲染 DOM 是否为空需要先提交再撤回的手法,其动态翻转会失去 paint 前的保证。
## 后果
步骤已挂载但尚未判定期间,应用保持可见且可交互:判定窗口内 `#root` 不再是 `inert`(此前在不透明图层背后被置灰)。对真正未配置的用户,接管层比从前晚一个 join 往返出现——但一出现就带着内容,而不是先露出空白展示层再填充。
步骤已挂载但尚未判定期间,应用保持可见且可交互:判定窗口内 `#root` 不再是 `inert`(此前在不透明图层背后处于 inert 状态)。对真正未配置的用户,接管层比从前晚一个 join 往返出现——但一出现就带着内容,而不是先露出空白展示层再填充。
未来若有步骤注册后不把可见内容包进 `OnboardingSurface`会无遮罩地裸渲染在应用之上slot 约定的 JSDoc 已把包裹写为注册方的义务。
## 测试
`packages/client/ui-primitives/tests/onboarding-surface.client.spec.tsx` 钉住原语行为:内容的 body portal、遮罩展示层类名存在、`#root``inert` 恰好持续挂载生命周期,以及无 `#root` 的组合。`packages/client/ui-settings-general/tests/settings-root.client.spec.tsx` 钉住反转后的外壳约定:已挂载步骤什么都不渲染时,无接管界面框架、无 inert。`apps/web/tests/onboarding-deepseek-config.e2e.ts` 新增本缺陷的整装回归钉:已配置世界刷新页面,同时在浏览器网络边界扣住所有 `settings.describe` 响应——把步骤的判定窗口从 loopback 下不可见拉宽到数百毫秒,这正是断言保持非空洞的关键——页内 8ms 采样器证明接管界面框架从未挂载、`#root` 从未变为 inert。该文件的既有场景与步骤 spec`ui-settings-general``ui-models`)原样通过——样式表逐字迁移,遮罩选择器与几何钉子得以幸存。
`packages/client/ui-primitives/tests/onboarding-surface.client.spec.tsx` 钉住原语行为:包裹内容的 body portal、遮罩展示层类名存在、`#root``inert` 恰好持续挂载生命周期,以及无 `#root` 的组合。`packages/client/ui-settings-general/tests/settings-root.client.spec.tsx` 钉住反转后的外壳约定:已挂载步骤什么都不渲染时,无接管界面框架、无 inert。`apps/web/tests/onboarding-deepseek-config.e2e.ts` 新增本缺陷的整装回归钉:已配置世界刷新页面,同时在浏览器网络边界扣住所有 `settings.describe` 响应——把步骤的判定窗口从 loopback 下不可见拉宽到数百毫秒,这正是断言保持非空洞的关键——页内 8ms 采样器证明接管界面框架从未挂载、`#root` 从未变为 inert。该文件的既有场景与步骤 spec`ui-settings-general``ui-models`)原样通过——样式表逐字迁移,遮罩选择器与几何钉子得以幸存。

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/bug-fix/2026-08-07-cancel-convergence-wake-latch.md
2026-08-07-cancel-convergence-wake-latch.md: de18dce87e28eaf0548eebd0d054d4ca7b59d0a6
2026-08-07-cancel-convergence-wake-latch.zh.md: bb67785512be9cd7b087d9f6c98f165a2a6468a6
2026-08-07-cancel-convergence-wake-latch.zh.md: 7ff536b58ed7eabf52f474a61e73b2f5cc93ae06

View File

@@ -6,24 +6,24 @@ Status: implemented
## 问题
`Agent.cancel(cause, { keepInbox: true })` 在触发 abort 信号后立即返回,但活动 driver 可能尚未收敛到 `idle`LLM 流拆除、工具取消与 `turn/end` 落盘都会在 `abort()` 返回后异步展开。在该窗口内到达的唤醒 send 被放入 `next-turn`,而 `wakeDriver()` 对仍处于 `running` 的 phase 直接返回,退出的 driver 也从不重放这次唤醒——消息会一直停放到下一条唤醒 send 到达。被中止的 `runMaintenance` 活动周围也存在同样的唤醒丢失窗口。多个测试固化了停放行为(「等待下一次唤醒」);该缺陷同时破坏了 `session.cancel``subagent.interrupt` 组合路径issue #1838)。拥有取消与发送约定既有决策[显式轮次取消](../architecture/2026-07-16-explicit-turn-cancellation.md)与[统一发送](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md);生产环境中的 `keepInbox` 消费方是[Web 停止保留队列](2026-07-31-web-stop-preserves-queue.md)。
`Agent.cancel(cause, { keepInbox: true })` 在触发 abort 信号后立即返回,但活动 driver 可能尚未收敛到 `idle`LLM(大语言模型)流拆除、工具取消与 `turn/end` 落盘都会在 `abort()` 返回后异步展开。在该窗口内到达的唤醒 send 被放入 `next-turn`,而 `wakeDriver()` 对仍处于 `running` 的 phase 直接返回,退出的 driver 也从不重放这次唤醒——消息会一直停放到下一条唤醒 send 到达。被中止的 `runMaintenance` 活动周围也存在同样的唤醒丢失窗口。多个测试固化了停放行为(「等待下一次唤醒」);该缺陷同时破坏了 `session.cancel``subagent.interrupt` 组合路径issue #1838)。取消与发送约定由以下既有决策定义:[显式轮次取消](../architecture/2026-07-16-explicit-turn-cancellation.md)与[统一发送](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md);生产环境中的 `keepInbox` 消费方是[Web 停止保留队列](2026-07-31-web-stop-preserves-queue.md)。
## 决策
`running` phase 携带 `wakeRequested` 锁存,与既有的 `maintenance` phase 字段对称。`wakeDriver()` 在当前活动无法投递唤醒时锁存——maintenance 任务从不读取队列,被中止的活动收敛后不会重启——而存活的 driver 不需要锁存,因为它自己会认领排队的工作。退出中的活动在其自身收敛边界(`kick``finally``runMaintenance``finally`)重放锁存:这一位置保证 `turn/end N` 先于重放 driver 打开 `turn/start N+1` 落盘,并保证 `whenIdle()` 通过其 `activityDone` 循环看到重放 driver。两个重放点仅在 `inbox.hasPending` 时执行,因此收敛前被从 inbox 移除的锁存唤醒不会启动空 driver。而 agent 已处于 idle 时发送的唤醒,即使消息在 driver 认领前被清除,仍会打开自己的 turn 边界——这趟 `idle → running → idle` 转换是可观察约定:goal-session driver 的 pause/disarm 回退依赖取消预订后的 `idle` 转换触发(把守卫放进 `wakeDriver()` 会抑制该边界)。不带 `keepInbox``cancel()` 会连同 inbox 一起清除锁存。
`running` phase 携带 `wakeRequested` 锁存,与既有的 `maintenance` phase 字段对称。`wakeDriver()` 在当前活动无法投递唤醒时锁存——maintenance 任务从不读取队列,被中止的活动收敛后不会重启——而存活的 driver 不需要锁存,因为它自己会认领排队的工作。退出中的活动在其自身收敛边界(`kick``finally``runMaintenance``finally`)重放锁存:这一位置保证 `turn/end N` 先于重放 driver 打开 `turn/start N+1` 落盘,并保证 `whenIdle()` 通过其 `activityDone` 循环看到重放 driver。两个重放点仅在 `inbox.hasPending` 时执行,因此收敛前被从 inbox 移除的锁存唤醒不会启动空 driver。而 agent(智能体)已处于 idle 时发送的唤醒,即使消息在 driver 认领前被清除,仍会打开自己的轮次边界——这趟 `idle → running → idle` 转换是可观察约定:目标会话 driver 的 pause/disarm 回退依赖取消预订后的 `idle` 转换触发(把守卫放进 `wakeDriver()` 会抑制该边界)。不带 `keepInbox``cancel()` 会连同 inbox 一起清除锁存。
`signal.aborted` 判别项是承重的:它区分「中断前已排队的工作」——`keepInbox` 将其停放以待后续唤醒(`keepInbox` 停放约定——与「abort 后显式的唤醒」,后者必须在收敛后执行。
`signal.aborted` 这一判别条件至关重要:它区分「中断前已排队的工作」——`keepInbox` 将其停放以待后续唤醒(`keepInbox` 停放约定——与「abort 后显式的唤醒」,后者必须在收敛后执行。
## 备选方案
**让 `cancel()` 立即把 phase 置为 `idle`。** 不予采用driver 仍在展开收尾,这会重叠两个 driver。重放逻辑位于旧 driver 的 `finally`,而该 `finally` 此后不再执行——83 个测试中有 14 个失败,多个死锁。修复它需要基于身份的 phase 所有权外加 turn 打开时的 quiescence 屏障,机制上严格更重,而且该屏障就是换了个形态的锁存。
**让 `cancel()` 立即把 phase 置为 `idle`。** 不予采用driver 仍在展开收尾,这会重叠两个 driver。重放逻辑位于旧 driver 的 `finally`,而该 `finally` 此后不再执行——83 个测试中有 14 个失败,多个死锁。修复它需要基于身份的 phase 所有权外加轮次开启时的完全停稳屏障,机制上严格更重,而且整体上只是换了个形态的锁存。
**对每个非 idle 唤醒无条件锁存。** 不予采用:中断前的唤醒会在 `keepInbox` 取消后自动启动,违反 `keepInbox` 停放约定;「停放排队工作」测试与错误窗口的 steering 测试双双失败。
**对每个非 idle 唤醒无条件锁存。** 不予采用:中断前的唤醒会在 `keepInbox` 取消后自动启动,违反 `keepInbox` 停放约定;「停放排队工作」测试与错误窗口的 steering(中途引导)测试双双失败。
**通过链式 promise`activityDone.then(...)`)重放。** 不予采用:重放会运行在活动自身结算之外,`whenIdle()` 的循环可能在重放 driver 启动前就 resolve修复它需要在 send 时同步替换 `activityDone`,并依赖微任务反应顺序——比同步 flag 更脆弱。
**在 subagent adapter 中等待 quiescence** issue 范围否决:修复由取消/唤醒状态机拥有,而不是消费方。
**在 subagent(子智能体)适配器中等待完全停稳** issue 范围而不予采用:修复由取消/唤醒状态机拥有,而不是消费方。
## 影响
`running` phase 新增 `wakeRequested` 字段;不带 `keepInbox``cancel()` 会连同 inbox 一起清除它,且 `disposed` 取消从不锁存——dispose 开始后到达的唤醒保持停放,`whenIdle()` 不会在拆除中的会话上等待一个完整模型 turn。落在 driver 最后一次 `hasPending` 检查与退出之间微任务间隙的唤醒仍会停放——没有锁存触发,因为 phase 是 `running` 且未 abort关闭该间隙需要无条件锁存刻意留作范围。在被中止的 turn 与重放 driver 之间,状态转换会发出一次瞬态 `idle → running` 对。唤醒 send 的消息在任何 driver 认领前被清除时,仍会打开一个空的 completed turn,保留可观察的唤醒边界。
`running` phase 新增 `wakeRequested` 字段;不带 `keepInbox``cancel()` 会连同 inbox 一起清除它,且 `disposed` 取消从不锁存——dispose(资源释放)开始后到达的唤醒保持停放,`whenIdle()` 不会在拆除中的会话上等待一个完整模型轮次。落在 driver 最后一次 `hasPending` 检查与退出之间不到一个微任务间隙的唤醒仍会停放——没有锁存触发,因为 phase 是 `running` 且未 abort关闭该间隙需要无条件锁存刻意不纳入范围。在被中止的轮次与重放 driver 之间,状态转换会发出一次瞬态 `idle → running` 对。唤醒 send 的消息在任何 driver 认领前被清除时,仍会打开一个已完成的空轮次,保留可观察的唤醒边界。

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/bug-fix/2026-08-09-broken-preset-roster-rows.md
2026-08-09-broken-preset-roster-rows.md: 069585957d4d99598cc38e4a7c6bc8c8d82490ca
2026-08-09-broken-preset-roster-rows.zh.md: d541292b59496464eb91bc03278e0800af16c4a3
2026-08-09-broken-preset-roster-rows.zh.md: 3f8a8b8fa7aeee04cf0d6dc05c398456b6c67bb3

View File

@@ -1,4 +1,4 @@
# Agent Note损坏的 preset 是名单行,不是空缺
# Agent Note: 损坏的 preset 是名单行,不是空缺
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/bug-fix/2026-08-10-child-agents-join-their-parent-preset.md
2026-08-10-child-agents-join-their-parent-preset.md: 4534004ad54df69822872b9595a29443fc3a990b
2026-08-10-child-agents-join-their-parent-preset.zh.md: bdf9928bea4b75e2915c8adf5c15f8a01c6583e4
2026-08-10-child-agents-join-their-parent-preset.zh.md: 550a952efefd2490924ed11f7a0e110694571855

View File

@@ -4,7 +4,7 @@ Status: implemented
[English](2026-08-10-child-agents-join-their-parent-preset.md) | 中文
## Problem
## 问题
工具与提示段的可见性沿 `dsh-scope` 的父链继承,而 agent 的 scope key 铸造出来时没有父。[逐会话 agent preset](../architecture/2026-08-03-per-session-agent-presets.md) 把所有面向模型的行搬到了 agent 平面,并让 `AgentPresets.mount()` 成为绑定那条父链的唯一途径——调用点在 api-proxy 的会话创建、恢复与 fork 路径上。两个进程内 subagent 驱动通过 `applyChildComposition()` 组装子 agent而它只安装了逐子 agent 的 persona 与工具限制,于是子 agent 的 scope 链长度为一,其注册表视图只能解析到全局层。
@@ -12,7 +12,7 @@ Status: implemented
子 agent 的持久化 header 让问题更进一步。`childSessionMeta()` 不记录任何 preset于是冷读一个子会话解析到的是部署默认值——一套该子 agent 从未运行过的工具集,而这正是"模型可见 ⟺ 已记录"规则要杜绝的情形。
## Decision
## 决策
`AgentPresets.composeFrom(agentCtx, parentCtx)` 让一个 agent 加入另一个 agent 已在运行的常驻组装,并返回所加入的 preset id。它通过 `standingMountFor()` 定位父方的挂载——agent 的 key 认父到其 preset 的常驻 key正是 `serviceForAgent()` 读取的同一关系——再把子 agent 的 key 绑到同一个常驻 key 上,绑定句柄仍归 roster 独有的重链权威持有。未加入任何 preset 的父方不产生加入、也不报错,那就是无 roster 的部署:它面向模型的行位于宿主组装中,子 agent 已经能通过全局层解析到它们。
@@ -24,7 +24,7 @@ Status: implemented
把父方的工具交给子 agent 之后,暴露出同一次 agent 平面搬迁引入的第二个缺陷:`ToolRegistry` 把**作用域级**注册排除在限制之外、只过滤全局层,因此当所有面向模型的行都变成祖先贡献之后,子 agent 的 `toolFilter` 就不再约束任何东西——而且全局层为空时,`restrict()` 会把收到的每个名字都判为未知并直接让子 agent 创建失败。豁免集合应当是作用域**自己注册**的工具,而不是恰好位于全局层的工具;后一种读法只在这两个集合重合时才成立。`view()` 现在过滤作用域继承来的一切——全局层与每个祖先层——只豁免它自己那层。这条自身层豁免是承重的而非顺带的:委派运行时把子 agent 的 `report` 与结构化输出工具注册进子 agent 自己那层,而一个只点名子 agent 可用能力的过滤器绝不能把它回报所依赖的机制一并剥掉。
## Alternatives considered
## 考虑过的替代方案
**在子 agent 的 setup 里按 id 重新挂载父方的 preset。** 语义与机制两方面都不成立而被否决。它会重读 roster 并重新 stat 组装文件,因此父方启动后的一次编辑就会把子 agent 分叉到另一个代际,而此后被删除的 preset 会让子 agent 失败、父方却照常运行。`mount()` 还是异步的,同步的创建窗口无法在不重构两个驱动的前提下接受它。
@@ -38,7 +38,7 @@ Status: implemented
**只修活着的加入,不动持久化 header。** 否决,因为那样活着的子 agent 与冷读同一个子 agent 会对"哪份组装产出了这段历史"给出不同答案——同一类缺陷,只是被搬了个地方而不是被修掉。
## Testing
## 测试
`packages/preset/agent-presets/tests/mount.spec.ts` 用真实 fixture 组装覆盖该加入:子 agent 看到父方的工具与提示段、不会挂载出第二个代际、加入在父方 dispose 后依然成立(活得比父方久的后台子 agent、上报的 id 一致、没有 preset 的父方不产生加入、以及无 scope 的上下文被拒绝。
@@ -48,7 +48,7 @@ Status: implemented
组装记录这一层用的是真实 shipped Web 组装的 e2e而不是无密钥快照。本仓库所有可运行 example 都不组装 preset roster因此该缺陷在快照 harness 里根本不可观察:要做快照场景,得先有一个既挂载 roster 又发起委派的 example。Web e2e 启动的是真实的 `base` + `web-app` 补丁层与两个 shipped preset这正是测试政策要求的组装证据Web 浏览器 lane 的 subagent golden 承载了可见后果——记录了 preset 的子 agent 现在会显示与其父方相同的 preset 徽标。
## Consequences
## 后果
委派现在的成本是每个子 agent 一次 scope 认父,再无其他——没有额外的插件实例、没有 roster 读取、没有新的失败模式。子 agent 的能力恰好等于父方的能力,减去它自己的 `toolFilter` 所移除的部分;逐 subagent 的 preset"agent 类型")仍未构建,那会是一个新的请求字段,而不是对这次加入的改动。

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/bug-fix/2026-08-10-pre-plugin-theme-bootstrap.md
2026-08-10-pre-plugin-theme-bootstrap.md: 276d564dcef60e5130b4b2d0ef7cbc12eeb8b6d6
2026-08-10-pre-plugin-theme-bootstrap.zh.md: 7162b2d851b50f75b3af6320ad9348383933e50f
2026-08-10-pre-plugin-theme-bootstrap.zh.md: f9384df6e0ed5bf42c0671b70f484f57080060a0

View File

@@ -4,13 +4,13 @@ Status: implemented
[English](2026-08-10-pre-plugin-theme-bootstrap.md) | 中文
## Problem
## 问题
Web 壳在浏览器侧插件树激活前呈现 `Loading plugins…`。主题 token 已随壳样式加载,但 `color-scheme``body[data-ds-dark-theme]` 要等 ui-theme 的 ThemeService 与 ui-layout 的 ThemePresenter 激活后才写入;持久化偏好为深色时,加载页因此先按浅色调色板绘制,再切为深色。
`dshClient.immediately` 只把 bundle 纳入第一阶段预取,不会让插件在 HTML 解析或壳首次渲染前执行。仅调整客户端插件的加载档位无法关闭这段时间窗口。
## Decision
## 决策
ui-theme 的主机侧通过 `ctx.httpServer.tapIndex()` 转换每份 index HTML`<body>` 起始标签后紧接一段同步内联脚本。该转换通过可选的 `httpServer` 注入注册,因此不含该服务的组合仍会激活 ui-theme但不会安装转换。HTML 解析器执行该脚本时body 已存在,而壳的模块脚本与 React 根节点尚未运行。
@@ -18,11 +18,11 @@ settings provider 存在时,主机侧会注册 [`ui-theme.preference` settings
引导逻辑只认识内建的 `light``dark``system` 语义,不注册监听器,也不解析第三方主题或 token 覆盖。浏览器侧插件树激活后ThemeService 仍是主题状态的权威来源ThemePresenter 会把完整解析结果重新写入同一组 DOM 状态并负责后续更新与释放。
## Verification
## 验证
ui-theme 的单元测试覆盖不含任一可选 Host 服务时的激活、脚本位置、Host 设置优先级、系统偏好、缺少 `matchMedia`、不含 body 的输入、实时读取 settings以及 Host 注册随插件 fiber 一同释放。真实 Web 组合的 Chromium 场景会选择持久化深色偏好并拦住插件 bundle 请求,使加载页保持可观察,再断言 index 响应产生了深色背景、body 属性和根元素 `color-scheme`。该变化不改变可访问性树,因此不产生新的页面 golden。
## Alternatives considered
## 考虑过的替代方案
**把逻辑固定写进 `apps/web/index.html`。** 这样能在相同时机执行,但静态 HTML 无法嵌入当前 Host 设置,还会复制 ui-theme 拥有的偏好解析和 DOM 字段Host 转换会跟随主题插件的生命周期,并让应用壳无需了解主题领域。
@@ -32,6 +32,6 @@ ui-theme 的单元测试覆盖不含任一可选 Host 服务时的激活、脚
**在 `<head>` 中执行并给 html 添加临时类。** body 此时尚不存在,还需要一套与正式调色板属性不同的临时选择器。紧接 `<body>` 是能够直接写正式 DOM 字段的最早解析位置。
## Consequences
## 后果
加载页首帧与持久化内建偏好一致;未组合 settings provider 时则默认采用系统偏好。index 转换会为每份响应读取 Host settings而内联脚本只包含选定的内建值与 `system` 解析逻辑。内建偏好语义或 ThemePresenter DOM 字段变化时,必须同时更新脚本与 ThemeService。自定义主题仍会在浏览器插件激活后才完整应用加载期间页面使用该主题解析后的浅色或深色基础调色板。

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/bug-fix/2026-08-10-session-row-identity-covers-the-preset.md
2026-08-10-session-row-identity-covers-the-preset.md: 7a89dcb4e4ae292a06a1743842d2e9cf6bd96282
2026-08-10-session-row-identity-covers-the-preset.zh.md: 7ffa3423818bcc867c942651540db1975737e073
2026-08-10-session-row-identity-covers-the-preset.zh.md: c81509d9cffa8d4cf729d5a7ae490a9095d10906

View File

@@ -1,10 +1,10 @@
# Agent Note会话行的标识判定纳入 preset
# Agent Note: 会话行的标识判定纳入 preset
Status: implemented
[English](2026-08-10-session-row-identity-covers-the-preset.md) | 中文
## Problem
## 问题
`SessionManager.buildListSnapshot` 按值对列表行做记忆化:一次 wire 刷新会铸造全新的 summary 对象,因此与缓存项相等的行会被替换为缓存实例,下游每一个 `SessionListItem` memo 才能持续命中。它声明的约定是「每个字段都相同就复用缓存对象」,而那段比较是手写枚举字段的,其中没有 `agentPreset`
@@ -12,11 +12,11 @@ Status: implemented
hero 上的 chip 正是其中一个读取方,而且它在发出任何请求之前会拿这次选择和那一行比较。切回会话创建时的那个 preset在它看来就是「已经是这个 preset 了」,于是丢弃 stage、根本不发 RPC——chip 的标签变了,组成没变。一个会话可以从创建时的 preset 切走一次,然后再也切不回来。
## Decision
## 决策
标识判定把 `agentPreset` 与其余 summary 字段一起比较这本就是「每个字段都相同」所声称的内容。其他一概不动记忆化、合并、chip 的 no-op 检查各自都是对的——只要它们读到的那一行是对的。
## Alternatives considered
## 考虑过的替代方案
**让 chip 改为直接读宿主,而不是读列表行。** 这样能绕开过期的行,但会话头部的标签同样以这一行为准,过期状态会在最显眼的界面里留下来;而且将来任何 `SessionSummary.agentPreset` 的读取方都会继承同一个陷阱。
@@ -24,14 +24,14 @@ hero 上的 chip 正是其中一个读取方,而且它在发出任何请求之
**改成结构化比较,而不是逐字段枚举。** 通用的深比较不能盲目加:行上带有 `projectionValues`,它的引用标识本身就是「投影 store 重新发布了」这一有意为之的信号,把它折进值比较,要么每个投影 tick 都重渲染,要么把一次真实变化掩盖掉。
## Consequences
## 后果
会话行携带的每个字段现在都参与行标识,因此读取 `SessionSummary.agentPreset` 的界面会在宿主确认后立刻看到切换,会话头部标签也包含在内。该判定仍是手写枚举,所以将来给 `SessionSummary` 新增字段时必须同步加进来;`sessions-service` 的投影测试为下一个这样的字段点明了失效形态,而不只是钉住这一次。
## Testing
## 测试
`sessions-service.spec.ts` 入一行空会话、记录一次切换,并断言投影快照报告的是新 preset——在旧判定下它会失败因为这一行别处都没变。`agent-preset-selection` web e2e 先向下切再向上切,断言宿主认可第二次切换、`/` 目录随之回来;没有这次修复,第二次切换根本到不了宿主。
`sessions-service.spec.ts` 入一个空白行、记录一次切换,并断言投影快照报告的是新 preset——在旧判定下它会失败因为这一行别处都没变。`agent-preset-selection` web e2e 先向下切再向上切,断言宿主认可第二次切换、`/` 目录随之回来;没有这次修复,第二次切换根本到不了宿主。
## Related
## 相关内容
同一条 e2e 也覆盖[目录失效的修复](2026-08-10-slash-catalog-follows-preset-switch.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/bug-fix/2026-08-10-slash-catalog-follows-preset-switch.md
2026-08-10-slash-catalog-follows-preset-switch.md: ec85d4a3a6a053e91c368f65ecb12f123c45287d
2026-08-10-slash-catalog-follows-preset-switch.zh.md: b68fa90e99c3597c61b3edcc03399bb7ea7844f4
2026-08-10-slash-catalog-follows-preset-switch.zh.md: 9b464dc54c125e2e15433871dc804981d916744d

View File

@@ -1,10 +1,10 @@
# Agent Note斜杠目录跟随空会话的 preset 切换
# Agent Note: 斜杠目录跟随空会话的 preset 切换
Status: implemented
[English](2026-08-10-slash-catalog-follows-preset-switch.md) | 中文
## Problem
## 问题
preset 把决定 `/` 菜单内容的那些行搬走了。Web 组装禁用了宿主面的 `skill-local``tool-skill``plan-mode``command-compact`,改由 preset 提供,因此一个会话有哪些命令和技能,是它自身组成的属性,而不是部署的属性。
@@ -12,7 +12,7 @@ preset 把决定 `/` 菜单内容的那些行搬走了。Web 组装禁用了宿
于是菜单继续提供会话已经不再运行的那套组成。向下切换后 `compact``plan` 和全部项目技能仍列在菜单里;向上切换后留在原地的是更窄的目录——四条宿主面行加客户端自己的 `model` 贡献——而且完全没有技能,这正是 bug 报告描述的现象。只有当某个无关的注册表变化或一次重连恰好使其失效时,目录才会自愈。
## Decision
## 决策
这次切换的提交点是落账的 `agent-preset/selected` 事件。preset owner 将该提交重新发为 client-safe 的 cordis owner 事件 `agent-preset/selected(sessionId, agentPreset)`,宿主流原样转发它,两份目录各自通过 `ctx.remote.$on` 直接订阅:`ui-command` 软刷新该键(新快照落地前,旧快照继续服务已打开的菜单),`ui-skill` 让它失效(并中止在途的预热,使一次与切换赛跑的 warm 无法发布过期目录)。
@@ -20,7 +20,7 @@ preset 把决定 `/` 菜单内容的那些行搬走了。Web 组装禁用了宿
从落账事件而不是 RPC 处理器的返回值派生 owner 事件,使「这个会话的组成变了」只有一个权威来源:每个已连接的客户端都能观察到这次切换,而不只是发起它的那个标签页;不是发起方的客户端也无需从一个根本不会到来的注册表信号里去推断。
## Alternatives considered
## 考虑过的替代方案
**在客户端自己的 `agentPresets.select` 回调里就地失效。** 改动最小,而且第一轮之后 preset 就锁定hero 上的 chip 是切换唯一可能的发起处。否决理由是失效逻辑会落在恰好发起 RPC 的那个界面上,而不是提交点:同一个空会话在第二个标签页里仍是过期菜单,将来任何宿主侧的重组也完全没有信号。
@@ -28,11 +28,11 @@ preset 把决定 `/` 菜单内容的那些行搬走了。Web 组装禁用了宿
**复用转发的 `commands/change`。** 它是既有的目录失效事件,但它是注册表级的、不带会话、也与技能无关;客户端会把每个会话的命令都重拉一遍,却依然永远刷不新技能目录。
## Consequences
## 后果
转发名单加入了 preset owner 的类型化事件,而每一份由 preset 决定的目录从此有了统一的订阅点将来任何从组成派生的按会话界面都在同一个信号上失效而不必再发明一个。owner 事件仍是落账事实的第二次发布,因此将来若出现一条不落账就重组的切换路径,它将无人宣告。`ui-command` 保持软失效(已打开的菜单不会变空),而 `ui-skill` 直接丢弃该项,因为技能目录没有「部分可服务」的状态;在重拉窗口内打开的菜单,那一瞬间显示的是没有技能,而不是错误的技能。
## Testing
## 测试
`api-proxy-agent-preset.spec.ts` 断言已提交的切换恰好转发一次,并带上会话与新 preset`ui-agent-preset``ui-command``ui-skill` 的 spec 断言直接 Remote 订阅会合并会话行或只重拉被重组的会话。`agent-preset-selection` web e2e 播种一个项目技能,并在 hero chip 应用 `minimal` 之后断言 `/` 菜单丢掉了 `compact``plan` 和该技能,同时保留宿主面的那几行——这是面板跟随组成的整装应用证据。

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/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md
2026-08-11-preset-authoring-agent-validates-its-own-composition.md: eb21094f0d859a31d5f16d780cada6818a508b36
2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md: 02c245348a9c7e9968472044d7ff95e1ff21120c
2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md: 1db4197673f18979907c46376cbb7102160172b5

View File

@@ -4,7 +4,7 @@ Status: implemented
[English](2026-08-11-preset-authoring-agent-validates-its-own-composition.md) | 中文
## Problem
## 问题
`cordis` preset 随包发布 `editing-cordis-compositions`,它是 agent 创作 preset 时唯一的指导来源。其中四条陈述与事实不符,而分量最重的两条恰好指向该 skill 自称「最容易让人栽跟头的规则」。
@@ -18,7 +18,7 @@ Status: implemented
四条之下还压着一个能力断言agent「自己起不了会话」于是校验退化成肉眼核对 YAML 字段,再把结果经设置页的红色标记交给用户。那个标记是发现阶段的结构检查,远弱于这句话给人的印象。
## Decision
## 决策
skill 教 agent 通过 `ctx.agentPresets` 自行挂载校验其组装,其余每个示例都取自同一仓库中已发布的组装。
@@ -53,7 +53,7 @@ agent 按 `cordis_mount` 自身文档所述的方式够到 roster 服务:挂
skill 自带的 `cordis_mount` 代码片段经工具注册表逐字执行:它成功挂载,其 `preset_check` 工具在下一次读取时出现在组装该 agent 的目录中,对有效 preset 回答 `mounted OK`,对无效 preset 回答挂载拒绝原因。
## Alternatives considered
## 考虑过的替代方案
**把校验留给用户,只修四处错误。** 这些错误与那句能力断言同源——指导是按 preset 层的公开面写的,而不是按被组装出的 agent 实际够得到的东西写的——而无法自查的 agent 交出的组装,其缺陷设置页同样看不见。
@@ -61,7 +61,7 @@ skill 自带的 `cordis_mount` 代码片段经工具注册表逐字执行:它
**给 preset 加一个一等的 preset 校验工具。** 组合出的路径已经存在,且由 `cordis_mount` 自己的 schema 记载;专用工具会给一个「无需专用工具即可够到运行时」的 preset 再添一个面向模型的行。
## Consequences
## 后果
- 校验成功会留下一个永不回收的常驻代际,这是 roster 按代际本就承担的[常驻挂载](../architecture/2026-08-08-per-preset-standing-mounts.md)代价——由 agent 在编辑收尾时付一次,而不是由用户在首次会话时付。
- skill 现在依赖 `cordis_inspect` 生成的 API 目录对 `agentPresets` 保持最新;`doc-sync` 中的 `verify-cordis-api` 是守住这一点的门禁。