Merge remote-tracking branch 'origin/master' into xjt/generated-docs-zh-translation-apply

# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md
#	.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md
#	.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md
#	.agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.zh.md
#	.agents/notes/implemented/architecture/2026-07-28-directory-picker-capability-seam.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-28-directory-picker-capability-seam.zh.md
#	.agents/notes/implemented/architecture/2026-07-30-static-repository-plugin-format.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-30-static-repository-plugin-format.zh.md
#	.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md
#	.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md
#	.agents/notes/implemented/architecture/2026-08-01-packaged-ripgrep-search.i18n.yaml
#	.agents/notes/implemented/architecture/2026-08-01-packaged-ripgrep-search.zh.md
#	.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.i18n.yaml
#	.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.zh.md
#	.agents/notes/implemented/architecture/2026-08-05-large-session-jsonl-restore-pipeline.i18n.yaml
#	.agents/notes/implemented/architecture/2026-08-05-large-session-jsonl-restore-pipeline.zh.md
#	.agents/notes/implemented/architecture/2026-08-06-agent-event-payload-objects.i18n.yaml
#	.agents/notes/implemented/architecture/2026-08-06-agent-event-payload-objects.zh.md
#	.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.i18n.yaml
#	.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.zh.md
#	.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.i18n.yaml
#	.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.zh.md
#	.agents/notes/implemented/architecture/2026-08-08-client-tool-presentation-ownership.i18n.yaml
#	.agents/notes/implemented/architecture/2026-08-08-client-tool-presentation-ownership.zh.md
#	.agents/notes/implemented/bug-fix/2026-07-27-glob-sampling.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-07-27-glob-sampling.zh.md
#	.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.zh.md
#	.agents/notes/implemented/bug-fix/2026-07-28-web-gui-feedback-loop.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-07-28-web-gui-feedback-loop.zh.md
#	.agents/notes/implemented/bug-fix/2026-07-29-human-transcript-append-origin.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-07-29-human-transcript-append-origin.zh.md
#	.agents/notes/implemented/bug-fix/2026-07-29-web-details-session-lifecycle.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-07-29-web-details-session-lifecycle.zh.md
#	.agents/notes/implemented/bug-fix/2026-07-30-web-transcript-log-ordered-projection.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-07-30-web-transcript-log-ordered-projection.zh.md
#	.agents/notes/implemented/bug-fix/2026-07-31-fork-anchor-floors-to-event-seq.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-07-31-fork-anchor-floors-to-event-seq.zh.md
#	.agents/notes/implemented/bug-fix/2026-07-31-web-stop-preserves-queue.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-07-31-web-stop-preserves-queue.zh.md
#	.agents/notes/implemented/bug-fix/2026-08-02-goal-round-wrapup-message.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-08-02-goal-round-wrapup-message.zh.md
#	.agents/notes/implemented/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.zh.md
#	.agents/notes/implemented/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.zh.md
#	.agents/notes/implemented/bug-fix/2026-08-06-provider-credential-lifecycle.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-08-06-provider-credential-lifecycle.zh.md
#	.agents/notes/implemented/bug-fix/2026-08-06-token-surface-unpriced-replace-compatibility.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-08-06-token-surface-unpriced-replace-compatibility.zh.md
#	.agents/notes/implemented/feature/2026-07-05-skill-system.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-05-skill-system.zh.md
#	.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.zh.md
#	.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md
#	.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md
#	.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.zh.md
#	.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.zh.md
#	.agents/notes/implemented/feature/2026-07-23-web-permission-and-approval.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-23-web-permission-and-approval.zh.md
#	.agents/notes/implemented/feature/2026-07-25-session-list-browsing-and-manual-order.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-25-session-list-browsing-and-manual-order.zh.md
#	.agents/notes/implemented/feature/2026-07-27-skill-catalog-hot-refresh.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-27-skill-catalog-hot-refresh.zh.md
#	.agents/notes/implemented/feature/2026-07-27-web-session-fork-actions.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-27-web-session-fork-actions.zh.md
#	.agents/notes/implemented/feature/2026-07-27-web-session-search.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-27-web-session-search.zh.md
#	.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.zh.md
#	.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md
#	.agents/notes/implemented/feature/2026-07-28-skill-invocation-policy.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-28-skill-invocation-policy.zh.md
#	.agents/notes/implemented/feature/2026-07-28-web-terminal-card.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-28-web-terminal-card.zh.md
#	.agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.zh.md
#	.agents/notes/implemented/feature/2026-07-30-search-render-card.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-30-search-render-card.zh.md
#	.agents/notes/implemented/feature/2026-07-30-web-diff-card.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-30-web-diff-card.zh.md
#	.agents/notes/implemented/feature/2026-07-30-web-read-card.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-30-web-read-card.zh.md
#	.agents/notes/implemented/feature/2026-07-30-web-result-card-frontend.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-30-web-result-card-frontend.zh.md
#	.agents/notes/implemented/feature/2026-07-30-web-result-card.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-30-web-result-card.zh.md
#	.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.zh.md
#	.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.zh.md
#	.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.zh.md
#	.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.i18n.yaml
#	.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.zh.md
#	.agents/notes/implemented/feature/2026-08-02-pwsh-tool-bash-parity.i18n.yaml
#	.agents/notes/implemented/feature/2026-08-02-pwsh-tool-bash-parity.zh.md
#	.agents/notes/implemented/feature/2026-08-02-web-thinking-tail-scroll.i18n.yaml
#	.agents/notes/implemented/feature/2026-08-02-web-thinking-tail-scroll.zh.md
#	.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.i18n.yaml
#	.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.zh.md
#	.agents/notes/implemented/feature/2026-08-06-resolved-theme-color-metadata.i18n.yaml
#	.agents/notes/implemented/feature/2026-08-06-resolved-theme-color-metadata.zh.md
#	.agents/notes/implemented/feature/2026-08-06-web-skill-tool-row.i18n.yaml
#	.agents/notes/implemented/feature/2026-08-06-web-skill-tool-row.zh.md
#	.agents/notes/implemented/process/2026-06-17-ts-build-config.i18n.yaml
#	.agents/notes/implemented/process/2026-06-17-ts-build-config.zh.md
#	.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.i18n.yaml
#	.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.zh.md
#	.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.i18n.yaml
#	.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.zh.md
#	.agents/notes/implemented/process/2026-08-08-api-remotes-generated-contract-build.i18n.yaml
#	.agents/notes/implemented/process/2026-08-08-api-remotes-generated-contract-build.zh.md
#	.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.i18n.yaml
#	.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.zh.md
#	.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.i18n.yaml
#	.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.zh.md
#	.agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.i18n.yaml
#	.agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.zh.md
#	.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.i18n.yaml
#	.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md
#	.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.i18n.yaml
#	.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.zh.md
#	.agents/notes/proposed/architecture/2026-08-08-semantic-composer-chain-phases.i18n.yaml
#	.agents/notes/proposed/architecture/2026-08-08-semantic-composer-chain-phases.zh.md
#	.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.i18n.yaml
#	.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.zh.md
#	apps/cli/reference/README.i18n.yaml
#	apps/cli/reference/README.zh.md
#	docs/api-gateway.i18n.yaml
#	docs/api-gateway.zh.md
#	docs/cookbook/adding-a-package.i18n.yaml
#	docs/cookbook/adding-a-package.zh.md
#	docs/cookbook/adding-a-tool.i18n.yaml
#	docs/cookbook/adding-a-tool.zh.md
#	docs/development.i18n.yaml
#	docs/development.zh.md
#	docs/i18n/README.i18n.yaml
#	docs/i18n/README.zh.md
#	docs/i18n/style-samples.md
#	docs/i18n/terminology.md
#	docs/testing.i18n.yaml
#	docs/testing.zh.md
#	examples/web-cordis/README.i18n.yaml
#	examples/web-cordis/README.zh.md
#	packages/api/gateway/README.i18n.yaml
#	packages/api/gateway/README.zh.md
#	packages/bash/bash-env/README.i18n.yaml
#	packages/bash/bash-env/README.zh.md
#	packages/bash/tool-bash/README.i18n.yaml
#	packages/bash/tool-bash/README.zh.md
#	packages/bash/tool-pwsh/README.i18n.yaml
#	packages/bash/tool-pwsh/README.zh.md
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/README.zh.md
#	packages/client/ui-tool/README.i18n.yaml
#	packages/client/ui-tool/README.zh.md
#	packages/feedback/command-feedback/README.i18n.yaml
#	packages/feedback/command-feedback/README.zh.md
#	packages/host/webserver/README.i18n.yaml
#	packages/host/webserver/README.zh.md
#	packages/pty/tool-bash-persistent/README.i18n.yaml
#	packages/pty/tool-bash-persistent/README.zh.md
#	packages/subagent/subagent-claude-code/README.i18n.yaml
#	packages/subagent/subagent-claude-code/README.zh.md
#	packages/subagent/subagent-codex/README.i18n.yaml
#	packages/subagent/subagent-codex/README.zh.md
#	packages/typert/type-meta/README.i18n.yaml
#	packages/typert/type-meta/README.zh.md
#	packages/util/atomic-write/README.i18n.yaml
#	packages/util/atomic-write/README.zh.md
#	scripts/snapshots/translation-prompt-v4/request-response.expected.json
This commit is contained in:
xjt
2026-08-09 13:15:27 +08:00
871 changed files with 2178 additions and 2106 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-19-windows-atomic-write-dacl-preservation.md
2026-07-19-windows-atomic-write-dacl-preservation.md: be9f82174300a7d605c6a6e63878728f08cb37be
2026-07-19-windows-atomic-write-dacl-preservation.zh.md: d31b52b0a99240a1e4449427438e1e187b1b98da
2026-07-19-windows-atomic-write-dacl-preservation.zh.md: ed52773b0314e06ecdc470f9d569685c47a7aec3

View File

@@ -10,9 +10,9 @@ Status: implemented
## 决策
`dsh-fs-local` 通过 `GetFileSecurityW` 读取现有目标文件的 DACL,在写入内容前将其以禁止继承的形式应用到空临时文件,并通过 `ReplaceFileW` 发布已关闭的临时文件。受保护的暂存安全描述符可防止暂存目录中的继承条目扩大访问权限;`ReplaceFileW` 会保留原目标文件的访问策略及其他替换元数据。其 ACL 合并过程可能重新序列化自动继承状态或复制等价 ACE,因此不能把自相对安全描述符缓冲区的逐字节相等作为稳定契约。新的 Windows 文件没有既有描述符需要保留,因此仍继承目标目录的 DACL;其暂存目录也因此位于目标文件旁。POSIX 继续使用仅所有者可访问的暂存 mode,并保留现有目标文件的 mode。
`dsh-fs-local` 通过 `GetFileSecurityW` 读取现有目标文件的 DACL,在写入内容前将其以禁止继承的形式应用到空临时文件,并通过 `ReplaceFileW` 发布已关闭的临时文件。受保护的暂存安全描述符可防止暂存目录中的继承条目扩大访问权限;`ReplaceFileW` 会保留原目标文件的访问策略及其他替换元数据。其 ACL 合并过程可能重新序列化自动继承状态或复制等价 ACE,因此不能把自相对安全描述符缓冲区的逐字节相等作为稳定约定。新的 Windows 文件没有既有描述符需要保留,因此仍继承目标目录的 DACL;其暂存目录也因此位于目标文件旁。POSIX 继续使用仅所有者可访问的暂存 mode,并保留现有目标文件的 mode。
Windows 原生覆盖率测试会保护目标文件的 DACL、检查写入完成的暂存文件,并对比最终替换文件中保持顺序且去重后的 ACE 策略。与宿主平台无关的绑定测试覆盖 Win32 错误转换以及每个原生调用边界。mode 位断言仍仅适用于 POSIX;新文件的 DACL 继承由操作系统契约规定,不应通过特定机器的账户允许列表来断言。
Windows 原生覆盖率测试会保护目标文件的 DACL、检查写入完成的暂存文件,并对比最终替换文件中保持顺序且去重后的 ACE 策略。与宿主平台无关的绑定测试覆盖 Win32 错误转换以及每个原生调用边界。mode 位断言仍仅适用于 POSIX;新文件的 DACL 继承由操作系统约定规定,不应通过特定机器的账户允许列表来断言。
## 备选方案

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-21-compaction-summary-prefix-cache-reuse.md
2026-07-21-compaction-summary-prefix-cache-reuse.md: fb6524846f837b783423b3ed0e043cd4e1f7849d
2026-07-21-compaction-summary-prefix-cache-reuse.zh.md: e6072313b7089b9dda14c298fcb94db786220501
2026-07-21-compaction-summary-prefix-cache-reuse.zh.md: 96adc31d6d6c50b4e6bfee32a5b89388a33a6939

View File

@@ -42,4 +42,4 @@ Status: implemented
- **单元:** `compact-basic.spec.ts` 断言辅助调用转发 `system`/`tools`/前导消息,并把压缩指令作为最后一条消息追加,且 `compactRegion` 回放最新的已路由 header 前缀。现有的内容断言通过回放的消息而非 transcript 字符串来读取摘要器输入。
- **循环:** `compact-loop-repro.spec.ts` 依据摘要请求尾部 user 消息中的压缩指令对其分类,溢出恢复测试则继续在真实循环中固定对话请求与摘要请求的数量。
- **快照:** 无密钥回放会从带标记的 `compact/summary` 重建一条规范成功流;[compaction-seam Agent Note](../feature/2026-06-18-compaction-capability-seam.md) 负责持久标记契约。
- **快照:** 无密钥回放会从带标记的 `compact/summary` 重建一条规范成功流;[compaction-seam Agent Note](../feature/2026-06-18-compaction-capability-seam.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-21-semantic-session-checkpoints.md
2026-07-21-semantic-session-checkpoints.md: 697d878dccbfab1ded9f73e134d594ba6f0665e1
2026-07-21-semantic-session-checkpoints.zh.md: c6184b0b00db16f9e318f4cbd148b1f0525f03a5
2026-07-21-semantic-session-checkpoints.zh.md: c72f2bbad33967f54013f05568b9d1082d549357

View File

@@ -10,7 +10,7 @@ Status: implemented
## 决策
`dsh-session-checkpoint-policy` 以零配置插件的形式与持久化后端共同加载,并负责语义持久性屏障。在 `agent/pre-step` 时,该插件会在推导下一个请求前刷新待持久化的提示词输入或前一批响应/结果。该插件惰性包装 `llm/stream`,在记录 `request/header` 之后、构造适配器流之前,刷新活动会话。该插件还在有序的执行前策略之后包装顶层 `tools/execute`,在进入工具主体前刷新已记录的 `tool/call`;嵌套分发则复用外层模型可见调用。循环的最终 `turn/end` 检查点仍是轮次的收尾边界,并会在处理另一个已排队轮次或观察到空闲状态之前完成。
`dsh-session-checkpoint-policy` 以零配置插件的形式与持久化后端共同加载,并负责语义持久性屏障。在 `agent/pre-step` 时,该插件会在推导下一个请求前刷新待持久化的提示词输入或前一批响应/结果。该插件惰性包装 `llm/stream`,在记录 `request/header` 之后、构造适配器流之前,刷新当前会话。该插件还在有序的执行前策略之后包装顶层 `tools/execute`,在进入工具主体前刷新已记录的 `tool/call`;嵌套分发则复用外层模型可见调用。循环的最终 `turn/end` 检查点仍是轮次的收尾边界,并会在处理另一个已排队轮次或观察到空闲状态之前完成。
持久化与检查点调度仍是相互独立的 Cordis 插件。后端使请求的 `session/flush` 边界持久化,但不选择边界;只加载后端而不加载本策略仍是有效组合,并保留循环提供的较粗检查点。第一方持久化应用与运行时会显式加载两者,专用部署则可以有意省略或替换本策略。注册顺序决定其他 `agent/pre-step` 监听器追加的事件是否先于本检查点;提示词输入以及前一批由循环自身记录的助手消息与有序结果都已在日志中。
@@ -26,4 +26,4 @@ ACP(Agent Client Protocol)应用在一个有序 Cordis effect 中统一持
## 后果
发生硬崩溃时,崩溃恢复会在最近的语义边界保留完整的模型请求、持久化的工具意图与完整且已结束的步骤,但允许上一个边界之后的部分流式分片仍可能丢失。默认的 CLI(命令行界面)、TUI、ACP、Python SDK 运行时、headless 持久化测试与 JSON-RPC 组合都会在持久化后端旁加载该策略。单元测试覆盖顺序、检查点期间的取消、失败关闭行为、嵌套分发、dispose(资源释放)与 Loader 形状,以及最终检查点的顺序与故障隔离;一个被 `SIGKILL` 终止的真实子进程通过 JSONL 证明系统可以恢复请求与工具意图,共享持久化契约则证明各后端都支持这两种恢复分类。崩溃 harness 会等待预期的标记内容,而不是仅等待路径存在,因此文件在写入前因打开而可见时,不会导致该 harness 提前终止子进程。无密钥 ACP 与 SDK 快照证明重试风险指引会进入恢复后的历史记录与下一个模型轮次,取消流程正常收尾时系统会持久化由循环实际生成的闭合边界,且 SDK 关闭流程会观察到已完整持久化的轮次。
发生硬崩溃时,崩溃恢复会在最近的语义边界保留完整的模型请求、持久化的工具意图与完整且已结束的步骤,但允许上一个边界之后的部分流式分片仍可能丢失。默认的 CLI(命令行界面)、TUI、ACP、Python SDK 运行时、headless 持久化测试与 JSON-RPC 组合都会在持久化后端旁加载该策略。单元测试覆盖顺序、检查点期间的取消、失败关闭行为、嵌套分发、dispose(资源释放)与 Loader 形状,以及最终检查点的顺序与故障隔离;一个被 `SIGKILL` 终止的真实子进程通过 JSONL 证明系统可以恢复请求与工具意图,共享持久化约定则证明各后端都支持这两种恢复分类。崩溃 harness 会等待预期的标记内容,而不是仅等待路径存在,因此文件在写入前因打开而可见时,不会导致该 harness 提前终止子进程。无密钥 ACP 与 SDK 快照证明重试风险指引会进入恢复后的历史记录与下一个模型轮次,取消流程正常收尾时系统会持久化由循环实际生成的闭合边界,且 SDK 关闭流程会观察到已完整持久化的轮次。

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-27-glob-sampling.md
2026-07-27-glob-sampling.md: 67912cf290b127a96819d57f30387197af68323d
2026-07-27-glob-sampling.zh.md: 400df369e2a537abbd8c49e0df02cd0b80f86f1b
2026-07-27-glob-sampling.zh.md: b8d7bbe1bc70e94cc4239867244be554eaf33e63

View File

@@ -20,21 +20,21 @@ Status: implemented
## 考虑过的替代方案
**只保留按修改时间排序的前部。**测量实际故障形态后否决。某些部署需要这种稳定排序;但重视工作区定位的部署可以显式选择具有代表性的数据,而不必要求模型怀疑自己拿到的唯一一批路径。
**只保留按修改时间排序的前部。** 测量实际故障形态后否决。某些部署需要这种稳定排序;但重视工作区定位的部署可以显式选择具有代表性的数据,而不必要求模型怀疑自己拿到的唯一一批路径。
**为采样选项提供默认值。**否决。没有全产品范围的证据支持把任一排序作为隐式约定,因此每个组合都必须选择一种,配置错误则在加载时失败。
**为采样选项提供默认值。** 否决。没有全产品范围的证据支持把任一排序作为隐式约定,因此每个组合都必须选择一种,配置错误则在加载时失败。
**对所有结果采样。**否决。完整结果没有因截断损失任何信息,因此按修改时间排序仍有助于回答关注新旧时间的问题。只有当截取前部已经无法描述整体时,才开始采样。
**对所有结果采样。** 否决。完整结果没有因截断损失任何信息,因此按修改时间排序仍有助于回答关注新旧时间的问题。只有当截取前部已经无法描述整体时,才开始采样。
**改为最新优先排序。**否决。这只会改变哪一棵结果密集的子树可能占据主导;既取消了现有的最旧优先约定,也没有让受限页面更具代表性。
**改为最新优先排序。** 否决。这只会改变哪一棵结果集中的子树可能占据主导;既取消了现有的最旧优先约定,也没有让受限页面更具代表性。
**仅在偏斜超过阈值时采样。**否决。目前没有证据支持适用于所有部署的统一阈值,模型也无法判断当前采用的是哪一种排序约定。现有上限是可以清楚解释的切换点。
**仅在偏斜超过阈值时采样。** 否决。目前没有证据支持适用于所有部署的统一阈值,模型也无法判断当前采用的是哪一种排序约定。现有上限是可以清楚解释的切换点。
**在顶层以下递归平衡。**暂缓。按第一路径段做平衡已经修复观测到的故障;更深层的平衡需要一套尚无依据的深度与广度取舍策略。
**在顶层以下递归平衡。** 暂缓。按第一路径段做平衡已经修复观测到的故障;更深层的平衡需要一套尚无依据的深度与广度取舍策略。
**新增面向模型的 `list` 工具。**实现评审后否决。默认编程组合已经提供通用 bash,模型也理解 `ls`;重复工具会永久增加 schema 与提示词所占的 token,并引入排序、分页、符号链接、转义、UI 与快照约定,却没有独立的安全或策略收益。不向模型提供 bash 工具的精简部署也不会因本次改动获得目录定位能力。
**新增面向模型的 `list` 工具。** 实现评审后否决。默认编程组合已经提供通用 bash,模型也理解 `ls`;重复工具会永久增加 schema 与提示词所占的 token,并引入排序、分页、符号链接、转义、UI 与快照约定,却没有独立的安全或策略收益。不向模型提供 bash 工具的精简部署也不会因本次改动获得目录定位能力。
**拒绝 `*`,或在不含分隔符的模式前静默加上根目录锚点。**否决。同样的「在任意深度匹配文件名」行为使 `*.ts` 可以有效地跨目录树搜索。记录这条规则能够保留正常工作的 Ripgrep 语义。
**拒绝 `*`,或在不含分隔符的模式前静默加上根目录锚点。** 否决。同样的「在任意深度匹配文件名」行为使 `*.ts` 可以有效地跨目录树搜索。记录这条规则能够保留正常工作的 Ripgrep 语义。
## 影响

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-27-stable-snapshot-refresh-volatiles.md
2026-07-27-stable-snapshot-refresh-volatiles.md: c3eeeca01a7820b5f410bd895de998e944e58eb2
2026-07-27-stable-snapshot-refresh-volatiles.zh.md: 388b67c67052074fa7423eae294e00b4122b2fe8
2026-07-27-stable-snapshot-refresh-volatiles.zh.md: 79a2758e7a7353c983f4e8a9d2531830dec311e4

View File

@@ -6,7 +6,7 @@ Status: implemented
## 问题
ACP(Agent Client Protocol)快照比较会归一化生成的 UUID、cwd 别名、spill locator、嵌入的事件时间和省略字节数,但刷新写回会持久化本次生成的原始值。因此,即使比较契约将两份日志视为相等,一次行为未发生变化的刷新仍会用新的随机值或宿主特有的路径写法改写 fixture(测试前置数据)。
ACP(Agent Client Protocol)快照比较会归一化生成的 UUID、cwd 别名、spill locator、嵌入的事件时间和省略字节数,但刷新写回会持久化本次生成的原始值。因此,即使比较约定将两份日志视为相等,一次行为未发生变化的刷新仍会用新的随机值或宿主特有的路径写法改写 fixture(测试前置数据)。
消息身份所需的结构前提比记录对齐更弱:无关的日志事件可能破坏记录对齐,但继承而来的消息去除身份后的值在父级和子级日志之间仍保持不变。录制模式在替换现有 fixture 时也会从新生成的消息 UUID 开始。

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-28-load-pre-identity-session-messages.md
2026-07-28-load-pre-identity-session-messages.md: 2901527658421b37576bdf5b49e66829104a3b41
2026-07-28-load-pre-identity-session-messages.zh.md: 210064f6e7c1ea239da89c4542346e164c48e274
2026-07-28-load-pre-identity-session-messages.zh.md: bf6e58d8a895f41012449a1212533902e1211e99

View File

@@ -22,7 +22,7 @@ Status: implemented
**按照预发布兼容性立场拒绝这些日志。** 这是处理其他 v0 形状变动的默认方式,但即使每个旧字段都能明确映射到当前消息表示,它仍会导致真实的第一方会话无法恢复。
**就地重写完整的存储日志。** 这会使产物规范化,但违反仅追加存储契约,还需要为 JSONL 和 SQLite 分别实现原子替换机制,并将一次读取兼容性修复扩大为迁移系统。
**就地重写完整的存储日志。** 这会使产物规范化,但违反仅追加存储约定,还需要为 JSONL 和 SQLite 分别实现原子替换机制,并将一次读取兼容性修复扩大为迁移系统。
**每次加载时随机生成 id。** 这些消息会满足类型形状,却无法在检查、恢复、重启以及新旧形状混合追加之间保持稳定标识。
@@ -30,9 +30,9 @@ Status: implemented
消息标识机制引入前的 JSONL 和 SQLite 会话可以恢复,并保留原始的消息内容、来源、提供方溯源信息、工具调用关联、错误、元数据和 surface 替换。除此之外,返回事件与当前导入的消息快照无法区分,并且仍然经过深度冻结。
这是一个显式的同版本导入例外,而非通用的 v0 兼容层。若要增加另一个例外,必须在持久化边界提供另一套完整且无歧义的映射;当前数据若格式错误,系统仍会拒绝,而不会猜测如何将其变成有效数据。共享的协调器契约会在内存参考实现、JSONL 和 SQLite 后端上验证这项升级,包括重新加载时的确定性,以及工具结果替换时的标识继承。
这是一个显式的同版本导入例外,而非通用的 v0 兼容层。若要增加另一个例外,必须在持久化边界提供另一套完整且无歧义的映射;当前数据若格式错误,系统仍会拒绝,而不会猜测如何将其变成有效数据。共享的协调器约定会在内存参考实现、JSONL 和 SQLite 后端上验证这项升级,包括重新加载时的确定性,以及工具结果替换时的标识继承。
## 相关
- [将每条消息创建为带标识的不可变值](../architecture/2026-07-28-identified-immutable-message-values.md):该记录负责当前的消息标识与不可变性契约。
- [将每条消息创建为带标识的不可变值](../architecture/2026-07-28-identified-immutable-message-values.md):该记录负责当前的消息标识与不可变性约定。
- [会话持久化作为抽象服务](../architecture/2026-06-14-session-persistence.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-28-themed-scrollbars-and-reserved-gutter.md
2026-07-28-themed-scrollbars-and-reserved-gutter.md: ba3d9d3c94cf9a775c43c14e292187b3a43935fe
2026-07-28-themed-scrollbars-and-reserved-gutter.zh.md: c0577e158e111651c917a43b7aa65f1d904d7af1
2026-07-28-themed-scrollbars-and-reserved-gutter.zh.md: 1cf505e96122a66705fe42fc79757ac390ebf0ef

View File

@@ -20,7 +20,7 @@ Status: implemented
两种渲染互斥,而这种互斥是被强制的,不是假定的。`scrollbar-width` 或 `scrollbar-color` 只要取非 `auto` 值,Chromium 与 Safari 就会丢弃该元素上的全部 `::-webkit-scrollbar*` 规则,`::-webkit-scrollbar-thumb:hover` 也在其中。因此无条件地同时声明会让 hover token 在任何地方都得不到渲染:实现了 hover 伪元素的引擎,恰恰就是被标准属性静音的那些,而 Firefox 没有 hover 伪元素可作退路。于是标准属性写在 `@supports not selector(::-webkit-scrollbar)` 之内,该条件只在伪元素未被实现处为真,因此 Firefox 走标准属性路径,WebKit 系引擎走伪元素路径。WebKit 规则不再反向加门禁:不实现这些伪元素的引擎会把它们当作未知选择器丢弃,因此加门禁只是重述选择器匹配本身已经做的事。对于旧到不支持 `selector()` 函数的引擎,该条件无效,从而求值为假并选中伪元素路径——对于这条判断下现实存在的 16.4 之前的 Safari,这正是正确的一侧。
两条路径都读取同一组间接变量 `--dsh-scrollbar-thumb` 与 `--dsh-scrollbar-thumb-hover`,它们在 `body` 上绑定到 l1(基础表面)token。**这就是重新绑定约定,也是单看 CSS 无法得知的部分**:抬升表面在自己的容器上设置 `--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2)` 与 `--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2)`,这一次重新绑定同时作用于标准属性和 WebKit 伪元素。这组变量必须成对重新绑定;只改静止态滑块会让 hover 状态仍留在基础表面的 token 上。这组变量另一个合法的目标是 `transparent`,它随侧边栏滚动条[改为跟随指针](../feature/2026-08-04-pointer-revealed-sidebar-scrollbars.md)一并引入;下文的门禁只接受这两种目标。可由机械检查发现的子集归 `packages/client/ui-theme/tests/scrollbar-styles.spec.ts` 所有:任何既滚动又绘制抬升表面的样式表都必须重新绑定,因此本 Agent Note 不再维护完整的表面清单。多数把这组变量声明在抬升卡片上而非滚动的后代元素上,因为抬升层级属于这个表面,而自定义属性会继承到真正滚动的那个子元素。
两条路径都读取同一组间接变量 `--dsh-scrollbar-thumb` 与 `--dsh-scrollbar-thumb-hover`,它们在 `body` 上绑定到 l1(基础表面)token。**这就是重新绑定约定,也是单看 CSS 无法得知的部分**:抬升表面在自己的容器上设置 `--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2)` 与 `--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2)`,这一次重新绑定同时作用于标准属性和 WebKit 伪元素。这组变量必须成对重新绑定;只改静止态滑块会让 hover 状态仍留在基础表面的 token 上。这组变量另一个合法的目标是 `transparent`,它随侧边栏滚动条[改为跟随指针](../feature/2026-08-04-pointer-revealed-sidebar-scrollbars.md)一并引入;下文的门禁只接受这两种目标。可由机械检查发现的子集归 `packages/client/ui-theme/tests/scrollbar-styles.spec.ts` 所有:任何既滚动又绘制抬升表面的样式表都必须重新绑定,因此本 note 不再维护完整的表面清单。多数把这组变量声明在抬升卡片上而非滚动的后代元素上,因为抬升层级属于这个表面,而自定义属性会继承到真正滚动的那个子元素。
`Menu`、`InputBar`、`QuestionComposer` 与 `TodoPanel` 这四个表面在最初的实现里被漏掉、由评审发现,因此逐样式表的重新绑定约定由机械检查而非人工审阅把关。

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-28-web-gui-feedback-loop.md
2026-07-28-web-gui-feedback-loop.md: aa488e1df087722c072d98c67d47cdf63a42f6b8
2026-07-28-web-gui-feedback-loop.zh.md: 17d25eaf8ec49423b554b7993ff7f9e267f7c91c
2026-07-28-web-gui-feedback-loop.zh.md: 9b6954092b737920ed18bc412037e917512e40dc

View File

@@ -8,7 +8,7 @@ Status: implemented
Web agent(智能体)既无法识别承载当前会话的 GUI,也不知道用户正在查看哪个 URL。[运行时上下文决策](2026-07-28-web-agent-runtime-context.md)提供前一项事实,但 GUI 编辑仍然没有可执行的验收目标:源码编辑、产物构建、监听中的进程与用户已打开的页面只是互不关联的观察结果。仓库提供的入口让错误的替代方案显得合理,因为 `apps/web/package.json` 将 `vite` 暴露为 `dev` 脚本,而裸 Vite 即使无法注入 `window.__DSH_BOOT__`,仍会返回 HTTP 200。
[事故复盘(postmortem)](../../../../docs/postmortem/0003-web-agent-gui-feedback-loop.md)集中记录事件日志时间线,并解释原有检查为何会接受错误的页面、进程和端口。
[事故复盘](../../../../docs/postmortem/0003-web-agent-gui-feedback-loop.md)集中记录事件日志时间线,并解释原有检查为何会接受错误的页面、进程和端口。
## 决策
@@ -22,7 +22,7 @@ Web agent(智能体)既无法识别承载当前会话的 GUI,也不知道
## 验证
无密钥的 fresh-round-trip 浏览器场景会启动已交付的生产 Web 组合,驱动真实的回放会话,对包含 URL 和模式的系统提示词前缀生成快照,并调用组装后的 bash 工具,证明 `$DSH_WEB_URL` 和 `$DSH_WEB_MODE` 与实际绑定的运行时一致。真实 CLI(命令行界面)冒烟测试会启动 `dsh web --dev` 并捕获模型提供方请求,从而固定完整的双命令开发约定。`dev:web` watcher 测试会在源码发生变化后重新构建隔离的客户端 bundle;浏览器 HMR 场景会启动 `dsh web --dev`,修改生产初始 roster 中的 bundle,并在页面 identity 不变的情况下观察新 DOM。真实 Vite 子进程测试要求服务模式在给出改用完整宿主的纠正信息后自然退出,并通过插桩 `Server.listen()` 证明它从未被调用。真实 loader Web 服务器测试会在进程完成绑定后改写静态资源,并证明同一端口返回新的字节。这些断言检查提示词状态、进程退出状态、shell 输出、DOM identity 和 HTTP 字节,而不是 agent 的成功声明。
无密钥的 fresh-round-trip 浏览器场景会启动已交付的生产 Web 组合,驱动真实的回放会话,对包含 URL 和模式的系统提示词前缀生成快照,并调用组装后的 bash 工具,证明 `$DSH_WEB_URL` 和 `$DSH_WEB_MODE` 与实际绑定的运行时一致。真实 CLI 冒烟测试会启动 `dsh web --dev` 并捕获模型提供方请求,从而固定完整的双命令开发约定。`dev:web` watcher 测试会在源码发生变化后重新构建隔离的客户端 bundle;浏览器 HMR 场景会启动 `dsh web --dev`,修改生产初始 roster 中的 bundle,并在页面 identity 不变的情况下观察新 DOM。真实 Vite 子进程测试要求服务模式在给出改用完整宿主的纠正信息后自然退出,并通过插桩 `Server.listen()` 证明它从未被调用。真实 loader Web 服务器测试会在进程完成绑定后改写静态资源,并证明同一端口返回新的字节。这些断言检查提示词状态、进程退出状态、shell 输出、DOM identity 和 HTTP 字节,而不是 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-07-29-human-transcript-append-origin.md
2026-07-29-human-transcript-append-origin.md: 9804d3d8d67a4ad00c3d395c2081fd47e03c254a
2026-07-29-human-transcript-append-origin.zh.md: bfe39384f1f5a8badd4c859c89f450177f53ed62
2026-07-29-human-transcript-append-origin.zh.md: 030be076bec9c64ffc97957bbc0fc2b6084d2030

View File

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

View File

@@ -26,4 +26,4 @@ Status: implemented
## 后果
详情栏在启动时保持关闭,首次会话物化时亦然。显式打开操作会使用约定的默认宽度。切换到不同会话会忘记拖动后的详情宽度,因为关闭操作会写入零值,重新打开时则使用该默认值。未选中状态会将轨道的渲染宽度派生为零,同时保持首选几何信息不变;经由这些状态返回同一会话时,会恢复其宽度。重新加载会忘记侧边栏几何信息,并使详情栏恢复关闭状态。布局行为测试覆盖初始默认值、首次物化、直接及经 hero 中转的会话切换、返回同一会话,以及不存在布局存储的情况;无密钥浏览器 e2e 则通过已交付的组合驱动相同的 owner 过渡,同时检查完整网格轨道和浏览器错误。
详情栏在启动时保持关闭,首次会话物化时亦然。显式打开操作会使用约定默认宽度。切换到不同会话会忘记拖动后的详情宽度,因为关闭操作会写入零值,重新打开时则使用该默认值。未选中状态会将轨道的渲染宽度派生为零,同时保持首选几何信息不变;经由这些状态返回同一会话时,会恢复其宽度。重新加载会忘记侧边栏几何信息,并使详情栏恢复关闭状态。布局行为测试覆盖初始默认值、首次物化、直接及经 hero 中转的会话切换、返回同一会话,以及不存在布局存储的情况;无密钥浏览器 e2e 则通过已交付的组合驱动相同的 owner 过渡,同时检查完整网格轨道和浏览器错误。

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: 3b7aaeb1178ff79e38a1b9646a9dc78efaeae48b
2026-07-30-web-transcript-log-ordered-projection.zh.md: ec60a8cdb464ded8ddc2ec46f499fe4ccc4c2f8a
2026-07-30-web-transcript-log-ordered-projection.zh.md: 024d7dba7db7f06a684f5656abf7421e65cd4536

View File

@@ -1,18 +1,18 @@
# Agent Note: 浏览器会话是按日志顺序投影的面向人的 transcript(文本记录)
# Agent Note: 浏览器会话是按日志顺序投影的人类对话记录
Status: implemented
[English](2026-07-30-web-transcript-log-ordered-projection.md) | 中文
## 问题
## Problem
浏览器客户端从模型可见的 surface 构建会话:`FoldAdapter` 在历史窗口上运行核心 `SurfaceManager` 并读取 `surface.nodes`。一次成功的压缩(compaction)会用一个检查点节点替换一段 surface 范围,因此该替换一落地,Web 流就把它所遮蔽的每条消息折叠成一行灰暗的上下文——那是用户已经读过的对话。日志中什么都没丢失;缺陷完全在投影层,而[终端与宿主网关已按同一方式修复](2026-07-29-human-transcript-append-origin.md),浏览器留给了本次变更。
浏览器客户端从模型可见的 surface 构建会话:`FoldAdapter` 在历史窗口上运行核心 `SurfaceManager` 并读取 `surface.nodes`。一次成功的压缩会用一个检查点节点替换一段 surface 范围,因此该替换一落地,Web 流就把它所遮蔽的每条消息折叠成一行灰暗的上下文——那是用户已经读过的对话。日志中什么都没丢失;缺陷完全在投影层,而[终端与宿主历史网关已按同一方式修复](2026-07-29-human-transcript-append-origin.md),浏览器留给了本次变更。
surface 顺序还让另外两个问题成为结构性的。一次替换之后它并非按 seq 升序——`SurfaceManager` 把高 seq 的检查点拼接到它所遮蔽范围的位置上——因此按数值 seq 归并进该数组的仅日志节点(斜杠命令行、被打断的冻结节点)可能被冲刷到检查点之前,再也无法交错回保留下来的尾部。而且由于分页不再为 replacement 副本消耗 `maxMessages` 额度,一页现在可以携带一个 `surfaceOp.start` 落在窗口之外的检查点;核心 fold 拒绝该范围,于是 `nodes()` 退回到一次宽容的线性扫描、打印一条 `console.error`,并发布一个描述该失败的 `foldDegraded` 标志。
## 决策
## Decision
`TranscriptAdapter` 取代 `FoldAdapter`,并且从不查询 surface 顺序。它按日志顺序投影原始窗口:每个 append 来源的 surface 事件(`isAppendSurfaceEvent`)落在它自己的日志位置上,外加每次落地的压缩检查点一个 `CompactionSummaryNode` 标记。于是一次落地的压缩会保留它在模型侧遮蔽掉的对话,标记报告模型从哪里开始看不见那段历史,而不是把它抹掉。仅模型可见的 replacement 副本不进入 transcript:被裁剪的 `tool/result` 和重新生成的 `assistant/message` 只为模型重写一个节点,不在对话中标记任何边界。凡必须发送模型所见内容的一切仍读 surface;这是人类投影,两者现在在两个前端上都已分离。
`TranscriptAdapter` 取代 `FoldAdapter`,并且从不查询 surface 顺序。它按日志顺序投影原始窗口:每个 append 来源的 surface 事件(`isAppendSurfaceEvent`)落在它自己的日志位置上,外加每次落地的压缩检查点一个 `CompactionSummaryNode` 标记。于是一次落地的压缩会保留它在模型侧遮蔽掉的对话,标记报告模型从哪里开始看不见那段历史,而不是把它抹掉。仅模型可见的 replacement 副本不进入记录:被裁剪的 `tool/result` 和重新生成的 `assistant/message` 只为模型重写一个节点,不在对话中标记任何边界。凡必须发送模型所见内容的一切仍读 surface;这是人类投影,两者现在在两个前端上都已分离。
节点顺序天然按 seq 单调,由此有三个结果。仅日志的 `command/run` / `command/done` 对折叠成 `CommandNode`,按 seq 插入一个本已单调的数组——无锚点,无重排。`Session` 保留被打断的冻结节点的归属,用一次普通排序按其分数 seq 归并,而这现在恰好就是流顺序。检查点所引被遮蔽范围落在窗口之外的窗口没有范围需要解析,因此标记正常渲染且不打印任何日志。
@@ -26,7 +26,7 @@ surface 顺序还让另外两个问题成为结构性的。一次替换之后它
## 识别检查点:同一份声明,在编译期钉住
识别需要三个条件同时成立,与终端一致:`event.type === 'user/message'`、压缩 seam 的检查点插件来源,**以及** `isReplacementSurfaceEvent(event)`。一条 append 的插件来源 `user/message` 是注入上下文——跨会话引用卡片——不是压缩。
识别需要三个条件同时成立,与终端一致:`event.type === 'user/message'`、压缩缝隙的检查点插件来源,**以及** `isReplacementSurfaceEvent(event)`。一条 append 的插件来源 `user/message` 是注入上下文——跨会话引用卡片——不是压缩。
从 `packages/client/*` 程序无法到达的是 `dsh-compact` 的**根部**,而不是这个包。根部会到达 `dsh-session` 的根部,后者的 cordis `Context` 合并声明了宿主侧 `sessions: SessionStore`,与客户端的 `sessions: ISessions` 冲突——`TS2717`,即 [development.md](../../../../docs/development.md#typescript-project-layout) 中每侧一个 program 的规则;这一点对仅类型导入同样成立,因为该冲突是编译器事实而非打包器事实。
@@ -37,7 +37,7 @@ import type { COMPACT_CHECKPOINT_SOURCE } from '@deepseek-ai/dsh-compact/checkpo
const COMPACT_PLUGIN: typeof COMPACT_CHECKPOINT_SOURCE.plugin = 'compact'
```
重命名 seam 的插件 id 现在会在客户端产生编译错误:`TS2322: Type '"compact"' is not assignable to type '"compaction"'`。该导入必须保持**仅类型**——任何既非平台模块又非 inline-safe wire 层的 `@deepseek-ai` 包值导入都会被客户端纯度门禁(`packages/client/tsdown.client.ts`)拒绝,而它自己的报错信息就记录着仅类型导入会被擦除、永不抵达该门禁。仅类型的叶子导入同时需要 `tsconfig.base.json` 的一条 `paths` 条目和 `packages/client/runtime/tsconfig.json` `references` 中的 `{"path": "../../compact/compact"}`:composite 的 `rootDir` 规则同样适用于被擦除的导入,缺少该引用时的诊断是 `TS6059`/`TS6307`。
重命名缝隙的插件 id 现在会在客户端产生编译错误:`TS2322: Type '"compact"' is not assignable to type '"compaction"'`。该导入必须保持**仅类型**——任何既非平台模块又非 inline-safe wire 层的 `@deepseek-ai` 包值导入都会被客户端纯度门禁(`packages/client/tsdown.client.ts`)拒绝,而它自己的报错信息就记录着仅类型导入会被擦除、永不抵达该门禁。仅类型的叶子导入同时需要 `tsconfig.base.json` 的一条 `paths` 条目和 `packages/client/runtime/tsconfig.json` `references` 中的 `{"path": "../../compact/compact"}`:composite 的 `rootDir` 规则同样适用于被擦除的导入,缺少该引用时的诊断是 `TS6059`/`TS6307`。
`packages/client/runtime/tests/compact-checkpoint-pin.spec.ts` 作为行为侧的另一半保留,用由权威**值**构造的检查点驱动适配器。该测试以值导入方式从不含 cordis 的 `@deepseek-ai/dsh-compact/checkpoint` 叶子路径取得该值,并刻意不加载 compact 包根或经由它可达的宿主侧 `Context` 合并。
@@ -45,17 +45,17 @@ const COMPACT_PLUGIN: typeof COMPACT_CHECKPOINT_SOURCE.plugin = 'compact'
## #835 的位置锚点是为什么而存在,以及为什么它是被溶解而非丢失
尚未合并的排队式手动压缩分支用另一种方式修同一个交错缺陷:为每个事件记录一个锚点——追加时的 surface 尾部——并把被遮蔽的锚点重定向到检查点上。该机制的存在是为了让位置锚点在 surface **重排**中存活。面向人的 transcript 永不被重排,因此锚点没有任何东西需要重定向:前提被移除,修复并未被丢弃。该机制在本基线上并不存在,本次也未实现它。
尚未合并的排队式手动压缩分支用另一种方式修同一个交错缺陷:为每个事件记录一个锚点——追加时的 surface 尾部——并把被遮蔽的锚点重定向到检查点上。该机制的存在是为了让位置锚点在 surface **重排**中存活。人类对话记录永不被重排,因此锚点没有任何东西需要重定向:前提被移除,修复并未被丢弃。该机制在本基线上并不存在,本次也不撰写它。
## 考虑过的替代方案
## 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 测试只需一个文件,就精确消除了这一风险。
**一条纯形状规则**——任何 replacement `user/message` 都是压缩。已拒绝:它今天正确只因为压缩是 replacement `user/message` 的唯一生产者,一旦这点改变便无任何机制能捕获。那个 pin 测试只花一个文件,就精确消除了这一风险。
**在宿主侧给检查点打标**,经投影或线协议。已拒绝:这最贴合「经 cordis 服务协作」的规则,但客户端今天折叠的是原始 `SessionEvent`,因此这意味着一次线协议约定变更——为一个纯谓词付出的代价不成比例。
**在宿主侧给检查点打标**,经投影或线协议。已拒绝:这最贴合“经 cordis 服务协作”的规则,但客户端今天折叠的是原始 `SessionEvent`,因此这意味着一次线协议约定变更——为一个纯谓词付出的代价不成比例。
**把冻结节点的归属移进适配器**(`nodes(extraNodes)`),像那个未合并分支所做的那样。已拒绝:被打断的节点来自 `Session` 已经在窗口上运行的 `turn/end` 清扫,而在按 seq 单调的 transcript 之上,简单形态就是正确的——适配器返回节点,会话按 seq 归并冻结节点。加宽适配器签名什么也换不到,还会把清扫与它的产物拆开。
**把冻结节点的归属移进适配器**(`nodes(extraNodes)`),像那个未合并分支所做的那样。已拒绝:被打断的节点来自 `Session` 已经在窗口上运行的 `turn/end` 清扫,而在按 seq 单调的记录之上,简单形态就是正确的——适配器返回节点,会话按 seq 归并冻结节点。加宽适配器签名什么也换不到,还会把清扫与它的产物拆开。
**把 `foldDegraded` 留作一个防御性标志。** 已拒绝:它描述的是一个已不再运行的 fold 的特定失败。一个消费方无法据以行动、只能通过 `console.error` 到达的标志,是一份虚假约定。
@@ -63,16 +63,16 @@ const COMPACT_PLUGIN: typeof COMPACT_CHECKPOINT_SOURCE.plugin = 'compact'
**解析英文结算文本中的条目数量和 token 数量。** 已拒绝:处理器文案是呈现文本,而非稳定的数据约定。标记读取本已持有这两个值的结构化 `compact/summary` 载荷。
## 后果
## Consequences
压缩不再抹掉 Web 历史;一个被压缩多次的会话按日志顺序显示每次落地压缩一个标记,而同一窗口在实时与冷恢复之后渲染完全相同。分页缺口从设计上得以消除,而不是靠防御性处理来规避,`ConversationSnapshot` 少了一个已发布字段,这触及十三个文件。
压缩不再抹掉 Web 历史;一个被压缩多次的会话按日志顺序显示每次落地压缩一个标记,而同一窗口在实时与冷恢复之后渲染完全相同。分页缺口是被构造性闭合而非被防御,`ConversationSnapshot` 少了一个已发布字段,这触及十三个文件。
`ConversationNode` 增加第八个分支,因此每个穷尽消费方都多一个分支:`MessageItem` 通过新的 `CompactionItem` 渲染标记,trajectory 布局加宽它的「无单元格」分支,使标记不贡献单元格但仍推进耗时游标。
`ConversationNode` 增加第八个分支,因此每个穷尽消费方都多一个分支:`MessageItem` 通过新的 `CompactionItem` 渲染标记,trajectory 布局加宽它的“无单元格”分支,使标记不贡献单元格但仍推进耗时游标。
性能约定未变,且现在更易表述:一次追加物化一个节点,不改变任何节点的事件保持上一次的数组引用——因此分片风暴零成本、`nodes()` 甚至不会重算——未变化的节点保持其对象标识。窗口仍随会话长度而非随 surface 增长,这正是本修复有意作出的权衡;一次压缩过去恰好为压缩所服务的长会话限制了投影规模。
性能约定未变,且现在更易表述:一次追加物化一个节点,不改变任何节点的事件保持上一次的数组引用——因此分片风暴零成本、`nodes()` 甚至不会重算——未变化的节点保持其对象标识。窗口仍随会话长度而非随 surface 增长,这正是本修复存在所要做的交换;一次压缩过去恰好为压缩所服务的长会话限制了投影规模。
Web e2e 场景现在围绕它录制的那一轮上的压缩事务播种一次真实的手动命令生命周期,因此 aria 基准经真实宿主与真实浏览器钉住完整行为:录制的提问与完整工具输出仍在屏幕上,其后恰好一条 `compact` 行报告规模,展开后会显示确切摘要。录制本身未被触碰、保持模型真实——回放从录制自身的 surface 派生出手动压缩。
## 延后事项
## Deferred
终端的[已归档压缩进度决策](../../archived/feature/2026-07-30-compaction-progress-visibility.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-fork-anchor-floors-to-event-seq.md
2026-07-31-fork-anchor-floors-to-event-seq.md: 2cc8d66942442aca69c930be880cc41d56bced13
2026-07-31-fork-anchor-floors-to-event-seq.zh.md: 10a8a994f544834d31ec7b3b4ad7d08b4b7d731d
2026-07-31-fork-anchor-floors-to-event-seq.zh.md: 75fc1885f344e88c9ded575bc4aea58562e55673

View File

@@ -18,7 +18,7 @@ host 的切分规则从来不是障碍。被中止的轮次会记录一条 reaso
向下取整落在锚点自身所在的轮次内,不会回退:每一轮都以 `turn/start` 开头,所以 `turnEnd.seq - 1` 不可能是上一轮的 `turn/end`。host 随后按「首个位于锚点或其之后的 `turn/end`」收口,命中的正是读者点击的那一轮,与消息级 fork 按钮在已完成轮次上一贯承诺的整轮语义一致。
apiproxy 的 fork 用例固定了 host 这一侧的约定:落在被中止轮次内的取整锚点会将该轮纳入切分结果,并以该轮初始化子会话。
apiproxy 的 fork 用例固定了 host 这一侧的约定:落在被中止轮次内的取整锚点会切穿该轮,并把它种进子会话。
## 备选方案
@@ -30,6 +30,6 @@ apiproxy 的 fork 用例固定了 host 这一侧的约定:落在被中止轮
## 影响
从已停止的轮次 fork 会得到一个初始化内容截至该轮 `turn/end` 的子会话。被冻结的残缺文本是从分片事件重建出来的,从未成为 `assistant/message`,因此它不会进入子会话的模型 transcript(文本记录)——正如源会话恢复时它也不会进入一样,子会话拿到的上下文与源会话一致。
从已停止的轮次 fork 会得到一个种子切到该轮 `turn/end` 的子会话。被冻结的残缺文本是从 chunk 事件重建出来的,从未成为 `assistant/message`,因此它不会进入子会话的模型上下文——正如源会话恢复时它也不会进入一样,子会话拿到的上下文与源会话一致。
fork 失败在 chat 入口仍然是静默的。这个 bug 能存活至今,正是因为该调用点丢弃了自己的 rejection;把 fork 错误呈现到 UI 上是另一件事。

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: 39146527f13b20813bb6f5d5f1349ecab5724662
2026-07-31-resume-selector-batch-projection.zh.md: 3f824a3063849306e5c1c21699f6a19eefcfefe5
2026-07-31-resume-selector-batch-projection.zh.md: 19bea7211cc61fa7db80370ee3092e5c88bf931b

View File

@@ -26,9 +26,9 @@ session-query 与 session-persistence 的任何表面都未改变。随附的 TU
**只修复 `SessionCorpus.load()` 内部的 O(N²) 列表查询。** 作为主要修复被否决:在大日志上,按候选行执行的完整解压、回放验证和三重克隆才是主要开销。`load()` 中的冗余预列表查询仍是一个候选清理项,但涉及错误语义。
**通过 `listSnapshots`/`SessionRecord` 暴露最后修改时间。** 从 seam 角度最干净,但要触碰持久化契约、两个后端和查询记录形状,而 TUI 已能用 `locate()` 加一次 stat 得到同样的信息。若出现第二个需要元数据活动时间的消费者再引入。
**通过 `listSnapshots`/`SessionRecord` 暴露最后修改时间。** 从 seam 角度最干净,但要触碰持久化约定、两个后端和查询记录形状,而 TUI 已能用 `locate()` 加一次 stat 得到同样的信息。若出现第二个需要元数据活动时间的消费者再引入。
**专门的持久化标题索引或 TUI 本地标题缓存。** 否决:session-projection 缓存本身就是自有的持久 checkpoint 系统,并已带失效契约(`stateVersion`、身份绑定、日志收缩锚定);挂载它优于再造一套并行缓存。
**专门的持久化标题索引或 TUI 本地标题缓存。** 否决:session-projection 缓存本身就是自有的持久 checkpoint 系统,并已带失效约定(`stateVersion`、身份绑定、日志收缩锚定);挂载它优于再造一套并行缓存。
## Consequences

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-web-stop-preserves-queue.md
2026-07-31-web-stop-preserves-queue.md: b8bba45009c126495a8b03ee36d6eb2cccac9a93
2026-07-31-web-stop-preserves-queue.zh.md: dce4f431f1f433afce69689cd5220e36a68b85e2
2026-07-31-web-stop-preserves-queue.zh.md: af9c114fd06452e58322f2554a0f2ccd861c81c9

View File

@@ -16,7 +16,7 @@ Web 停止按钮调用 `session.cancel`,后者映射到广义 `agent.cancel({
AgentLoop 不会启动并发的替代轮次。它会关闭并 flush 被中断的轮次,达到取消的完全停稳,然后通过现有 FIFO 驱动器认领下一个可唤醒的 queued 入队项。该认领会发出 `agent/inbox/dequeue`,因此 Host 的权威 `session/queue` 快照会退役已认领行,并使剩余队尾保持可见。浏览器既不重发,也不提升任何行。忽略取消的工作会延迟这一交接,直到该工作结算。
该映射只更改 Web 客户端使用的 Host `session.cancel` 端点。`Agent.cancel()` 默认约定仍为广义取消,ACP(Agent Client Protocol)和 TUI 保留既有取消策略,`AgentHandle.dispose()` 在拆卸期间仍会清除待处理工作。移除 Queue 行仍是用于丢弃单个待处理入队项的显式 Web 操作。
该映射只更改 Web 客户端使用的 Host `session.cancel` 端点。`Agent.cancel()` 默认约定仍为广义取消,ACP 和 TUI 保留既有取消策略,`AgentHandle.dispose()` 在拆卸期间仍会清除待处理工作。移除 Queue 行仍是用于丢弃单个待处理入队项的显式 Web 操作。
## 考虑过的替代方案
@@ -30,7 +30,7 @@ AgentLoop 不会启动并发的替代轮次。它会关闭并 flush 被中断的
## 验证
AgentLoop 覆盖测试会保持一个活动模型流,将两个可唤醒轮次排队,使用 `keepInbox` 取消,并固定验证先中止、后完成的轮次原因,FIFO 用户消息顺序,不存在 discard 事件,以及最终空闲状态。无密钥 Web 场景通过 HTTP/SSE(Server-Sent Events)驱动已组装的组合:它停止一个卡住的轮次,观察队尾保持可见时下一个 queued 入队项开始,再停止该轮次,并观察最后一个 queued 入队项完成。其可访问性快照固定了中间的 Queue 保留状态。
AgentLoop 覆盖会保持一个活动模型流,将两个可唤醒轮次排队,使用 `keepInbox` 取消,并固定验证先中止、后完成的轮次原因,FIFO 用户消息顺序,不存在 discard 事件,以及最终空闲状态。无密钥 Web 场景通过 HTTP/SSE 驱动已组装组合:它停止一个卡住的轮次,观察队尾保持可见时下一个 queued 入队项开始,再停止该轮次,并观察最后一个 queued 入队项完成。其可访问性快照固定了中间的 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/bug-fix/2026-08-02-goal-round-wrapup-message.md
2026-08-02-goal-round-wrapup-message.md: c6bc3d5912b0789efde55880c2be892e98e34a5b
2026-08-02-goal-round-wrapup-message.zh.md: 46876f09edfea631b9621caf417ecf4df4ccff2d
2026-08-02-goal-round-wrapup-message.zh.md: f223516e1255f2dafe026a48d1faafa838af6cb8

View File

@@ -1,4 +1,4 @@
# Agent Note: Goal Round 收尾消息
# Agent Note:Goal Round 收尾消息
Status: implemented
@@ -6,26 +6,26 @@ Status: implemented
## 问题
自主 Goal Round 报告 `update_goal` `complete` 或 `blocked` 时,物理轮次在工具结果处直接终结,模型在调用之后再无发言机会。会话终止在一张裸的 `update_goal` 卡片上,内测同学的观感是 agent(智能体)话说到一半戛然而止:模型调用前的文本通常预告了一份汇报(「目标达成,标记完成:」)却永远没有下文,因为标准 tool-use 预期是工具结果之后还有一条 assistant 消息,而 Goal Round 提示词与工具描述都没有说明这次调用是终点。硬停止来自 [goal 工具决策](../feature/2026-07-19-model-facing-goal-tools.md),本 Agent Note 取代其中的轮次停止条款。
自主 Goal Round 报告 `update_goal` `complete` 或 `blocked` 时,物理轮次在工具结果处直接终结,模型在调用之后再无发言机会。会话终止在一张裸的 `update_goal` 卡片上,内测同学的观感是 agent 话说到一半戛然而止:模型调用前的文本通常预告了一份汇报(“目标达成,标记完成:”)却永远没有下文,因为标准 tool-use 预期是工具结果之后还有一条 assistant 消息,而 Goal Round 提示词与工具描述都没有说明这次调用是终点。硬停止来自 [goal 工具决策](../feature/2026-07-19-model-facing-goal-tools.md),本 note 取代其中的轮次停止条款。
## 决策
Goal Round 的 `complete` 或 `blocked` 成功不再调用 `concludeTurn()`。工具改为在自己的结果上附带一条收尾上下文:以 `{ kind: 'plugin', plugin: 'tool-goal' }` 为 source 的 user 消息,携带 `<goal_complete>`/`<goal_blocked>` 指令,要求模型向用户写出有依据的收尾消息且不再调用工具。之后轮次经由 agent loop(智能体循环)常规的无工具调用停止路径结束,因此不存在新的 loop 原语,steering(中途引导)语义不受影响。人类直接变更保持原样、不注入指令。代价是每个 goal 生命周期一次额外模型请求,而非每个 Round 一次。
Goal Round 的 `complete` 或 `blocked` 成功不再调用 `concludeTurn()`。工具改为在自己的结果上附带一条收尾上下文:以 `{ kind: 'plugin', plugin: 'tool-goal' }` 为 source 的 user 消息,携带 `<goal_complete>`/`<goal_blocked>` 指令,要求模型向用户写出有依据的收尾消息且不再调用工具。之后轮次经由 agent loop 常规的无工具调用停止路径结束,因此不存在新的 loop 原语,steering 语义不受影响。人类直接变更保持原样、不注入指令。代价是每个 goal 生命周期一次额外模型请求,而非每轮一次。
指令措辞通过在 `deepseek-v4-pro` 上用重构的 Goal Round transcript(文本记录)做 A/B 采样选定:结构化指令(结果、验证、产物、后续)在完整度上稳定优于极简「总结一下」;补充「以会话内证据为准」的 grounding 条款让无依据细节从断言事实退为带保留的建议;而无指令对照组的收尾方差很大,包括言之凿凿的文件级细节编造。
指令措辞通过在 `deepseek-v4-pro` 上用重构的 Goal Round 转录做 A/B 采样选定:结构化指令(结果、验证、产物、后续)在完整度上稳定优于极简“总结一下”;补充“以会话内证据为准”的 grounding 条款让无依据细节从断言事实退为带保留的建议;而无指令对照组的收尾方差很大,包括言之凿凿的文件级细节编造。
为让 keyless 证明可脚本化,快照 harness 补了一项能力:`dsh-llm-replay` 会针对实时请求解析脚本条目中的 `{{fromRequest:<regex>}}` 占位符,因为静态伴随文件不可能预知模型必须回填进 `update_goal` 的随机生成 goal id。
为让 keyless 证明可脚本化,快照设施补了一项能力:`dsh-llm-replay` 会针对实时请求解析脚本条目中的 `{{fromRequest:<regex>}}` 占位符,因为静态伴随文件不可能预知模型必须回填进 `update_goal` 的随机生成 goal id。
## 验证
`tool-goal` 包测试钉住两个终态 action 注入的上下文(source、标签、objective、禁止再调工具条款)与不存在的 `concludesTurn`,以及人类直接 pause 与 complete 的不注入路径,文件覆盖率 100%。`llm-replay` 单元测试钉住占位符约定:最后一次匹配取胜的捕获、无捕获组时整体匹配回退,以及未匹配、非法、未闭合模式的明确报错。新增 keyless ACP 快照 `goal-wrapup` 驱动成品应用走完 create → 第一个 Round → 自主 complete,并在持久会话日志与 ACP stdout 流中同时断言 plugin 来源的收尾注入、同一轮次内的收尾 assistant 消息与 `completed` 轮次结束。
`tool-goal` 包测试钉住两个终态 action 注入的上下文(source、标签、objective、禁止再调工具条款)与不存在的 `concludesTurn`,以及人类直接 pause 与 complete 的不注入路径,文件覆盖率 100%。`llm-replay` 单元测试钉住占位符约定:最后一次匹配取胜的捕获、无捕获组时整体匹配回退,以及未匹配、非法、未闭合模式的明确报错。新增 keyless ACP 快照 `goal-wrapup` 驱动成品应用走完 create → 第一轮 → 自主 complete,并在持久会话日志与 ACP stdout 流中同时断言 plugin 来源的收尾注入、同轮内的收尾 assistant 消息与 `completed` 轮次结束。
## 曾考虑的替代方案
- **在 `update_goal` 的 UI 卡片上展示完成文本** — 拒绝:`complete` 如今不携带任何自由文本;新增 `summary` 参数会让面向用户的汇报走工具参数通道,而且依然砍掉了模型在结果之后的自然发言。
- **保留 `concludeTurn()` 并新增「再多一步纯文本」的 loop 原语** — 拒绝:为常规停止路径已经能提供的行为(只要没有结果终结轮次)增加新的 `agent-loop` 机制。
- **保留 `concludeTurn()` 并新增“再多一步纯文本”的 loop 原语** — 拒绝:为常规停止路径已经能提供的行为(只要没有结果终结轮次)增加新的 `agent-loop` 机制。
- **把指令写进工具结果内容** — 拒绝:goal 工具的规范输出是被程序化消费的紧凑 JSON;在其中混入散文指令会把模型侧约定和工具的可回放值搅在一起。
## 后果
## Consequences
每个自主 goal 都以一条面向用户的收尾消息结束,而非一张裸工具卡片,代价是每个 goal 生命周期一次模型请求。`concludeTurn()` 保留其 loop 语义,但在 subagent 结构化输出之外失去了唯一的第一方调用者。快照场景现在可以通过 `{{fromRequest:...}}` 脚本化只在运行时才存在的值,为任何「回显 id」类工具流程(不限于 goal)解锁 keyless 覆盖。
每个自主 goal 都以一条面向用户的收尾消息结束,而非一张裸工具卡片,代价是每个 goal 生命周期一次模型请求。`concludeTurn()` 保留其 loop 语义,但在 subagent 结构化输出之外失去了唯一的一方调用者。快照场景现在可以通过 `{{fromRequest:...}}` 脚本化只在运行时才存在的值,为任何“回显 id”类工具流程(不限于 goal)解锁 keyless 覆盖。

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-hmr-initial-scan-boot-deadlock.md
2026-08-03-hmr-initial-scan-boot-deadlock.md: 1a70fbafc74a1bf7024c675ddad82acefc995b72
2026-08-03-hmr-initial-scan-boot-deadlock.zh.md: c52c2c2f4e7375e4c783e43c58e6fe0e3eb6b44b
2026-08-03-hmr-initial-scan-boot-deadlock.zh.md: a023420182971a62f125808f777f877bafe3e9e2

View File

@@ -1,6 +1,6 @@
# Agent Note: HMR(热模块替换)初始扫描使失败的启动陷入死锁,最终静默地以 13 退出
# Agent Note:HMR 初始扫描使失败的启动死锁为静默的 exit 13
Status: implemented
状态:已实现
[English](2026-08-03-hmr-initial-scan-boot-deadlock.md) | 中文
@@ -11,16 +11,16 @@ Status: implemented
两个缺陷叠加:
1. **并发的 Include apply 破坏事务化的 group update。** HMR 主 watcher 的 chokidar 初始扫描会把每个已存在的文件重新宣告为 `add`。其中配置文件的 `add` 在 Include 的首次 apply 尚未结束时触发了 `Include.refresh()`(内容去重键 `this.content` 只在 apply 完成后才提交)。同一 group 上两个并发的 `EntryGroup.update` 会在相同条目上交错执行 create 与回滚,导致 Include fiber 永远无法结算:`loader.create` 挂起,`boot()` 既不 resolve 也不 reject,事件循环排空后 Node 以 13 退出。
2. **仅序列化 apply 会让失败回滚死锁。** 将 Include 的变更排入队列后,首次 apply 失败时的回滚会对每个已挂载条目执行 dispose(资源释放)——包括 `hmr`,而它的拆卸会等待自身的 refresh 任务排空。扫描触发的 refresh 任务正排在 Include 队列中、位于正在回滚的那次 apply 之后:回滚等 HMR,HMR 等 refresh,refresh 等 apply。
2. **仅序列化 apply 会让失败回滚死锁。** 将 Include 的变更排入队列后,首次 apply 失败时的回滚会释放每个已挂载条目——包括 `hmr`,而它的拆卸会等待自身的 refresh 任务排空。扫描触发的 refresh 任务正排在 Include 队列中、位于正在回滚的那次 apply 之后:回滚等 HMR,HMR 等 refresh,refresh 等 apply。
## 决策
## 决定
两处修复都落在 vendored 包中(记录于 `vendor/README.md`):
- `include/src/index.ts` 将每次子树变更——首次 apply、refresh、`internal/update` 补丁重应用——汇入每个 Include 各自的一条 promise 队列。group 的事务化 `update` 不可重入,因此序列化是正确性要求,而不是吞吐取舍。`refresh()` 也在队列内读取文件,使其内容变更判断与前一任务提交后的状态比较。
- `include/src/index.ts` 将每次子树变更——首次 apply、refresh、`internal/update` 补丁重应用——汇入每个 Include 一条的 promise 队列。group 的事务化 `update` 不可重入,因此序列化是正确性要求,而不是吞吐取舍。`refresh()` 也在队列内读取文件,使其内容变更判断与前一任务提交后的状态比较。
- `hmr/src/index.ts` 给主 watcher 传入 `ignoreInitial: true`。初始扫描只会重新宣告启动刚刚消费过的文件;抑制它同时消除了启动期 refresh 和对已加载模块的多余 `add` 事件。`registerConfig()` 保留自己 `ignoreInitial: false` 的 watcher,因为注册时已存在的个人配置必须恰好应用一次。
两者齐备后,失败的启动走上预期路径:唯一一次 apply 失败,回滚对整棵树执行 dispose(执行 TUI 自身的 shutdown、恢复终端),`loader.create` reject,`boot()` 重新抛出带标签的诊断并以 1 退出。
两者齐备后,失败的启动走上预期路径:唯一一次 apply 失败,回滚释放整棵树(执行 TUI 自身的 shutdown、恢复终端),`loader.create` reject,`boot()` 重新抛出带标签的诊断并以 1 退出。
## 曾考虑的替代方案
@@ -28,14 +28,14 @@ Status: implemented
**只做序列化。** 把破坏转化为上述回滚死锁;进程仍然静默地以 13 退出。
**在 HMR 拆卸时取消排队中的 refresh。** 需要在 `refreshConfig` 的任务循环和 Include 队列中铺设取消机制,而 `ignoreInitial` 已把该场景从每次启动中移除;在真实触发条件出现之前不值得引入这套机制。
**在 HMR 拆卸时取消排队中的 refresh。** 需要在 `refreshConfig` 的任务循环和 Include 队列中铺设取消机制,而 `ignoreInitial` 已把该场景从每次启动中移除;在真实触发条件出现之前不值得引入这套机构。
## 后果
落在 watcher 启动扫描窗口内的配置文件编辑,现在由下一个 `change` 事件而非扫描本身拾取;稳态的重载行为不变。
仍留有一个潜在缺口:在一次*失败的*首次 apply 期间进行的配置编辑,仍可能排入一个被回滚的 HMR 拆卸所等待的 refresh——同样的死锁形态,但触发窗口达到一次失败启动这样的人可操作时间尺度。若它真的发生,修复方向是在 HMR 拆卸时取消 refresh 任务。
仍留有一个潜在缺口:在一次*失败的*首次 apply 期间进行的配置编辑,仍可能排入一个被回滚的 HMR 拆卸所等待的 refresh——同样的死锁形态,但触发窗口缩小到一次失败启动的人力尺度。若它真的发生,修复方向是在 HMR 拆卸时取消 refresh 任务。
## 测试
`apps/cli/tests/tui-keyless-smoke.e2e.ts` 中 `dsh` 无效 provider 的 PTY 用例钉住了端到端约定:以 1 退出、带标签的 `dsh: plugin tree failed to load:` 诊断指明 `$.providers`、以及证明整棵树已完成 dispose 的 bracketed-paste 复位序列。此修复之前,同一用例观察到的是无诊断的 exit 13。重载行为仍由 `packages/boot/app-boot/tests/config-reload.spec.ts` 与 `packages/boot/app-boot/tests/hmr-config.spec.ts` 覆盖。
`apps/cli/tests/tui-keyless-smoke.e2e.ts` 中 `dsh` 无效 provider 的 PTY 用例钉住了端到端约定:以 1 退出、带标签的 `dsh: plugin tree failed to load:` 诊断指明 `$.providers`、以及证明整棵树已被释放的 bracketed-paste 复位序列。此修复之前,同一用例观察到的是无诊断的 exit 13。重载行为仍由 `packages/boot/app-boot/tests/config-reload.spec.ts` 与 `packages/boot/app-boot/tests/hmr-config.spec.ts` 覆盖。

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: 4b3bbbc03c4494359297ae6e54bcc9a74c387e80
2026-08-06-onboarding-step-owned-takeover-chrome.zh.md: 88e27649d719c0ef7cd2b4604c507c40845a715d
2026-08-06-onboarding-step-owned-takeover-chrome.zh.md: ba4f503f7bc318fcb16f4413465a86609fa1bc61

View File

@@ -1,6 +1,6 @@
# Agent Note: 首次使用引导的接管界面框架移入步骤自身
# Agent Note:首次使用引导的接管界面框架移入步骤自身
Status: implemented
状态:已实现
[English](2026-08-06-onboarding-step-owned-takeover-chrome.md) | 中文
@@ -10,26 +10,26 @@ Status: implemented
于是每次在 hero(空白或无会话)状态下刷新页面,会话列表一变 `ready` 就弹出整屏不透明层——亮色主题下是白色——并阻断全部交互,时长恰好等于一次凭据/设置 RPC 往返;之后已配置好的步骤自我完成,图层消失。用户看到的就是每次刷新在 workspace/会话列表落地的瞬间闪一下白屏。
## 决策
## 决定
接管界面框架属于步骤,不属于外壳。新增零 Cordis 原语 `OnboardingSurface`(ui-primitives):渲染 portal 到 body 的浮层/遮罩/展示层——CSS 类名与几何从 `SettingsRoot.module.css` 逐字迁移——并在自身挂载生命周期内保持 `#root` 为 `inert`。两个步骤组件只把各自的**可见**分支包进该原语;既有的 `null` 分支由此在构造上不绘制、不阻塞任何内容,因为界面框架已是同一次渲染决策的一部分。
接管界面框架属于步骤,不属于外壳。新增零 cordis 原语 `OnboardingSurface`(ui-primitives):渲染 portal 到 body 的浮层/遮罩/展示层——CSS 类名与几何从 `SettingsRoot.module.css` 逐字迁移——并在自身挂载生命周期内保持 `#root` 为 `inert`。两个步骤组件只把各自的**可见**分支包进该原语;既有的 `null` 分支由此在构造上不绘制、不阻塞任何内容,因为界面框架已是同一次渲染决策的一部分。
`SettingsRoot` 的协调器原样保留(有序账本投影、每次挂载一个步骤、本地完成集合、`stepId`/`complete`/`openSection` currency),但对当选步骤裸渲染——不再有 portal、展示层和 inert 效果。`settings.onboarding` 的 slot 约定现在写明:注册方持有外层包裹,且在私有事实未决时必须渲染 `null`。
## 曾考虑的替代方案
**条件注册(账本即有内容信号)。** 私有 join 解析出「需要介入」后才注册条目。架构上干净(在 commit point 发布),但改动更大:join 的加载必须从对话框上移到各插件的 apply,注册/dispose(资源释放)在两个包里都变成响应式接线。对本缺陷而言过重,否决。
**条件注册(账本即有内容信号)。** 私有 join 解析出「需要介入」后才注册条目。架构上干净(在 commit point 发布),但改动更大:join 的加载必须从对话框上移到各插件的 apply,注册/销毁在两个包里都变成响应式接线。对本缺陷而言过重,否决。
**把 `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`(此前在不透明图层背后处于 inert 状态)。对真正未配置的用户,接管层比从前晚一个 join 往返出现——但一出现就带着内容,而不是先露出空白展示层再填充。
步骤已挂载但尚未判定期间,应用保持可见且可交互:判定窗口内 `#root` 不再是 `inert`(此前是在不透明图层背后被置灰)。对真正未配置的用户,接管层比从前晚一个 join 往返出现——但一出现就带着内容,而不是先露出空白展示层再填充。
未来若有步骤注册后不把可见内容包进 `OnboardingSurface`,会无遮罩地裸渲染在应用之上;slot 约定的 JSDoc 已把包裹写为注册方的义务。
## 测试
`packages/client/ui-primitives/tests/onboarding-surface.spec.tsx` 钉住原语行为:包裹内容的 body portal、遮罩/展示层类名存在、`#root` 的 `inert` 恰好持续挂载生命周期,以及无 `#root` 的组合。`packages/client/ui-settings/tests/settings-root.spec.tsx` 钉住反转后的外壳约定:已挂载步骤什么都不渲染时,无接管界面框架、无 inert。`apps/web/tests/onboarding-deepseek-config.e2e.ts` 新增本缺陷的整装回归钉:已配置世界刷新页面,同时在浏览器网络边界扣住所有 `settings.describe` 响应——把步骤的判定窗口从 loopback 下不可见拉宽到数百毫秒,这正是断言保持非空洞的关键——页内 8 ms 采样器证明接管界面框架从未挂载、`#root` 从未变为 inert。该文件的既有场景与步骤 spec(`ui-settings-general`、`ui-models`)原样通过——样式表逐字迁移,遮罩选择器与几何钉子得以幸存。
`packages/client/ui-primitives/tests/onboarding-surface.spec.tsx` 钉住原语行为:内容外的 body portal、遮罩/展示层类名存在、`#root` 的 `inert` 恰好持续挂载生命周期,以及无 `#root` 的组合。`packages/client/ui-settings/tests/settings-root.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-06-provider-credential-lifecycle.md
2026-08-06-provider-credential-lifecycle.md: c28788921e8f1b233b44e19b29ad4d4acaa25022
2026-08-06-provider-credential-lifecycle.zh.md: d11aee5c5f014315c016e77f157cbac2624f844c
2026-08-06-provider-credential-lifecycle.zh.md: 2791baa7761b243cbdd34251d1dc11678b8a9a65

View File

@@ -10,18 +10,18 @@ Models 编辑器横跨互相独立的 settings 与凭据 RPC 领域。之前它
## 决策
提供方保存仍在现有 wire 领域上按先 settings、后凭据的两阶段顺序执行,但卡片会把成功的 settings 响应视为提交检查点。它会在尝试 `credentials.set` 之前,用返回的脱敏 descriptor 替换比较基准子树与预期 revision;如果第二阶段失败,草稿密钥与卡片会继续显示,重试不会产生 settings op,只会再次写入凭据。首次 settings 提交之前发生的真实并发变更仍会以 `settings-conflict` 失败。UI 与 DeepSeek 直连 resolver 边界均会去除所输密钥的首尾空白,且只有标准化密钥非空时,pi-ai 才会记录派生引用;留空密钥会生成一个空的、不带引用的 profile,以便使用提供方原生凭据发现。
提供方保存仍在现有 wire 领域上按先 settings、后凭据的两阶段顺序执行,但卡片会把成功的 settings 响应视为提交检查点。它会在尝试 `credentials.set` 之前,用返回的脱敏 descriptor 替换比较基准子树与预期 revision;如果第二阶段失败,草稿密钥与卡片会继续显示,重试不会产生 settings op,只会再次写入凭据。首次 settings 提交之前发生的真实并发变更仍会以 `settings-conflict` 失败。UI 与 DeepSeek 直连 resolver 边界均会去除所输密钥的首尾空白,且只有标准化密钥非空时,pi-ai 才会记录派生引用;留空密钥会具化一个空的、不带引用的 profile,以便使用提供方原生凭据发现。
只有当联接所得的行识别出该页面派生的精确 `<ROUTE>_API_KEY` 引用,并将其报告为已配置且可写时,删除操作才会清除该凭据。它会先取消设置该凭据,再取消设置用户层 profile;如果 settings 阶段失败,该行及其已冻结的目标仍可见,便于重试。两项 unset 都具备幂等性。自定义引用、环境凭据、缺失的凭据,以及联接无法识别目标的凭据均会保留。行的无障碍 Edit/Delete 名称以及破坏性对话框的标题、说明和最终操作都使用同一个稳定的 `Display Name (route-id)` 标识;当两个字符串相同时,标识会简化为路由 id。对话框会说明是否一并删除已存密钥,并在自身内显示操作失败,而不是用加载错误横幅替换整个页面。行只根据不含值的联接结果展示 API 密钥状态:确认已配置的引用凭据显示为绿色实心点,确认缺失的具名引用显示为红色实心点,无引用的提供方原生认证或无法取得凭据补充信息时则不显示状态点。每个状态点都有无障碍文案和工具提示;「应用」成功后的本地状态消息会使用同一个提供方标识,且绝不回显任何机密内容。
## 曾考虑的替代方案
**添加跨领域事务 RPC。** settings 与凭据分属不同的主管服务与持久存储;引入新的 Host 事务会扩大公开 wire 面,而且仍需要补偿提供方特定的持久化失败。UI 检查点让当前的有序阶段变得可恢复,无需添加第四项配置约定。
**添加跨领域事务 RPC。**settings 与凭据分属不同的主管服务与持久存储;引入新的 Host 事务会扩大公开 wire 面,而且仍需要补偿提供方特定的持久化失败。UI 检查点让当前的有序阶段变得可恢复,无需添加第四项配置约定。
**删除被移除 profile 所指定的每一个凭据引用。** 自定义引用可能被共享、由外部管理,或有意在 profile 反复增删时存留。与该页面派生目标精确相等,再加上已配置且可写的状态,是页面所能获得的最小范围证据;比这更弱的判定都有可能删除不属于它的凭据。
**删除被移除 profile 所指定的每一个凭据引用。**自定义引用可能被共享、由外部管理,或有意在 profile 反复增删时存留。与该页面派生目标精确相等,再加上已配置且可写的状态,是页面所能获得的最小范围证据;比这更弱的判定都有可能删除不属于它的凭据。
**先删除 settings,再重建 profile 以作补偿。** 浏览器只持有脱敏后的子树,无法忠实重建并发编辑。先删除凭据可以让权威 profile 在部分失败时仍然可见,并且无需合成配置就能安全重试。
**先删除 settings,再重建 profile 以作补偿。**浏览器只持有脱敏后的子树,无法忠实重建并发编辑。先删除凭据可以让权威 profile 在部分失败时仍然可见,并且无需合成配置就能安全重试。
## 后果
Models 页可以从任一第二阶段失败中恢复,无需重新加载,也不会泄露机密或产生虚假的并发冲突;空密钥的 pi-ai profile 会保留 Bedrock、Vertex 与其他提供方原生认证。已确认的状态清晰可见,同时不会把路由存活状态、原生认证或凭据查询失败误报为错误;即使该行继续显示绿色,密钥替换成功也仍然可观察。删除由页面管理的提供方不再遗留可重用的本地密钥,而存在歧义的凭据会有意保留,交由手动管理。保存与删除在跨持久存储时仍非原子操作:进程可能在两个阶段之间崩溃,但它们的顺序与幂等性会留下可观察、可重试的状态。组件测试固定了部分成功后的重试、空密钥原生认证、密钥首尾空白处理、状态可见性、目标标识、清理所有权,以及凭据/settings 拒绝顺序;无密钥的浏览器场景固定了双语无障碍文案,并验证确认删除会同时清除 `settings.yaml` profile 与 `.credentials.yaml` 条目。此决策细化了 [web 配置平面 Agent Note](../architecture/2026-07-30-web-config-plane.md) 中记录的 Models 应用语义。
Models 页可以从任一第二阶段失败中恢复,无需重新加载,也不会泄露机密或产生虚假的并发冲突;空密钥的 pi-ai profile 会保留 Bedrock、Vertex 与其他提供方原生认证。已确认的状态清晰可见,同时不会把路由存活状态、原生认证或凭据查询失败误报为错误;即使该行继续显示绿色,密钥替换成功也仍然可观察。删除由页面管理的提供方不再遗留可重用的本地密钥,而存在歧义的凭据会有意保留,交由手动管理。保存与删除在跨持久存储时仍非原子操作:进程可能在两个阶段之间崩溃,但它们的顺序与幂等性会留下可观察、可重试的状态。组件测试固定了部分成功后的重试、空密钥原生认证、密钥首尾空白处理、状态可见性、目标标识、清理所有权,以及凭据/settings 拒绝顺序;无密钥的浏览器场景固定了双语无障碍文案,并验证确认删除会同时清除 `settings.yaml` profile 与 `.credentials.yaml` 条目。此决策细化了 [web 配置平面 note](../architecture/2026-07-30-web-config-plane.md) 中记录的 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-06-token-surface-unpriced-replace-compatibility.md
2026-08-06-token-surface-unpriced-replace-compatibility.md: 77b778fba695f560eb68af6da2f416e61ccff181
2026-08-06-token-surface-unpriced-replace-compatibility.zh.md: 1a18551b1e0c1e7e78f32d69caec1d0956a4075d
2026-08-06-token-surface-unpriced-replace-compatibility.zh.md: 0aff9db9b95e8670d85c8239dc2313b4e20f321c

View File

@@ -14,13 +14,13 @@ Status: implemented
到达时没有已就位声明的替换以价格中性的方式折叠:`foldSurfaceProjection` 返回 `deltaTokens: 0`,相当于把被替换区间计价为恰好等于其替换内容的成本,回放随即继续。因中间插入的事件而过期的声明也走同一条中性路径,因为折叠无法把它与从未计量过的日志区分开。
已就位但指向**另一个**区间的声明仍会抛出异常。此时计量事件确实相邻,说明生产方写入了互相矛盾的相邻事件:这是现行影子价格约定的违规,不是历史数据,必须明确报错,而不能任由总量悄然漂移。
已就位但指向**另一个**区间的声明仍会抛出异常。此时计量事件确实相邻,说明生产方写入了互相矛盾的相邻事件:这是现行影子价格约定的违规,不是历史数据,必须响亮失败,而不能任由总量悄然漂移。
两个投影共用同一个折叠,因此二者都不新增状态字段,也不提升 `stateVersion`。`surface-fold.ts` 与 `ctx.tokenMeter.measure()` 不受影响:它们持有逐节点的已计价表层,本来就不需要声明协议。
## 备选方案
**维持抛出异常。**保住了严格的生产方约定,但协议之前的每个会话都将永远无法回放,而投影本就是为回放服务的。
**维持抛出异常。**保住了严格的生产方约定,但协议之前的每个会话都将永远无法回放,而投影本就是为服务回放而存在的。
**在投影状态中持久化完整的已计价表层。**可以对任意被替换区间精确计价,但检查点会随每条模型可见消息各增加一个节点、无上限地增长,恰恰破坏了影子价格协议所要守住的 O(1) 约束(见[上下文仪表的 Agent Note](2026-08-05-context-meter-blind-to-compaction.md))。
@@ -28,7 +28,7 @@ Status: implemented
未计价的替换让总量保持不动而不是缩小,因此被压缩(compaction)掉的区段仍被计入:`contextBreakdown.messageTokens` 保留这部分多计的量;`contextPressure.projectedTokens` 会高估占用率,但只持续到下一个用量样本重新锚定为止,因为该数字追踪的是自样本以来的增减,而非绝对水平。误差方向是安全的:高估占用率最坏不过是招致一次更早的压缩。
明确报错机制仍保留在有意义的场景中:区间不匹配的相邻声明是现行生产方的缺陷,仍会抛出异常。
响亮失败保留在它仍有意义的地方:区间不匹配的相邻声明是现行生产方的缺陷,仍会抛出异常。
## 测试

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: fe00c78bdfadac0cc6c9c173fd04256f77a22051
2026-08-07-cancel-convergence-wake-latch.zh.md: f76ad360e2d8b70f76c6baf53e2fde963a80b6c4
2026-08-07-cancel-convergence-wake-latch.zh.md: 736c5eb39809811363ed93529d90878ea9acdc89

View File

@@ -6,11 +6,11 @@ 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()` 后该边界被抑制,CI 发现了这一点)。不带 `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 认领前被清除,仍会打开自己的 turn 边界——这趟 `idle → running → idle` 转换是可观察约定:goal-session driver 的 pause/disarm 回退依赖取消预订后的 `idle` 转换触发(把守卫放进 `wakeDriver()` 后该边界被抑制,CI 发现了这一点)。不带 `keepInbox` 的 `cancel()` 会连同 inbox 一起清除锁存。
`signal.aborted` 判别项是承重的:它区分「中断前已排队的工作」——`keepInbox` 将其停放以待后续唤醒(验收条件 1)——与「abort 后显式的唤醒」,后者必须在收敛后执行。