mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
Merge remote-tracking branch 'origin/master' into worktree/session-turn-nav-styling-7715ae
# Conflicts: # packages/client/ui-chat/README.i18n.yaml # packages/client/ui-chat/README.md # packages/client/ui-chat/README.zh.md
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/archived/feature/2026-08-18-product-subagent-failure-facts.md
|
||||
2026-08-18-product-subagent-failure-facts.md: b1d80cf66172ac67d38dbad873fa4cbd970a775c
|
||||
2026-08-18-product-subagent-failure-facts.zh.md: df4b14b4a243f7768240678b8d434c7aef7d48a7
|
||||
+7
-13
@@ -1,6 +1,7 @@
|
||||
# Agent Note: Product subagents expose bounded structured failure facts
|
||||
|
||||
Status: implemented
|
||||
Archived: 2026-08-21
|
||||
|
||||
English | [中文](2026-08-18-product-subagent-failure-facts.zh.md)
|
||||
|
||||
@@ -12,7 +13,7 @@ Copying SDK error text, app-server payloads, or stderr into the result would exp
|
||||
|
||||
## Decision
|
||||
|
||||
Each product Provider owns the mapping from its pinned official error union, current operation, and managed process outcome to one fixed safe diagnostic line. `SubagentResult` remains unchanged: consumers receive the existing bounded `diagnostic` string and do not parse its product-private fields.
|
||||
Each product Provider owns the mapping from its pinned official structured failures, current operation, and managed process outcome to one fixed safe diagnostic line. `SubagentResult` remains unchanged: consumers receive the existing bounded `diagnostic` string and do not parse its product-private fields. The [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md) supersedes this note's complete Claude Code subtype mirror; this note continues to own the current detailed Codex categories until that provider adopts the same simplification.
|
||||
|
||||
### Safe diagnostic
|
||||
|
||||
@@ -28,14 +29,7 @@ Successful results and local cancellation expose no failure fact. Raw product er
|
||||
|
||||
### Claude Code facts
|
||||
|
||||
Agent SDK 0.3.220 defines four error subtypes: `error_during_execution`, `error_max_turns`, `error_max_budget_usd`, and `error_max_structured_output_retries`. The Claude Code Provider preserves each exact subtype as the category while keeping the shared stop reason `error`. An error-marked or blank success uses `invalid-success`, a missing result uses `missing-result`, a process exit before an SDK terminal result uses `process-exit`, and an unrecognized value or exception uses `unknown` without copying the value.
|
||||
|
||||
| Stage | Owned operation | Observable failure |
|
||||
| --- | --- | --- |
|
||||
| `query-start` | SDK query construction, native platform-payload startup, and unpublished rollback | `start()` rejects with fixed safe facts and any process outcome observed before rollback |
|
||||
| `query-run` | Published SDK message iteration and strict terminal-result validation | The run resolves as `error` with the exact known subtype or a fixed result category |
|
||||
| `process` | Managed CLI exits before the SDK supplies a terminal result | The run resolves as `error` with `process-exit` and the available exit code and signal |
|
||||
| `teardown` | Query close and managed process-tree release | `dispose()` rejects independently with fixed safe facts after cleanup still reaches its final exit wait |
|
||||
The [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md) exclusively owns Claude Code categories, stages, process facts, permission ordering, and verification for Agent SDK 0.3.241 and Claude Code 2.1.241. This note carries no separate Claude category contract.
|
||||
|
||||
### Codex facts
|
||||
|
||||
@@ -56,7 +50,7 @@ Codex app-server 0.147.0 defines eleven string categories and five object varian
|
||||
|
||||
| Fact or resource | Owner | Consumer behavior |
|
||||
| --- | --- | --- |
|
||||
| Product error category | Pinned official SDK or app-server version | The Provider maps only the declared structured union and uses `unknown` outside it |
|
||||
| Codex error category | Codex Provider over its pinned official app-server | The Provider preserves its current structured category and uses `unknown` outside the recognized set |
|
||||
| Current failure stage | Product Provider operation | Derived at the failure site; never persisted or used as a recovery state |
|
||||
| Exit code and signal | `dsh-subprocess` process handle | The Provider displays observed values without inferring missing ones |
|
||||
| Diagnostic bytes and delivery | `dsh-subagent`, foreground tool, and Job runtime | The same bounded text is presented separately from assistant output in both scheduling modes |
|
||||
@@ -64,7 +58,7 @@ Codex app-server 0.147.0 defines eleven string categories and five object varian
|
||||
|
||||
## Verification
|
||||
|
||||
Claude Code package tests pin all four SDK subtypes, invalid success, missing result, unknown values and exceptions, all four stages, independent exit code and signal fields, permission-fact ordering, sanitization, successful-result and cancellation omission, concurrent-run isolation, and cleanup completion. Codex package tests pin all sixteen error-info variants, HTTP status presence and absence, all six stages, unknown fallback, stop-reason preservation, permission ordering, sanitization, cancellation, concurrency, and cleanup aggregation. The real SDK/CLI fixture produces an actual Claude `error_max_turns`; the real app-server fixture produces an actual Codex `internalServerError`; both fixtures cover process/protocol failure and whole-tree quiescence. The keyless ACP snapshot records each product's exact diagnostic in foreground error output, a background completion notice, and `job_output`.
|
||||
Claude Code verification is owned by the [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md). Codex package tests pin all sixteen current error-info variants, HTTP status presence and absence, all six stages, unknown fallback, stop-reason preservation, permission ordering, sanitization, cancellation, concurrency, and cleanup aggregation. The real app-server fixture produces an actual Codex `internalServerError` and covers process/protocol failure and whole-tree quiescence. The keyless ACP snapshot records the Codex diagnostic in foreground error output, a background completion notice, and `job_output`.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -80,8 +74,8 @@ Claude Code package tests pin all four SDK subtypes, invalid success, missing re
|
||||
|
||||
## Consequences
|
||||
|
||||
The parent can distinguish important Claude Code limits and Codex budget, usage, service, policy, request, connection, stream, rollback, sandbox, and active-turn failures without receiving raw product text. Foreground and background scheduling preserve the same fact because both consume one `SubagentResult`.
|
||||
The parent can distinguish the current Codex budget, usage, service, policy, request, connection, stream, rollback, sandbox, and active-turn categories without receiving raw product text. The [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns the corresponding Claude result. Foreground and background scheduling preserve the same fact because both consume one `SubagentResult`.
|
||||
|
||||
The diagnostic is display text rather than a new public protocol. Callers may present it but must not branch on its punctuation or product-private category names. A pinned product-version upgrade must update the Provider mapping and evidence when its official error union changes.
|
||||
The diagnostic is display text rather than a new public protocol. Callers may present it but must not branch on its punctuation or product-private category names. A pinned product-version upgrade revalidates the Provider mapping and evidence without requiring every official error member to remain model-visible.
|
||||
|
||||
This decision adds no product session persistence, retry policy, recovery state, stderr classifier, authentication or configuration taxonomy, progress stream, or human interaction path.
|
||||
+7
-13
@@ -1,6 +1,7 @@
|
||||
# Agent Note: 产品 subagent 公开有界结构化失败事实
|
||||
|
||||
Status: implemented
|
||||
Archived: 2026-08-21
|
||||
|
||||
[English](2026-08-18-product-subagent-failure-facts.md) | 中文
|
||||
|
||||
@@ -12,7 +13,7 @@ Status: implemented
|
||||
|
||||
## Decision
|
||||
|
||||
每个产品提供方分别拥有从锁定版本官方错误联合、当前操作和受管进程结果到一行固定安全诊断的映射。`SubagentResult` 保持不变:消费方仍接收现有的有界 `diagnostic` 字符串,而且不解析其中由产品私有的字段。
|
||||
每个产品提供方分别拥有从锁定版本官方结构化失败、当前操作和受管进程结果到一行固定安全诊断的映射。`SubagentResult` 保持不变:消费方仍接收现有的有界 `diagnostic` 字符串,而且不解析其中由产品私有的字段。[最小诊断决策](../simplification/2026-08-21-product-subagent-minimal-diagnostics.zh.md)已经取代本说明对 Claude Code 完整 subtype 的镜像;在 Codex 采用同一简化前,本说明继续负责其当前详细类别。
|
||||
|
||||
### 安全诊断
|
||||
|
||||
@@ -28,14 +29,7 @@ Product subagent failure (product: <product>; stage: <stage>; category: <categor
|
||||
|
||||
### Claude Code 事实
|
||||
|
||||
Agent SDK 0.3.220 定义四种错误子类型:`error_during_execution`、`error_max_turns`、`error_max_budget_usd` 和 `error_max_structured_output_retries`。Claude Code 提供方会把每种准确子类型保留为类别,同时维持共享终止原因 `error`。标记为错误或内容空白的成功消息使用 `invalid-success`,缺失结果使用 `missing-result`,SDK 给出终态结果前发生的进程退出使用 `process-exit`,无法识别的值或异常使用 `unknown`,且不会复制原值。
|
||||
|
||||
| 阶段 | 归属操作 | 可观察失败 |
|
||||
| --- | --- | --- |
|
||||
| `query-start` | SDK query 构造、原生平台载荷启动与未发布回滚 | `start()` 以固定安全事实和回滚前已观测到的进程结果拒绝 |
|
||||
| `query-run` | 已发布 SDK 消息迭代与严格终态结果校验 | 运行以 `error` 兑现,并携带准确已知子类型或固定结果类别 |
|
||||
| `process` | SDK 提供终态结果之前受管 CLI 已退出 | 运行以 `error` 兑现,并携带 `process-exit` 以及可用的退出码和信号 |
|
||||
| `teardown` | Query 关闭与受管进程树释放 | `dispose()` 独立拒绝并携带固定安全事实,同时清理仍会完成最终退出等待 |
|
||||
[最小诊断决策](../simplification/2026-08-21-product-subagent-minimal-diagnostics.zh.md)独占负责 Agent SDK 0.3.241 与 Claude Code 2.1.241 的 Claude Code 类别、阶段、进程事实、权限顺序与验证。本说明不再承载独立的 Claude 类别约定。
|
||||
|
||||
### Codex 事实
|
||||
|
||||
@@ -56,7 +50,7 @@ Codex app-server 0.147.0 定义十一种字符串类别与五种对象 variant
|
||||
|
||||
| 事实或资源 | Owner | 消费方行为 |
|
||||
| --- | --- | --- |
|
||||
| 产品错误类别 | 锁定版本的官方 SDK 或 app-server | 提供方只映射已声明的结构化联合,并对联合外值使用 `unknown` |
|
||||
| Codex 错误类别 | Codex 提供方及其锁定的官方 app-server | 提供方保留当前结构化类别,并在已识别集合之外使用 `unknown` |
|
||||
| 当前失败阶段 | 产品提供方操作 | 只在失败点派生;绝不持久化,也不作为恢复状态 |
|
||||
| 退出码与信号 | `dsh-subprocess` 进程句柄 | 提供方展示已观测值,不推测缺失值 |
|
||||
| 诊断字节与送达 | `dsh-subagent`、前台工具与 Job 运行时 | 两种调度模式都把同一份有界文本与 assistant 输出分开呈现 |
|
||||
@@ -64,7 +58,7 @@ Codex app-server 0.147.0 定义十一种字符串类别与五种对象 variant
|
||||
|
||||
## Verification
|
||||
|
||||
Claude Code 包测试固定四种 SDK 子类型、无效成功、缺失结果、未知值与异常、四个阶段、相互独立的退出码与信号字段、权限事实顺序、脱敏、成功结果与取消时省略诊断、并发运行隔离和清理完成。Codex 包测试固定全部十六种 error-info variant、HTTP status 存在与缺失、六个阶段、unknown 回退、终止原因保持不变、权限顺序、脱敏、取消、并发与清理聚合。真实 SDK/CLI fixture 会产生真实的 Claude `error_max_turns`,真实 app-server fixture 会产生真实的 Codex `internalServerError`;两个 fixture 都覆盖进程/协议失败与整棵进程树完全停稳。无密钥 ACP snapshot 会在前台错误输出、后台完成通知和 `job_output` 中记录两个产品各自的准确诊断。
|
||||
Claude Code 验证由[最小诊断决策](../simplification/2026-08-21-product-subagent-minimal-diagnostics.zh.md)负责。Codex 包测试固定当前全部十六种 error-info variant、HTTP status 存在与缺失、六个阶段、unknown 回退、终止原因保持不变、权限顺序、脱敏、取消、并发与清理聚合。真实 app-server fixture 会产生实际 Codex `internalServerError`,并覆盖进程/协议失败与整棵进程树完全停稳。无密钥 ACP snapshot 会在前台错误输出、后台完成通知和 `job_output` 中记录 Codex 诊断。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -80,8 +74,8 @@ Claude Code 包测试固定四种 SDK 子类型、无效成功、缺失结果、
|
||||
|
||||
## Consequences
|
||||
|
||||
父 agent 可以区分重要的 Claude Code 限制,以及 Codex 预算、用量、服务、策略、请求、连接、stream、回滚、sandbox 和 active-turn 失败,而不会收到原始产品文本。前台与后台调度会保留同一事实,因为二者都消费同一个 `SubagentResult`。
|
||||
父 agent 可以区分当前 Codex 的预算、用量、服务、策略、请求、连接、stream、回滚、sandbox 与 active-turn 类别,而不会收到原始产品文本。[最小诊断决策](../simplification/2026-08-21-product-subagent-minimal-diagnostics.zh.md)负责对应的 Claude 结果。前台与后台调度会保留同一事实,因为二者都消费同一个 `SubagentResult`。
|
||||
|
||||
诊断只是展示文本,不是新的公开协议。调用方可以呈现它,但不得根据其标点或产品私有类别名称进行分支。锁定产品版本升级并改变官方错误联合时,必须同步更新提供方映射与证据。
|
||||
诊断只是展示文本,不是新的公开协议。调用方可以呈现它,但不得根据其标点或产品私有类别名称进行分支。锁定产品版本升级时必须重新验证提供方映射与证据,但不要求每个官方错误成员都继续模型可见。
|
||||
|
||||
本决策不增加产品会话持久化、重试策略、恢复状态、stderr 分类器、身份验证或配置分类体系、进度流或人工交互路径。
|
||||
@@ -295,6 +295,9 @@
|
||||
"feature/2026-08-11-web-export-command-and-dialog.i18n.yaml": "sha256:db7d523a2a1f82a86f532661bd2953ee8538d971d91f886e4bd4e0d88f7226b2",
|
||||
"feature/2026-08-11-web-export-command-and-dialog.md": "sha256:ec44b47589ca7924018dc24f7fa73379a97b8f053d9e8ccce2aebb600230e47b",
|
||||
"feature/2026-08-11-web-export-command-and-dialog.zh.md": "sha256:ad28e67d397c87300cfe1705ba3d206cc4d054e07f5647c095c718ac8cf4ec98",
|
||||
"feature/2026-08-18-product-subagent-failure-facts.i18n.yaml": "sha256:0aa7a873fdd878ee7f4b0a850ecf16d7b652b4f85de979acb7efcdf90883b6c1",
|
||||
"feature/2026-08-18-product-subagent-failure-facts.md": "sha256:f7e05703c44106359798e6e4b76e442a4107b62ff0363554382d4767e4806788",
|
||||
"feature/2026-08-18-product-subagent-failure-facts.zh.md": "sha256:19d2619fb5b5c6e40305dd82432d837357afa433ab735504ec204a2c25582ce6",
|
||||
"feature/2026-08-18-web-home-path-tilde.i18n.yaml": "sha256:f151e3e3514f59784fc646c2feb3075dc954c65110d48c2cc482ad486fc0b86f",
|
||||
"feature/2026-08-18-web-home-path-tilde.md": "sha256:8c7ecf120ff8c81826160acab5fc906a2a0a14213bcd2958343cfea47328d68e",
|
||||
"feature/2026-08-18-web-home-path-tilde.zh.md": "sha256:3486c5b42aed5bcadf12c62c5e1e6cf7c1b493fc1085ad7d154cdf2ec34076cc",
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-26-packed-chunk-rows-by-default.md
|
||||
2026-07-26-packed-chunk-rows-by-default.md: 141c9a32a07b5cb4885a21b419df30d45dc1061b
|
||||
2026-07-26-packed-chunk-rows-by-default.zh.md: 30088b8640efa783380a3fe083ed82efdad9e5c8
|
||||
2026-07-26-packed-chunk-rows-by-default.md: 14da6b3cbe650e80118e7c960c96bf618acd1e48
|
||||
2026-07-26-packed-chunk-rows-by-default.zh.md: f62a8e52a67adc960ac3150552594b4f061e6205
|
||||
|
||||
@@ -18,7 +18,7 @@ Reading is unconditional and layout-blind. Packed, unpacked, and mixed files loa
|
||||
|
||||
### Logical events and physical rows
|
||||
|
||||
Packing stays at the `dsh-session` storage seam through `packChunkRuns()` and `decodeStorageRecord()`. The encoder recognizes exact delta-event shapes, preserves unrecognized events verbatim, and packs only runs of at least three. A packed row is storage vocabulary, not a `SessionEventMap` member: it never enters `Session.events` or fires `session/event`.
|
||||
The JSONL packing path stays at the `dsh-session` storage seam through `packChunkRuns()` and `decodeStorageRecord()`. The encoder recognizes exact delta-event shapes, preserves unrecognized events verbatim, and packs only runs of at least three. A packed row is encoding vocabulary, not a `SessionEventMap` member: it never enters `Session.events` or fires `session/event`. The [packed session-history transport decision](2026-08-15-packed-session-history-transport.md) reuses this vocabulary for a bounded lossless wire interval without changing those event semantics.
|
||||
|
||||
The JSONL backend packs each durable append batch. Raw `compression: 'none'` and default Zstandard framing carry the same logical storage records; selecting raw mode for reviewable fixtures does not disable packing. Repository replay readers and normalizers decode the shared row format instead of maintaining snapshot-specific codecs.
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ JSONL 存储 seam 可以在不改变逻辑日志的情况下减少这部分封
|
||||
|
||||
### 逻辑事件与物理行
|
||||
|
||||
打包保留在 `dsh-session` 的存储 seam,并通过 `packChunkRuns()` 和 `decodeStorageRecord()` 实现。编码器识别精确的增量事件形态,原样保留无法识别的事件,并且只打包至少包含 3 个事件的连续段。打包行属于存储词汇,不是 `SessionEventMap` 成员:它绝不会进入 `Session.events`,也不会触发 `session/event`。
|
||||
JSONL 打包路径保留在 `dsh-session` 的存储 seam,并通过 `packChunkRuns()` 和 `decodeStorageRecord()` 实现。编码器识别精确的增量事件形态,原样保留无法识别的事件,并且只打包至少包含 3 个事件的连续段。打包行属于编码词汇,不是 `SessionEventMap` 成员:它绝不会进入 `Session.events`,也不会触发 `session/event`。[打包会话历史传输决策](2026-08-15-packed-session-history-transport.zh.md)会为有界的无损协议区间复用该词汇,而不改变这些事件语义。
|
||||
|
||||
JSONL 后端会打包每个持久追加批次。原始模式 `compression: 'none'` 与默认 Zstandard 帧承载相同的逻辑存储记录;为使 fixture 便于评审而选择原始模式,不会禁用打包。仓库中的回放读取器和规范化器会解码共享行格式,而不维护快照专用编解码器。
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.md
|
||||
2026-07-30-client-locale-full-rollout.md: aeb4deae28b0dfdb9ab75fd64fe3143958cd6910
|
||||
2026-07-30-client-locale-full-rollout.zh.md: a642b6062cb3dc7a2dfa22dd5d8cf7d9a02e3104
|
||||
2026-07-30-client-locale-full-rollout.md: dedfe98ca2b3e64a56518dfa6157244e4d4c16df
|
||||
2026-07-30-client-locale-full-rollout.zh.md: e9bd1ed19e8b485d812140ab044c779a2ce6e9d3
|
||||
|
||||
@@ -14,6 +14,8 @@ After the typed locale standard seat landed (`locale:` on register → framework
|
||||
|
||||
**Component copy rides the standard `t` seat; deep children take `t` as a plain prop** typed `XxxProps['t']`. The dictionary canon is unchanged: `zh satisfies Record<string, string>` is the key source and `en satisfies Record<XxxKey, string>` locks bilingual balance.
|
||||
|
||||
**The built-in locale set is closed; the language catalog is extensible.** The package contributes only `zh` and `en`, and typed namespace registration continues to require that bilingual pair. An external client plugin adds a language through `ctx.effect(() => ctx.locale.addLanguage({ id, label, fallback }))` and contributes partial translations through the existing single-locale dictionary registration; language definitions and dictionaries may register in either order. An external language id is its validated BCP 47 tag for preference storage, dictionary lookup, browser matching, and `<html lang>`; `LocaleId` remains a string because the tag carries interoperable language semantics rather than opaque identity. The built-in `zh` definition retains its internal `zh-CN` document tag. Every added language names a registered fallback whose own definition supplies the next fallback, and the chain must terminate at `en`; unknown targets and cycles fail at registration. For each key, lookup walks that chain in the requested namespace, then repeats it in `common`, before displaying the key itself. The Host stores an open string preference; an unavailable saved id remains pending until its language registers, while removal returns an active selection to the available browser match or `en`. Catalog changes advance the `LocaleFace` revision so the Language row follows registration and disposal.
|
||||
|
||||
**Zero-Cordis atoms (ui-primitives) take copy as required props.** `HoverCard`, structured Tool blocks, JSON/Markdown renderers, `ConnectionBanner`, and modal chrome remain runtime-independent; localized plugins pass complete dictionary-driven label objects from their own `t` seat and memoize cache-sensitive objects on the `t` identity. The removal of language-bearing defaults and the complete prop inventory are owned by the [locale-owned copy decision](2026-08-23-locale-owned-client-ui-copy.md).
|
||||
|
||||
**Every product-authored UI phrase is translated.** Client fallbacks, design labels, trajectory inspection, accessibility names, and formatter units are dictionary-owned under the [locale-owned copy decision](2026-08-23-locale-owned-client-ui-copy.md). User/model/provider/wire text and protocol or code tokens remain verbatim data. Framework-free boot markup still runs before the locale service; the localized application replaces its product copy after activation.
|
||||
@@ -37,4 +39,4 @@ The "apply layer subscribes to `locale/change` and re-registers for fresh labels
|
||||
- A language switch refreshes the whole UI instantly with zero re-registration; adopting a new package is three steps (dictionary + declare-merge + `locale: NS`), no hand-written glue.
|
||||
- Cost: list-label consumers must know `resolveSlotLabel` (a raw `options.label` read can now hold a function); the `SlotLabel` type catches most misuse statically.
|
||||
- ui-primitives require localized label props, so adding a primitive render site also adds an explicit copy owner; omission fails typechecking instead of selecting a hidden language.
|
||||
- Pinning e2e to English means the zh copy surface is covered mainly by package-level component specs and the settings language-switch scenario; browser e2e no longer asserts zh copy. The opening/fallback locale (a browser naming no shipped language, or a non-browser run) is `en`, not zh — see [browser-derived initial locale](../feature/2026-07-31-browser-derived-initial-locale.md).
|
||||
- Pinning e2e to English means the zh copy surface is covered mainly by package-level component specs and the settings language-switch scenario; browser e2e no longer asserts zh copy. The opening/fallback locale (a browser naming no registered language, or a non-browser run) is `en`, not zh — see [browser-derived initial locale](../feature/2026-07-31-browser-derived-initial-locale.md).
|
||||
|
||||
@@ -14,6 +14,8 @@ typed locale 标准席位(`locale:` 注册声明 → 框架注入强类型 `t`
|
||||
|
||||
**组件文案走标准 `t` 席位;深层子组件用 prop 下传**,类型写 `XxxProps['t']`。字典规范形态不变:`zh satisfies Record<string, string>` 为 key 源、`en satisfies Record<XxxKey, string>` 锁双语平衡。
|
||||
|
||||
**内置 locale 集合封闭,语言目录可扩展。** 本包只提供 `zh` 与 `en`,类型化命名空间注册仍要求这对双语字典。外部 client 插件通过 `ctx.effect(() => ctx.locale.addLanguage({ id, label, fallback }))` 增加语言,并通过既有的单 locale 字典注册贡献不完整翻译;语言定义与字典可以按任意顺序注册。外部语言 id 是经过校验的 BCP 47 标签,同时用于偏好存储、字典查找、浏览器匹配和 `<html lang>`;该标签承载可互操作的语言语义而非不透明身份,因此 `LocaleId` 保持 string。内置 `zh` 定义继续使用内部 `zh-CN` 文档标签。每个新增语言都声明一个已注册的 fallback,fallback 自身的定义给出下一层 fallback,整条链必须终止于 `en`;未知目标和循环在注册时失败。每个 key 先在请求的命名空间中沿链查找,再在 `common` 中重复同一条链,最后显示 key 本身。Host 存储开放字符串偏好;不可用的已保存 id 会保持待采用,直至对应语言注册;定义移除后,正在使用的选择会回落到可用的浏览器匹配或 `en`。目录变更推进 `LocaleFace` revision,使语言设置行跟随注册和 dispose。
|
||||
|
||||
**zero-Cordis 原子组件(ui-primitives)通过必填 prop 接收文案。** `HoverCard`、结构化工具块、JSON/Markdown 渲染器、`ConnectionBanner` 和 modal chrome 均保持运行时独立;已本地化插件从自己的 `t` 席位传入完整的字典驱动 label 对象,对缓存敏感的对象按 `t` 身份 memo。移除带语言默认值以及完整 prop 清单由 [locale 归属文案决策](2026-08-23-locale-owned-client-ui-copy.zh.md)负责。
|
||||
|
||||
**所有产品编写的 UI 短语都翻译。** client 兜底文案、设计 label、trajectory 检查面、无障碍名称和格式化单位均按 [locale 归属文案决策](2026-08-23-locale-owned-client-ui-copy.zh.md)进入字典。用户/模型/提供方/wire 文本以及协议或代码 token 仍作为数据原样呈现。不依赖框架的 boot 标记仍早于 locale 服务运行;本地化应用激活后会替换其中的产品文案。
|
||||
@@ -37,4 +39,4 @@ typed locale 标准席位(`locale:` 注册声明 → 框架注入强类型 `t`
|
||||
- 语言切换全 UI 即时刷新且零重注册;新包接入 = 字典 + declare-merge + `locale: NS` 三步,无手写胶水。
|
||||
- 代价:list label 的消费方必须知道 `resolveSlotLabel`(裸读 `options.label` 现在可能拿到函数);类型上 `SlotLabel` 已挡住多数误用。
|
||||
- ui-primitives 要求本地化 label prop,因此新增原子组件渲染点也必须新增明确的文案 owner;遗漏会在类型检查失败,而不是选择隐藏语言。
|
||||
- e2e 英文钉死意味着 zh 文案面主要靠包级组件测试与 settings 语言切换用例覆盖,浏览器 e2e 不再验证 zh 文案。开场/回落 locale(声明了本应用都不支持语言的浏览器,或非浏览器运行)是 `en` 而非 `zh`,见 [browser-derived initial locale](../feature/2026-07-31-browser-derived-initial-locale.zh.md)。
|
||||
- e2e 英文钉死意味着 zh 文案面主要靠包级组件测试与 settings 语言切换用例覆盖,浏览器 e2e 不再验证 zh 文案。开场/回落 locale(浏览器未声明任何已注册语言,或非浏览器运行)是 `en` 而非 `zh`,见 [browser-derived initial locale](../feature/2026-07-31-browser-derived-initial-locale.zh.md)。
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md
|
||||
2026-08-09-client-conversation-node-assembly.md: ea2505d4a72f483a9df6fcd78d7e5c9a96b02f5c
|
||||
2026-08-09-client-conversation-node-assembly.zh.md: b87f127d753cadf2805ed5cd948fc58ad01830aa
|
||||
2026-08-09-client-conversation-node-assembly.md: 12069d129227cce13eb5f9f39e636921d4bf9efa
|
||||
2026-08-09-client-conversation-node-assembly.zh.md: 957c2b291293761ff2417f60093b7962bb75bbdf
|
||||
|
||||
+44
-42
@@ -8,13 +8,13 @@ English | [中文](2026-08-09-client-conversation-node-assembly.zh.md)
|
||||
|
||||
Client Session owned transport windows, connection state, and pending interactions while also interpreting Assistant, Tool, message, command, compaction, retry, and turn-tail events in a centralized transcript fold. Adding one business node required changes to Session switches, history replay, indexes, caches, and React grouping; business identity, state evolution, and final presentation had no independent owner.
|
||||
|
||||
The old path also placed running Assistant and Tool values outside the finalized flow. They entered the log-ordered node list only after settlement, so their React parent changed and remounted them even when the business ID and `key` remained stable. Full history loads, older prepends, live appends, and token streaming used separate update paths, leaving reference stability and local recomputation dependent on specialized caches spread across the client.
|
||||
Without target-neutral assembly, running Assistant and Tool values sit outside the finalized flow and enter the log-ordered node list only after settlement. Their React parent then changes and remounts them even when the business ID and `key` remain stable. Separate update paths for full history loads, older prepends, live appends, and token streaming also make reference stability and local recomputation depend on specialized caches spread across the client.
|
||||
|
||||
Business events also use different correlation models. Tool has call IDs, Assistant correlates by turn and step, Compaction has its own lifecycle and checkpoint, and an Inbox splice represents one instantaneous state in a sequence. Keeping all these distinctions in one fold would make every business change pass through a global lookup and invalidate unrelated caches.
|
||||
|
||||
## Decision
|
||||
|
||||
Client Runtime provides a target-neutral Conversation Node assembly engine. Business plugins register Event Definitions, and view plugins register per-Session View Builders. `ui-conversation` registers the first built-in Definitions and the `chat` builder; Session only submits the current contiguous Event window to the engine and publishes its snapshot instead of interpreting individual conversation businesses.
|
||||
Client Runtime provides a target-neutral Conversation Node assembly engine. Business plugins register Event Definitions, and view plugins register per-Session View Builders. `ui-conversation` registers the first built-in Definitions and the `chat` builder; Session only submits the current contiguous `SessionEventLikeEntry` window to the engine and publishes its snapshot instead of interpreting individual conversation businesses. The entry's outer discriminator distinguishes standard and packed records, while both carry an aligned inner `SessionEventLike` for Definition dispatch.
|
||||
|
||||
This Note retains the derivation, business-by-business validation, responsibilities, algorithms, and trade-offs that remain relevant after implementation.
|
||||
|
||||
@@ -22,20 +22,20 @@ This Note retains the derivation, business-by-business validation, responsibilit
|
||||
|
||||
| Layer | Durable responsibility | Explicitly does not own |
|
||||
|---|---|---|
|
||||
| Session | Maintain the contiguous Event window, distinguish replace, prepend, and append, and schedule snapshot notifications | Interpret Tool, Assistant, Compaction, or other business events |
|
||||
| Session | Maintain the contiguous logical-event window, distinguish replace, prepend, and scalar append, and schedule snapshot notifications | Interpret Tool, Assistant, Compaction, or other business events |
|
||||
| Event Registry | Retain the unique-`kind` Definitions and sole fallback under Cordis lifecycles | Store one Session's Context or State |
|
||||
| Assembler | Match Events and maintain Contexts, Locations, dependencies, and the publication dirty set | Interpret business State fields or Chat ordering |
|
||||
| Assembler | Match standard events or packed runs and maintain Contexts, Locations, dependencies, and the publication dirty set | Interpret business State fields or Chat ordering |
|
||||
| Node Definition | Define one business object's identity, State transitions, Location data, and target Node | Create Contexts, mutate another business's State, or scan all Contexts |
|
||||
| View Builder | Incrementally organize final target Nodes into that view's snapshot | Reinterpret raw Session Events |
|
||||
| View Builder | Incrementally organize final target Nodes into that view's snapshot | Reinterpret `SessionEventLike` inputs |
|
||||
| React renderer | Render renderer-owned data by the final Node's `kind` and read business data from the current Node's Location | Pair business Events, scan global Nodes, or decide business lifecycle state |
|
||||
|
||||
Registry contributions are Cordis effects. Removing a Definition causes a low-frequency registry rebuild for existing Sessions; ordinary business Events do not change the Registry or rebuild every business type.
|
||||
|
||||
### Overall `ConversationNodeDefinition` contract
|
||||
|
||||
Each [`ConversationNodeDefinition`](../../../../packages/client/ui-conversation/src/client/contract/conversation.ts) independently owns one business object's conversion from Events to State and final view Nodes. A Definition's `kind` is its unique Registry name and the namespace for its business IDs.
|
||||
Each [`ConversationNodeDefinition`](../../../../packages/client/ui-conversation/src/client/contract/conversation.ts) independently owns one business object's conversion from `SessionEventLike` inputs to State and final view Nodes. A Definition's `kind` is its unique Registry name and the namespace for its business IDs.
|
||||
|
||||
One Event may be claimed by several ordinary Definitions. For example, an Assistant Event updates both the Assistant Node and Turn Tail, while a Retry Event updates Retry, Assistant, and Turn Tail. The Assembler asks the fallback only when every ordinary Definition returns `null`.
|
||||
One input may be claimed by several ordinary Definitions. For example, an Assistant event or packed run updates both the Assistant Node and Turn Tail, while a Retry Event updates Retry, Assistant, and Turn Tail. The Assembler asks the fallback only when every ordinary Definition returns `null`.
|
||||
|
||||
A Definition holds no mutable business data across Sessions. Each Session's Assembler isolates that Session's Contexts, State, dependencies, and View Builders.
|
||||
|
||||
@@ -49,9 +49,9 @@ Each `(kind, id)` has at most one start Match. A second start fails immediately;
|
||||
|
||||
#### `match(event)`
|
||||
|
||||
`match(event)` reads only the current raw `SessionEvent` and returns `{ id, role: 'start' | 'update' }` or `null`. It cannot access a Context, history, a Reader, a Location, or the view envelope.
|
||||
`match(event)` reads only the current `SessionEventLike` and returns `{ id, role: 'start' | 'update' }` or `null`. It cannot access a Context, history, a Reader, a Location, or the view envelope. A `chunkrow/*` event can only be an update; the Assembler rejects it as a start, and `start()` receives a `ConversationStartMatch` containing a standard `SessionEvent`.
|
||||
|
||||
This restriction makes one Event's routing cost depend only on the number of registered Definitions. The Assembler never scans a Definition's historical Contexts to decide which one owns an update.
|
||||
This restriction makes one scalar event or packed run's routing cost depend only on the number of registered Definitions. The Assembler never scans a Definition's historical Contexts to decide which one owns an update.
|
||||
|
||||
Start, result, resource, checkpoint, and business-owned terminal Events must carry or directly imply the same ID. If one Event cannot yield that ID, its producer extends the Event protocol; the Client does not guess from the "nearest unfinished object."
|
||||
|
||||
@@ -59,9 +59,9 @@ The `role` describes the State lifecycle, not visibility. A start may produce a
|
||||
|
||||
#### `ConversationMatch`
|
||||
|
||||
After a successful match, the Assembler combines the raw Event, optional wire presentation view, `role`, and engine-computed `location` into a read-only `ConversationMatch`.
|
||||
After a successful match, the Assembler combines the standard or packed event, `role`, and engine-computed `location` into a read-only `ConversationMatch`. A packed run remains one Match and retains its fragment and timestamp-gap arrays.
|
||||
|
||||
A Context's `matches` always remain in ascending Event `seq` order, not network arrival or pagination ingestion order. If a tail page supplies a result before an older page supplies its call, the final Match order still places the call before the result.
|
||||
A Context's `matches` always remain in ascending first-`seq` order, not network arrival or pagination ingestion order. The Session journal has already rejected overlapping logical ranges. If a tail page supplies a result before an older page supplies its call, the final Match order still places the call before the result.
|
||||
|
||||
Location can change when prepend fills a boundary or append closes one. The Assembler replaces the affected Matches' read-only Locations and replays the Context; business code does not retain an old Location copy as authority.
|
||||
|
||||
@@ -71,8 +71,8 @@ Location can change when prepend fills a boundary or append closes one. The Asse
|
||||
|---|---|---|
|
||||
| `key` | Assembler | Stable final identity derived from `kind + id` |
|
||||
| `kind` / `id` | Definition + Assembler | Current business namespace and business ID |
|
||||
| `matches` | Assembler | Complete business evidence loaded in the current window and sorted by `seq` |
|
||||
| `start` | Assembler | Unique start Match, or `undefined` before it loads |
|
||||
| `matches` | Assembler | Complete scalar and packed business evidence loaded in the current window and sorted by first `seq` |
|
||||
| `start` | Assembler | Unique scalar start Match, or `undefined` before it loads |
|
||||
| `state` | Returned by Definition, held by Assembler | Most recent `start`/`update` return value, or `undefined` before initialization |
|
||||
| `current` | Assembler | Most recently materialized Node or `null` for each target |
|
||||
|
||||
@@ -108,7 +108,7 @@ Dependencies point strictly from earlier starts to later starts, so transitive r
|
||||
|
||||
#### `update(context, match)`
|
||||
|
||||
`update()` handles a post-start Match that `match()` has already routed exactly to the current `(kind, id)`. It does not decide which Context owns the Event.
|
||||
`update()` handles a post-start scalar or packed Match that `match()` has already routed exactly to the current `(kind, id)`. It does not decide which Context owns the input. A Definition that consumes Assistant deltas folds each matching `chunkrow/*` value as one batch without constructing member events.
|
||||
|
||||
The Assembler invokes `update()` in ascending `seq` order. A live tail update can apply incrementally; any non-tail insertion, newly loaded start, or invalidated dependency causes a complete replay from `start()`.
|
||||
|
||||
@@ -126,9 +126,9 @@ The Assembler does not use State reference equality to decide publication or pro
|
||||
| `animation-frame` | Coalesce high-frequency updates into materialization on the next frame |
|
||||
| `none` | Do not schedule a flush for this Match; retain its State and dirty marker |
|
||||
|
||||
Omitting `publication()` means `immediate`. Assistant token deltas use `animation-frame`, invisible Inbox Contexts use `none`, and finals, dependency replays, and Location boundaries publish the latest result through an immediate path.
|
||||
Omitting `publication()` means `immediate`. Assistant token deltas and packed runs use `animation-frame`, invisible Inbox Contexts use `none`, and finals, dependency replays, and Location boundaries publish the latest result through an immediate path.
|
||||
|
||||
Every delta within a frame still executes update. Only `buildViewNode()`, View Builder work, and React snapshot notification are coalesced; no tokens are lost.
|
||||
Every live delta within a frame still executes `update()`, while one historical packed run executes one batch `update()`. Only `buildViewNode()`, View Builder work, and React snapshot notification are coalesced; no fragments are lost.
|
||||
|
||||
#### `buildLocationData(context, scope)`
|
||||
|
||||
@@ -160,7 +160,7 @@ IDs are never reused. Completed Contexts remain in the current window, providing
|
||||
|
||||
### Location is a first-class engine fact
|
||||
|
||||
[`ConversationLocationIndex`](../../../../packages/client/ui-conversation/src/client/conversation/location-index.ts) maps Events to Locations from `turn/start`, `step/start`, explicit turn and step payloads, `step/end`, and `turn/end`.
|
||||
[`ConversationLocationIndex`](../../../../packages/client/ui-conversation/src/client/conversation/location-index.ts) maps standard events and packed runs to Locations from `turn/start`, `step/start`, explicit turn and step payloads, `step/end`, and `turn/end`. All members of a row share its turn, step, block index, and delta kind, so the row needs one Location entry at its first `seq`.
|
||||
|
||||
Location has four shapes: `session`, `turn`, `step`, and `unresolved`. Turns and Steps each carry `open`, `closed`, or `unknown` status plus any loaded start and end Events.
|
||||
|
||||
@@ -168,31 +168,31 @@ Each Turn and Step also carries a reference-stable Location data store. A Defini
|
||||
|
||||
`unresolved` means the current history window lacks sufficient preceding boundaries; it does not mean session-level. When older prepend supplies those boundaries, the index corrects Match Locations and replays only Contexts that own those seqs.
|
||||
|
||||
An appended ordinary Event only inherits current coordinates, while an appended boundary recalculates only its owning Turn. Prepend rebuilds Location facts from the expanded contiguous window, but reference-stability logic retains unchanged Turn and Step objects.
|
||||
An appended standard Event only inherits current coordinates, while an appended boundary recalculates only its owning Turn. Prepend rebuilds Location facts from the contiguous `SessionEventLikeEntry` window, but reference-stability logic retains unchanged Turn and Step objects.
|
||||
|
||||
The Assembler also passes a reference-stable timeline to each View Builder. Businesses do not separately maintain turn order, step lists, last-step values, or boundary Maps.
|
||||
|
||||
## Three Event-window paths
|
||||
## Three input-window paths
|
||||
|
||||
"Backward history scanning" describes the UI loading pages from the newest tail toward the Session beginning; it does not mean a Definition executes `update()` in reverse. Regardless of history API order or page-loading direction, the Assembler canonicalizes each current window and each fresh page in ascending `seq` order.
|
||||
"Backward history scanning" describes the UI loading pages from the newest tail toward the Session beginning; it does not mean a Definition executes `update()` in reverse. The Session journal validates each record's logical range before publication. Regardless of page-loading direction, the Assembler orders every accepted standard event or packed run by its first `seq`.
|
||||
|
||||
| Scenario | Input range | Context and State handling | View Builder |
|
||||
|---|---|---|---|
|
||||
| Initial history tail or resync | Current complete contiguous window | Clear and rebuild all Contexts in ascending `seq` order | `replace()` |
|
||||
| Load one older-history page | Only deduplicated fresh Events before the window | Retain existing Context identity, then add Matches, Locations, dependencies, and local replays | `apply(upserts)` |
|
||||
| Initial history tail or resync | Current complete contiguous logical window | Clear and rebuild all Contexts in ascending first-`seq` order | `replace()` |
|
||||
| Load one older-history page | Only range-validated fresh standard events or packed runs before the window | Retain existing Context identity, then add Matches, Locations, dependencies, and local replays | `apply(upserts)` |
|
||||
| Live append | One contiguous tail Event | Match Definitions and update only the exact IDs; boundaries affect only their owning Turn | `apply(upserts)` |
|
||||
|
||||
### Initial history tail and logical backward scanning
|
||||
|
||||
1. `Session.open()` loads the latest tail page and passes its contiguous History Entries to `replaceWindow(entries, hasMore)`.
|
||||
1. `Session.open()` loads the latest tail page and passes its contiguous `SessionEventLike` entries to `replaceWindow(entries, hasMore)`.
|
||||
2. `replaceWindow` clears old Contexts, start-seq indexes, seq reverse indexes, Reader dependencies, and the input Map.
|
||||
3. It sorts every entry by Event `seq` and stores the resulting current window.
|
||||
3. It sorts every entry by its first logical `seq` and stores the resulting current window.
|
||||
4. LocationIndex rebuilds Turn and Step facts for that window.
|
||||
5. The Assembler visits Events in ascending order and invokes every ordinary Definition's `match(event)`.
|
||||
5. The Assembler visits standard events and packed runs in ascending order and invokes every ordinary Definition's `match(event)`.
|
||||
6. Each result gets or creates its `(kind, id)` Context and enters that Context's ordered Match array.
|
||||
7. A start runs `start()`; a tail update on initialized State runs `update()` directly.
|
||||
8. If the page contains only a result or resource and omits its start, the ID still creates a Context and collects Matches, while State remains `undefined`.
|
||||
9. After matching all Events, the Assembler rechecks Reader dependencies so earlier instantaneous states in the same window stabilize before later consumers read them.
|
||||
9. After matching all inputs, the Assembler rechecks Reader dependencies so earlier instantaneous states in the same window stabilize before later consumers read them.
|
||||
10. Every Context becomes dirty, and the next flush fully rebuilds Location data in Step→Turn order before invoking `buildViewNode()` for every target.
|
||||
11. Some businesses return `null` without a start; Compaction, Command, Tool result, and Turn Error can construct fallback Nodes from sufficient update evidence.
|
||||
12. Each View Builder receives the complete Node set and timeline and establishes the initial snapshot through `replace()`.
|
||||
@@ -206,12 +206,12 @@ If an update with the same ID is genuinely earlier than the start in log order,
|
||||
### Prepending a newly loaded older page
|
||||
|
||||
1. `Session.loadOlder()` requests the immediately preceding page using the current `baseSeq` and first verifies continuity between the page tail and current window.
|
||||
2. Session prepends the raw Event and view arrays to its own window and passes only that page to `assembler.prepend(entries, hasMore)`.
|
||||
3. The Assembler removes seqs that overlap the current window, then sorts the fresh page internally in ascending order.
|
||||
2. Session prepends the accepted standard or packed entries to its own window and passes only that page to `assembler.prepend(entries, hasMore)`.
|
||||
3. The journal has already removed complete duplicate ranges and rejected partial overlaps; the Assembler sorts the fresh page by first `seq`.
|
||||
4. Existing Contexts, State, current Nodes, and View Builder instances remain intact.
|
||||
5. LocationIndex rebuilds facts over the expanded complete input and reports seqs whose Location identity actually changed.
|
||||
5. LocationIndex rebuilds facts over the extended complete input and reports seqs whose Location identity actually changed.
|
||||
6. Contexts owning those seqs update their Match Locations and replay from start; unrelated Contexts do not join Location replay.
|
||||
7. Fresh Events run Definition matchers and enter existing or new Contexts by stable ID.
|
||||
7. Fresh standard events and packed runs enter existing or new Contexts through the same Definition matcher and stable ID.
|
||||
8. If the new page supplies a pending Context's start, that Context initializes from the start and then applies every already-collected update in ascending order.
|
||||
9. If the page establishes a nearer Reader predecessor, changes a predecessor revision, or removes a window gap, the consumer recomputes from `start()`.
|
||||
10. Reader dependencies propagate replay toward later start seqs; no Event is applied in reverse within the propagation batch.
|
||||
@@ -226,7 +226,7 @@ Reader gap repair is the largest algorithmic difference between prepend and ordi
|
||||
|
||||
### Forward live append
|
||||
|
||||
1. Session accepts only a live Event immediately after the current tail seq; it deduplicates overlap and runs tail-page repair before accepting a gap.
|
||||
1. Session accepts only a standard live Event immediately after the current logical tail seq; it deduplicates overlap and runs tail-page repair before accepting a gap.
|
||||
2. A non-boundary Event enters the current Turn and Step coordinates incrementally; a boundary Event updates Location facts for its owning Turn.
|
||||
3. The Assembler invokes `match()` once on every ordinary Definition for this Event and scans no Definition's Context set.
|
||||
4. Each successful result directly locates one Context through `(kind, id)`.
|
||||
@@ -249,7 +249,7 @@ All three paths preserve the same invariants: Context Matches are seq-ordered, S
|
||||
|
||||
`replaceWindow` is the low-frequency complete replacement for initial open, resync, gap repair, and registry changes; it does not implement ordinary load older. Both `prepend` and `append` retain existing Builder and Context identity.
|
||||
|
||||
Page size, the number of history loads, and RAF coalescing affect only when evidence arrives or publishes. They do not change final Context State and Nodes for an equal Event window.
|
||||
Page size, record packing, the number of history loads, and RAF coalescing affect only when evidence arrives or publishes. They do not change final Context State and Nodes for equal logical evidence.
|
||||
|
||||
## How built-in businesses use Definitions
|
||||
|
||||
@@ -261,7 +261,7 @@ Page size, the number of history loads, and RAF coalescing affect only when evid
|
||||
| Next-step Inbox / `inbox-next-step` | Splice Event seq | Each `agent/inbox/spliced` targeting next-step | None | Build the same per-instruction instantaneous state; Message reads its claimed set |
|
||||
| Message / `input-message` | Message ID | Append-surface `user/message` | None | Use source for a context message, or read the nearest next-step Inbox to distinguish user from steering |
|
||||
| Request Prompt / `request-prompt` | Header Event seq | Each `request/header` | None | Read the preceding Request Prompt through Reader, retain the full prompt state, and classify system/tool changes |
|
||||
| Assistant / `assistant-step` | `turn:step` | `step/start` | `assistant/chunk`, final `assistant/message`, and same-step Retry | Aggregate blocks, usage, first-token time, final evidence, and retry-hidden state, then publish same-key Step data |
|
||||
| Assistant / `assistant-step` | `turn:step` | `step/start` | Scalar or packed `assistant/chunk`, final `assistant/message`, and same-step Retry | Aggregate blocks, usage, first-token time, final evidence, and retry-hidden state, then publish same-key Step data |
|
||||
| Tool / `tool-call` | Root call ID | Root `tool/call` | Root result and Code Dispatch start/result | Aggregate the root, children, and parent Map; Dispatch Events route exactly through `rootCallId` |
|
||||
| Command / `command` | Command ID | `command/run` | `command/done` and compact lifecycle/checkpoint Events carrying a source command ID | Aggregate command outcome and manual-compaction evidence |
|
||||
| Automatic Compaction / `compaction` | Compaction ID | `compaction/start` without a source command ID | Summary, end, and replacement checkpoint | Aggregate summary/checkpoint; sufficient checkpoint evidence supports fallback without a start |
|
||||
@@ -278,7 +278,7 @@ Page size, the number of history loads, and RAF coalescing affect only when evid
|
||||
| Inbox | `none` | No Node | Recompute instantaneous states along the Reader chain when prepend supplies earlier splices |
|
||||
| Message | Immediate by default | `user`, `steering`, or `context` | Window-gap repair can reclassify the same message key |
|
||||
| Request Prompt | Immediate by default | One `system-prompt` for every header carrying a non-empty system field | A step's first header anchors before its request messages; a later same-step series anchors after its surface rewrite; prepend of the preceding header can correct a partial-window anchor |
|
||||
| Assistant | RAF for chunks, immediate for final, none for pure usage/finish | Same-key `assistant-step` with running/settled/interrupted status | Matches support fallback without `step/start`; Location close produces interruption presentation |
|
||||
| Assistant | RAF for scalar chunks and packed runs, immediate for final, none for pure usage/finish | Same-key `assistant-step` with running/settled/interrupted status | Scalar and packed reducers are equivalent; Matches support fallback without `step/start`; Location close produces interruption presentation |
|
||||
| Tool | Immediate by default | One recursive `tool-call` root containing all `subCalls` | A result-only history window supports fallback; running→settled retains its key |
|
||||
| Command | Immediate by default | Ordinary `command` or integrated `manual-compaction` | Checkpoint arrival may change the anchor without changing the Context key |
|
||||
| Compaction | Immediate by default | `compaction` marker | A checkpoint may render before start; an older start triggers forward replay |
|
||||
@@ -326,20 +326,20 @@ Slot-level contextual Hooks and entry-owned `inject.hooks` remain independent pa
|
||||
|
||||
The standard `useSession` remains available to every session-scoped slot renderer. `useTurnData()` narrows the common read path rather than acting as a permission sandbox. Whole-window statistics or arbitrary object indexes may still read the Session snapshot explicitly, but they are not modeled as current-Node Turn data.
|
||||
|
||||
Assistant streaming to final and Tool running to settled update only one Seat's data and necessary ordering properties. They no longer move from a tail running container into finalized flow, so settlement does not reset component-local State.
|
||||
Assistant streaming to final and Tool running to settled stay in one Seat while updating its data and necessary ordering properties. Settlement therefore does not reset component-local State through a parent move.
|
||||
|
||||
When business logic deliberately changes a materialized Node to hidden, it leaves visible order and remounts when visible again. This is explicit business withdrawal of presentation, distinct from the stable-Seat guarantee for running→settled.
|
||||
|
||||
The concrete Tool renderer remains governed by the [`ui-tool ownership decision`](2026-08-08-client-tool-presentation-ownership.md). Tool Definition supplies recursive root/subcall data, and `ui-tool` dispatches concrete presentation by the Tool-name keyed slot.
|
||||
|
||||
Trajectory registers its own target and business Definitions against the same Assembler and Session event window as Chat. Its target builder preserves the stage-oriented read model without consuming the Chat Builder's legacy slice or running an independent history fold. The Chat Builder retains its legacy slice for StatsLine and the top-level public compatibility fields; target-specific Definitions do not change the shared Context, Reader, or Location contracts.
|
||||
Trajectory registers its own target and business Definitions against the same Assembler and `SessionEventLikeEntry` window as Chat. Its target builder preserves the stage-oriented read model without consuming the Chat Builder's legacy slice or running an independent history fold. Chat and Trajectory keep independent scalar and packed Assistant reducers; target-specific Definitions do not change the shared Context, Reader, or Location contracts.
|
||||
|
||||
The target-specific Trajectory Definitions, retained stage model, Steering adaptation, complexity bounds, and presentation hot paths are owned by the [Trajectory Context assembly decision](2026-08-11-trajectory-conversation-context-assembly.md).
|
||||
|
||||
## Runtime and render path
|
||||
|
||||
```text
|
||||
Session Event window
|
||||
SessionEventLike window
|
||||
-> ConversationNodeAssembler
|
||||
-> Definition.match(event) -> (kind, id, start/update)
|
||||
-> Context matches + State + Location
|
||||
@@ -361,7 +361,7 @@ Slot type/runtime tests pin required parent-provided common inject, the `hookCon
|
||||
|
||||
Assembled Web snapshots, GUI tests, and browser scenarios cover the real plugin graph. Browser evidence compares Assistant streaming→settled, Bash running→settled, and Code Mode root + nested subcalls against master layout.
|
||||
|
||||
History-path tests cover complete replace, non-overlapping prepend, overlapping-seq deduplication, empty-page `hasMore` convergence, and live append. Equal Event windows ingested through different paths produce equal business State and final Nodes.
|
||||
History-path tests cover complete replace, non-overlapping prepend, complete-range deduplication, partial-overlap rejection, empty-page `hasMore` convergence, and scalar live append. Scalar and packed representations of the same Assistant history produce equal Chat and Trajectory State, timing boundaries, and final Nodes; one packed run remains one Match through replace, prepend, Location replay, and registry rebuild.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -377,6 +377,8 @@ History-path tests cover complete replace, non-overlapping prepend, overlapping-
|
||||
|
||||
**Define a reverse State fold for backward history scanning.** Rejected: every business would maintain two inverse algorithms, and deletion, non-invertible aggregation, and cross-Context dependencies would be difficult to keep equivalent. Ordered Matches followed by forward replay from start preserve one business meaning.
|
||||
|
||||
**Add a separate chunk-run matcher and update lifecycle.** Rejected: a second Definition path would duplicate dispatch, replay, publication, and Context types. `ChunkRowEvent` uses the existing `match(event)` and `update(context, match)` lifecycle while making packed handling explicit through its `chunkrow/*` discriminant.
|
||||
|
||||
**Make Inbox a first-class engine concept or one window-wide Context.** Rejected: Inbox is ordinary business State and does not belong in the generic engine. Per-splice instantaneous State plus a strictly backward Reader supports prepend, append, and Message lookup together.
|
||||
|
||||
**Register specialized query methods for cross-business reads.** Rejected: consumers would still depend on provider APIs, and each new relationship would expand a central interface. Reader exposes a named kind's read-only predecessor Context; the provider writes useful State and the consumer interprets it.
|
||||
@@ -399,14 +401,14 @@ A new business node can register its matcher, State transitions, optional Locati
|
||||
|
||||
Host business packages declaration-merge their durable Event members into `@deepseek-ai/dsh-session/types`, while Client Definitions type-only import the corresponding business package `/types` subpaths. Augmenting the declaring interface rather than a re-export barrel gives the independent Host and Client TypeScript programs the same Event narrowing without pulling Host runtime into the Client graph.
|
||||
|
||||
Initial tail, older prepend, and live append share one set of Context invariants. Missing starts, Reader window gaps, unknown Locations, and high-frequency deltas are explicit engine states and require no direction-specific business cache.
|
||||
Initial tail, older prepend, and live append share one set of Context invariants. Missing starts, Reader window gaps, unknown Locations, and packed high-frequency deltas are explicit engine states and require no direction-specific business cache.
|
||||
|
||||
Append does not scan historical Contexts; prepend replays only Contexts whose Matches, Locations, or Reader answers actually changed. A structural Chat change may still recompute visible order and indexes, but does not rerun unrelated business folds or replace unchanged Node identity.
|
||||
|
||||
Separating State updates from publication cadence folds every Assistant delta while materializing at most once per animation frame. Step or Turn close and final Events can immediately publish the latest State.
|
||||
Separating State updates from publication cadence folds every live Assistant delta and each historical packed run while materializing at most once per animation frame. Step or Turn close and final Events can immediately publish the latest State.
|
||||
|
||||
Steps and Turns become stable homes for cross-business aggregates. Turn Tail and Deliverables no longer depend on renderers scanning global Nodes; slot-level `useTurnData()` narrows common reads to the current Node's Turn and uses selector equality to isolate unrelated updates.
|
||||
Steps and Turns are stable homes for cross-business aggregates. Turn Tail and Deliverables derive their values without renderer scans of global Nodes; slot-level `useTurnData()` narrows common reads to the current Node's Turn and uses selector equality to isolate unrelated updates.
|
||||
|
||||
The cost is new Runtime contracts for Registry, Assembler, Location data, dependency replay, and per-target Builders, plus parent-owned common inject and per-occurrence `hookContext` in UI Slots. Definition authors must understand stable IDs, unique starts, forward replay, Step→Turn publication order, read-only Reader access, and the prohibition on Node withdrawal.
|
||||
The cost is new Runtime contracts for Registry, Assembler, Location data, dependency replay, and per-target Builders, plus parent-owned common inject and per-occurrence `hookContext` in UI Slots. Definitions that consume Assistant deltas also maintain equivalent scalar and packed update branches. Definition authors must understand stable IDs, unique scalar starts, forward replay, Step→Turn publication order, read-only Reader access, and the prohibition on Node withdrawal.
|
||||
|
||||
`useTurnData()` does not revoke the standard `useSession` capability from session-scoped renderers, so this boundary relies on API guidance and tests rather than capability isolation. Registry changes remain low-frequency full rebuilds; the Chat Builder still maintains a legacy slice for StatsLine and the top-level public fields, while Trajectory owns target-specific Definitions and a Builder over the shared Session window. Built-in Definitions remain in their respective UI packages, and these compatibility boundaries do not return business interpretation to Session.
|
||||
|
||||
+44
-42
@@ -8,13 +8,13 @@ Status: implemented
|
||||
|
||||
Client Session 既维护传输窗口、连接状态和待处理交互,也在中心化 transcript fold 中解释 Assistant、Tool、消息、命令、压缩、重试及 turn tail 等业务事件。每增加一种业务节点,都要修改 Session 的 switch、历史 replay、索引、缓存和 React 分组;业务 identity、状态演进与最终展示没有独立所有者。
|
||||
|
||||
旧链路还把运行中的 Assistant 和 Tool 放在 finalized flow 之外。它们结算后才进入按日志排序的节点列表,因此 React parent 会改变,即使业务 ID 和 `key` 不变也会重新挂载。全量历史加载、older prepend、实时 append 与 token streaming 又分别走不同更新路径,使引用稳定和局部重算只能靠各处特化缓存维持。
|
||||
缺少 target-neutral assembly 时,运行中的 Assistant 和 Tool 会位于 finalized flow 之外,结算后才进入按日志排序的节点列表。React parent 因而改变,即使业务 ID 和 `key` 稳定也会重新挂载。全量历史加载、older prepend、实时 append 与 token streaming 若分别走不同更新路径,引用稳定和局部重算也只能依赖各处特化缓存。
|
||||
|
||||
业务事件之间的关联方式并不统一。Tool 有 call ID,Assistant 以 turn/step 关联,Compaction 有独立生命周期和 checkpoint,Inbox splice 则表示一个连续状态的瞬间。把这些差异继续塞进统一 fold,会让任一业务变化都经过全局查表并使无关缓存失效。
|
||||
|
||||
## 决策
|
||||
|
||||
Client Runtime 提供 target-neutral 的 Conversation Node 组装引擎,业务插件注册 Event Definition,视图插件注册 per-Session View Builder。`ui-conversation` 注册第一批内建 Definition 和 `chat` builder;Session 只负责把当前连续事件窗口送入引擎并发布它的 snapshot,不再解释具体 conversation 业务。
|
||||
Client Runtime 提供 target-neutral 的 Conversation Node 组装引擎,业务插件注册 Event Definition,视图插件注册 per-Session View Builder。`ui-conversation` 注册第一批内建 Definition 和 `chat` builder;Session 只负责把当前连续 `SessionEventLikeEntry` window 送入引擎并发布它的 snapshot,且不解释具体 conversation 业务。entry 的外层 discriminator 区分标准与 packed record,两者都携带字段对齐的内部 `SessionEventLike`,供 Definition dispatch。
|
||||
|
||||
本 Note 保留实现后仍有价值的方案推导、逐业务适配、职责、算法和取舍。
|
||||
|
||||
@@ -22,20 +22,20 @@ Client Runtime 提供 target-neutral 的 Conversation Node 组装引擎,业务
|
||||
|
||||
| 层 | 长期职责 | 明确不负责 |
|
||||
|---|---|---|
|
||||
| Session | 维护连续 Event 窗口,区分 replace、prepend、append,调度 snapshot 通知 | 解释 Tool、Assistant、Compaction 等业务事件 |
|
||||
| Session | 维护连续逻辑 event window,区分 replace、prepend 与 scalar append,调度 snapshot 通知 | 解释 Tool、Assistant、Compaction 等业务事件 |
|
||||
| Event Registry | 按 Cordis 生命周期保存唯一 `kind` 的 Definition 和唯一 fallback | 保存某个 Session 的 Context 或 State |
|
||||
| Assembler | 匹配 Event,维护 Context、Location、依赖和发布脏集 | 理解业务 State 字段或 Chat 排序 |
|
||||
| Assembler | 匹配标准 event 或 packed run,维护 Context、Location、依赖和发布脏集 | 理解业务 State 字段或 Chat 排序 |
|
||||
| Node Definition | 定义一个业务对象的 identity、State 演进、Location data 和 target Node | 创建 Context、修改别的业务 State 或扫描全部 Context |
|
||||
| View Builder | 把最终 target Node 增量整理成该视图的 snapshot | 重新解释原始 Session Event |
|
||||
| View Builder | 把最终 target Node 增量整理成该视图的 snapshot | 重新解释 `SessionEventLike` input |
|
||||
| React renderer | 按最终 Node 的 `kind` 展示 renderer-owned data,并读取当前 Node 所属 Location 的只读业务 data | 配对业务 Event、扫描全局 Nodes 或决定业务生命周期 |
|
||||
|
||||
Registry 注册是 Cordis effect,Definition 卸载会触发现有 Session 的低频 registry rebuild。普通业务 Event 不改变 Registry,也不会因此重建全部业务类型。
|
||||
|
||||
### `ConversationNodeDefinition` 总体契约
|
||||
|
||||
每个 [`ConversationNodeDefinition`](../../../../packages/client/ui-conversation/src/client/contract/conversation.ts) 独立拥有一种业务对象从 Event 到 State 和最终 view Node 的转换。Definition 的 `kind` 是 Registry 内唯一名称,也是业务 ID 的命名空间。
|
||||
每个 [`ConversationNodeDefinition`](../../../../packages/client/ui-conversation/src/client/contract/conversation.ts) 独立拥有一种业务对象从 `SessionEventLike` input 到 State 和最终 view Node 的转换。Definition 的 `kind` 是 Registry 内唯一名称,也是业务 ID 的命名空间。
|
||||
|
||||
同一个 Event 可以被多个普通 Definition 认领。例如一条 Assistant Event 同时更新 Assistant Node 和 Turn Tail;一条 Retry Event 同时更新 Retry、Assistant 和 Turn Tail。Assembler 只有在全部普通 Definition 都返回 `null` 时才询问 fallback。
|
||||
同一个 input 可以被多个普通 Definition 认领。例如一条 Assistant event 或 packed run 同时更新 Assistant Node 和 Turn Tail;一条 Retry Event 同时更新 Retry、Assistant 和 Turn Tail。Assembler 只有在全部普通 Definition 都返回 `null` 时才询问 fallback。
|
||||
|
||||
Definition 不持有跨 Session 的可变业务数据。每个 Session 的 Context、State、依赖和 View Builder 都由该 Session 的 Assembler 隔离持有。
|
||||
|
||||
@@ -49,9 +49,9 @@ Assembler 使用 `conversationContextKey(kind, id)` 组合无碰撞 key;不同
|
||||
|
||||
#### `match(event)`
|
||||
|
||||
`match(event)` 只读取当前原始 `SessionEvent`,返回 `{ id, role: 'start' | 'update' }` 或 `null`。它拿不到 Context、历史、Reader、Location 或 view envelope。
|
||||
`match(event)` 只读取当前 `SessionEventLike`,返回 `{ id, role: 'start' | 'update' }` 或 `null`。它拿不到 Context、历史、Reader、Location 或 view envelope。`chunkrow/*` event 只能作为 update;Assembler 会拒绝 packed start,`start()` 接收的 `ConversationStartMatch` 只包含标准 `SessionEvent`。
|
||||
|
||||
这项限制使单条 Event 的路由成本只随已注册 Definition 数量增长。Assembler 不会为了判断一条 update 属于谁而遍历该 Definition 的历史 Context。
|
||||
这项限制使单条 scalar event 或 packed run 的路由成本只随已注册 Definition 数量增长。Assembler 不会为了判断一条 update 属于谁而遍历该 Definition 的历史 Context。
|
||||
|
||||
start、result、resource、checkpoint 及业务自有终止 Event 必须携带或可直接推导同一 ID。若单个 Event 不能算出 ID,生产 Event 的协议负责补足关联字段,Client 不通过“最近一个未完成对象”猜测。
|
||||
|
||||
@@ -59,9 +59,9 @@ start、result、resource、checkpoint 及业务自有终止 Event 必须携带
|
||||
|
||||
#### `ConversationMatch`
|
||||
|
||||
匹配成功后,Assembler 把原始 Event、可选的 wire presentation view、`role` 和引擎计算的 `location` 组成只读 `ConversationMatch`。
|
||||
匹配成功后,Assembler 把标准或 packed event、`role` 和引擎计算的 `location` 组成只读 `ConversationMatch`。一个 packed run 始终只占一个 Match,并保留 fragment 与 timestamp-gap 数组。
|
||||
|
||||
Context 的 `matches` 永远按 Event `seq` 升序保存,而不是按网络到达或分页摄入顺序保存。历史尾页先出现 result、older 页后出现 call 时,最终 Match 顺序仍然是 call 在前、result 在后。
|
||||
Context 的 `matches` 永远按首 `seq` 升序保存,而不是按网络到达或分页摄入顺序保存。Session journal 已经拒绝逻辑 range 重叠。历史尾页先出现 result、older 页后出现 call 时,最终 Match 顺序仍然是 call 在前、result 在后。
|
||||
|
||||
Location 可以随 prepend 补齐边界或 append 关闭边界而改变。Assembler 替换受影响 Match 的只读 Location 并 replay Context;业务不把旧 Location 副本当权威保存。
|
||||
|
||||
@@ -71,8 +71,8 @@ Location 可以随 prepend 补齐边界或 append 关闭边界而改变。Assemb
|
||||
|---|---|---|
|
||||
| `key` | Assembler | `kind + id` 的稳定最终 identity |
|
||||
| `kind` / `id` | Definition + Assembler | 当前业务命名空间和业务 ID |
|
||||
| `matches` | Assembler | 当前窗口已收集且按 `seq` 排序的完整业务证据 |
|
||||
| `start` | Assembler | 唯一 start Match;尚未加载时为 `undefined` |
|
||||
| `matches` | Assembler | 当前窗口已收集且按首 `seq` 排序的完整 scalar 与 packed 业务证据 |
|
||||
| `start` | Assembler | 唯一 scalar start Match;尚未加载时为 `undefined` |
|
||||
| `state` | Definition 返回、Assembler 持有 | 最近一次 `start`/`update` 返回值;未初始化时为 `undefined` |
|
||||
| `current` | Assembler | 各 target 最近一次 materialize 的 Node 或 `null` |
|
||||
|
||||
@@ -108,7 +108,7 @@ Reader 每次查询都记录 `{ key, revision, windowGap }` 依赖。命中前
|
||||
|
||||
#### `update(context, match)`
|
||||
|
||||
`update()` 只处理已经由 `match()` 精确路由到当前 `(kind, id)` 的 post-start Match。它不再判断 Event 属于哪个 Context。
|
||||
`update()` 只处理已经由 `match()` 精确路由到当前 `(kind, id)` 的 post-start scalar 或 packed Match。它不判断 input 属于哪个 Context。消费 Assistant delta 的 Definition 会把每个匹配的 `chunkrow/*` 值作为一个 batch fold,而不构造成员 event。
|
||||
|
||||
Assembler 按 `seq` 升序调用 `update()`。实时尾部 update 可以直接增量应用;任何非尾部证据插入、start 补齐或依赖失效都会从 `start()` 完整 replay。
|
||||
|
||||
@@ -126,9 +126,9 @@ Assembler 不以 State 引用相等判断是否需要发布或传播。每次成
|
||||
| `animation-frame` | 把多条高频更新合并到下一帧 materialize |
|
||||
| `none` | 本 Match 不主动安排 flush,State 和 dirty 标记仍被保留 |
|
||||
|
||||
省略 `publication()` 等于 `immediate`。Assistant token delta 使用 `animation-frame`,不可见 Inbox Context 使用 `none`,final、依赖 replay 和 Location 边界会以 immediate 路径发布最新结果。
|
||||
省略 `publication()` 等于 `immediate`。Assistant token delta 与 packed run 使用 `animation-frame`,不可见 Inbox Context 使用 `none`,final、依赖 replay 和 Location 边界会以 immediate 路径发布最新结果。
|
||||
|
||||
一帧内的每条 delta 仍执行 update;合并的只是 `buildViewNode()`、View Builder 和 React snapshot 通知,不会丢失 token。
|
||||
一帧内的每条 live delta 仍执行 `update()`,一个历史 packed run 则执行一次 batch `update()`;合并的只是 `buildViewNode()`、View Builder 和 React snapshot 通知,不会丢失 fragment。
|
||||
|
||||
#### `buildLocationData(context, scope)`
|
||||
|
||||
@@ -160,7 +160,7 @@ ID 不复用,完成的 Context 继续存在于当前窗口,既提供稳定
|
||||
|
||||
### Location 是一级引擎事实
|
||||
|
||||
[`ConversationLocationIndex`](../../../../packages/client/ui-conversation/src/client/conversation/location-index.ts) 根据 `turn/start`、`step/start`、显式 turn/step payload、`step/end` 和 `turn/end` 建立 Event 到 Location 的映射。
|
||||
[`ConversationLocationIndex`](../../../../packages/client/ui-conversation/src/client/conversation/location-index.ts) 根据 `turn/start`、`step/start`、显式 turn/step payload、`step/end` 和 `turn/end` 建立标准 event 与 packed run 到 Location 的映射。同一 row 的成员共享 turn、step、block index 与 delta kind,因此只需以首 `seq` 建立一条 Location entry。
|
||||
|
||||
Location 有 `session`、`turn`、`step` 和 `unresolved` 四种形状。Turn/Step 各自带 `open`、`closed` 或 `unknown` 状态,以及已加载的 start/end Event。
|
||||
|
||||
@@ -168,31 +168,31 @@ Location 有 `session`、`turn`、`step` 和 `unresolved` 四种形状。Turn/St
|
||||
|
||||
`unresolved` 表示当前历史窗口缺少足够前序边界,不等于 session-level。older prepend 补入边界后,索引修正 Match Location,并只 replay 拥有这些 seq 的 Context。
|
||||
|
||||
Append 普通 Event 只继承当前坐标;append 边界只重算所属 Turn。Prepend 会基于扩展后的完整连续窗口重建 Location facts,但引用稳定逻辑保留未变化 Turn/Step 对象。
|
||||
Append 标准 Event 只继承当前坐标;append 边界只重算所属 Turn。Prepend 会基于连续 `SessionEventLikeEntry` window 重建 Location facts,但引用稳定逻辑保留未变化 Turn/Step 对象。
|
||||
|
||||
Assembler 还把 reference-stable timeline 交给 View Builder。业务不重复维护 turn order、step list、last step 或边界 Map。
|
||||
|
||||
## 三种事件窗口链路
|
||||
## 三种 input window 链路
|
||||
|
||||
“历史反扫”描述 UI 从最新尾页向 Session 起点逐页加载的方向,不表示 Definition 逆序执行 `update()`。无论历史 API 返回顺序或页面加载方向如何,Assembler 对每个当前窗口和每个 fresh page 都按 `seq` 升序 canonicalize。
|
||||
“历史反扫”描述 UI 从最新尾页向 Session 起点逐页加载的方向,不表示 Definition 逆序执行 `update()`。Session journal 会在发布前校验每条 record 的逻辑 range;无论分页加载方向如何,Assembler 都按每个已接受标准 event 或 packed run 的首 `seq` 排序。
|
||||
|
||||
| 场景 | 输入范围 | Context/State 处理 | View Builder |
|
||||
|---|---|---|---|
|
||||
| 初始历史尾页或 resync | 当前完整连续窗口 | 清空并按 `seq` 正序重建全部 Context | `replace()` |
|
||||
| 加载一页 older history | 只传更早且去重后的 fresh Events | 保留现有 Context identity,补 Match、Location 和依赖后局部 replay | `apply(upserts)` |
|
||||
| 初始历史尾页或 resync | 当前完整连续逻辑窗口 | 清空并按首 `seq` 正序重建全部 Context | `replace()` |
|
||||
| 加载一页 older history | 只传通过 range 校验的更早标准 event 或 packed run | 保留现有 Context identity,补 Match、Location 和依赖后局部 replay | `apply(upserts)` |
|
||||
| 实时 append | 一条连续尾部 Event | 只匹配 Definitions 并精确更新命中 ID,边界只影响所属 Turn | `apply(upserts)` |
|
||||
|
||||
### 初始历史尾页与逻辑反扫
|
||||
|
||||
1. `Session.open()` 拉取最新 tail page,并把连续 History Entries 交给 `replaceWindow(entries, hasMore)`。
|
||||
1. `Session.open()` 拉取最新 tail page,并把连续 `SessionEventLike` entry 交给 `replaceWindow(entries, hasMore)`。
|
||||
2. `replaceWindow` 清空旧 Context、start-seq 索引、seq 反向索引、Reader 依赖和输入 Map。
|
||||
3. 全部 entries 按 Event `seq` 升序排序并写入当前窗口。
|
||||
3. 全部 entry 按首个逻辑 `seq` 升序排序并写入当前窗口。
|
||||
4. LocationIndex 对这个窗口重建 Turn/Step facts。
|
||||
5. Assembler 按升序 Event 逐条调用每个普通 Definition 的 `match(event)`。
|
||||
5. Assembler 按升序访问标准 event 与 packed run,并逐条调用每个普通 Definition 的 `match(event)`。
|
||||
6. 每个命中结果按 `(kind, id)` 取得或创建 Context,并把 Match 插入该 Context 的有序数组。
|
||||
7. 遇到 start 时执行 `start()`;已有 State 的尾部 update 直接执行 `update()`。
|
||||
8. 当前页只含 result/resource 而缺 start 时,Context 仍会按 ID 创建并收集 Matches,但 State 保持 `undefined`。
|
||||
9. 全部 Event 匹配后,Assembler 复查 Reader 依赖,使同一窗口内较早瞬间态先稳定、较晚消费者再读取它。
|
||||
9. 全部 input 匹配后,Assembler 复查 Reader 依赖,使同一窗口内较早瞬间态先稳定、较晚消费者再读取它。
|
||||
10. 所有 Context 标记 dirty,下一次 flush 先按 Step→Turn 完整重建 Location data,再对每个 target 调用 `buildViewNode()`。
|
||||
11. 某些业务在缺 start 时返回 `null`;Compaction、Command、Tool result 或 Turn Error 等可根据充分 update 证据构造 fallback Node。
|
||||
12. 每个 View Builder 收到完整 Node 集和 timeline,通过 `replace()` 建立初始 snapshot。
|
||||
@@ -206,12 +206,12 @@ Assembler 还把 reference-stable timeline 交给 View Builder。业务不重复
|
||||
### 新 older 分页的 prepend
|
||||
|
||||
1. `Session.loadOlder()` 以当前 `baseSeq` 拉取紧邻前页,并先验证页尾与当前窗口连续。
|
||||
2. Session 把 raw Event/view 数组 prepend 到自己的窗口,只把这一页传给 `assembler.prepend(entries, hasMore)`。
|
||||
3. Assembler 按 seq 去掉与当前窗口重叠的 Events,再把 fresh page 内部升序排列。
|
||||
2. Session 把已接受的标准或 packed entry prepend 到自己的窗口,只把这一页传给 `assembler.prepend(entries, hasMore)`。
|
||||
3. Journal 已经丢弃完整重复 range 并拒绝部分重叠;Assembler 再按首 `seq` 排列 fresh page。
|
||||
4. 已存在的 Context、State、current Nodes 和 View Builder 实例不清空。
|
||||
5. LocationIndex 用扩展后的完整输入重建 facts,并报告 Location identity 真正变化的 seq。
|
||||
6. 拥有这些 seq 的 Context 更新 Match Location,并从 start replay;无关 Context 不参与 Location replay。
|
||||
7. fresh Events 逐条执行 Definition matcher,并按稳定 ID 插入已有或新 Context 的有序 Matches。
|
||||
7. fresh 标准 event 与 packed run 通过同一 Definition matcher 和稳定 ID 进入已有或新 Context。
|
||||
8. 新页补出 pending Context 的 start 时,该 Context 从 start 初始化,再正序应用已经收集的所有 updates。
|
||||
9. 新页建立更近的 Reader predecessor、改变 predecessor revision 或消除 window gap 时,消费者从 `start()` 重算。
|
||||
10. Reader 依赖沿 start seq 向后传递 replay;同一传播批次不会把 Event 逆序应用。
|
||||
@@ -226,7 +226,7 @@ Reader gap 修复是 prepend 与普通 append 最大的算法差异。新页不
|
||||
|
||||
### 正向实时 append
|
||||
|
||||
1. Session 只接受紧邻当前 tail seq 的 live Event;重叠 seq 去重,出现 gap 时先走 tail-page repair。
|
||||
1. Session 只接受紧邻当前逻辑 tail seq 的标准 live Event;重叠时去重,出现 gap 时先走 tail-page repair。
|
||||
2. 非边界 Event 增量写入当前 Turn/Step 坐标;边界 Event 更新所属 Turn 的 Location facts。
|
||||
3. Assembler 对这一个 Event 的每个普通 Definition 调用一次 `match()`,不会遍历任何 Definition 的 Context 集合。
|
||||
4. 每个命中结果通过 `(kind, id)` 直接定位一个 Context。
|
||||
@@ -249,7 +249,7 @@ Chat `order` 的结构性变化仍可能重排当前可见 key;纯 data 更新
|
||||
|
||||
`replaceWindow` 是初始打开、resync、gap repair 和 registry 变化的低频完整替换,不用于实现普通 load older。`prepend` 与 `append` 都保留现有 Builder 和 Context identity。
|
||||
|
||||
分页页宽、历史加载次数和 RAF 合批只影响何时得到更多证据或何时发布,不改变窗口证据相同时的最终 Context State 与 Node。
|
||||
分页页宽、record packing、历史加载次数和 RAF 合批只影响何时得到更多证据或何时发布,不改变逻辑证据相同时的最终 Context State 与 Node。
|
||||
|
||||
## 内建业务如何使用 Definition
|
||||
|
||||
@@ -261,7 +261,7 @@ Chat `order` 的结构性变化仍可能重排当前可见 key;纯 data 更新
|
||||
| Next-step Inbox / `inbox-next-step` | splice Event seq | 每条目标为 next-step 的 `agent/inbox/spliced` | 无 | 同样形成逐指令瞬间态,claimed 集合供 Message 读取 |
|
||||
| Message / `input-message` | message ID | append-surface `user/message` | 无 | 根据 source 生成 context message,或读取最近 next-step Inbox 判断 user/steering |
|
||||
| Request Prompt / `request-prompt` | header Event seq | 每条 `request/header` | 无 | 通过 Reader 读取前一条 Request Prompt,保留完整 prompt 状态,并判定 system/tool 变化 |
|
||||
| Assistant / `assistant-step` | `turn:step` | `step/start` | `assistant/chunk`、final `assistant/message`、同 step Retry | 聚合 blocks、usage、首 token 时间、final 和 retry 隐藏状态,并发布同 key Step data |
|
||||
| Assistant / `assistant-step` | `turn:step` | `step/start` | scalar 或 packed `assistant/chunk`、final `assistant/message`、同 step Retry | 聚合 blocks、usage、首 token 时间、final 和 retry 隐藏状态,并发布同 key Step data |
|
||||
| Tool / `tool-call` | root call ID | root `tool/call` | root result、Code Dispatch start/result | 聚合 root、children 和 parent Map;Dispatch Event 用 `rootCallId` 精确路由 |
|
||||
| Command / `command` | command ID | `command/run` | `command/done`、带 source command ID 的 compact lifecycle/checkpoint | 聚合 command outcome 和手动压缩证据 |
|
||||
| Automatic Compaction / `compaction` | compaction ID | 无 source command ID 的 `compaction/start` | summary、end、replacement checkpoint | 聚合 summary/checkpoint;checkpoint 足够时可在缺 start 下 fallback |
|
||||
@@ -278,7 +278,7 @@ Chat `order` 的结构性变化仍可能重排当前可见 key;纯 data 更新
|
||||
| Inbox | `none` | 不生成 Node | prepend 补前序 splice 时沿 Reader 链重算瞬间态 |
|
||||
| Message | 默认 immediate | `user`、`steering` 或 `context` | window gap 修复可让同一 message key 重新分类 |
|
||||
| Request Prompt | 默认 immediate | 每条带非空 system 字段的 header 都生成一个 `system-prompt` | Step 首条 header 锚定在请求消息之前;同 step 后续序列锚定在表层改写之后;prepend 补入前序 header 后可纠正部分窗口的锚点 |
|
||||
| Assistant | chunk 为 RAF,final immediate,纯 usage/finish 为 none | 同 key `assistant-step`,状态为 running/settled/interrupted | 缺 `step/start` 可先用 Matches fallback;Location close 生成中断表现 |
|
||||
| Assistant | scalar chunk 与 packed run 为 RAF,final immediate,纯 usage/finish 为 none | 同 key `assistant-step`,状态为 running/settled/interrupted | scalar 与 packed reducer 等价;缺 `step/start` 可先用 Matches fallback;Location close 生成中断表现 |
|
||||
| Tool | 默认 immediate | 一个递归 `tool-call` root,包含全部 `subCalls` | result-only 历史窗口可 fallback;running→settled 保持 key |
|
||||
| Command | 默认 immediate | 普通 `command` 或集成 `manual-compaction` | checkpoint 到达可改变 anchor,但不改变 Context key |
|
||||
| Compaction | 默认 immediate | `compaction` marker | checkpoint 可先展示,older 补 start 后正序 replay |
|
||||
@@ -326,20 +326,20 @@ Slot-level contextual Hook 与 entry-owned `inject.hooks` 是两条独立路径
|
||||
|
||||
标准 `useSession` 仍属于所有 session-scoped slot renderer 的公开能力,`useTurnData()` 是收窄常见读取方式而不是权限沙箱。全窗口统计或任意对象索引仍可显式使用 Session snapshot;它们不能伪装成“当前 Node 的 Turn data”。
|
||||
|
||||
Assistant streaming 到 final、Tool running 到 settled 只更新同一个 Seat 的 data 和必要的排序属性,不再从末尾 running container 移入 finalized flow,因此组件内部 State 不因结算自动归零。
|
||||
Assistant streaming 到 final、Tool running 到 settled 始终留在同一个 Seat,只更新 data 和必要的排序属性。结算不会因跨 parent 移动而重置组件内部 State。
|
||||
|
||||
业务主动把已发布 Node 改成 hidden 时,它会退出 visible order,恢复 visible 时会重新 mount。这是明确的业务撤显语义,与 running→settled 的稳定 Seat 保证不同。
|
||||
|
||||
具体 Tool renderer 仍由 [`ui-tool ownership decision`](2026-08-08-client-tool-presentation-ownership.zh.md) 约束。Tool Definition 只交付递归 root/subcall data,`ui-tool` 再按 Tool name keyed slot 分发具体表现。
|
||||
|
||||
Trajectory 针对与 Chat 相同的 Assembler 和 Session 事件窗口注册自己的 target 与业务 Definition。它的 target builder 保留 stage-oriented read model,既不消费 Chat Builder 的 legacy slice,也不运行独立 history fold。Chat Builder 为 StatsLine 和顶层公共兼容字段保留 legacy slice;target 专属 Definition 不改变共享的 Context、Reader 或 Location 契约。
|
||||
Trajectory 针对与 Chat 相同的 Assembler 和 `SessionEventLikeEntry` window 注册自己的 target 与业务 Definition。它的 target builder 保留 stage-oriented read model,既不消费 Chat Builder 的 legacy slice,也不运行独立 history fold。Chat 与 Trajectory 分别维护独立的 scalar 和 packed Assistant reducer;target 专属 Definition 不改变共享的 Context、Reader 或 Location 契约。
|
||||
|
||||
target 专属 Trajectory Definition、保留的 stage model、Steering 适配、复杂度上界与表现层热点由 [Trajectory Context 组装决策](2026-08-11-trajectory-conversation-context-assembly.zh.md)负责。
|
||||
|
||||
## 运行时与渲染链路
|
||||
|
||||
```text
|
||||
Session Event window
|
||||
SessionEventLike window
|
||||
-> ConversationNodeAssembler
|
||||
-> Definition.match(event) -> (kind, id, start/update)
|
||||
-> Context matches + State + Location
|
||||
@@ -361,7 +361,7 @@ Slot type/runtime tests 固定父注册必须提供声明的 common inject、`ho
|
||||
|
||||
Assembled Web snapshot、GUI 和浏览器场景覆盖真实 plugin graph。浏览器证据比较 Assistant streaming→settled、Bash running→settled 以及 Code Mode root + nested subcalls 与 master 的布局。
|
||||
|
||||
历史链路验证同时覆盖完整 replace、非重叠 prepend、重叠 seq 去重、空页 `hasMore` 收敛和 live append。相同 Event 窗口通过不同摄入路径得到相同业务 State 与最终 Node。
|
||||
历史链路验证同时覆盖完整 replace、非重叠 prepend、完整 range 去重、部分重叠拒绝、空页 `hasMore` 收敛和 scalar live append。相同 Assistant 历史的 scalar 与 packed 表示产生相同 Chat/Trajectory State、timing boundary 与最终 Node;一个 packed run 在 replace、prepend、Location replay 与 registry rebuild 中始终只保留一个 Match。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -377,6 +377,8 @@ Assembled Web snapshot、GUI 和浏览器场景覆盖真实 plugin graph。浏
|
||||
|
||||
**为历史反扫定义逆向 State fold。** 拒绝:每个业务都要维护互为逆运算的两套逻辑,删除、非可逆聚合和跨 Context 依赖很难保持一致。统一 Matches 后从 start 正序 replay 只有一套业务语义。
|
||||
|
||||
**增加独立的 chunk-run matcher 与 update lifecycle。** 拒绝:第二条 Definition 路径会重复 dispatch、replay、publication 与 Context 类型。`ChunkRowEvent` 使用既有 `match(event)` 与 `update(context, match)` lifecycle,并通过 `chunkrow/*` discriminator 明确标记 packed 处理。
|
||||
|
||||
**把 Inbox 做成引擎一级公民或一个窗口级 Context。** 拒绝:Inbox 是普通业务状态,不应污染通用引擎;逐 splice 瞬间态加严格前序 Reader 同时支持 prepend、append 和 Message 查询。
|
||||
|
||||
**给跨业务查询注册特化 query method。** 拒绝:消费者仍要依赖提供方 API,新增关系会扩张中心接口。Reader 暴露指定 kind 的只读前序 Context,由提供方写好 State、消费者读懂 State。
|
||||
@@ -395,18 +397,18 @@ Assembled Web snapshot、GUI 和浏览器场景覆盖真实 plugin graph。浏
|
||||
|
||||
## 后果
|
||||
|
||||
新增业务节点可以局部注册自己的 matcher、State 转换、可选 Location data、最终 target Node 和 renderer,不再修改 Session 的业务 switch。`ChatNodeDataMap` 和 Location data maps 允许业务 package 通过 declaration merging 合入强类型 data;所有相关 Event 仍须暴露可单 Event 推导的稳定 ID。
|
||||
新增业务节点可以局部注册自己的 matcher、State 转换、可选 Location data、最终 target Node 和 renderer,无需修改 Session 的业务 switch。`ChatNodeDataMap` 和 Location data maps 允许业务 package 通过 declaration merging 合入强类型 data;所有相关 Event 仍须暴露可单 Event 推导的稳定 ID。
|
||||
|
||||
Host 业务 package 把自己的持久 Event 成员 declaration-merge 到 `@deepseek-ai/dsh-session/types`,Client Definition 则通过对应业务 package 的 `/types` 子路径进行 type-only import。增强实际声明接口而不是重导出 barrel,使 Host 和 Client 的独立 TypeScript Program 都能获得相同的 Event narrowing,同时不把 Host runtime 带入 Client 图。
|
||||
|
||||
初始尾页、older prepend 和 live append 共享一套 Context 不变量。缺 start、Reader window gap、Location unknown 以及高频 delta 都是引擎明确表达的状态,不需要业务另建方向相关 cache。
|
||||
初始尾页、older prepend 和 live append 共享一套 Context 不变量。缺 start、Reader window gap、Location unknown 以及 packed 高频 delta 都是引擎明确表达的状态,不需要业务另建方向相关 cache。
|
||||
|
||||
Append 不扫描历史 Context;prepend 只 replay Match、Location 或 Reader 答案真正受影响的 Context。Chat 结构变化仍可能重算 visible order 和索引,但不会重跑无关业务 fold 或替换未变化 Node identity。
|
||||
|
||||
State 更新与发布频率分离后,Assistant 每条 delta 都被 fold,同时每 animation frame 最多 materialize 一次。step/turn close 和 final 可立即发布最新 State。
|
||||
State 更新与发布频率分离后,Assistant 的每条 live delta 与每个历史 packed run 都会被 fold,同时每 animation frame 最多 materialize 一次。step/turn close 和 final 可立即发布最新 State。
|
||||
|
||||
Step/Turn 成为业务间共享聚合的稳定宿主。Turn Tail 和 Deliverables 不再依赖 renderer 扫描全局 Nodes;Slot-level `useTurnData()` 把常见读取限制到当前 Node 所属 Turn,并通过 selector equality 隔离无关更新。
|
||||
Step/Turn 是业务间共享聚合的稳定宿主。Turn Tail 和 Deliverables 无需由 renderer 扫描全局 Nodes 即可派生值;Slot-level `useTurnData()` 把常见读取限制到当前 Node 所属 Turn,并通过 selector equality 隔离无关更新。
|
||||
|
||||
代价是 Runtime 新增 Registry、Assembler、Location data、依赖重放和 per-target Builder 契约,UI Slots 也新增 parent-owned common inject 与 per-occurrence `hookContext`。Definition 作者必须理解稳定 ID、唯一 start、正序 replay、Step→Turn 发布顺序、只读 Reader 和 Node 不撤回规则。
|
||||
代价是 Runtime 新增 Registry、Assembler、Location data、依赖重放和 per-target Builder 契约,UI Slots 也新增 parent-owned common inject 与 per-occurrence `hookContext`。消费 Assistant delta 的 Definition 还需要维护等价的 scalar 与 packed update 分支。Definition 作者必须理解稳定 ID、唯一 scalar start、正序 replay、Step→Turn 发布顺序、只读 Reader 和 Node 不撤回规则。
|
||||
|
||||
`useTurnData()` 不撤销 session-scoped renderer 的标准 `useSession`,因此该边界依靠 API 引导和测试,而不是能力隔离。Registry 变化仍是低频完整 rebuild;Chat Builder 继续为 StatsLine 和顶层公共字段维护 legacy slice,Trajectory 则在共享 Session 窗口上拥有 target 专属 Definition 与 Builder。内建 Definition 分别留在所属 UI package;这些兼容边界不把业务解释权交还给 Session。
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
|
||||
2026-08-10-product-subagent-providers-in-shared-host.md: 196e28c1263c4b6d71eaeb59b9ba8457b36f3ff4
|
||||
2026-08-10-product-subagent-providers-in-shared-host.zh.md: 1451890e1b250a2095e3366c59d6ce0873b55fe9
|
||||
2026-08-10-product-subagent-providers-in-shared-host.md: a998408c4dd01ba8f7ab09eab453a963beb855a4
|
||||
2026-08-10-product-subagent-providers-in-shared-host.zh.md: cce746399b7ec553d145441cc3d51df3e051ca9b
|
||||
|
||||
+2
-2
@@ -14,9 +14,9 @@ The placement decision must preserve two independent facts. Loading a provider m
|
||||
|
||||
Product providers remain process-scoped host-plane registrations. The [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) supersedes only this note's former base-bundle installation choice: production `dsh-base` neither depends on nor mounts them. A Profile that opts in installs the selected provider Bundle; its patch mounts the default instance, and the Profile may mount additional named instances on the host plane. The [named-instance decision](../feature/2026-08-18-product-subagent-named-instances.md) owns each row's registry identity: both products accept multiple unique `providerName` values while preserving `codex` and `claude-code` as their defaults. Loading either plugin only registers a dormant backend; the corresponding Codex or Claude process starts on the first actual delegation call. Agent Presets independently contribute ordinary `dsh-tool-subagent` rows whose `provider` and `toolName` values expose exactly the configured instances needed by one agent without changing the Host registry.
|
||||
|
||||
Each provider package owns its directly installable Bundle patch and private product runtime. This note continues to own process-wide Host placement whenever either provider is installed. The provider-contract note continues to own each product protocol, result mapping, cancellation, process-tree lifecycle, and evidence tiers. The [Agent Preset architecture](2026-08-03-per-session-agent-presets.md) continues to own the Host/Agent split, preset authoring, and the rule that edits affect only newly composed sessions.
|
||||
Each provider package owns its directly installable Bundle patch and private product runtime. This note continues to own process-wide Host placement whenever either provider is installed. The provider-contract note continues to own each product protocol, result mapping, cancellation, process-tree lifecycle, and evidence tiers. The [named-instance decision](../feature/2026-08-18-product-subagent-named-instances.md) owns both optional product models and other per-instance configuration. The [Agent Preset architecture](2026-08-03-per-session-agent-presets.md) continues to own the Host/Agent split, preset authoring, and the rule that edits affect only newly composed sessions.
|
||||
|
||||
Each Bundle delegates executable selection to its package-owned product runtime: the Codex package runs its declared wrapper, while the Claude Code package lets its pinned Agent SDK select the private native executable. Neither provider consults or falls back to a host product command. Profile loading creates no product state, probes no version or authentication, and may supply each mounted Provider instance's deployment configuration, including the product-specific `permissionMode` values owned by the [non-interactive permissions decision](../feature/2026-08-15-product-subagent-noninteractive-permissions.md), without moving those choices into an Agent Preset or model-facing tool. Missing platform payloads and product failures remain local to the attempted delegation.
|
||||
Each Bundle delegates executable selection to its package-owned product runtime: the Codex package runs its declared wrapper, while the Claude Code package lets its pinned Agent SDK select the private native executable. Neither provider consults or falls back to a host product command. Profile loading creates no product state, probes no version or authentication, and may supply each mounted Provider instance's deployment configuration, including an optional opaque model and the product-specific `permissionMode` values owned by the [non-interactive permissions decision](../feature/2026-08-15-product-subagent-noninteractive-permissions.md), without moving those choices into an Agent Preset or model-facing tool. Missing platform payloads and product failures remain local to the attempted delegation.
|
||||
|
||||
## Verification
|
||||
|
||||
|
||||
+2
-2
@@ -14,9 +14,9 @@ Status: implemented
|
||||
|
||||
产品提供方仍是进程级的 host plane(宿主平面)注册。[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md)只取代本说明原先由 base bundle 安装提供方的选择:生产 `dsh-base` 既不依赖也不挂载它们。选择产品集成的 Profile 会安装目标提供方 Bundle;其 patch 挂载默认实例,而 Profile 可以在 host plane 挂载更多命名实例。[命名实例决策](../feature/2026-08-18-product-subagent-named-instances.zh.md)负责每个配置项的注册身份:两个产品都接受多个唯一的 `providerName`,同时保留 `codex` 与 `claude-code` 作为默认值。加载任一插件只会注册一个休眠后端;对应的 Codex 或 Claude 进程直到第一次实际委派调用时才启动。Agent Preset 通过普通 `dsh-tool-subagent` 配置项的 `provider` 与 `toolName` 准确公开单个 agent 所需的已配置实例,而无需更改 Host 注册表。
|
||||
|
||||
每个提供方包都拥有可直接安装的 Bundle patch 与私有产品运行时。本说明继续负责每个已安装提供方的进程级 Host 放置。提供方约定说明继续负责每个产品的协议、结果映射、取消、进程树生命周期与证据层级。[Agent Preset 架构](2026-08-03-per-session-agent-presets.zh.md)继续负责宿主与 agent 的划分、preset 创作,以及改动只影响新组装会话的规则。
|
||||
每个提供方包都拥有可直接安装的 Bundle patch 与私有产品运行时。本说明继续负责每个已安装提供方的进程级 Host 放置。提供方约定说明继续负责每个产品的协议、结果映射、取消、进程树生命周期与证据层级。[命名实例决策](../feature/2026-08-18-product-subagent-named-instances.zh.md)负责两个可选产品模型及其他逐实例配置。[Agent Preset 架构](2026-08-03-per-session-agent-presets.zh.md)继续负责宿主与 agent 的划分、preset 创作,以及改动只影响新组装会话的规则。
|
||||
|
||||
每个 Bundle 都把可执行文件选择交给包自有的产品运行时:Codex 包运行自身声明的 wrapper,Claude Code 包则让锁定的 Agent SDK 选择私有原生可执行文件。两个提供方都不会查询或回退宿主产品命令。加载 Profile 不会创建产品状态、探测版本或测试身份验证;它可以提供每个已挂载 Provider 实例的部署配置,包括由[非交互权限决策](../feature/2026-08-15-product-subagent-noninteractive-permissions.zh.md)负责的产品专属 `permissionMode` 值,但不会把这些选择移入 Agent Preset 或面向模型的工具。平台载荷缺失和产品故障仍局限于发生问题的那次委派。
|
||||
每个 Bundle 都把可执行文件选择交给包自有的产品运行时:Codex 包运行自身声明的 wrapper,Claude Code 包则让锁定的 Agent SDK 选择私有原生可执行文件。两个提供方都不会查询或回退宿主产品命令。加载 Profile 不会创建产品状态、探测版本或测试身份验证;它可以提供每个已挂载 Provider 实例的部署配置,包括可选的不透明模型,以及由[非交互权限决策](../feature/2026-08-15-product-subagent-noninteractive-permissions.zh.md)负责的产品专属 `permissionMode` 值,但不会把这些选择移入 Agent Preset 或面向模型的工具。平台载荷缺失和产品故障仍局限于发生问题的那次委派。
|
||||
|
||||
## 验证
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
# Agent Note: Session log versioning — one integer, an upgrade chain, and a per-event ignorable marker
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-10-session-log-version-mechanism.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Session logs must be upgradable after release, and the runtime that ships first is the floor for every later decision: whatever refusal and degradation behavior is missing from the first released reader can never be added to the copies users already run. Release issue #1901 required at minimum that an old runtime reading a newer session format reports "unsupported" instead of misreading it. The pre-change reader did the opposite on both axes: `assertVersion` rejected any version mismatch with one direction-blind message, and the JSONL decoder passed unknown event types through untouched, so reconstruction silently skipped them — resuming a gutted session with no diagnostic at all.
|
||||
|
||||
## Decision
|
||||
|
||||
**One monotonic integer, no major/minor split.** Whether a version step is auto-upgradable is a property of that step — expressed by whether its upgrader exists — not something a two-level numbering scheme should promise in advance (you rarely know at design time whether the next change will turn out "major"). This matches the SQLite backend's `SCHEMA_VERSION` precedent.
|
||||
|
||||
**The writer decides bumps, not the reader.** A bump is required exactly when an old runtime could no longer handle a new log with full semantic correctness. "Parses without error" is not the bar: silently skipping content that shapes reconstruction is a wrong read. Only structural changes qualify — header shape, event envelope, core event semantics, the surface mechanism (`SurfaceEventType` set, `SurfaceOp` variants). When unsure, bump: a near-identity upgrader is almost free, a missed bump silently corrupts old readers.
|
||||
|
||||
**Read rules by direction.** Equal version: read normally. Newer than the reader: refuse, name the direction ("written by a newer harness — upgrade"), and point at the raw log artifact so the user can still see the text (`SessionFormatUnsupportedError`, distinct from `SessionPersistenceCorruptionError` because nothing is damaged). Older than the reader: convert in memory through the chain of n→n+1 upgraders for viewing; persist the converted log only when the session is actually continued (atomic temp-file replace, original kept as backup). A step whose upgrader cannot be written is left empty, which cuts off every version at or below it — those degrade to raw-text viewing.
|
||||
|
||||
**A per-event `ignorable` marker covers vocabulary growth, so ordinary event additions never bump the version.** The event vocabulary is decided by which plugins are mounted, which a single version integer cannot describe. A reader meeting an unrecognized event type refuses to interpret the log unless the event carries `ignorable: true` in its envelope. The default is *required*: forgetting the marker over-refuses a resumable session (an inconvenience), while a default of ignorable would make the same mistake silently resume a gutted one (a safety failure). The architecture makes this sound: model-visible content flows only through the three `surfaceOp`-marked surface event types plus the `request/header`/`request/context` folds, so the dangerous unknowns are exactly the non-surface events that change how the rest of the log is read (`session/end-seed` is the existing example).
|
||||
|
||||
## Consequences
|
||||
|
||||
What shipped in v0 (release 0812): direction-aware refusal with the raw-log path; the unknown-event guard against a generated known-vocabulary list (`KNOWN_SESSION_EVENT_TYPES`, emitted by `gen-persistence-catalog` from every `SessionEventMap` merge and kept fresh by `verify-persistence-catalog`); the `ignorable` envelope field accepted by seed validation, both backends (a dedicated SQLite column, `SCHEMA_VERSION` 15), and the BFF wire schema. The upgrader chain itself is deferred until the first real v0→v1 step exists to test it against; writers do not yet set `ignorable` (no producer needs it), so `Session.append` gains that surface with its first user. Until a registration surface exists, an out-of-repo plugin's events refuse resume under first-party readers — the pre-release stance accepts that, and the refusal is loud rather than silent. The unknown-type guard is read-side only: `appendCore` keeps rejecting retired legacy shapes but does not vocabulary-check new types, because an append-time refusal would stall a live session's durability mid-flight, which costs more than a loud refusal at the log's next load. The JSONL backend additionally refuses a foreign version from the raw header line before validating this format version's header shape or decoding any event row, so a structurally different future format still reports the upgrade direction instead of "corrupt"; SQLite gates whole-file structure through its own `SCHEMA_VERSION` pragma first.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Major/minor versioning** — the "is it convertible" bit lives on each step's upgrader, and pre-committing it into a number shape invites wrong promises.
|
||||
- **Default-ignorable unknown events** — inverts the failure mode of a forgotten marker from visible over-refusal into silent corruption.
|
||||
- **Auto-migrating on view** — rewriting the artifact on open turns a read into a destructive write: a converter bug corrupts logs at browse time, and a same-directory older runtime loses access because a newer one merely looked.
|
||||
- **Per-plugin runtime registration of known event types** — would make the known set composition-dependent, so a leaner same-version composition would refuse logs a fuller one wrote. The generated repo-wide list keeps same-version reads uniform; out-of-repo plugin events are outside it by construction, and a registration surface for them is deferred until such a consumer exists.
|
||||
@@ -1,30 +0,0 @@
|
||||
# Agent Note: Session log 版本机制:单调整数、升级器链、逐事件可忽略标记
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-10-session-log-version-mechanism.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Session log 在发布后必须能升级格式,而最先发布的运行时决定了此后一切的下限:第一个发布版的读取器缺少哪种拒绝和降级行为,用户手里已经装上的副本就永远补不上。发布 issue #1901 的最低要求是老运行时读到新 Session 格式时明确报不支持,而不是读错。改动前的读取器在两个方向上都做反了:`assertVersion` 对任何版本不匹配抛出同一条不区分方向的消息;JSONL 解码器把不认识的事件类型原样放行,重建时静默跳过,恢复出一个内容残缺的会话且没有任何诊断。
|
||||
|
||||
## 决定
|
||||
|
||||
**一个单调递增的整数,不分大小版本。**某一步能不能自动升级是那一步自己的属性,由它的升级器存在与否表达,不该由两级编号方案提前承诺(设计时很少能预知下一个变更算不算"大")。这与 SQLite 后端 `SCHEMA_VERSION` 的先例一致。
|
||||
|
||||
**升不升版本由写入方决定,与读取方能力无关。**当且仅当老运行时无法在语义上完全正确地处理新日志时才必须升版本。"解析不报错"不是标准:静默跳过影响重建的内容就是读错。只有结构性变更够得上这条线:header 形状、事件信封、核心事件语义、surface 机制(`SurfaceEventType` 集合、`SurfaceOp` 变体)。拿不准就升:近似恒等的升级器几乎没有成本,漏升一次会让老读取器静默读坏。
|
||||
|
||||
**读取规则按方向区分。**版本相等:正常读。比读取器新:拒绝,说明方向("由更新的 harness 写入,请升级"),并给出原始日志文件的路径,用户仍能看到文本(`SessionFormatUnsupportedError`,与 `SessionPersistenceCorruptionError` 区分,因为数据没有损坏)。比读取器旧:查看时经 n→n+1 升级器链在内存中逐级转换;只有会话真正被继续时才把转换落盘(临时文件原子替换,原文件留备份)。写不出升级器的那一步留空,这会切断该步及更早所有版本的升级路径,它们降级为只能看原文。
|
||||
|
||||
**逐事件的 `ignorable` 标记吸收词汇表增长,普通的新增事件永远不用升版本。**事件词汇表由挂载了哪些插件决定,单个版本整数描述不了它。读取器遇到不认识的事件类型时拒绝解读日志,除非该事件的信封带 `ignorable: true`。默认为必需:忘写标记的后果是把一个本可恢复的会话拒绝过头(体验问题),而默认可忽略会让同样的疏忽静默恢复出残缺会话(安全事故)。架构保证了这条规则成立:模型可见内容只经三种带 `surfaceOp` 标记的 surface 事件加 `request/header`、`request/context` 折叠进入重建,危险的未知事件恰好是那些不进 surface 但改变日志其余部分解读方式的事件(`session/end-seed` 是现存例子)。
|
||||
|
||||
## 影响
|
||||
|
||||
v0(0812 发布)交付的内容:分方向的拒绝并带原始日志路径;基于生成的已知词汇清单(`KNOWN_SESSION_EVENT_TYPES`,由 `gen-persistence-catalog` 从所有 `SessionEventMap` 声明合并生成,`verify-persistence-catalog` 保证新鲜)的未知事件守卫;`ignorable` 信封字段被种子校验、两个后端(SQLite 专用列,`SCHEMA_VERSION` 升到 15)和 BFF 线上 schema 接受。升级器链本身推迟到第一个真实的 v0→v1 变更出现、有真实对象可测时再建;写入侧目前不写 `ignorable`(还没有生产者需要它),`Session.append` 的这一表面随第一个使用者一起落地。在注册表面出现之前,仓库外插件的事件在第一方读取器下无法恢复会话,预发布立场接受这一点,而且拒绝是显式的而非静默的。未知类型守卫只在读取侧生效:`appendCore` 继续拒绝已淘汰的 legacy 形状,但不对新类型做词汇检查,因为写入时拒绝会让活跃会话的持久化中途停摆,代价大于下次加载时的显式拒绝。JSONL 后端还会在校验本格式版本的 header 形状、解码任何事件行之前,直接从原始 header 行拒绝外来版本,因此结构完全不同的未来格式仍会报告升级方向而不是"损坏";SQLite 则先由自己的 `SCHEMA_VERSION` pragma 把关整个文件的结构。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **大小两级版本号**:能否转换这一位信息属于每一步的升级器,把它预先固化进编号形状会做出错误承诺。
|
||||
- **未知事件默认可忽略**:把忘写标记的后果从可见的过度拒绝反转成静默损坏。
|
||||
- **查看时自动迁移落盘**:打开即改写把读操作变成破坏性写操作,转换器的 bug 会在浏览时损坏日志,同目录的旧版本运行时也会因为新版本只是看了一眼就失去访问能力。
|
||||
- **插件运行时注册已知事件类型**:会让已知集依赖插件组合,同版本的精简组合会拒绝完整组合写出的日志。生成的全仓库清单保证同版本读取行为一致;仓库外插件的事件按构造就在清单之外,为它们提供注册表面推迟到真有这样的消费者时再做。
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md
|
||||
2026-08-10-session-log-version-mechanism.md: 81108ceaf23405c8f2def9aaef88505d635808a3
|
||||
2026-08-10-session-log-version-mechanism.zh.md: cbb127420e2695853fdc2ad0bb98a7a0bf230b5b
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-15-packed-session-history-transport.md
|
||||
2026-08-15-packed-session-history-transport.md: 01e36509b7ad2c878ae4ea04c3a10f029e1b8f3d
|
||||
2026-08-15-packed-session-history-transport.zh.md: 590385dcfac901ab01e472ee75e766e51bf4b001
|
||||
@@ -0,0 +1,61 @@
|
||||
# Agent Note: Carry packed chunk rows through session history
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-15-packed-session-history-transport.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`session.page` and the opening `session.follow` snapshot serve a bounded logical Session-event interval to remote clients. Provider streams can place hundreds of thousands of token-sized `assistant/chunk` events in one incomplete tail. Expanding every persisted row and then serializing every logical event repeats the same envelope on the wire. Expanding a packed response at the Client boundary recreates the same event objects, journal entries, Location indexing, Definition matches, and State updates before conversation replay can finish.
|
||||
|
||||
The transport must remain lossless. Session sequence numbers are pagination and reconnect evidence; exact fragment boundaries and timestamps remain useful to diagnostics and non-UI API consumers; live streaming, durable export, replay, and model-history derivation continue to require the canonical event stream. Browser presentation does not require one allocated event object and one Definition callback per historical fragment when a Definition can fold the lossless run directly.
|
||||
|
||||
## Decision
|
||||
|
||||
History pages and follow opening snapshots carry `records: SessionHistoryRecord[]`. An ordinary record is `{ type: 'event', event: SessionWireEvent }`; consecutive same-block Assistant delta events use `{ type: 'chunks', event: ChunkRowEvent }` and the shared lossless codec from [the packed JSONL decision](2026-07-26-packed-chunk-rows-by-default.md). The Host constructs the event-shaped value once when it packs the selected page. Its `type` is `chunkrow/text-chunks`, `chunkrow/reasoning-chunks`, or `chunkrow/tool-call-chunks`; `seq` and `time` identify the first member, while `data` retains the original fragment and timestamp-gap arrays. The explicit outer discriminator selects the record class without interpreting that detailed chunk kind. The page is selected from logical events before packing, so message-aligned pagination remains independent of physical persistence layout.
|
||||
|
||||
The generated Remote decoder validates the response fields. `SessionEventStream` passes the original wire records to `RemoteJournalStream` and supplies each record's inclusive logical sequence range: an event covers `[event.seq, event.seq]`, while a row covers `[event.seq, event.seq + memberCount - 1]`. The journal checks page continuity, pagination joins, reconnect repair, complete duplicates, partial overlaps, and live-event deduplication before publishing records. The durable address in the page request selects either an ordinary Session or an authorized direct subagent child without a second history protocol.
|
||||
|
||||
The Client narrows the accepted `SessionHistoryRecord[]` to `SessionEventLikeEntry[]` without allocating replacement entries. The outer `type` remains available to the journal, Session, and assembler; both variants carry an inner value with aligned `type`, `seq`, `time`, and `data` fields. `ChunkRowEvent` is Client history data, not a durable Session event: it is absent from `SessionEventMap`, `Session.events`, and `session/event`.
|
||||
|
||||
Conversation accepts the same `{ type, event }` entries retained by Session. Definitions receive the inner `SessionEventLike`: `match()` and `update()` accept standard or packed values, while `start()` accepts only a standard `SessionEvent`; the assembler uses the outer discriminator to reject a packed start. Chat Assistant, Turn Tail, and Trajectory Assistant handle the three packed tags in their existing reducers. One row therefore remains one Client entry, Conversation input, and Match, while those reducers preserve scalar replay's final blocks, tool-call fields, first-token time, first-visible boundary, retry behavior, and interruption state.
|
||||
|
||||
Live `session.follow` frames remain individual events and use the scalar path, so visible streaming cadence is unchanged. Session persistence, raw export, replay, model-history derivation, and the canonical in-memory log are unchanged.
|
||||
|
||||
## Measured result
|
||||
|
||||
A production-sized private session sample was measured without retaining or committing its content. Its tail page contained 416,756 logical events. The lossless packed response used 696 top-level records, including 116 packed rows.
|
||||
|
||||
| Representation | Top-level records | JSON bytes | gzip bytes | Brotli bytes |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| Raw logical events | 416,756 | 69,433,638 | 4,190,226 | 1,972,998 |
|
||||
| Completed-step projection candidate | 228,129 | 38,427,209 | 2,324,688 | 957,350 |
|
||||
| Lossless packed history | 696 | 6,362,724 | 1,154,206 | 528,145 |
|
||||
|
||||
Packing reduced uncompressed JSON by 90.8% relative to raw logical events and by 83.4% relative to the lossy completed-step projection candidate. Brotli output was 73.2% smaller than raw and 44.8% smaller than that projection candidate. These figures describe this sample rather than a protocol guarantee; savings scale with the length and regularity of delta runs.
|
||||
|
||||
One-to-one Client retention keeps the same sample at 696 history entries and Conversation inputs instead of restoring 416,756 event entries. A local synthetic benchmark run measured Client parse, validation, retention, and two-Definition fold at 4,682.11 ms for scalar input and 276.10 ms for packed input, with sampled additional V8 heap peaks of 612,523,344 and 199,436,928 bytes respectively. These machine-dependent values are observations rather than thresholds.
|
||||
|
||||
The opt-in `packages/client/ui-conversation/tests/history-transport.perf.client.ts` benchmark constructs the same logical-event, ordinary-event, and delta-run cardinalities from synthetic content. `DSH_SNAPSHOT=replay pnpm exec vitest run --config vitest.web.perf.config.ts packages/client/ui-conversation/tests/history-transport.perf.client.ts` reports wire sizes, Host/client timing, uncompressed chunked Node loopback transfer medians, combined synthetic API-wait/UI-ready timing, and sampled additional V8 heap peaks under `HISTORY_TRANSPORT_PERF_RESULT`; a second inventory reports batch-fold medians for 10,000-, 20,000-, and 40,000-member whitespace-prefix runs under `HISTORY_WHITESPACE_PREFIX_PERF_RESULT`. The combined timing starts from an in-memory event array and omits cold persistence reads, the production API bridge and RPC envelope, and Chromium scheduling, so it is comparative inventory rather than production wall-clock latency. Heap measurements force garbage collection before three runs and report the median peak observed after each major Host construction/serialization or Client parse/validation/retention/fold stage, relative to the same initialized benchmark state; they do not measure process RSS, external or ArrayBuffer memory, or transients within a sampled stage. The manual performance inventory does not run in CI and carries no machine-dependent timing or memory assertions; structural assertions pin the fixture cardinalities, one Client input per wire record, and identical final state—including delta count and last-delta sequence—from its two-consumer Assistant fold fixture.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Discard completed-step chunks on the Host.** This lowers logical event count but makes transport semantics depend on the current transcript policy, removes exact evidence from all consumers, and still sends every retained incomplete-step token as a separate envelope. The measured packed response is smaller while remaining lossless.
|
||||
|
||||
**Expand each packed row before the Session object layer.** This preserves one callback per historical delta but recreates the browser allocation, indexing, and fold costs that packed transport can avoid. Consumers that require scalar events can still call `decodeStorageRecord()` explicitly.
|
||||
|
||||
**Put the raw row under a distinct `.chunks` payload.** This forces downstream consumers either to retain two payload field names or to allocate an aligned wrapper before assembly. The shared `.event` field preserves fast outer classification and one inner Definition path.
|
||||
|
||||
**Rely on HTTP content encoding.** gzip and Brotli reduce bytes on the network but do not remove repeated JSON parsing, validation, allocation, indexing, and fold work.
|
||||
|
||||
**Page directly over physical persistence rows.** This could also avoid logical expansion in a cold Host read, but page cuts depend on append-origin messages and replacement provenance rather than backend row boundaries. The current decision keeps the API independent of JSONL, SQLite, and future persistence layouts.
|
||||
|
||||
**Return only assembled Assistant snapshots.** The [assembled-messages-only rejection](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md) remains applicable: event families outside finalized messages carry user-visible and diagnostic state, and incomplete steps need their actual accumulated chunks.
|
||||
|
||||
## Consequences
|
||||
|
||||
History responses preserve every logical event while reducing wire bytes, Host response serialization and heap, browser JSON parsing and validation, Client entry allocation, and Conversation dispatch for long delta runs. The journal validates logical ranges before publication, so packed records neither create false gaps nor hide partial overlap. Direct `session.page` consumers must switch on `SessionHistoryRecord.type` and explicitly expand `record.event.data` when they require one event per member.
|
||||
|
||||
Cold persisted history is still decoded into the complete logical `SessionEvent[]` before the Host selects and repacks a page. This decision therefore improves transport and browser work, not the Host's cold-read decode memory. Eliminating that expansion requires a persistence-neutral message-boundary index or a separate streaming page reader and remains a distinct optimization.
|
||||
|
||||
The default Client history path exposes `SessionEventLike`, so consumers that require only canonical durable events must remain on Host `Session.events`, `session/event`, or an explicit decode path. A Definition that consumes Assistant deltas maintains equivalent scalar and packed branches. Scalar deltas already received live remain scalar in the current window; online replacement with a packed row is separate work, while reopen and reconnect install packed history.
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
# Agent Note: 在会话历史中传输打包分片行
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-15-packed-session-history-transport.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
`session.page` 与 `session.follow` opening snapshot 会向远程 Client 提供一段有界的逻辑 Session event 区间。提供方流可能在一个未完成尾部中产生数十万个 token 大小的 `assistant/chunk` 事件。先展开每条持久化行,再序列化每个逻辑事件,会在协议中重复相同 envelope。在 Client 边界展开 packed response 还会重新创建同样数量的 event object、journal entry、Location index、Definition match 和 State update,拖慢 conversation replay。
|
||||
|
||||
传输必须保持无损。Session seq 是分页与重连证据;精确 fragment 边界和时间戳对诊断与非 UI API 消费方仍然有用;实时流式传输、持久导出、回放与模型历史派生仍然需要规范事件流。当 Definition 可以直接 fold 无损 run 时,浏览器表现并不需要为每个历史 fragment 分配一个 event object 并执行一次 Definition callback。
|
||||
|
||||
## 决策
|
||||
|
||||
历史页与 follow opening snapshot 携带 `records: SessionHistoryRecord[]`。普通 record 为 `{ type: 'event', event: SessionWireEvent }`;连续且属于同一 block 的 Assistant delta event 使用[打包 JSONL 决策](2026-07-26-packed-chunk-rows-by-default.zh.md)中的共享无损 codec,表示为 `{ type: 'chunks', event: ChunkRowEvent }`。Host 在打包已选页面时只构造一次 event-shaped value。其 `type` 为 `chunkrow/text-chunks`、`chunkrow/reasoning-chunks` 或 `chunkrow/tool-call-chunks`;`seq` 与 `time` 表示首成员,`data` 保留原 fragment 与 timestamp-gap 数组。显式外层 discriminator 无需解释详细 chunk kind 即可选择 record 类别。系统先从逻辑 event 中选择页面,再执行打包,因此按消息对齐的分页不依赖物理持久化布局。
|
||||
|
||||
生成的 Remote decoder 会校验响应字段。`SessionEventStream` 把原始 wire record 交给 `RemoteJournalStream`,并提供每条 record 的逻辑 seq 闭区间:event 覆盖 `[event.seq, event.seq]`,row 覆盖 `[event.seq, event.seq + memberCount - 1]`。Journal 在发布 record 前检查页面连续性、分页拼接、重连修复、完整重复、部分重叠和实时 event 去重。页面请求中的 durable address 既可选择普通 Session,也可选择已授权的 direct subagent child,无需第二套历史协议。
|
||||
|
||||
Client 不分配替换 entry,直接把已接受的 `SessionHistoryRecord[]` 收窄为 `SessionEventLikeEntry[]`。外层 `type` 会一直保留到 journal、Session 与 assembler;两个分支都携带字段对齐的内部值,其中包含 `type`、`seq`、`time` 与 `data`。`ChunkRowEvent` 是 Client 历史数据,不是持久 Session event:它不会进入 `SessionEventMap`、`Session.events` 或 `session/event`。
|
||||
|
||||
Conversation 接受 Session 保留的同一组 `{ type, event }` entry。Definition 接收内部 `SessionEventLike`:`match()` 与 `update()` 接受标准或 packed value,`start()` 只接受标准 `SessionEvent`;assembler 使用外层 discriminator 拒绝 packed start。Chat Assistant、Turn Tail 和 Trajectory Assistant 在既有 reducer 中处理三种 packed tag。一条 row 因此始终只对应一个 Client entry、Conversation input 与 Match,而这些 reducer 会保留 scalar replay 的最终 block、tool-call 字段、首 token 时间、首个可见边界、retry 行为和 interruption 状态。
|
||||
|
||||
实时 `session.follow` frame 仍是单个 event 并走 scalar 路径,因此可见 streaming cadence 不变。Session persistence、原始导出、回放、模型历史派生与规范内存日志均不改变。
|
||||
|
||||
## 测量结果
|
||||
|
||||
测量使用了一份生产规模的私有会话样本,未保留或签入其内容。其尾页包含 416,756 个逻辑事件。无损打包响应使用 696 条顶层记录,其中包含 116 条打包行。
|
||||
|
||||
| 表示 | 顶层记录数 | JSON 字节 | gzip 字节 | Brotli 字节 |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| 原始逻辑事件 | 416,756 | 69,433,638 | 4,190,226 | 1,972,998 |
|
||||
| 已完成步骤投影候选 | 228,129 | 38,427,209 | 2,324,688 | 957,350 |
|
||||
| 无损打包历史 | 696 | 6,362,724 | 1,154,206 | 528,145 |
|
||||
|
||||
与原始逻辑事件相比,打包使未压缩 JSON 减少 90.8%;与有损的已完成步骤投影候选相比减少 83.4%。Brotli 输出相对原始形式减少 73.2%,相对该投影候选减少 44.8%。这些数字描述该样本,并非协议保证;收益随 delta run 的长度与规律性变化。
|
||||
|
||||
一对一 Client 保留使同一规模样本保持为 696 个 history entry 与 Conversation input,而不会恢复成 416,756 个 event entry。一次本地合成 benchmark 观测到:Client parse、validation、retention 与双 Definition fold 在 scalar input 下耗时 4,682.11 ms,在 packed input 下耗时 276.10 ms;采样额外 V8 heap 峰值分别为 612,523,344 与 199,436,928 字节。这些依赖机器的数值是观测结果,不是门槛。
|
||||
|
||||
可选运行的 `packages/client/ui-conversation/tests/history-transport.perf.client.ts` benchmark 使用合成内容构造相同的逻辑 event 数、普通 event 数与 delta run 数。`DSH_SNAPSHOT=replay pnpm exec vitest run --config vitest.web.perf.config.ts packages/client/ui-conversation/tests/history-transport.perf.client.ts` 会在 `HISTORY_TRANSPORT_PERF_RESULT` 下报告 wire 体积、Host/Client 计时、未压缩且采用 chunked response 的 Node loopback 传输中位数、组合后的合成 API 等待/UI 就绪时间,以及采样的额外 V8 heap 峰值;第二组清单会在 `HISTORY_WHITESPACE_PREFIX_PERF_RESULT` 下报告 10,000、20,000 与 40,000 个成员 whitespace-prefix run 的 batch fold 中位数。组合计时从内存 event 数组开始,不包含冷持久化读取、生产 API bridge 与 RPC envelope,也不包含 Chromium 调度,因此它是对比清单,而非生产环境 wall-clock 延迟。Heap 测量会在三次运行前强制执行垃圾回收,并相对于相同的已初始化 benchmark 状态,报告 Host 构造/序列化或 Client 解析/校验/保留/fold 各主要阶段之后所观察峰值的中位数;该指标不测量进程 RSS、external 或 ArrayBuffer 内存,也可能遗漏单个采样阶段内部的瞬态峰值。CI 不执行这组手动性能用例,其中也没有依赖机器性能的耗时或内存断言;结构断言固定 fixture 规模、每条 wire record 对应一个 Client input,以及双消费方 Assistant fold fixture 的一致最终状态,包括 delta 数量与末个 delta seq。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**在 Host 丢弃已完成步骤的分片。** 这会减少逻辑事件数,但会让传输语义取决于当前 transcript 策略,从所有消费方移除精确证据,同时仍把保留的未完成步骤 token 逐个装入信封。实测打包响应在保持无损的同时更小。
|
||||
|
||||
**在进入 Session 对象层前展开每条 packed row。** 这会保留每个历史 delta 一次 callback 的语义,但也会重新产生 packed transport 原本可以避免的浏览器分配、索引和 fold 成本。确实需要 scalar event 的消费方仍可显式调用 `decodeStorageRecord()`。
|
||||
|
||||
**把原始 row 放在独立的 `.chunks` payload 下。** 这会迫使下游消费方保留两种 payload 字段名,或在进入 assembly 前分配字段对齐的包装层。共享 `.event` 字段既保留快速外层分类,也保留一条内部 Definition 路径。
|
||||
|
||||
**只依赖 HTTP 内容编码。** gzip 与 Brotli 会减少网络字节,但不会移除重复的 JSON 解析、校验、分配、索引与 fold 工作。
|
||||
|
||||
**直接按物理持久化行分页。** 这还可以避免冷 Host 读取时的逻辑展开,但页面切分取决于追加来源消息与替换 provenance,而不是后端行边界。当前决策让 API 保持对 JSONL、SQLite 与未来持久化布局的独立性。
|
||||
|
||||
**只返回组装后的 Assistant 快照。** [仅保留组装消息的否决记录](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.zh.md)仍然适用:final message 之外的事件族承载用户可见状态与诊断状态,未完成步骤也需要其实际累计分片。
|
||||
|
||||
## 后果
|
||||
|
||||
历史响应保留每个逻辑 event,同时减少长 delta run 的 wire 字节、Host 响应序列化与 heap、浏览器 JSON 解析与校验、Client entry 分配,以及 Conversation dispatch。Journal 在发布前校验逻辑 range,因此 packed record 既不会产生伪 gap,也不会隐藏部分重叠。直接调用 `session.page` 的消费方必须按 `SessionHistoryRecord.type` 分支;需要逐 member event 时再显式展开 `record.event.data`。
|
||||
|
||||
冷持久历史仍会先解码成完整的逻辑 `SessionEvent[]`,Host 再选择页面并重新打包。因此,本决策改善的是传输与浏览器工作,不是 Host 冷读取的解码内存。消除该展开需要提供方无关的消息边界索引或单独的流式页面读取器,属于另一项优化。
|
||||
|
||||
默认 Client 历史路径公开 `SessionEventLike`,因此只接受规范持久 event 的消费方必须继续使用 Host `Session.events`、`session/event` 或显式 decode 路径。消费 Assistant delta 的 Definition 需要维护等价的 scalar 与 packed 分支。当前窗口已经实时接收的 scalar delta 仍保持 scalar;在线替换为 packed row 属于另一项工作,reopen 与 reconnect 则安装 packed 历史。
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-18-experimental-agent-teams-packages.md
|
||||
2026-08-18-experimental-agent-teams-packages.md: cfaf7b23d03a1adecf8acc18a110717a62aa9ed1
|
||||
2026-08-18-experimental-agent-teams-packages.zh.md: 1d06addff6c8f347726408d3bb02bc72374492cb
|
||||
2026-08-18-experimental-agent-teams-packages.md: 495922d57bd88a78f0ca6b61367ef49c820db632
|
||||
2026-08-18-experimental-agent-teams-packages.zh.md: 65aed57e223d8fa9b8ff97e2fcc109057b51474d
|
||||
|
||||
+6
-2
@@ -12,12 +12,16 @@ An experimental directory without a current package previously imposed placement
|
||||
|
||||
## Decision
|
||||
|
||||
`packages/experimental/agent-team` and `packages/experimental/tool-agent-team` are private workspace packages. The [experimental package naming decision](2026-08-19-experimental-package-name-prefix.md) owns their npm names and promotion rename; this note owns their placement, release exclusion, and dependency isolation.
|
||||
`packages/experimental/agent-team`, `packages/experimental/tool-agent-team`, and `packages/experimental/agent-team-profile` are private workspace packages. The [experimental package naming decision](2026-08-19-experimental-package-name-prefix.md) owns their npm names and promotion rename; this note owns their placement, release exclusion, and dependency isolation.
|
||||
|
||||
The dsh pack and publish set and the local baseline publisher exclude every manifest below `packages/experimental/`. `release:dsh` still advances their manifest versions with the shared dsh version without creating release tags. Workspace constraints require each experimental package to set `private: true` and omit `publishConfig`. The same top-level check rejects `dependencies`, `optionalDependencies`, and `peerDependencies` from release packages, release apps, or the Python runtime to an experimental package. Experimental packages may depend on release packages and each other; tests may use them through `devDependencies`, and examples may load them explicitly.
|
||||
|
||||
The generic caller-reserved continuable child identity and selective direct-child drain remain in the stable Subagent service. They own Subagent identity and Activation lifecycle without importing or naming Agent Teams; the experimental Team service consumes them in the permitted direction.
|
||||
|
||||
The private Agent Teams profile bundle depends on the Team packages and applies after `dsh-base`. It inserts the Team rows, disables the global continuable-child controls whose model-visible names overlap the Team tools, and leaves the shipped base, CLI, Web, and Python runtime dependency graphs unchanged.
|
||||
|
||||
Profile startup resolves selected bundles before healing module fallbacks. The shared fallback retains the dsh installation's carrier-specific entries: symlinks under plain Node and ESM proxies in a packaged executable. Missing packages from selected bundle closures are linked under the current profile's own `node_modules`, while pnpm-managed profile entries remain authoritative. Closure discovery starts from each explicit external bundle's real package directory and traverses every listed root even when an earlier dependency has the same package name. It excludes dsh-owned profile projections from later discovery, so a projected dependency cannot feed back into its own closure. Link ownership compares canonical parent paths so junction-normalized targets remain removable. A private profile layer can therefore carry experimental plugin rows without adding those plugins to a release app, requiring profile users to install transitive packages directly, weakening packaged-runtime module identity, or changing another profile's resolution.
|
||||
|
||||
Experimental status changes publication and compatibility expectations only. The packages retain the repository's ordinary documentation, invariant, lifecycle, security, unit, real-composition, and snapshot requirements. Promotion requires review of the public contracts, limitations, test evidence, release payload, runtime dependents, and a named owner accepting stable-package obligations.
|
||||
|
||||
## Alternatives considered
|
||||
@@ -30,6 +34,6 @@ Experimental status changes publication and compatibility expectations only. The
|
||||
|
||||
## Consequences
|
||||
|
||||
Agent Teams can use the full repository graph and quality checks without entering official tarballs or becoming a supported runtime dependency. A release package cannot expose Team until the Team packages are promoted, so CLI and Web experiments use explicit example or experimental compositions instead of the shipped base bundles.
|
||||
Agent Teams can use the full repository graph and quality checks without entering official tarballs or becoming a supported runtime dependency. A release package cannot expose Team until the Team packages are promoted, so the CLI experiment installs an explicit private profile layer instead of changing shipped bundles. The generic profile launcher accepts that layer without making its plugin dependencies part of the dsh release closure.
|
||||
|
||||
The product-role grouping is less direct while the packages incubate. Promotion creates path and npm-name churn as specified by the experimental package naming decision.
|
||||
|
||||
+6
-2
@@ -12,12 +12,16 @@ Agent Teams 的服务与工具约定仍在变化,但它需要使用真实 Sess
|
||||
|
||||
## 决策
|
||||
|
||||
`packages/experimental/agent-team` 与 `packages/experimental/tool-agent-team` 是私有 workspace 包。[实验性包命名决策](2026-08-19-experimental-package-name-prefix.zh.md)负责其 npm 名和 promotion 重命名;本记录负责其目录归属、发布排除与依赖隔离。
|
||||
`packages/experimental/agent-team`、`packages/experimental/tool-agent-team` 与 `packages/experimental/agent-team-profile` 是私有 workspace 包。[实验性包命名决策](2026-08-19-experimental-package-name-prefix.zh.md)负责其 npm 名和 promotion 重命名;本记录负责其目录归属、发布排除与依赖隔离。
|
||||
|
||||
dsh pack 与 publish 集合以及本地 baseline 发布器均排除 `packages/experimental/` 下的所有 manifest。`release:dsh` 仍会让这些 manifest 跟随 dsh 共享版本递增,但不会创建发布 tag。workspace 约束要求每个实验性包设置 `private: true` 并省略 `publishConfig`。同一个顶层检查会拒绝发布包、发布 app 或 Python runtime 通过 `dependencies`、`optionalDependencies` 或 `peerDependencies` 依赖实验性包。实验性包可以依赖发布包和其他实验性包;测试可以通过 `devDependencies` 使用它们,示例可以显式加载它们。
|
||||
|
||||
通用的调用方预留 continuable child 身份和精确 direct-child drain 仍属于稳定 Subagent 服务。它们负责 Subagent 身份与 Activation 生命周期,不 import 或命名 Agent Teams;实验性 Team 服务沿允许的方向消费这些能力。
|
||||
|
||||
私有 Agent Teams profile bundle 依赖 Team 包,并应用在 `dsh-base` 之后。它插入 Team 配置行,禁用模型可见名称与 Team 工具重叠的全局 continuable-child control,并保持已发布 base、CLI、Web 与 Python runtime 的依赖图不变。
|
||||
|
||||
profile 启动会先解析所选 bundle,再修复模块 fallback。共享 fallback 保留 dsh 安装的载体专用条目:普通 Node 下使用 symlink,打包 executable 中使用 ESM proxy。仅由所选 bundle 闭包携带的缺失包会链接到当前 profile 自己的 `node_modules` 下,而 pnpm 管理的 profile 条目仍具有优先权。闭包发现从每个显式外部 bundle 的真实包目录开始;即使前一个依赖具有相同包名,也会遍历所有列出的根。后续发现会排除 dsh 所有的 profile projection,避免投影后的依赖重新进入自己的闭包。link ownership 通过规范化父路径比较,使 junction 规范化后的 target 仍可删除。因此,私有 profile 层可以携带实验性 plugin 配置行,而无需把这些 plugin 加入发布 app、要求 profile 用户直接安装传递依赖、破坏 packaged-runtime 的模块身份,或改变其他 profile 的解析结果。
|
||||
|
||||
实验性状态只改变发布与兼容性预期。这些包仍须满足仓库的一般文档、不变式、生命周期、安全、单元测试、真实组合测试和快照要求。promotion 前必须评审公开约定、限制、测试证据、发布 payload、运行时依赖方,并由一名具名 owner 接受稳定包义务。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
@@ -30,6 +34,6 @@ dsh pack 与 publish 集合以及本地 baseline 发布器均排除 `packages/ex
|
||||
|
||||
## 后果
|
||||
|
||||
Agent Teams 可以使用完整仓库依赖图与质量检查,而不进入正式 tarball,也不会成为受支持的运行时依赖。在 Team 包 promotion 前,发布包不能暴露 Team,因此 CLI 和 Web 实验使用显式示例或实验性组合,而不是已发布的基础组合包。
|
||||
Agent Teams 可以使用完整仓库依赖图与质量检查,而不进入正式 tarball,也不会成为受支持的运行时依赖。在 Team 包 promotion 前,发布包不能暴露 Team,因此 CLI 实验会安装显式的私有 profile 层,而不是修改已发布 bundle。通用 profile launcher 可以接受该层,而不会让它的 plugin 依赖进入 dsh 发布闭包。
|
||||
|
||||
孵化期间的产品职责分组不够直接。promotion 会按照实验性包命名决策产生路径和 npm 名改动。
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-18-sqlite-physical-chunk-row-compression.md
|
||||
2026-08-18-sqlite-physical-chunk-row-compression.md: e46adf26ab4ce0a495f3509977ab0835631c16a9
|
||||
2026-08-18-sqlite-physical-chunk-row-compression.zh.md: d93aa64a53effa9d456b3eba2e1681b9478c448d
|
||||
2026-08-18-sqlite-physical-chunk-row-compression.md: 34aac2f183d386ffe22f86a6b62fe5e3105b3dfa
|
||||
2026-08-18-sqlite-physical-chunk-row-compression.zh.md: 1845185d543f565b55ace6adac973dad5535ad7b
|
||||
|
||||
+8
-8
@@ -12,15 +12,15 @@ A physical row that represents several events affects append contiguity, crash r
|
||||
|
||||
## Decision
|
||||
|
||||
`@deepseek-ai/dsh-session-persistence-sqlite` uses the packed schema-17 implementation. It is the only SQLite persistence package and provider; the predecessor scalar layout and the temporary versioned sibling are not retained. SQLite remains an opt-in switch, while shipped default compositions continue to use JSONL. Both backends implement the same `SessionPersistence` service through `PersistenceCoordinator`, so physical packing changes neither live event delivery nor the logical session API.
|
||||
`@deepseek-ai/dsh-session-persistence-sqlite` uses the packed schema-18 implementation. It is the only SQLite persistence package and provider; the predecessor scalar layout and the temporary versioned sibling are not retained. SQLite remains an opt-in switch, while shipped default compositions continue to use JSONL. Both backends implement the same `SessionPersistence` service through `PersistenceCoordinator`, so physical packing changes neither live event delivery nor the logical session API.
|
||||
|
||||
Schema 17 keeps ordinary ROWID tables and the composite `events(session_id, seq)` primary-key index. Scalar rows represent one logical event. Packed rows use the storage tags `text-chunks`, `reasoning-chunks`, and `tool-call-chunks`; the SQL `seq` and `time` columns hold the first logical member, and `data` holds the packed payload. Packed rows set `ignorable=0` as a physical discriminator and leave `source_event_seqs` and `surface_op` as `NULL`; scalar rows use `ignorable=1` only for logical ignorable events and `NULL` otherwise. A future ignorable logical event may therefore reuse a storage-tag name without being decoded as a packed row. The tags are storage vocabulary, not `SessionEventMap` members.
|
||||
Schema 18 keeps ordinary ROWID tables and the composite `events(session_id, seq)` primary-key index. Scalar rows represent one logical event. Packed rows use the storage tags `text-chunks`, `reasoning-chunks`, and `tool-call-chunks`; the SQL `seq` and `time` columns hold the first logical member, and `data` holds the packed payload. Packed rows set `is_packed=1`, while scalar rows set `is_packed=0`; the explicit discriminator prevents a scalar event whose type matches a storage tag from being decoded as packed. The tags are storage vocabulary, not `SessionEventMap` members.
|
||||
|
||||
SQLite owns chunk encoding and validation inside the schema-17 package. Exact-field whitelisting means unknown fields, surface metadata, incompatible chunk identity, sequence gaps, and unsafe timestamps remain scalar rather than losing information. One packed row represents at most 1,024 events and 1 MiB of uncompressed UTF-8 `data`; the encoder partitions longer runs, and the decoder rejects rows outside those format limits.
|
||||
SQLite owns chunk encoding and validation inside the schema-18 package. Exact-field whitelisting means unknown fields, surface metadata, incompatible chunk identity, sequence gaps, and unsafe timestamps remain scalar rather than losing information. One packed row represents at most 1,024 events and 1 MiB of uncompressed UTF-8 `data`; the encoder partitions longer runs, and the decoder rejects rows outside those format limits.
|
||||
|
||||
The `data` column accepts `TEXT` or `BLOB`. Serialized values below 4 KiB remain text. At or above the threshold, the writer uses Zstandard level 3 and retains the frame only when it is smaller than the text; the reader decompresses the blob before strict UTF-8 decoding and JSON parsing. The fixed moderate level and threshold limit frame overhead and synchronous CPU work while capturing the repeated payloads that dominate retained bytes.
|
||||
|
||||
`source_event_seqs` remains the complete ordered list of earlier events cited by a surface node, including every streamed chunk behind an assembled assistant message. Schema 17 stores the first sequence as an unsigned varint and every subsequent signed difference as a ZigZag varint. This preserves arbitrary order and every sequence while exploiting the overwhelmingly consecutive lists produced by streaming. An empty list is an empty non-null blob, distinct from absent provenance.
|
||||
`source_event_seqs` remains the complete ordered list of earlier events cited by a surface node, including every streamed chunk behind an assembled assistant message. Schema 18 stores the first sequence as an unsigned varint and every subsequent signed difference as a ZigZag varint. This preserves arbitrary order and every sequence while exploiting the overwhelmingly consecutive lists produced by streaming. An empty list is an empty non-null blob, distinct from absent provenance.
|
||||
|
||||
### Transactional append packing
|
||||
|
||||
@@ -32,11 +32,11 @@ Normal append never deletes or replaces an earlier event row. Fixed write-behind
|
||||
|
||||
Full reads decode each physical row as one all-or-nothing logical span and validate contiguous logical sequences. A reverse pass identifies the last valid `turn/end` without retaining a second decoded copy of the full physical scan; the forward pass decodes one row at a time into the required logical result. A malformed row or gap before that committed boundary is corruption; a malformed final physical row becomes the opaque repair marker at that row's base sequence. Recovery re-reads and validates that marker while holding the write lock, then deletes the whole physical row and any later rows before binding synthetic closers as scalar events. A stale repair cannot delete a newer writer's valid suffix.
|
||||
|
||||
`readFrom(id, fromSeq)` examines packed predecessors only within the maximum schema-17 row span, then reads from the earliest candidate that may contain `fromSeq`. The decoder filters reconstructed members below `fromSeq`, so a suffix may begin inside a packed row without parsing an unrelated earlier scalar row. Reading from that candidate also exposes an overlapping scalar row to contiguity validation instead of letting it hide the packed member. Packed data exceeding the uncompressed format byte limit rejects before JSON parsing.
|
||||
`readFrom(id, fromSeq)` examines packed predecessors only within the maximum schema-18 row span, then reads from the earliest candidate that may contain `fromSeq`. The decoder filters reconstructed members below `fromSeq`, so a suffix may begin inside a packed row without parsing an unrelated earlier scalar row. Reading from that candidate also exposes an overlapping scalar row to contiguity validation instead of letting it hide the packed member. Packed data exceeding the uncompressed format byte limit rejects before JSON parsing.
|
||||
|
||||
### Schema ownership
|
||||
|
||||
A pristine database initializes at schema 17. Older physical schemas, foreign application identities, non-pristine unversioned databases, and incompatible schema objects reject; the pre-release package supplies no migration. Every connection disables trusted schemas and memory-mapped I/O before inspecting durable schema, then reads both settings back. After selecting and verifying the journal mode, the provider pins `synchronous=FULL` and verifies it so SQLite build defaults cannot weaken committed-append durability. Package code loads every statement and fixed pragma from closed-name `.sql` resources and binds runtime values as parameters.
|
||||
A pristine database initializes at schema 18. Older physical schemas, foreign application identities, non-pristine unversioned databases, and incompatible schema objects reject; the pre-release package supplies no migration. Every connection disables trusted schemas and memory-mapped I/O before inspecting durable schema, then reads both settings back. After selecting and verifying the journal mode, the provider pins `synchronous=FULL` and verifies it so SQLite build defaults cannot weaken committed-append durability. Package code loads every statement and fixed pragma from closed-name `.sql` resources and binds runtime values as parameters.
|
||||
|
||||
### Physical-write regression
|
||||
|
||||
@@ -58,11 +58,11 @@ The repository regression guard writes 1,000 streamed deltas in 40-event durable
|
||||
|
||||
**Compress every payload.** Rejected because small independent Zstandard frames add headers and synchronous CPU work while losing the cross-record dictionary opportunity of a whole-file stream. On the 105-session comparison corpus, a threshold sweep produced 75.01 MB at 4 KiB, versus 93.87 MB at 16 KiB and 60.92 MB at 1 KiB. The writer fixes level 3 rather than inheriting a library default, matching the moderate level used by [Codex cold-rollout compression](https://github.com/openai/codex/blob/main/codex-rs/rollout/src/compression.rs) while retaining independent row access.
|
||||
|
||||
The final frozen comparison used 105 sessions, 2,507,860 logical events, 512-event durable batches, three independent builds per backend, and three read passes per build. SQLite used 75.01 MB, wrote in 8.58 s, read complete sessions at 3.95/21.58 ms p50/p95, read 50-event tails at 0.253/0.378 ms, and forked every session in 13.10 s. Zstandard JSONL used 30.65 MB and measured 28.21 s, 4.49/23.36 ms, 10.58/80.90 ms, and 14.48 s. The predecessor scalar SQLite layout used 709.57 MB and measured 10.64 s, 9.02/69.16 ms, 0.189/0.293 ms, and 19.30 s. The packed layout is 89.4% smaller than the predecessor, writes 19.4% faster, improves complete-read p50/p95 by 56.2%/68.8%, and reduces 2,507,860 physical event rows to 65,810. Scalar tail-50 and list micro-latency are lower, but the packed provider remains materially faster than JSONL on those paths and wins the dominant size, write, full-read, and fork costs. The 4 KiB threshold is the accepted balance rather than a strict dominance claim.
|
||||
The final frozen comparison used 105 sessions, 2,507,860 logical events, 512-event durable batches, three independent builds per backend, and three read passes per build. SQLite used 75.01 MB, wrote in 8.58 s, read complete sessions at 3.95/21.58 ms p50/p95, read 50-event tails at 0.253/0.378 ms, and forked every session in 13.10 s. Zstandard JSONL used 30.65 MB and measured 28.21 s, 4.49/23.36 ms, 10.58/80.90 ms, and 14.48 s. The predecessor scalar SQLite layout used 709.57 MB and measured 10.64 s, 9.02/69.16 ms, 0.189/0.293 ms, and 19.30 s. The packed layout is 89.4% smaller than the predecessor, writes 19.4% faster, improves complete-read p50/p95 by 56.2%/68.8%, and reduces 2,507,860 physical event rows to 65,810. Scalar tail-50 and list micro-latency are lower, but the packed provider remains materially faster than JSONL on those paths and wins the dominant size, write, full-read, and fork costs. The 4 KiB threshold is the accepted balance rather than a strict dominance claim. This comparison measured schema 17; schema 18 retains the chunk codec and bounds but changes the row discriminator, so the exact size and timing values remain schema-17 evidence until schema 18 is remeasured.
|
||||
|
||||
**Store packed payloads under the logical `assistant/chunk` type.** Rejected because payload heuristics make malformed rows ambiguous and couple physical decoding to future logical payload fields. Explicit tags fail loudly.
|
||||
|
||||
**Store `SessionHeader` fields in an extensible metadata blob.** Rejected for schema 17 because `agentPreset` is a typed core resume invariant shared by JSONL and SQLite, not provider extension metadata. Persisting validated core fields directly keeps both backends aligned; an untyped catch-all would add another compatibility mechanism without a current producer. Revisit this only with a core-owned, namespaced `SessionHeader` extension protocol implemented by every backend.
|
||||
**Store `SessionHeader` fields in an extensible metadata blob.** Rejected for schema 18 because `agentPreset` is a typed core resume invariant shared by JSONL and SQLite, not provider extension metadata. Persisting validated core fields directly keeps both backends aligned; an untyped catch-all would add another compatibility mechanism without a current producer. Revisit this only with a core-owned, namespaced `SessionHeader` extension protocol implemented by every backend.
|
||||
|
||||
**Expose compression rules through configuration or a live registry.** Rejected because same-version databases must be readable independently of runtime topology. The codec is modular source code, but the durable rule set is fixed by schema version.
|
||||
|
||||
|
||||
+8
-8
@@ -12,15 +12,15 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
`@deepseek-ai/dsh-session-persistence-sqlite` 使用打包后的 schema 17 实现。它是唯一的 SQLite 持久化包和提供方;仓库不保留此前的标量布局与临时版本化同级包。SQLite 仍是可选开关,随产品交付的默认组合继续使用 JSONL。两个后端都通过 `PersistenceCoordinator` 实现同一 `SessionPersistence` 服务,因此物理打包既不改变实时事件投递,也不改变逻辑会话 API。
|
||||
`@deepseek-ai/dsh-session-persistence-sqlite` 使用打包后的 schema 18 实现。它是唯一的 SQLite 持久化包和提供方;仓库不保留此前的标量布局与临时版本化同级包。SQLite 仍是可选开关,随产品交付的默认组合继续使用 JSONL。两个后端都通过 `PersistenceCoordinator` 实现同一 `SessionPersistence` 服务,因此物理打包既不改变实时事件投递,也不改变逻辑会话 API。
|
||||
|
||||
Schema 17 保留普通 ROWID 表以及复合主键索引 `events(session_id, seq)`。标量行表示一个逻辑事件。打包行使用存储标签 `text-chunks`、`reasoning-chunks` 与 `tool-call-chunks`;SQL 的 `seq` 和 `time` 列保存第一个逻辑成员,`data` 保存打包 payload。打包行把 `ignorable=0` 用作物理判别值,并让 `source_event_seqs` 与 `surface_op` 保持 `NULL`;标量行仅在逻辑事件可忽略时使用 `ignorable=1`,否则使用 `NULL`。因此,未来的可忽略逻辑事件即使复用了某个存储标签名称,也不会被解码为打包行。这些标签属于存储词汇,而不是 `SessionEventMap` 成员。
|
||||
Schema 18 保留普通 ROWID 表以及复合主键索引 `events(session_id, seq)`。标量行表示一个逻辑事件。打包行使用存储标签 `text-chunks`、`reasoning-chunks` 与 `tool-call-chunks`;SQL 的 `seq` 和 `time` 列保存第一个逻辑成员,`data` 保存打包 payload。打包行设置 `is_packed=1`,标量行设置 `is_packed=0`;显式判别值可防止类型与存储标签同名的标量事件被解码为打包行。这些标签属于存储词汇,而不是 `SessionEventMap` 成员。
|
||||
|
||||
SQLite 在 schema 17 包内拥有分片编码和验证。字段完全匹配的白名单意味着未知字段、surface 元数据、不兼容的分片身份、序列缺口和不安全时间戳仍保持标量表示,不会丢失信息。一个打包行最多表示 1,024 个事件和 1 MiB 未压缩 UTF-8 `data`;编码器会分割更长的连续段,解码器则拒绝超出这些格式上限的行。
|
||||
SQLite 在 schema 18 包内拥有分片编码和验证。字段完全匹配的白名单意味着未知字段、surface 元数据、不兼容的分片身份、序列缺口和不安全时间戳仍保持标量表示,不会丢失信息。一个打包行最多表示 1,024 个事件和 1 MiB 未压缩 UTF-8 `data`;编码器会分割更长的连续段,解码器则拒绝超出这些格式上限的行。
|
||||
|
||||
`data` 列接受 `TEXT` 或 `BLOB`。序列化值小于 4 KiB 时保持为文本。达到或超过该阈值时,写入方使用 Zstandard level 3,并且只在 frame 小于原文本时保留该 frame;读取方会先解压,再进行严格 UTF-8 解码和 JSON 解析。固定的适中级别与阈值限制 frame 开销与同步 CPU 工作,同时覆盖占据大部分保留字节的重复 payload。
|
||||
|
||||
`source_event_seqs` 是 surface 节点引用的早期事件的完整有序列表,包括组装后的 assistant 消息背后的每个流式分片。Schema 17 把第一个序列存为无符号 varint,把后续每个有符号差值存为 ZigZag varint。这样既能保留任意顺序和每个序列,又能利用流式处理所产生的绝大多数连续列表。空列表表示为空的非 `NULL` blob,与不存在来源区分开来。
|
||||
`source_event_seqs` 是 surface 节点引用的早期事件的完整有序列表,包括组装后的 assistant 消息背后的每个流式分片。Schema 18 把第一个序列存为无符号 varint,把后续每个有符号差值存为 ZigZag varint。这样既能保留任意顺序和每个序列,又能利用流式处理所产生的绝大多数连续列表。空列表表示为空的非 `NULL` blob,与不存在来源区分开来。
|
||||
|
||||
### 事务化追加打包
|
||||
|
||||
@@ -32,11 +32,11 @@ SQLite 在 schema 17 包内拥有分片编码和验证。字段完全匹配的
|
||||
|
||||
完整读取把每个物理行解码为全有或全无的逻辑范围,并验证逻辑序列连续。反向扫描会定位最后一个有效 `turn/end`,但不会保留完整物理扫描的第二份解码副本;正向扫描则逐行解码并写入必需的逻辑结果。在该已提交边界之前出现的畸形行或缺口属于损坏;畸形最终物理行则以该行的起始序列作为不透明修复标记。恢复会在持有写锁时重新读取并验证该 marker,再删除整个物理行及其后所有行,然后把合成 closers 绑定为标量事件。陈旧修复无法删除较新写入方的有效后缀。
|
||||
|
||||
`readFrom(id, fromSeq)` 只检查 schema 17 最大行跨度内的打包前驱,再从可能包含 `fromSeq` 的最早候选项开始读取。解码器会过滤重建后序列小于 `fromSeq` 的成员,因此后缀可以从打包行内部开始,而无需解析无关的更早标量行。从该候选项开始读取,还会让连续性验证看到相互重叠的标量行,而不是让它隐藏打包成员。打包数据超出未压缩格式字节上限时,会在解析 JSON 前拒绝。
|
||||
`readFrom(id, fromSeq)` 只检查 schema 18 最大行跨度内的打包前驱,再从可能包含 `fromSeq` 的最早候选项开始读取。解码器会过滤重建后序列小于 `fromSeq` 的成员,因此后缀可以从打包行内部开始,而无需解析无关的更早标量行。从该候选项开始读取,还会让连续性验证看到相互重叠的标量行,而不是让它隐藏打包成员。打包数据超出未压缩格式字节上限时,会在解析 JSON 前拒绝。
|
||||
|
||||
### Schema 所有权
|
||||
|
||||
全新数据库初始化为 schema 17。旧物理 schema、外部 application identity、非空未版本化数据库以及不兼容 schema 对象都会被拒绝;该预发布提供方不提供迁移。每个连接都会在检查持久 schema 前禁用可信 schema 和内存映射 I/O,然后读回这两项设置。选择并验证 journal mode 后,提供方会把 `synchronous` 固定为 `FULL` 并验证该设置,避免 SQLite 构建默认值削弱已提交追加的持久性。包代码通过封闭名称的 `.sql` 资源加载每条语句和固定 pragma,并把运行时值作为参数绑定。
|
||||
全新数据库初始化为 schema 18。旧物理 schema、外部 application identity、非空未版本化数据库以及不兼容 schema 对象都会被拒绝;该预发布提供方不提供迁移。每个连接都会在检查持久 schema 前禁用可信 schema 和内存映射 I/O,然后读回这两项设置。选择并验证 journal mode 后,提供方会把 `synchronous` 固定为 `FULL` 并验证该设置,避免 SQLite 构建默认值削弱已提交追加的持久性。包代码通过封闭名称的 `.sql` 资源加载每条语句和固定 pragma,并把运行时值作为参数绑定。
|
||||
|
||||
### 物理写入回归
|
||||
|
||||
@@ -58,11 +58,11 @@ SQLite 在 schema 17 包内拥有分片编码和验证。字段完全匹配的
|
||||
|
||||
**压缩每个 payload。** 不予采用,因为小型独立 Zstandard frame 会增加 header 和同步 CPU 工作,也无法利用整文件流的跨记录字典。在 105 个会话的对比语料上,阈值扫描结果为:4 KiB 生成 75.01 MB,16 KiB 为 93.87 MB,1 KiB 为 60.92 MB。写入方固定使用 level 3,而不是继承库默认值;这与 [Codex 冷 rollout 压缩](https://github.com/openai/codex/blob/main/codex-rs/rollout/src/compression.rs)所用的适中级别一致,同时保留独立行访问。
|
||||
|
||||
最终冻结对比包含 105 个会话、2,507,860 个逻辑事件,以 512 个事件为持久批次;每个后端独立构建三次,每次构建执行三轮读取。SQLite 使用 75.01 MB,写入耗时 8.58 秒,完整读取 p50/p95 为 3.95/21.58 毫秒,读取最后 50 个事件为 0.253/0.378 毫秒,对所有会话执行 fork 为 13.10 秒。Zstandard JSONL 使用 30.65 MB,对应指标为 28.21 秒、4.49/23.36 毫秒、10.58/80.90 毫秒和 14.48 秒。此前的标量 SQLite 布局使用 709.57 MB,对应指标为 10.64 秒、9.02/69.16 毫秒、0.189/0.293 毫秒和 19.30 秒。打包布局比此前布局小 89.4%,写入快 19.4%,完整读取 p50/p95 改善 56.2%/68.8%,并把 2,507,860 个物理事件行减少到 65,810 行。标量布局的最后 50 个事件读取与 list 微延迟更低,但打包提供方在这些路径上仍明显快于 JSONL,并改善主要的空间、写入、完整读取和 fork 成本。4 KiB 阈值是接受的平衡点,而不是严格支配所有指标的结论。
|
||||
最终冻结对比包含 105 个会话、2,507,860 个逻辑事件,以 512 个事件为持久批次;每个后端独立构建三次,每次构建执行三轮读取。SQLite 使用 75.01 MB,写入耗时 8.58 秒,完整读取 p50/p95 为 3.95/21.58 毫秒,读取最后 50 个事件为 0.253/0.378 毫秒,对所有会话执行 fork 为 13.10 秒。Zstandard JSONL 使用 30.65 MB,对应指标为 28.21 秒、4.49/23.36 毫秒、10.58/80.90 毫秒和 14.48 秒。此前的标量 SQLite 布局使用 709.57 MB,对应指标为 10.64 秒、9.02/69.16 毫秒、0.189/0.293 毫秒和 19.30 秒。打包布局比此前布局小 89.4%,写入快 19.4%,完整读取 p50/p95 改善 56.2%/68.8%,并把 2,507,860 个物理事件行减少到 65,810 行。标量布局的最后 50 个事件读取与 list 微延迟更低,但打包提供方在这些路径上仍明显快于 JSONL,并改善主要的空间、写入、完整读取和 fork 成本。4 KiB 阈值是接受的平衡点,而不是严格支配所有指标的结论。该对比测量 schema 17;schema 18 保留分片 codec 与上限,但改变行判别值,因此在重新测量 schema 18 前,精确的大小与时延值仍是 schema 17 证据。
|
||||
|
||||
**把打包 payload 存在逻辑 `assistant/chunk` 类型下。** 不予采用,因为 payload 启发式判断会使畸形行产生歧义,并把物理解码耦合到未来逻辑 payload 字段。显式标签会明确失败。
|
||||
|
||||
**把 `SessionHeader` 字段存入可扩展元数据 blob。** Schema 17 不采用该方案,因为 `agentPreset` 是 JSONL 与 SQLite 共同使用的强类型核心恢复不变量,而不是提供方扩展元数据。直接持久化已校验的核心字段可使两个后端保持一致;在没有当前生产方的情况下加入无类型兜底字段,只会增加另一套兼容机制。只有核心层定义由所有后端实现、带命名空间的 `SessionHeader` 扩展协议后,才应重新考虑该方案。
|
||||
**把 `SessionHeader` 字段存入可扩展元数据 blob。** Schema 18 不采用该方案,因为 `agentPreset` 是 JSONL 与 SQLite 共同使用的强类型核心恢复不变量,而不是提供方扩展元数据。直接持久化已校验的核心字段可使两个后端保持一致;在没有当前生产方的情况下加入无类型兜底字段,只会增加另一套兼容机制。只有核心层定义由所有后端实现、带命名空间的 `SessionHeader` 扩展协议后,才应重新考虑该方案。
|
||||
|
||||
**通过配置或实时注册表暴露压缩规则。** 不予采用,因为同一版本数据库必须能独立于运行时拓扑被读取。Codec 在源码层保持模块化,但持久规则集由 schema 版本固定。
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.md
|
||||
2026-08-19-projection-cache-per-session-files.md: 9e102e786a6c06d82d1a0f45cc2f96a50c8abcd8
|
||||
2026-08-19-projection-cache-per-session-files.zh.md: d875c3f57800936f66fbf65233637df9bf300e2d
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Agent Note: Projection cache as per-session files
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-19-projection-cache-per-session-files.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The persisted projection cache was one global `session_projcache.json` — a `sessions` table in a single file at the storage root. Every throttled checkpoint rewrote the whole file containing every session's rows, so write amplification grew with session count, and one malformed file took the entire cache down at once.
|
||||
|
||||
## Decision
|
||||
|
||||
The cache opens the `session_projcache` storage domain in the new `per-record` layout, added to the json backend: one version-stamped document per session at `<root>/session_projcache/sessions/<id>.json`, owned by the storage stack — `storage` / `storage-json` / `storage-domain` live in the shared base bundle alongside the cache, and the cache itself is a plain domain consumer again. Every shipped base-backed profile keeps the cache enabled, so the session producer records checkpoints independently of whether its current application exposes a listing interface; `sdk-minimal`, which does not use the base bundle, remains outside this composition. The cache never consults the persistence layer: no `locate`, no dependency on which backend is mounted.
|
||||
|
||||
Reads and writes share ONE coherent state: every read (`cachedSnapshot`) is a synchronous lookup in the domain's in-memory tables (zero I/O), and every write queues on the domain's per-unit write chain, mutating memory only after durability — no direct disk reads that could lag the throttled writes. The cache keeps every other responsibility: checkpoint fold, write policy (turn/end + disposal mandatory, count/interval throttle), fail-soft durability, and the listing read. `cachedSnapshot(meta)` is synchronous. The cache runs no cold-refold ladder (that would require reading the session log, which belongs to the persistence layer); a consumer that needs a guaranteed cold snapshot refolds from the log itself. The json backend creates its tree owner-only (`0o700`).
|
||||
|
||||
## Consequences
|
||||
|
||||
- Per-session write isolation: each throttled write replaces only that session's small document, removing the global write amplification. The domain write chain serializes writes, so a newer cut never lands before an older one; domain close drains in-flight writes.
|
||||
- Listing is a synchronous in-memory read; a session without a record document simply lacks the projection column.
|
||||
- ACP, headless, SDK, and Web sessions publish cache rows for later consumers. The log-leading durability barrier may flush a covered prefix at the cache cadence and split otherwise coalesced physical JSONL runs; recorded profile snapshots re-pack the logical event stream so cache timing does not define fixture layout.
|
||||
- The per-record contract scopes failure: a malformed or stale-version document reads as an absent record at open, so one bad file never bricks the cache, and a checkpoint schema bump discards stale sessions per record instead of rejecting the whole domain.
|
||||
- The json backend bootstraps the per-record tree from the legacy whole-unit cache only when enumeration finds no new-layout document path. Any new document path, including an unreadable or stale file, suppresses the bootstrap for the whole unit; missing session rows refold from the log. The legacy file remains untouched.
|
||||
- The cache record is bound to the same log lifecycle as before: the stored `{createdAt, cwd}` identity guards against a recreated id.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Keep the global sessions table.** Preserves one-load listing, but keeps the global write amplification and single-file blast radius that motivated the change.
|
||||
- **Cache-owned per-session files** (`<root>/<session-id>/projection_cache.json`, the first revision of this change). Tried and reverted in review: the cache hand-rolled the medium — paths, per-path write chains, in-flight tracking, owner-only file modes, and a sqlite no-path special case — and its listing read hit the disk directly on every call while writes were throttled, so reads and writes were never consistent.
|
||||
- **Resolve the path through `sessionPersistence.locate(meta)`** (the file beside the session log). Rejected: the cache would have to guess "beside the log" from a log artifact path (`dirname` + fixed filename), coupling the cache to the persistence service and to a backend's layout.
|
||||
- **Make `per-record` a mode of the existing unit instead of a separate unit class.** Rejected: the two layouts have genuinely different state models — `single` is memory-authoritative with whole-file publish, `per-record` is stateless (the directory is the state; `loadAll` re-reads the tree) — so they are separate small classes behind one backend, with record keys validated path-safe instead of encoded.
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Agent Note:投影缓存改为每会话文件
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-19-projection-cache-per-session-files.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
持久投影缓存曾是单个全局 `session_projcache.json`——存储根目录下一个文件里的 `sessions` 表。每次节流检查点都会重写包含所有会话行的整个文件,写放大随会话数量增长;且一个畸形文件会让整个缓存一起失效。
|
||||
|
||||
## Decision
|
||||
|
||||
缓存打开采用新增 `per-record` 布局的 `session_projcache` 存储域:每个会话一个带版本戳的文档,位于 `<root>/session_projcache/sessions/<id>.json`,介质归存储栈所有——`storage` / `storage-json` / `storage-domain` 与缓存一起落在共享 base 装配里,缓存重新变回纯粹的域消费方。所有随附且基于 base 的 profile 都保持启用缓存,因此会话生产方会记录检查点,不取决于当前应用是否提供列表接口;不使用 base 组合包的 `sdk-minimal` 不在此装配范围内。缓存绝不咨询持久化层:没有 `locate`、不依赖挂载的是哪个后端。
|
||||
|
||||
读写共享同一份一致状态:每次读取(`cachedSnapshot`)都是对域内存表的同步查找(零 I/O);每次写入排进该域的单条写链,先落盘成功才改内存——不再有落后于节流写入的直读磁盘。缓存保留其余全部职责:检查点折叠、写策略(turn/end + dispose 强制点、count/interval 节流)、fail-soft 持久化与列表读。`cachedSnapshot(meta)` 是同步的。缓存不运行冷重折叠阶梯(那需要读取会话日志,属于持久化层的职责);需要保证冷快照的消费方自行从日志重折叠。json 后端以仅属主权限(`0o700`)创建自己的目录树。
|
||||
|
||||
## Consequences
|
||||
|
||||
- 每会话写入隔离:每次节流写入只替换该会话的小文档,消除全局写放大。域写链将写入串行化,新切面绝不会先于旧切面落盘;域关闭时会排空在途写入。
|
||||
- 列表读取是同步内存读;没有记录文档的会话只是缺少投影列。
|
||||
- ACP、headless、SDK 与 Web 会话都会发布缓存行,供后续消费方使用。确保日志领先的持久性屏障可能按缓存节奏 flush 已覆盖的前缀,并拆分原本会合并的物理 JSONL 行;各 profile 的录制快照会重新 pack 逻辑事件流,因此缓存时序不会决定 fixture 布局。
|
||||
- per-record 契约把故障范围缩小到单记录:畸形或过期版本的文档在打开时读作"无此记录",单个坏文件不会拖垮整个缓存;检查点 schema 升级按会话丢弃过期行,而不是拒绝整个域。
|
||||
- json 后端仅在枚举时没有发现任何新布局文档路径,才从旧整单元缓存引导 per-record 目录树。只要存在任意新文档路径,即使文件不可读或版本陈旧,也会对整个单元禁用引导;缺失的会话行从日志重折叠。旧文件保持不变。
|
||||
- 缓存记录仍绑定同一日志生命周期:存储的 `{createdAt, cwd}` 身份防止被重建的 id 误导。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **保留全局 sessions 表。** 保留一次加载式列表,但保留了促成此改动的全局写放大与单文件爆炸半径。
|
||||
- **缓存自持的每会话文件**(`<root>/<session-id>/projection_cache.json`,本改动的第一版)。试过并在评审中回退:缓存手搓了介质——路径、按路径的写链、在途跟踪、仅属主文件权限,以及 sqlite 无路径特判——而且它的列表读每次调用都直读磁盘、写却在节流,读写永不一致。
|
||||
- **经 `sessionPersistence.locate(meta)` 解析路径**(文件放在会话日志旁)。未采用:缓存得从日志 artifact 路径"猜"日志旁边(`dirname` + 固定文件名),把缓存耦合到持久化服务与后端的布局。
|
||||
- **把 `per-record` 做成既有单元的一种模式而非独立单元类。** 未采用:两种布局的状态模型本质不同——`single` 内存权威、整文件发布;`per-record` 无状态(目录即状态,`loadAll` 重扫目录树)——所以它们是同一后端下的两个小型独立类,记录键做路径安全校验而非编码。
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/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: a8e89c2e989680a1d1bf98f0cd2817b5900d36d0
|
||||
2026-07-31-resume-selector-batch-projection.md: 387d05e055c2b90f3aa7ee39d624c117ba54b4b1
|
||||
2026-07-31-resume-selector-batch-projection.zh.md: febd744b3f7ec58dab94f5d8437feaa270dfffcf
|
||||
|
||||
@@ -12,7 +12,7 @@ Opening the TUI `/resume` selector called `sessionQuery.readSession()` once per
|
||||
|
||||
Selector rows fold nothing but titles, and everything else a row shows comes from metadata:
|
||||
|
||||
- Titles come from the projection system: `session-title` already registers a `title` unit, so a live row reads the registry snapshot, a persisted row reads the durable checkpoint row (`sessionProjectionCache.cachedSnapshot`, zero I/O), and only a row without a usable checkpoint pays a `coldSnapshot` — checkpoint plus a `readFrom` tail, written back so the next scan is zero-I/O. Cold reads are bounded by the TUI `resumeScanConcurrency` config. A composition without the cache falls back to one bounded `readTitleSnapshots` batch over the logs; either path isolates a per-row failure into the disabled "Unreadable session" fallback.
|
||||
- Titles come from the projection system: `session-title` already registers a `title` unit, so a live row reads the registry snapshot, a persisted row reads the durable checkpoint row (`sessionProjectionCache.cachedSnapshot`, one file read per session), and only a row without a usable checkpoint pays a `coldSnapshot` — checkpoint plus a `readFrom` tail, written back so the next scan is zero-I/O. Cold reads are bounded by the TUI `resumeScanConcurrency` config. A composition without the cache falls back to one bounded `readTitleSnapshots` batch over the logs; either path isolates a per-row failure into the disabled "Unreadable session" fallback.
|
||||
- The activity timestamp never reads a log: a live session uses its last in-memory event time; a persisted session stats the artifact named by the optional `sessionPersistence.locate()` (mtime), falling back to the header's creation time when the backend locates no per-session artifact (SQLite) or the stat fails. Any append moves the mtime, so a mere pickup boundary now floats a browsed session up — accepted as the price of a metadata-only timestamp.
|
||||
- The last-turn label, provider/model route, and goal phase columns are gone from rows. Route availability is now enforced by the Enter-time preflight, which fully reads and replay-validates the one chosen log through `readSession` before handoff.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
选择器行除标题外不折叠任何内容,行内其余信息全部来自元数据:
|
||||
|
||||
- 标题来自投影系统:`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`,每会话一次文件读取),只有没有可用 checkpoint 的行才付出一次 `coldSnapshot`——checkpoint 加 `readFrom` 尾部折叠,并写回使下次扫描每会话一次文件读取。冷读取受 TUI `resumeScanConcurrency` 配置约束。未挂载缓存的组合回退到一次对日志的有界 `readTitleSnapshots` 批量读取;两条路径都把单行失败隔离为禁用的「Unreadable session」回退。
|
||||
- 活动时间戳从不读取日志:实时会话取内存中最后一个事件的时间;持久化会话对可选 `sessionPersistence.locate()` 命名的产物做 stat(mtime),当后端定位不到按会话的产物(SQLite)或 stat 失败时回退到 header 的创建时间。任何追加都会移动 mtime,因此仅仅一次 pickup 边界也会让浏览过的会话上浮——这是元数据时间戳的代价,予以接受。
|
||||
- 行内不再有最后轮次标签、提供方/模型路由和目标阶段列。路由可用性改由 Enter 时的预检强制:预检通过 `readSession` 完整读取并回放验证选中的那一份日志后才移交。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-22-acp-subagent-backend.md
|
||||
2026-06-22-acp-subagent-backend.md: 41a60bc8e73cbda1d382226acfbd9c0a4b146fa6
|
||||
2026-06-22-acp-subagent-backend.zh.md: 3d6f98f3c168603591da5d1293adfe52463dc8d5
|
||||
2026-06-22-acp-subagent-backend.md: 85129ec6a5b1c5607ef3a89e6ebaf6cbfb29e7c9
|
||||
2026-06-22-acp-subagent-backend.zh.md: ed0b3488a91782cc1249f65e95935e2f7c00e9c3
|
||||
|
||||
@@ -30,7 +30,7 @@ The child's working directory is an explicit resolution, never the harness proce
|
||||
|
||||
### StopReason mapping
|
||||
|
||||
ACP `StopReason` → harness `SubagentStopReason`: `end_turn`→`completed`, `max_tokens`→`max-tokens`, `refusal`→`refusal`, `cancelled`→`aborted`, `max_turn_requests`→`error` (no clean equivalent — the task did not finish), unknown→`error`. A spawn/transport/RPC failure resolves `error` (or `aborted` if a cancel was requested); `result` never rejects on a child-level failure, per the seam contract.
|
||||
ACP `StopReason` → harness `SubagentStopReason`: `end_turn`→`completed`, `max_tokens`→`max-tokens`, `refusal`→`refusal`, `cancelled`→`aborted`, `max_turn_requests`→`error` (no clean equivalent — the task did not finish), unknown→`error`. Spawn, initialize, and session-creation failures reject `start()` before publication after provider-owned cleanup; prompt/RPC/transport failures after publication settle `result` as `error` (or `aborted` after local cancellation), and `result` never rejects on a child-level failure. Non-completed and lifecycle failures add only the bounded provider stage, coarse category, closed permission decision, and observed process facts defined by the [out-of-process diagnostics decision](2026-08-21-out-of-process-subagent-minimal-diagnostics.md); raw ACP errors and stderr remain Host-only.
|
||||
|
||||
### Security: scrubbed child environment
|
||||
|
||||
@@ -41,7 +41,8 @@ The child is a separate process, so it inherits an environment. Credential-shape
|
||||
- **Keyless unit/integration:** A scripted ACP subprocess exercises real stdio for prompt/output flow, every stop-reason mapping, signal and disposal cancellation (including pre-abort, pre-session race, and torn-pipe cases), both permission policies, ignored non-message updates, missing-command cleanup, provider reload, and namespace exports.
|
||||
- **Keyless Loader composition:** A test-only cordis.yml boots the stdio app through the real Loader with the backend's `cwd` omitted; a scripted model delegates once and the scripted child proves it ran in — and was announced — the parent session's workspace (the cwd-inheritance branch end to end).
|
||||
- **With-key e2e:** The backend spawns the real ACP example; its model answers `PONG`, writes `proof.txt`, and the parent verifies the file.
|
||||
- **Snapshot gap:** Each ACP child is a separate process with its own replay session, unlike in-process per-session replay. Deterministic mock-server coverage exists, while `TODO(acp-subagent-replay)` tracks parent replay against a replaying child.
|
||||
- **Keyless snapshot:** The ACP example boots the real provider and scripted child through Loader-backed replay, pinning foreground and one-shot background diagnostics while keeping the child process, permission decision, partial output, and cleanup lifecycle deterministic.
|
||||
- **Snapshot gap:** Each ACP child still has its own replay session; `TODO(acp-subagent-replay)` continues to track parent replay against a replaying child harness rather than the scripted protocol child used by the diagnostic scenario.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ subagent seam([seam Agent Note](2026-06-21-subagent-capability-seam.zh.md))
|
||||
|
||||
### StopReason 映射
|
||||
|
||||
ACP `StopReason` → harness `SubagentStopReason`:`end_turn`→`completed`、`max_tokens`→`max-tokens`、`refusal`→`refusal`、`cancelled`→`aborted`、`max_turn_requests`→`error`(无对等语义,任务未完成)、未知→`error`。spawn/传输/RPC 失败时,结果为 `error`(如果已请求取消则为 `aborted`);按 seam 约定,`result` 在子 agent 级别失败时从不 reject。
|
||||
ACP `StopReason` → harness `SubagentStopReason`:`end_turn`→`completed`、`max_tokens`→`max-tokens`、`refusal`→`refusal`、`cancelled`→`aborted`、`max_turn_requests`→`error`(无对等语义,任务未完成)、未知→`error`。spawn、initialize 与会话创建失败会在提供方自有清理后、发布前拒绝 `start()`;发布后的 prompt/RPC/传输失败会把 `result` 确定为 `error`(本地取消后为 `aborted`),而 `result` 在子 agent 级别失败时绝不 reject。非完成结果与生命周期失败只会附加[进程外诊断决策](2026-08-21-out-of-process-subagent-minimal-diagnostics.zh.md)定义的有界 provider stage、粗粒度 category、闭集权限决定和已观测进程事实;原始 ACP 错误与 stderr 仍只留在 Host。
|
||||
|
||||
### 安全:清洗子进程环境
|
||||
|
||||
@@ -41,7 +41,8 @@ ACP `StopReason` → harness `SubagentStopReason`:`end_turn`→`completed`、`
|
||||
- **无需密钥的单元/集成测试:** 一个脚本化的 ACP 子进程通过真实 stdio 测试提示词输入/输出流程、所有 stop-reason 映射、信号与 dispose 取消(包括 pre-abort、会话前竞态和管道断裂场景)、两种权限策略、被忽略的非消息更新、命令缺失时的清理、提供方重载以及命名空间导出。
|
||||
- **无需密钥的 Loader 组合测试:** 仅用于测试的 cordis.yml 通过真实 Loader 启动 stdio 应用,并省略后端的 `cwd`;脚本化模型委派一次,脚本化子进程则证明它在父会话工作区中运行,且 ACP 也对外公布了该工作区,从而端到端覆盖 cwd 继承分支。
|
||||
- **需要密钥的 e2e 测试:** 后端 spawn 真实的 ACP 示例;其模型回答 `PONG`,写入 `proof.txt`,父进程验证该文件。
|
||||
- **快照缺口:** 每个 ACP 子 agent 是独立进程,拥有自己的回放会话,不同于进程内的按会话回放。已有确定性 mock 服务器覆盖;`TODO(acp-subagent-replay)` 跟踪父进程对回放中子 agent 的回放支持。
|
||||
- **无密钥快照:** ACP 示例通过 Loader 支持的回放启动真实提供方与脚本化子进程,固定前台和一次性后台诊断,同时保持子进程、权限决定、部分输出与清理生命周期确定。
|
||||
- **快照缺口:** 每个 ACP 子 agent 仍拥有自己的回放会话;`TODO(acp-subagent-replay)` 继续跟踪父进程对回放中子 harness 的回放,而不是诊断场景使用的脚本化协议子进程。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md
|
||||
2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: 8bfd6654e449bb762b153a1b0b950a504cf25a4b
|
||||
2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: f59126f70fb0b1a88ff874840a49993d4dc2f988
|
||||
2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: c410e7ffadb448bee08c9a3e296ba71aae679f2d
|
||||
2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: 13b7a498574a3d72808d24fb284da95ee00912a1
|
||||
|
||||
+8
-8
@@ -12,20 +12,20 @@ The stdio JSON-RPC serving surface (`@deepseek-ai/dsh-sdk-jsonrpc-server`, the [
|
||||
|
||||
Three packages, layered exactly like the existing Python stack, plus one Service Provider registration:
|
||||
|
||||
- **`@deepseek-ai/dsh-sdk-protocol`** (`packages/sdk/protocol/`) — the wire made shared and nominal. `JsonRpcLineTransport` moves here verbatim from `dsh-sdk-jsonrpc-server` (which now imports it), and `types.ts` names every payload the server speaks: `InitializeParams/Result`, `SessionPromptParams/Result`, the four notification payloads, and the `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` indexes. The package root explicitly exports that complete interface and provides no source-module deep imports. The server's `notify()` call sites are typed against these named payloads, so server drift breaks compilation, not clients. One behavioral change: an error response now rejects with `JsonRpcResponseError` carrying the wire `code`/`data` (the Python client already preserved these; the old transport threw a bare `Error` with only the message).
|
||||
- **`@deepseek-ai/dsh-sdk-client`** (`packages/sdk/client/`) — the TypeScript twin of `python/sdk`: `HarnessClient` (spawn, frame, fan out notifications, typed error surfaces, close-to-quiescence via the shared dispose ladder) under `DeepSeekHarness`/`HarnessSession` (lazy start, memoized `initialize`, `run()` pairing one `session/prompt` with its `session.finished`). Its package-root consumer interface explicitly exports both client layers, caller-facing types, and the protocol-owned `JsonRpcResponseError`; source modules, normalization helpers, and the notification producer stay internal. `RunResult.events` contains only the root session's typed events, while `notifications` retains session ids across the root and descendants discovered from `subagent.started`; session-tree scoping is client-side, mirroring `client.py`. The launch interface resolves the same-version `@deepseek-ai/dsh` dependency and selects a named profile, with optional `dshBin`, ordered patches, an explicit Harness home, process cwd, environment, and timeouts; arbitrary command/argv launch remains an internal fake-runtime adapter. A clean checkout without `lib/bin.js` uses that package's source entry through an absolute `tsx/esm` loader and an internal patch that omits build-generated Typert contribution loading, which the SDK protocol does not consume. `env` replaces rather than merges and is read when `start()` spawns, so callers own credential policy and can finish preparing it before first use. `RunResult` carries the structured `reason` (Python exposes only `status`); teardown walks a private stdin-EOF → SIGTERM → SIGKILL ladder to actual exit (the client runs outside any harness context, so it cannot ride `ctx.subprocess`).
|
||||
- **`@deepseek-ai/dsh-subagent-dsh-sdk`** (`packages/subagent/subagent-dsh-sdk/`) — the second out-of-process `SubagentProvider`, structured as `subagent-acp`'s sibling: same all-false capabilities and `inheritsParentContext: false`, same publish-after-handshake ownership transaction, same result-never-rejects flattening through an `onError` sink, same parent-namespace run id. The child answer is read from streamed `session.event`s — the last complete `assistant/message`, else accumulated `text-delta` chunks, so partial answers survive cancellation. Stop reasons map from the child's structured `TurnEndReason` (`completed`/`max-tokens`/`aborted` pass through; everything else, including a settled-without-turn child, is `error`). Its `dshBin`/profile/patch/home config selects an isolated SDK application, `provider`/`model` feeds the child's `initialize`, and `env` supplies explicit child-only values such as its API key.
|
||||
- **`@deepseek-ai/dsh-sdk-protocol`** (`packages/sdk/protocol/`) — the wire made shared and nominal. `JsonRpcLineTransport` lives here, and `types.ts` names every payload the server speaks: `InitializeParams/Result`, `SessionPromptParams/Result`, the four notification payloads, and the `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` indexes. `InitializeParams` carries provider, model, optional adapter-owned reasoning effort, and optional output cap. The package root explicitly exports that complete interface and provides no source-module deep imports. The server's `notify()` call sites are typed against these named payloads, so server drift breaks compilation, not clients. Error responses reject with `JsonRpcResponseError` carrying the wire `code`/`data`, matching the Python client.
|
||||
- **`@deepseek-ai/dsh-sdk-client`** (`packages/sdk/client/`) — the TypeScript twin of `python/sdk`: `HarnessClient` (spawn, frame, fan out notifications, typed error surfaces, close-to-quiescence via the shared dispose ladder) under `DeepSeekHarness`/`HarnessSession` (lazy start, memoized `initialize`, and `run()` pairing one `session/prompt` with its owned activity). Its package-root consumer interface explicitly exports both client layers, caller-facing types, and the protocol-owned `JsonRpcResponseError`; source modules, normalization helpers, and the notification producer stay internal. `RunResult.events` contains only the root session's typed events, while `notifications` retains session ids across the root and descendants discovered from `subagent.started`; session-tree scoping is client-side, mirroring `client.py`. The result carries the final root-session assistant text but no prompt-level status or turn reason. The launch interface resolves the same-version `@deepseek-ai/dsh` dependency and selects a named profile, with optional `dshBin`, ordered patches, an explicit Harness home, process cwd, environment, and timeouts; arbitrary command/argv launch remains an internal fake-runtime adapter. `initialize` carries provider, model, optional reasoning effort, and optional output cap. A clean checkout without `lib/bin.js` uses that package's source entry through an absolute `tsx/esm` loader and an internal patch that omits build-generated Typert contribution loading, which the SDK protocol does not consume. `env` replaces rather than merges and is read when `start()` spawns, so callers own credential policy and can finish preparing it before first use. A failed handshake installs a fresh client after successful cleanup so a later call retries with a new process; if initialization and SDK-owned cleanup both fail, `start()` rejects with an ordered `AggregateError` and retains the failed client rather than spawning beside a process whose exit was not proved. Teardown walks a private stdin-EOF → SIGTERM → SIGKILL ladder to actual exit because the client runs outside any harness context.
|
||||
- **`@deepseek-ai/dsh-subagent-dsh-sdk`** (`packages/subagent/subagent-dsh-sdk/`) — the second out-of-process `SubagentProvider`, structured as `subagent-acp`'s sibling but advertising `agentOptions: true`: each run merges provider/model/reasoning/maxTokens over instance defaults and sends only those fields through the child `initialize`. Other start capabilities remain false, and `inheritsParentContext: false`. The provider retains the publish-after-handshake ownership transaction, result-never-rejects flattening through an `onError` sink, and parent-namespace run id. The child answer is read from streamed `session.event`s — the last complete `assistant/message`, else accumulated `text-delta` chunks, so partial answers survive cancellation. Stop reasons map from the child's structured `TurnEndReason`: `completed`, `max-tokens`, and ordinary `aborted` pass through; `blocked` becomes `refusal`; other non-completed values become `error`. Reachable child failures and SDK errors add the bounded safe diagnostic defined by the [out-of-process diagnostics decision](2026-08-21-out-of-process-subagent-minimal-diagnostics.md), using one category plus the current provider stage. Its `dshBin`/profile/patch/home config selects an isolated SDK application, while `env` supplies explicit child-only values such as its API key.
|
||||
- **The subagent seam grows `out-of-process.ts`**: the provider-side vocabulary both out-of-process backends share — `NO_START_CAPABILITIES`, timing-bound validation, child cwd resolution (config override, else the delegating parent session's workspace), the never-reject `settleRunResult`, and the `subprocessRunHandle` publication. Process mechanics (spawn, env scrub, tree-scoped teardown) live in the `dsh-subprocess` seam; `subagent-acp` spawns through `ctx.subprocess`, while this backend spawns through the SDK client (the subprocess README's documented exception for SDK-managed transports) and applies the seam's `scrubbedParentEnv()` itself.
|
||||
|
||||
`dsh-sdk-jsonrpc-server` keeps serving unchanged (the wire is byte-identical). TypeScript and Python clients both consume the shared protocol through `dsh --profile sdk`; the Python wheel packages that CLI and its closed dependency tree.
|
||||
`dsh-sdk-jsonrpc-server` validates the exact provider/model/effort route during `initialize`, stores only explicitly supplied effort and token values, and creates every SDK root Agent from that fixed process-wide route. Because JSON-RPC requests can dispatch concurrently, it rejects `session/prompt` until one initialization has completed successfully, preventing pending or invalid routes from falling back to constructor defaults. TypeScript and Python clients both expose the same initialization fields through `dsh --profile sdk`; the Python wheel packages that CLI and its closed dependency tree.
|
||||
|
||||
## Testing
|
||||
|
||||
Four tiers, per [testing policy](../../../../docs/testing.md):
|
||||
|
||||
- **Keyless unit** — `sdk-client` drives a scripted fake runtime (`tests/fake-runtime.ts`, env-scripted, protocol-only — the Python `test_client.py` pattern) over real stdio; `subagent-dsh-sdk` drives the same fake through the real provider. 100% per-file coverage on all three packages.
|
||||
- **Keyless Loader composition** — `subagent-dsh-sdk/tests/loader-composition.e2e.ts` boots its package-owned test composition (`packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/`), where the child is a real second `dsh --profile sdk` runtime with its own isolated home and ordered patch; asserts the parent tool result and the child's own persisted transcript both carry the parent session's cwd.
|
||||
- **Keyless snapshot** — `snapshots/sdk/sdk.snapshot.ts` drives the real `dsh --profile sdk` runtime through the real `dsh-sdk-client`, replaying recorded fixtures through an ordered `llm-replay` patch. Each scenario pins the normalized notification stream, SDK turn result, and persisted parent and child logs. This also closes the protocol-tier gap the single-exe note's Python-side snapshot left on the vitest side.
|
||||
- **Keyless unit** — `sdk-client` drives a scripted fake runtime (`tests/fake-runtime.ts`, env-scripted, protocol-only — the Python `test_client.py` pattern) over real stdio; `subagent-dsh-sdk` drives the same fake through the real provider, covering per-run route overrides, reachable child reasons, typed errors, and initialize/session-run/shutdown diagnostics. 100% per-file coverage on all three packages.
|
||||
- **Keyless Loader composition** — `subagent-dsh-sdk/tests/loader-composition.e2e.ts` boots its package-owned test composition (`packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/`), where the child is a real second `dsh --profile sdk` runtime with its own isolated home and ordered patch; the tool result and persisted request header prove provider, model, reasoning effort, maxTokens, and parent-session cwd, while the failure case pins a model-visible child-error diagnostic separately from partial output.
|
||||
- **Keyless snapshot** — `snapshots/sdk/sdk.snapshot.ts` drives the real `dsh --profile sdk` runtime through the real `dsh-sdk-client`, replaying recorded fixtures through ordered `llm-replay` patches. One DSH SDK scenario pins a model-selected route through the delegation tool, a second SDK runtime, and the child's persisted request header; another pins the normalized notification stream, SDK result, persisted log, and foreground/background failure text for safe diagnostics.
|
||||
- **With-key e2e** — the snapshot suite's `DSH_SNAPSHOT=record` mode is the live-API path (it produced the committed fixtures); the composition e2e needs no key by design.
|
||||
|
||||
## Alternatives considered
|
||||
@@ -44,6 +44,6 @@ Four tiers, per [testing policy](../../../../docs/testing.md):
|
||||
|
||||
## Consequences
|
||||
|
||||
**Bought**: the SDK runtime protocol has named, compiler-checked types shared by its server and both client SDKs; TypeScript consumers get the same subprocess-driving capability Python has, with typed errors, structured turn reasons, and package roots that expose only caller-owned operations; the subagent seam has a harness-native out-of-process backend whose children are full peers (own config, persistence, tools); the SDK profile has snapshot coverage through the SDK path itself.
|
||||
**Bought**: the SDK runtime protocol has named, compiler-checked types shared by its server and both client SDKs; TypeScript consumers get the same subprocess-driving capability Python has, with typed errors, structured turn reasons, and package roots that expose only caller-owned operations; the subagent seam has a harness-native out-of-process backend whose children are full peers (own config, persistence, tools), and parent agents receive minimal safe child/SDK failure facts through the same SDK path; the SDK profile pins both successful and failed delegation behavior through the SDK path itself.
|
||||
|
||||
**Paid**: a third package in the `sdk/` group and a fourth subagent backend to keep current; the SDK backend boots a complete plugin tree per child (heavier per-run than an ACP child; pooling remains future work, same as ACP); the wire still has no cancel method, so both the SDK's `RequestTimeoutError` and the backend's dispose settle locally while the server-side turn runs on until process teardown; fixtures for the snapshot suite were recorded against `deepseek-v4-flash` and re-record on model-behavior drift like every other recorded corpus.
|
||||
|
||||
+8
-8
@@ -12,20 +12,20 @@ stdio JSON-RPC 对外服务接口(`@deepseek-ai/dsh-sdk-jsonrpc-server`,见[
|
||||
|
||||
三个包,分层与既有 Python 栈完全一致,外加一个 Service Provider 注册:
|
||||
|
||||
- **`@deepseek-ai/dsh-sdk-protocol`**(`packages/sdk/protocol/`)—— 把线协议做成共享且具名。`JsonRpcLineTransport` 从 `dsh-sdk-jsonrpc-server` 原样移入(后者现在导入它),`types.ts` 为服务器所说的每个载荷命名:`InitializeParams/Result`、`SessionPromptParams/Result`、四个通知载荷,以及 `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` 索引。该包根显式导出这一完整接口,且不提供指向源模块的深层导入。服务器的 `notify()` 调用点以这些具名载荷标注类型,服务器漂移会先破坏编译而不是破坏客户端。一处行为变化:错误响应现在以携带线上 `code`/`data` 的 `JsonRpcResponseError` 拒绝(Python 客户端本就保留这些;旧传输只抛携带消息的裸 `Error`)。
|
||||
- **`@deepseek-ai/dsh-sdk-client`**(`packages/sdk/client/`)—— `python/sdk` 的 TypeScript 孪生:`HarnessClient`(spawn、分帧、通知扇出、有类型的错误表面、经共享 dispose(资源释放)阶梯关闭至完全停稳)之上是 `DeepSeekHarness`/`HarnessSession`(惰性启动、记忆化 `initialize`、`run()` 把一个 `session/prompt` 与其 `session.finished` 配对)。其包根消费方接口显式导出两层客户端、面向调用方的类型,以及协议包所拥有的 `JsonRpcResponseError`;源模块、规范化辅助函数和通知投递端都保留为内部实现。`RunResult.events` 只包含根会话的类型化事件,而 `notifications` 则保留根会话及从 `subagent.started` 发现的后代各自的会话 id;基于 `subagent.started` 血缘边的会话树范围限定在客户端完成,镜像 `client.py`。启动接口解析同版本 `@deepseek-ai/dsh` 依赖并选择具名 profile,可选配置包括 `dshBin`、有序 patch、显式 Harness home、进程 cwd、环境和超时;任意 command/argv 启动只作为内部 fake-runtime 适配器。干净 checkout 中若不存在 `lib/bin.js`,client 会通过绝对 `tsx/esm` loader 使用该包的源码入口,并应用一个省略构建期生成 Typert 贡献加载的内部 patch;SDK 协议不消费这些贡献。`env` 整体替换而非合并,并在 `start()` spawn 时读取,因此凭据策略归调用方,且调用方可在首次使用前完成环境准备。`RunResult` 携带结构化 `reason`(Python 只暴露 `status`);拆除走私有的 stdin-EOF → SIGTERM → SIGKILL 阶梯直到真正退出(client 运行在任何 harness 上下文之外,无法搭乘 `ctx.subprocess`)。
|
||||
- **`@deepseek-ai/dsh-subagent-dsh-sdk`**(`packages/subagent/subagent-dsh-sdk/`)—— 第二个进程外 `SubagentProvider`,采用与 `subagent-acp` 对等的结构:同样的全 false 能力与 `inheritsParentContext: false`,同样的握手后发布所有权事务,同样通过 `onError` sink 将结果归一为绝不拒绝,同样的父命名空间 run id。子答案从流式 `session.event` 读取——最后一条完整 `assistant/message`,否则累积的 `text-delta` 块,部分答案在取消时得以保留。停止原因由子进程的结构化 `TurnEndReason` 映射(`completed`/`max-tokens`/`aborted` 直通;其余一切、包括未运行任何轮次便已结束的子进程,都是 `error`)。其 `dshBin`/profile/patch/home 配置选择隔离的 SDK 应用,`provider`/`model` 写入子进程 `initialize`,`env` 则提供子进程专用的显式值,例如其 API key。
|
||||
- **`@deepseek-ai/dsh-sdk-protocol`**(`packages/sdk/protocol/`)—— 把协议格式做成共享且具名。`JsonRpcLineTransport` 位于此处,`types.ts` 为服务器所说的每个载荷命名:`InitializeParams/Result`、`SessionPromptParams/Result`、四个通知载荷,以及 `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` 索引。`InitializeParams` 携带提供方、模型、可选且由适配器持有的推理强度,以及可选输出上限。该包根显式导出完整接口,且不提供指向源模块的深层导入。服务器的 `notify()` 调用点以这些具名载荷标注类型,服务器漂移会先破坏编译而不是破坏客户端。错误响应以携带协议 `code`/`data` 的 `JsonRpcResponseError` 拒绝,与 Python 客户端一致。
|
||||
- **`@deepseek-ai/dsh-sdk-client`**(`packages/sdk/client/`)—— `python/sdk` 的 TypeScript 孪生:`HarnessClient`(spawn、分帧、通知扇出、有类型的错误表面、经共享 dispose(资源释放)阶梯关闭至完全停稳)之上是 `DeepSeekHarness`/`HarnessSession`(惰性启动、记忆化 `initialize`,以及让一次 `run()` 与其自有 `session/prompt` 活动配对)。其包根消费方接口显式导出两层客户端、面向调用方的类型,以及协议包所拥有的 `JsonRpcResponseError`;源模块、规范化辅助函数和通知投递端都保留为内部实现。`RunResult.events` 只包含根会话的类型化事件,而 `notifications` 则保留根会话及从 `subagent.started` 发现的后代各自的会话 id;会话树范围限定在客户端完成,镜像 `client.py`。结果携带根会话最终的助手文本,但不包含提示词级状态或轮次原因。启动接口解析同版本 `@deepseek-ai/dsh` 依赖并选择具名 profile,可选配置包括 `dshBin`、有序 patch、显式 Harness home、进程 cwd、环境和超时;任意 command/argv 启动只作为内部 fake-runtime 适配器。`initialize` 携带提供方、模型、可选推理强度与可选输出上限。干净 checkout 中若不存在 `lib/bin.js`,client 会通过绝对 `tsx/esm` loader 使用该包的源码入口,并应用一个省略构建期生成 Typert 贡献加载的内部 patch;SDK 协议不消费这些贡献。`env` 整体替换而非合并,并在 `start()` spawn 时读取,因此凭据策略归调用方,且调用方可在首次使用前完成环境准备。握手失败但清理成功时,实例会换入全新 client,使后续调用通过新进程重试;若初始化与 SDK 自有清理均失败,`start()` 会以有序 `AggregateError` 拒绝并保留失败的 client,而不会在尚未证明原进程退出时并排 spawn 新进程。拆除走私有的 stdin-EOF → SIGTERM → SIGKILL 阶梯直到真正退出,因为 client 运行在任何 harness 上下文之外。
|
||||
- **`@deepseek-ai/dsh-subagent-dsh-sdk`**(`packages/subagent/subagent-dsh-sdk/`)—— 第二个进程外 `SubagentProvider`,采用与 `subagent-acp` 对等的结构,但声明 `agentOptions: true`:每次运行都会把提供方/模型/推理强度/maxTokens 合并到实例默认值之上,并且只把这些字段送入子进程 `initialize`。其他启动能力保持 false,`inheritsParentContext: false`。提供方保留握手后发布所有权事务、通过 `onError` sink 将结果归一为绝不拒绝,以及父命名空间 run id。子答案从流式 `session.event` 读取——最后一条完整 `assistant/message`,否则累积的 `text-delta` 块,部分答案在取消时得以保留。停止原因由子进程的结构化 `TurnEndReason` 映射:`completed`、`max-tokens` 与普通 `aborted` 直通,`blocked` 变为 `refusal`,其他非完成值变为 `error`。可达子失败与 SDK 错误会附加[进程外诊断决策](2026-08-21-out-of-process-subagent-minimal-diagnostics.zh.md)定义的有界安全诊断,只使用一个 category 和当前提供方 stage。其 `dshBin`/profile/patch/home 配置选择隔离的 SDK 应用,`env` 则提供子进程专用的显式值,例如其 API key。
|
||||
- **subagent seam 新增 `out-of-process.ts`**:两个进程外后端共享的 provider 侧词汇——`NO_START_CAPABILITIES`、时限校验、子进程 cwd 解析(配置覆盖、否则发起委托的父会话工作区)、绝不拒绝的 `settleRunResult`、以及 `subprocessRunHandle` 发布。进程机制(spawn、环境清理、进程树清理)属于 `dsh-subprocess` seam;`subagent-acp` 经 `ctx.subprocess` spawn 子进程,本后端则经 SDK 客户端 spawn 子进程(subprocess README 记载的 SDK 托管传输例外)并自行应用该 seam 的 `scrubbedParentEnv()`。
|
||||
|
||||
`dsh-sdk-jsonrpc-server` 的服务不变(协议字节完全一致)。TypeScript 与 Python 客户端都通过 `dsh --profile sdk` 消费共享协议;Python wheel 会打包该 CLI 及其封闭依赖树。
|
||||
`dsh-sdk-jsonrpc-server` 会在 `initialize` 期间校验确切的提供方/模型/推理强度路由,只保存显式提供的推理强度与 token 值,并使用这条固定的进程级路由创建每个 SDK 根 Agent。由于 JSON-RPC 请求可能并发分派,它会在一次初始化成功完成前拒绝 `session/prompt`,避免待定或非法路由回退到构造期默认值。TypeScript 与 Python 客户端都通过 `dsh --profile sdk` 公开同一组初始化字段;Python wheel 会打包该 CLI 及其封闭依赖树。
|
||||
|
||||
## 测试
|
||||
|
||||
四层,依[测试政策](../../../../docs/testing.zh.md):
|
||||
|
||||
- **免密钥单元**——`sdk-client` 通过真实 stdio 驱动脚本化伪运行时(`tests/fake-runtime.ts`,环境变量脚本化、纯协议——即 Python `test_client.py` 的模式);`subagent-dsh-sdk` 经真实提供方驱动同一伪运行时。三个包全部 100% 逐文件覆盖。
|
||||
- **免密钥 Loader 组合**——`subagent-dsh-sdk/tests/loader-composition.e2e.ts` 启动包自有测试组合(`packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/`),其中子进程是真实的第二个 `dsh --profile sdk` 运行时,拥有独立 home 与有序 patch;断言父工具结果与子进程自己持久化的 transcript(文本记录)都携带父会话 cwd。
|
||||
- **免密钥快照**——`snapshots/sdk/sdk.snapshot.ts` 通过真实 `dsh-sdk-client` 驱动真实 `dsh --profile sdk` 运行时,并通过有序 `llm-replay` patch 回放已录制 fixture(测试前置数据)。每个场景都钉住规范化通知流、SDK 轮次结果,以及持久化的父日志与子日志。这也补上了单文件可执行 Note 的 Python 侧快照在 vitest 侧留下的协议层缺口。
|
||||
- **免密钥单元**——`sdk-client` 通过真实 stdio 驱动脚本化伪运行时(`tests/fake-runtime.ts`,环境变量脚本化、纯协议——即 Python `test_client.py` 的模式);`subagent-dsh-sdk` 经真实提供方驱动同一伪运行时,覆盖逐次路由覆盖、可达子原因、typed 错误,以及 initialize/session-run/shutdown 诊断。三个包全部 100% 逐文件覆盖。
|
||||
- **免密钥 Loader 组合**——`subagent-dsh-sdk/tests/loader-composition.e2e.ts` 启动包自有测试组合(`packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/`),其中子进程是真实的第二个 `dsh --profile sdk` 运行时,拥有独立 home 与有序 patch;工具结果与持久化请求 header 会证明提供方、模型、推理强度、maxTokens 与父会话 cwd,失败场景则固定与部分输出分离的模型可见子错误诊断。
|
||||
- **免密钥快照**——`snapshots/sdk/sdk.snapshot.ts` 通过真实 `dsh-sdk-client` 驱动真实 `dsh --profile sdk` 运行时,并通过有序 `llm-replay` patch 回放已录制 fixture(测试前置数据)。一个 DSH SDK 场景把模型选择的路由固定在委派工具、第二个 SDK 运行时及子级持久化请求 header 中;另一个场景固定安全诊断的规范化通知流、SDK 结果、持久日志与前台/后台失败文本。
|
||||
- **带密钥 e2e**——快照套件的 `DSH_SNAPSHOT=record` 模式即真实 API 路径(已提交 fixture 由它产出);组合 e2e 设计上无需密钥。
|
||||
|
||||
## 考虑过的替代方案
|
||||
@@ -44,6 +44,6 @@ stdio JSON-RPC 对外服务接口(`@deepseek-ai/dsh-sdk-jsonrpc-server`,见[
|
||||
|
||||
## 后果
|
||||
|
||||
**收益**:SDK 运行时协议拥有服务器与两个客户端 SDK 共享的、编译器校验的具名类型;TypeScript 消费方获得与 Python 相同的子进程驱动能力,且带类型化错误与结构化轮次原因,包根也只暴露归调用方所有的操作;subagent seam 拥有一个 harness 原生的进程外后端,其子进程是完整对等体(自有配置、持久化、工具);SDK profile 通过 SDK 路径本身获得快照覆盖。
|
||||
**收益**:SDK 运行时协议拥有服务器与两个客户端 SDK 共享的、编译器校验的具名类型;TypeScript 消费方获得与 Python 相同的子进程驱动能力,且带类型化错误与结构化轮次原因,包根也只暴露归调用方所有的操作;subagent seam 拥有一个 harness 原生的进程外后端,其子进程是完整对等体(自有配置、持久化、工具),父 agent 还能经同一 SDK 路径收到最小安全的子轮次/SDK 失败事实;SDK profile 通过 SDK 路径本身同时固定成功与失败的委派行为。
|
||||
|
||||
**代价**:`sdk/` 组多了第三个包、subagent 多了第四个要保持最新的后端;SDK 后端每个子进程启动完整插件树(单次成本高于 ACP 子进程;池化与 ACP 一样留作未来工作);协议仍无取消方法,SDK 的 `RequestTimeoutError` 与后端的 dispose 都只在本地结算、服务器侧轮次会继续运行到进程清理为止;快照 fixture 录制于 `deepseek-v4-flash`,与其他录制语料一样随模型行为漂移而重录。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-28-sdk-max-output-tokens.md
|
||||
2026-07-28-sdk-max-output-tokens.md: 72a9e87484ca87e0a750f52d7e46db7aee436d21
|
||||
2026-07-28-sdk-max-output-tokens.zh.md: 820008ec7293cfee20c0a9c26037f746c9e081c2
|
||||
2026-07-28-sdk-max-output-tokens.md: 1d2915b7f7169b0784c648aad5900a85fac4c977
|
||||
2026-07-28-sdk-max-output-tokens.zh.md: ba59f745bc921d3cc0d5c01f83808dd495220bc7
|
||||
|
||||
@@ -14,7 +14,7 @@ The high-level SDKs expose one optional process-wide output cap: Python names it
|
||||
|
||||
Each SDK-created root Agent receives the cap through `AgentOptions.maxTokens`. Agent Loop places that value in the initial `LlmCallConfig`; final call preparation preserves the explicit value or materializes an exact-model adapter default, logs the effective cap in the request header, and reconstructs every dispatched conversation request from that durable header. Omitting the SDK option therefore allows the selected adapter or provider route default to apply.
|
||||
|
||||
In-process subagents inherit the parent's provider, model, and output cap. An explicit `SubagentStartRequest.agentOptions.maxTokens`, including one configured by `dsh-tool-subagent`, overrides the inherited value for that child and its descendants. Out-of-process providers own the configuration of their separate runtime; `subagent-dsh-sdk` therefore exposes its own optional `maxTokens` and forwards it through that child runtime's SDK handshake.
|
||||
In-process subagents inherit the parent's provider, model, and output cap. An explicit `SubagentStartRequest.agentOptions.maxTokens`, including one configured by `dsh-tool-subagent`, overrides the inherited value for that child and its descendants. `subagent-dsh-sdk` owns a separate runtime per run: request `maxTokens` overrides its optional instance default, and the resolved cap crosses that child runtime's SDK handshake.
|
||||
|
||||
Compaction, session-title generation, web search, and other auxiliary calls keep their independently owned output limits. `maxTokensAsSuccess` remains outcome mapping only: it does not set or alter the cap.
|
||||
|
||||
@@ -30,4 +30,4 @@ Compaction, session-title generation, web search, and other auxiliary calls keep
|
||||
|
||||
SDK callers can bound model output without editing Cordis composition, and direct Agent creation uses the same validated `AgentOptions` contract. The cap is visible in durable request headers and reaches provider adapters as `GenerateOptions.maxTokens`; DeepSeek serialization maps it to `max_tokens`.
|
||||
|
||||
One SDK runtime has one default cap. A caller needing different caps runs separate runtime instances or explicitly overrides an in-process child through its agent options. Reaching the cap still produces the existing `max-tokens` stop reason, whose `ok` or `error` mapping remains deployment policy.
|
||||
One SDK runtime has one default cap. A caller needing different caps runs separate runtime instances or uses a subagent provider that advertises `agentOptions`; DSH SDK naturally creates one such runtime per child run. Reaching the cap still produces the existing `max-tokens` stop reason, whose `ok` or `error` mapping remains deployment policy.
|
||||
|
||||
@@ -14,7 +14,7 @@ Python 与 TypeScript SDK 可以选择提供方和模型,却无法限制对话
|
||||
|
||||
每个由 SDK 创建的根 Agent 都通过 `AgentOptions.maxTokens` 获得该上限。agent loop(智能体循环)将它放入初始 `LlmCallConfig`;最终调用准备会保留显式值,或填入确切模型的适配器默认值,再将生效上限记录到请求 header,并从该持久化 header 重建每次分派的对话请求。因此,省略 SDK 选项时会应用所选适配器或提供方路由的默认值。
|
||||
|
||||
进程内 subagent 继承父级的提供方、模型和输出上限。显式的 `SubagentStartRequest.agentOptions.maxTokens`(包括通过 `dsh-tool-subagent` 配置的值)会覆盖该子级及其后代的继承值。进程外提供方自行持有其独立运行时的配置;因此 `subagent-dsh-sdk` 公开独立的可选 `maxTokens`,并通过该子运行时自己的 SDK 握手传入。
|
||||
进程内 subagent 继承父级的提供方、模型和输出上限。显式的 `SubagentStartRequest.agentOptions.maxTokens`(包括通过 `dsh-tool-subagent` 配置的值)会覆盖该子级及其后代的继承值。`subagent-dsh-sdk` 为每次运行持有独立运行时:请求 `maxTokens` 会覆盖可选的实例默认值,解析后的上限再经过该子运行时自己的 SDK 握手。
|
||||
|
||||
压缩、会话标题生成、网页搜索和其他辅助调用继续使用各自持有的独立输出上限。`maxTokensAsSuccess` 仍然只负责结果映射,不会设置或改变上限。
|
||||
|
||||
@@ -30,4 +30,4 @@ Python 与 TypeScript SDK 可以选择提供方和模型,却无法限制对话
|
||||
|
||||
SDK 调用方无需修改 Cordis 组合即可限制模型输出,直接创建 Agent 也使用同一套经过校验的 `AgentOptions` 约定。该上限在持久化请求 header 中可见,并以 `GenerateOptions.maxTokens` 到达提供方适配器;DeepSeek 序列化会将其映射为 `max_tokens`。
|
||||
|
||||
一个 SDK 运行时只有一个默认上限。需要不同上限的调用方应运行独立的运行时实例,或通过 agent options 显式覆盖某个进程内子级。达到上限时仍产生现有的 `max-tokens` 停止原因;将其映射为 `ok` 还是 `error` 仍由部署策略决定。
|
||||
一个 SDK 运行时只有一个默认上限。需要不同上限的调用方应运行独立的运行时实例,或使用声明 `agentOptions` 的 subagent 提供方;DSH SDK 会自然地为每次子级运行创建一个这样的运行时。达到上限时仍产生现有的 `max-tokens` 停止原因;将其映射为 `ok` 还是 `error` 仍由部署策略决定。
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.md
|
||||
2026-07-31-browser-derived-initial-locale.md: 66fd56327aeb4463bfb8f6426ce7f7962d339782
|
||||
2026-07-31-browser-derived-initial-locale.zh.md: 721a785aa476951e7254c50230ddc092b9f8b211
|
||||
2026-07-31-browser-derived-initial-locale.md: 28b5c98d5148854cc231e70064e91b21cd5c1184
|
||||
2026-07-31-browser-derived-initial-locale.zh.md: 34741af01a737b8ab9bb4394385a9140ace8f506
|
||||
|
||||
@@ -8,21 +8,21 @@ English | [中文](2026-07-31-browser-derived-initial-locale.zh.md)
|
||||
|
||||
The Settings Language row opened every first visit in Chinese: `LocaleRuntime` read `dsh.locale` from localStorage and fell straight back to `zh` when nothing was stored. The browser already states which languages its user reads — `navigator.languages` is that statement — and the app ignored it, so an English reader met a Chinese product and had to find a Chinese-labelled settings row to escape it. The fallback was doing two jobs at once: the last resort for an unresolvable locale, and the answer for every user who had simply never chosen.
|
||||
|
||||
Reading the browser fixed the readers whose browser names a language this app ships, but left the residual case wrong: a browser asking for neither `zh` nor `en` (`fr`, `de`) still fell back to `zh`. Those readers are the least likely to read Chinese.
|
||||
Reading the browser fixes readers whose browser names a registered language, but the product still needs a stable residual when the current catalog has no match. With only the built-in catalog, a browser asking for neither `zh` nor `en` (`fr`, `de`) reaches that case, and those readers are the least likely to read Chinese.
|
||||
|
||||
## Decision
|
||||
|
||||
**The provisional locale resolves through the browser, then `FALLBACK_LOCALE` (`en`); an explicit Host preference replaces it live.** `resolveInitialLocale()` in `packages/client/locale/src/client/index.ts` runs at service construction and expresses the browser/fallback order. The nonblocking settings lifecycle then applies optional `locale.preference` from `$DSH_HOME/settings.yaml`; absence leaves the browser-derived value active.
|
||||
**The provisional locale resolves through the browser, then `FALLBACK_LOCALE` (`en`); an explicit Host preference replaces it live.** `resolveInitialLocale()` in `packages/client/locale/src/client/index.ts` runs at service construction and after each language-catalog change, expressing the browser/fallback order over the definitions currently registered. The nonblocking settings lifecycle then applies optional `locale.preference` from `$DSH_HOME/settings.yaml`; absence leaves the browser-derived value active, while an unavailable saved id remains pending and takes effect if that language registers later.
|
||||
|
||||
**One constant serves both the opening locale and the dictionary fallback, because the dictionaries are symmetric.** `FALLBACK_LOCALE` answers both "which language does the UI open in when the browser names none we ship" and "which dictionary backs a key the active locale misses". Those are different questions, and splitting them into two constants would be right if either answer had to differ — but every shipped `zh`/`en` pair declares identical key sets, so the fallback step always resolves and both answers are `en`. The residual case points at English rather than zh because a browser naming neither shipped language is the reader least likely to read Chinese. `scripts/locale-dictionary-parity.spec.ts` gates the symmetry the shared constant depends on: a key added to one side only fails that spec by name, instead of surfacing later as a bare key such as `list.aria` in a running UI.
|
||||
**One constant serves both the opening residual and the dictionary-chain terminus.** `FALLBACK_LOCALE` answers both "which language does the UI open in when the browser names none registered" and "where must every declared dictionary fallback chain end". Those are different questions, and splitting them into two constants would be right if either answer had to differ. External languages may contribute partial dictionaries and declare intermediate fallbacks; every chain still reaches `en`. Every built-in `zh`/`en` pair declares identical key sets, so its final fallback resolves, while `scripts/locale-dictionary-parity.spec.ts` rejects a key added to only one built-in side instead of letting it surface later as a bare key such as `list.aria` in a running UI.
|
||||
|
||||
**Browser matching is on the primary subtag, over the ordered list.** `detectBrowserLocale()` walks `[...(navigator.languages ?? []), navigator.language]` and returns the first entry whose primary subtag names a shipped locale, so `zh-Hans-CN` and `zh-TW` both land on `zh` and `en-GB` on `en`, while a browser asking only for languages this app does not ship (`fr`, `de`) yields nothing and leaves `FALLBACK_LOCALE` in charge. `navigator.language` trails the list and covers its absence on hosts that ship a Navigator without `languages` — the DOM lib types it as always present, so that tolerance carries a narrow lint exception, the same environment-boundary distrust the `localStorage` guards already express.
|
||||
**Browser matching uses the registered catalog and the browser's ordered list.** `detectBrowserLocale()` walks `[...(navigator.languages ?? []), navigator.language]`. Each browser tag first matches a registered id exactly and then by primary subtag, so a registered `pt-BR` wins for that exact request, while `zh-Hans-CN` and an unmatched `zh-TW` land on the built-in `zh`, and `en-GB` lands on `en`. A browser asking only for unregistered languages (`fr`, `de` with the built-in catalog) yields nothing and leaves `FALLBACK_LOCALE` in charge. Registering or removing a language recomputes this provisional result. `navigator.language` trails the list and covers its absence on hosts that ship a Navigator without `languages`; tolerating that runtime omission follows the same environment-boundary distrust as the `localStorage` guards.
|
||||
|
||||
**`window`, not `navigator`, is the browser test.** Node ≥ 21 exposes a global `navigator` reporting the machine's own language, so gating on `navigator` would let a node boot of the client tree resolve to the machine's language instead of the documented fallback. Gating on `window` keeps every non-browser run on `FALLBACK_LOCALE`.
|
||||
|
||||
**An explicit choice is durable.** `setLocale` writes through the Host settings API, so a user who picked a language keeps it across browser origins and system languages that share the same DSH home. Nothing writes the detected locale back: detection is re-derived every boot and stays invisible to the “has the user chosen?” question.
|
||||
|
||||
**`<html lang>` follows the resolved locale, and the served markup cannot.** `apps/web/index.html` is one static file serving every visitor, so whatever it declares is wrong for somebody: resolution happens in the client, after the document is parsed. The locale plugin therefore sets `document.documentElement.lang` from the active locale — once at activation, because detection or an adopted Host preference may already disagree with the markup, and again on every switch. The markup declares the product default (`en`) so the pre-boot document is not actively misleading. Assistive technology and browser features (pronunciation rules, translation offers, font fallback, spell check) read this attribute, so a stale value misreports the document language rather than merely looking untidy. The attribute carries a BCP 47 tag rather than the app's locale id: `zh` alone leaves the script ambiguous, so the shipped Chinese copy declares `zh-CN`.
|
||||
**`<html lang>` follows the resolved locale, and the served markup cannot.** `apps/web/index.html` is one static file serving every visitor, so whatever it declares is wrong for somebody: resolution happens in the client, after the document is parsed. The locale plugin therefore sets `document.documentElement.lang` from the active locale — once at activation, because detection or an adopted Host preference may already disagree with the markup, and again on every switch. The markup declares the product default (`en`) so the pre-boot document is not actively misleading. Assistive technology and browser features (pronunciation rules, translation offers, font fallback, spell check) read this attribute, so a stale value misreports the document language rather than merely looking untidy. An external language id is already its BCP 47 tag and reaches the attribute unchanged; the built-in `zh` shorthand remains the sole exception and declares `zh-CN`, because `zh` alone leaves the script ambiguous.
|
||||
|
||||
**The browser e2e lane pins browser language.** Scenarios asserting Chinese copy (`access-confirmation`, `models-settings`, `onboarding-deepseek-config`, `settings-chrome`) open their page with `locale: ZH_BROWSER_LOCALE` from `apps/web/tests/support.ts`; `newEnglishPage` advertises `en-US`. `settings-chrome.e2e.ts` opens a fresh Host home with no explicit locale twice: an `en-US` browser and an `fr-FR` one both reach an English surface. The `fr-FR` scenario is the one that pins the fallback — an `en-US` browser would land on English under detection or fallback alike, so only an unshipped language distinguishes them, and the zh scenarios prove detection still overrides the fallback.
|
||||
|
||||
@@ -30,7 +30,7 @@ Reading the browser fixed the readers whose browser names a language this app sh
|
||||
|
||||
- **`Intl.DateTimeFormat().resolvedOptions().locale` or a single `navigator.language` read**: both collapse the user's ordered preference list to one tag, so a `['de', 'en', 'zh']` reader gets zh instead of en. The list is the part of the browser statement worth reading.
|
||||
- **Persisting the detected locale on first boot**: it would make detection a one-time event and let a stale first visit outlive a changed browser language, and it destroys the distinction the resolution order rests on — a stored value would no longer mean "the user chose this".
|
||||
- **Full BCP 47 negotiation (`Intl.LocaleMatcher`-style lookup, region and script weighting)**: with exactly two shipped locales that differ in language, primary-subtag matching is the whole of the correct answer; a negotiation layer would be untestable surface with no behavior to justify it.
|
||||
- **Full BCP 47 negotiation (`Intl.LocaleMatcher`-style lookup, region and script weighting)**: language registrations provide explicit ids, while dictionary fallback is separately explicit. Exact-id then primary-subtag matching preserves the built-in behavior without inventing an implicit distance policy between externally registered variants.
|
||||
- **A cordis config key for the fallback locale**: the deployment does not vary here — the fallback is the product's answer for "no signal at all", not a knob. Repo policy reserves `Config` fields for deployment-varying choices with a current consumer.
|
||||
- **Two constants, one for the opening locale and one for the dictionary fallback**: it separates two genuinely different questions, and would be required if the answers differed. They do not: the dictionaries are symmetric, so both are `en`, and a second constant would be two names for one value plus a rule nothing enforces. The symmetry itself is worth enforcing, so it is gated directly instead.
|
||||
- **Keeping `zh` as the dictionary fallback while opening in `en`**: it reads as the conservative choice, but with symmetric dictionaries it never resolves a key that `en` would not, so it buys nothing; and where it would matter — a key present only in `zh` — rendering Chinese text inside an otherwise English UI is worse than the bare key a reviewer would notice.
|
||||
@@ -39,8 +39,8 @@ Reading the browser fixed the readers whose browser names a language this app sh
|
||||
|
||||
## Consequences
|
||||
|
||||
- A first visit from an English browser lands in English, a Chinese browser in Chinese, and a browser naming neither lands in English rather than Chinese. The Language row still shows the same two self-described options, so the escape hatch is unchanged in either direction.
|
||||
- Dictionary resolution reverses direction: a key missing from the active locale now falls to `en`, not `zh`. With symmetric dictionaries no shipped key changes behavior, which is why the parity gate exists — it is the assumption that reversal rests on.
|
||||
- A first visit chooses the first registered language matched from the browser's ordered list. With only the built-in catalog, an English browser lands in English, a Chinese browser in Chinese, and a browser naming neither lands in English rather than Chinese; external registrations join the same Language row and matching process.
|
||||
- Dictionary resolution ends at `en`: a built-in `zh` miss reaches it directly, while an external language follows its declared per-key chain first. Symmetric built-in dictionaries keep shipped copy complete, which is why the parity gate exists.
|
||||
- `<html lang>` now reports the language on screen in both directions, which closes [#2160](https://github.com/deepseek-harness/deepseek-harness/issues/2160). A client that never activates the locale plugin keeps the served default, so the attribute degrades to the old static behavior rather than to a blank value.
|
||||
- Non-browser runs of the client tree (node boots, the non-jsdom unit lane) now open in `en`. Specs that assert shipped Chinese copy must set `setLocale('zh')` explicitly on the runtime they construct; a suite-level `usePinnedBrowserLanguages('zh-CN')` only works in files that also declare `@vitest-environment jsdom`, because without a `window` the detection path never reads `navigator` at all. Seven `*.client.spec.ts` files carried such a dead pin and were relying on the old `zh` fallback instead.
|
||||
- Detection cost is one array walk per service construction and no implicit settings write; an explicit Host preference may cause one live convergence after plugin activation.
|
||||
- Detection cost is one array walk per service construction or language-catalog change and no implicit settings write; an explicit Host preference may cause one live convergence after plugin activation or when its pending language registers.
|
||||
|
||||
@@ -8,21 +8,21 @@ Status: implemented
|
||||
|
||||
设置里的语言行在每一次首访时都以中文开场:`LocaleRuntime` 从 localStorage 读取 `dsh.locale`,读不到就直接回落到 `zh`。浏览器本已声明其使用者阅读哪些语言——`navigator.languages` 就是这份声明——而应用对此视而不见,于是英文读者迎面撞上一个中文产品,还得先找到一行中文标签的设置项才能脱身。回落值当时同时承担两份职责:既是无法解析出 locale 时的最后兜底,也是所有从未做过选择的用户拿到的答案。
|
||||
|
||||
读取浏览器修好了那些浏览器声明了本应用所提供语言的读者,但残余情形依然是错的:既不请求 `zh` 也不请求 `en` 的浏览器(`fr`、`de`)仍会回落到 `zh`。这些读者恰恰最不可能阅读中文。
|
||||
读取浏览器可以让浏览器声明了已注册语言的读者获得对应界面,但当当前目录没有匹配项时,产品仍需一个稳定的最终选择。若目录中只有内置语言,请求既非 `zh` 也非 `en` 的浏览器(`fr`、`de`)就会进入这种情形,而这些读者恰恰最不可能阅读中文。
|
||||
|
||||
## Decision
|
||||
|
||||
**暂定 locale 先经浏览器、再经 `FALLBACK_LOCALE`(`en`)解析;显式 Host 偏好会实时替换它。** `packages/client/locale/src/client/index.ts` 中的 `resolveInitialLocale()` 在服务构造时运行,并表达浏览器/回落顺序。随后,非阻塞 settings 生命周期会应用 `$DSH_HOME/settings.yaml` 中可选的 `locale.preference`;若该值缺失,则继续使用由浏览器派生的值。
|
||||
**暂定 locale 先经浏览器、再经 `FALLBACK_LOCALE`(`en`)解析;显式 Host 偏好会实时替换它。** `packages/client/locale/src/client/index.ts` 中的 `resolveInitialLocale()` 在服务构造时和每次语言目录变化后运行,依据当时已注册的定义表达浏览器/回落顺序。随后,非阻塞 settings 生命周期会应用 `$DSH_HOME/settings.yaml` 中可选的 `locale.preference`;若该值缺失,则继续使用由浏览器派生的值;若已保存的 id 暂不可用,则保留待采用状态,并在对应语言注册后生效。
|
||||
|
||||
**开场 locale 与字典回落值共用一个常量,因为两侧字典是对称的。** `FALLBACK_LOCALE` 同时回答「浏览器未声明任何本应用提供的语言时,界面以哪种语言开场」与「当前 locale 的字典缺失某个 key 时由哪本字典兜住」。这是两个不同的问题,若其中任一答案必须不同,拆成两个常量才是对的——但每一对已提供的 `zh`/`en` 字典都声明了完全相同的 key 集合,因此回落这一步总能解析成功,两个答案都是 `en`。残余情形指向英文而非 `zh`,是因为一个声明了本应用都不支持的语言的浏览器,其读者最不可能读中文。`scripts/locale-dictionary-parity.spec.ts` 为这个共用常量所依赖的对称性设了门禁:只加在一侧的 key 会让该用例指名失败,而不是日后在运行中的界面里显现为形如 `list.aria` 的裸 key。
|
||||
**开场时的最终回落与字典链终点共用一个常量。** `FALLBACK_LOCALE` 同时回答「浏览器未声明任何已注册语言时,界面以哪种语言开场」与「每条已声明的字典 fallback 链必须在哪里结束」。这是两个不同的问题,若其中任一答案必须不同,拆成两个常量才是对的。外部语言可以贡献不完整字典并声明中间 fallback,但每条链最终仍到达 `en`。每一对内置 `zh`/`en` 字典都声明完全相同的 key 集合,因此最后一次回落能够解析;`scripts/locale-dictionary-parity.spec.ts` 会拒绝只加在内置一侧的 key,避免它日后在运行中的界面里显现为形如 `list.aria` 的裸 key。
|
||||
|
||||
**浏览器匹配按主子标签进行,且遍历有序列表。** `detectBrowserLocale()` 遍历 `[...(navigator.languages ?? []), navigator.language]`,返回主子标签命中已提供 locale 的首个条目,因此 `zh-Hans-CN` 与 `zh-TW` 同归 `zh`、`en-GB` 归 `en`;而只请求本应用不提供的语言(`fr`、`de`)的浏览器则什么都匹配不到,交由 `FALLBACK_LOCALE` 接管。`navigator.language` 排在列表之后,并兜住那些 Navigator 上没有 `languages` 的宿主——DOM 库把它标注为必然存在,所以这份容忍带一条窄口径 lint 例外,与 `localStorage` 守卫表达的环境边界不信任同源。
|
||||
**浏览器匹配使用已注册目录和浏览器的有序列表。** `detectBrowserLocale()` 遍历 `[...(navigator.languages ?? []), navigator.language]`。每个浏览器标签先精确匹配已注册 id,再按主子标签匹配,因此已注册的 `pt-BR` 会响应同名请求;`zh-Hans-CN` 与未精确命中的 `zh-TW` 会落到内置 `zh`,`en-GB` 会落到 `en`。若浏览器只请求未注册语言(在只有内置目录时如 `fr`、`de`),匹配不会产生结果,并由 `FALLBACK_LOCALE` 接管。语言注册或移除时会重新计算这一暂定结果。`navigator.language` 排在列表之后,并兜住那些 Navigator 上没有 `languages` 的宿主;容忍该运行时缺失与 `localStorage` 守卫表达的环境边界不信任同源。
|
||||
|
||||
**判定浏览器用的是 `window` 而非 `navigator`。** Node ≥ 21 暴露全局 `navigator` 并报告机器自身语言,因此以 `navigator` 把关会让 node 启动客户端树时解析成机器语言,而非文档约定的回落值。以 `window` 把关可使所有非浏览器运行都停留在 `FALLBACK_LOCALE`。
|
||||
|
||||
**显式选择具有持久性。** `setLocale` 通过 Host settings API 写入,因此选过语言的用户可在共享同一 DSH home 的不同浏览器 origin 与系统语言之间保留原选择。没有任何代码把探测到的 locale 写回:探测在每次启动时重新推导,对「用户是否做过选择」这一问题始终不可见。
|
||||
|
||||
**`<html lang>` 跟随解析出的 locale,而所服务的 markup 做不到这一点。** `apps/web/index.html` 是一份静态文件,服务所有访问者,因此它声明什么都必然对某些人是错的:解析发生在客户端,在文档被解析之后。于是由 locale 插件依据当前 locale 设置 `document.documentElement.lang`——激活时设置一次,因为探测结果或已采纳的 Host 偏好可能已与 markup 不一致;此后每次切换再设置一次。markup 声明产品默认值(`en`),使启动前的文档不至于主动误导。无障碍技术与浏览器功能(发音规则、翻译提示、字体回退、拼写检查)都读取该属性,因此陈旧的值是在误报文档语言,而不只是看起来不整齐。该属性承载 BCP 47 标签而非应用内部的 locale id:单独的 `zh` 会使文字(script)含义不明,因此已提供的中文文案声明 `zh-CN`。
|
||||
**`<html lang>` 跟随解析出的 locale,而所服务的 markup 做不到这一点。** `apps/web/index.html` 是一份静态文件,服务所有访问者,因此它声明什么都必然对某些人是错的:解析发生在客户端,在文档被解析之后。于是由 locale 插件依据当前 locale 设置 `document.documentElement.lang`——激活时设置一次,因为探测结果或已采纳的 Host 偏好可能已与 markup 不一致;此后每次切换再设置一次。markup 声明产品默认值(`en`),使启动前的文档不至于主动误导。无障碍技术与浏览器功能(发音规则、翻译提示、字体回退、拼写检查)都读取该属性,因此陈旧的值是在误报文档语言,而不只是看起来不整齐。外部语言 id 本身就是 BCP 47 标签,会原样进入该属性;内置 `zh` 简写是唯一例外,它声明为 `zh-CN`,因为单独的 `zh` 会使文字(script)含义不明。
|
||||
|
||||
**浏览器 e2e 车道固定浏览器语言。** 断言中文文案的场景(`access-confirmation`、`models-settings`、`onboarding-deepseek-config`、`settings-chrome`)以 `apps/web/tests/support.ts` 的 `locale: ZH_BROWSER_LOCALE` 打开页面;`newEnglishPage` 声明 `en-US`。`settings-chrome.e2e.ts` 两次使用没有显式 locale 的全新 Host home:`en-US` 浏览器与 `fr-FR` 浏览器都会抵达英文界面。真正钉住回落值的是 `fr-FR` 那个场景——`en-US` 浏览器无论走探测还是走回落都会落在英文,因此只有本应用不提供的语言才能区分二者,而中文场景则证明探测仍然覆盖回落值。
|
||||
|
||||
@@ -30,7 +30,7 @@ Status: implemented
|
||||
|
||||
- **`Intl.DateTimeFormat().resolvedOptions().locale` 或单读 `navigator.language`**:两者都把用户的有序偏好列表塌缩成一个标签,于是 `['de', 'en', 'zh']` 的读者拿到的是 zh 而非 en。列表恰恰是浏览器这份声明里最值得读的部分。
|
||||
- **首次启动即持久化探测结果**:那会把探测变成一次性事件,让一次陈旧的首访凌驾于此后改变的浏览器语言之上,也摧毁了整个解析顺序所依赖的区分——存储值将不再意味着「用户选了它」。
|
||||
- **完整的 BCP 47 协商(`Intl.LocaleMatcher` 式查找、地区与文字权重)**:在只提供两个语言互异的 locale 时,主子标签匹配就是正确答案的全部;协商层只会带来无行为支撑、也无从测试的表面积。
|
||||
- **完整的 BCP 47 协商(`Intl.LocaleMatcher` 式查找、地区与文字权重)**:语言注册会提供明确的 id,字典 fallback 也有独立的显式配置。先精确匹配 id、再匹配主子标签,既保留了内置行为,也无需在外部注册的变体之间虚构隐式距离策略。
|
||||
- **为回落 locale 增加一个 Cordis 配置键**:此处部署之间并无差异——回落值是产品对「完全没有信号」给出的答案,不是旋钮。仓库策略把 `Config` 字段留给有当前消费方、且随部署变化的选择。
|
||||
- **拆成两个常量,一个管开场 locale、一个管字典回落**:它区分了两个确实不同的问题,若两个答案不同也确有必要。但它们并不不同:字典是对称的,因此两者都是 `en`,第二个常量只会是同一个值的两个名字,外加一条无人强制的规则。对称性本身值得强制,所以直接为它设门禁。
|
||||
- **开场用 `en`、字典回落仍保留 `zh`**:这看起来是保守选择,但在字典对称的前提下,它能解析的 key 与 `en` 完全相同,因此毫无收益;而在它真正会起作用的情形——某个 key 只存在于 `zh`——在整体英文的界面里渲染出中文文本,比让 reviewer 一眼看见裸 key 更糟。
|
||||
@@ -39,8 +39,8 @@ Status: implemented
|
||||
|
||||
## Consequences
|
||||
|
||||
- 来自英文浏览器的首访落在英文界面,中文浏览器落在中文界面,而两者皆未声明的浏览器落在英文而非中文界面。语言行依然呈现同样两个以自身语言自述的选项,两个方向的脱身通道都未改变。
|
||||
- 字典解析方向发生反转:当前 locale 缺失的 key 现在回落到 `en` 而非 `zh`。在字典对称的前提下,没有任何已提供的 key 行为发生变化——这正是那道对称性门禁存在的原因:它是这次反转所依赖的前提。
|
||||
- 首次访问会从浏览器的有序列表中选择第一个匹配的已注册语言。若目录中只有内置语言,英文浏览器进入英文界面,中文浏览器进入中文界面,两者皆未声明的浏览器则进入英文而非中文界面;外部注册项会加入同一个语言行与匹配过程。
|
||||
- 字典解析最终到达 `en`:内置 `zh` 缺失 key 时直接到达它,外部语言则先按自己声明的链逐 key 回落。内置字典对称性保证已提供的文案完整,这正是对称性门禁存在的原因。
|
||||
- `<html lang>` 现在在两个方向上都如实报告屏幕上的语言,这也关闭了 [#2160](https://github.com/deepseek-harness/deepseek-harness/issues/2160)。若某个客户端从未激活 locale 插件,则保留所服务的默认值,因此该属性退化为旧的静态行为,而不会退化为空值。
|
||||
- 客户端树的非浏览器运行(node 启动、非 jsdom 单测车道)现在以 `en` 开场。断言已提供中文文案的用例必须在其构造的 runtime 上显式调用 `setLocale('zh')`;套件级的 `usePinnedBrowserLanguages('zh-CN')` 仅在同时声明了 `@vitest-environment jsdom` 的文件中生效,因为没有 `window` 时探测路径根本不会读取 `navigator`。此前有七个 `*.client.spec.ts` 文件带着这样一条失效的固定语句,实际依赖的是旧的 `zh` 回落值。
|
||||
- 探测的代价是每次服务构造遍历一次数组,且不会隐式写入 settings;插件激活后,显式 Host 偏好可能引发一次实时收敛。
|
||||
- 探测的代价是每次服务构造或语言目录变化时遍历一次数组,且不会隐式写入 settings;插件激活后或待采用语言注册时,显式 Host 偏好可能引发一次实时收敛。
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
|
||||
2026-08-04-claude-code-and-codex-subagent-backends.md: 9b47fcf49d47d2c3561245fa1e16ff8c5da0a35c
|
||||
2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 8ba4d259872558ced89083bb53f9228f46c3d45c
|
||||
2026-08-04-claude-code-and-codex-subagent-backends.md: 04b8fcb9c3fd899a485a12ce9c002b8ab5d6bbb3
|
||||
2026-08-04-claude-code-and-codex-subagent-backends.zh.md: de9248ed7c82266bd6083020ab32190a3dfd8d1f
|
||||
|
||||
+13
-13
@@ -12,7 +12,7 @@ The product integrations must not become second owners for task text, cwd, cance
|
||||
|
||||
## Decision
|
||||
|
||||
The harness publishes two sibling one-shot provider packages whose default registry names are `codex` and `claude-code`. This note owns their product protocols, result mapping, and process lifecycle; the [named-instance decision](2026-08-18-product-subagent-named-instances.md) owns Profile-selected provider identity and static tool binding, the [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) owns their independent optional Bundles and host-plane placement, the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md) owns the model-visible scheduling choice, the [non-interactive permissions decision](2026-08-15-product-subagent-noninteractive-permissions.md) owns each product Provider's Profile-selected mode and safe permission decisions, and the [structured failure-facts decision](2026-08-18-product-subagent-failure-facts.md) owns version-pinned product categories, lifecycle stages, and process outcomes exposed through the same diagnostic. Both packages accept multiple named instances. Loading either provider starts no product process, and each tool accepts only a standalone text task; product and instance selection remain deployment configuration.
|
||||
The harness publishes two sibling one-shot provider packages whose default registry names are `codex` and `claude-code`. This note owns their product protocols, result mapping, and process lifecycle; the [named-instance decision](2026-08-18-product-subagent-named-instances.md) owns Profile-selected provider identity, optional instance model, and static tool binding; the [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) owns their independent optional Bundles and host-plane placement; the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md) owns the model-visible scheduling choice; the [non-interactive permissions decision](2026-08-15-product-subagent-noninteractive-permissions.md) owns each product Provider's Profile-selected mode and safe permission decisions; and the [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns coarse product action categories. Both packages accept multiple named instances. Loading either provider starts no product process, and each tool accepts only a standalone text task; product and instance selection remain deployment configuration.
|
||||
|
||||
Both providers report `inheritsParentContext: false`, advertise no optional start capabilities, and pass the parent Session cwd without copying the parent conversation. Their documented tools use `backgroundMode: 'one-shot'` and `maxDepth: 'provider-managed'`: the consumer keeps foreground collection as the default and may place the same run in the generic Job runtime, while recursion policy stays with the out-of-process product. Every call creates a fresh product process and a non-resumable product conversation. `ctx.subagents` owns named-request resolution and paired lifecycle events; `dsh-tool-subagent` owns model-visible scheduling and foreground-versus-Job adaptation; `ctx.jobs` and `dsh-tool-jobs` own Job ids, state, output, controls, notices, and parent-owner cancellation; each product provider owns native result mapping, while `dsh-subprocess` owns credential scrubbing, process-tree termination, and whole-tree exit observation.
|
||||
|
||||
@@ -34,25 +34,25 @@ configured tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> pro
|
||||
|
||||
## Codex provider
|
||||
|
||||
`@deepseek-ai/dsh-subagent-codex` registers a Profile-selected provider name that defaults to `codex`, resolves the `codex` bin declared by its pinned `@openai/codex@0.147.0` package, and starts that wrapper through the current Node executable with `app-server --stdio`. The wrapper selects the private native platform payload; the provider neither resolves nor falls back to a host `codex`. Its public configuration contains a non-empty `providerName`, an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a three-value native `permissionMode` that defaults to `never`. Each named instance retains those resolved values for its own runs. Installation, login, `CODEX_HOME`, model selection, base URL, and product-session settings remain native Codex or deployment responsibilities; the selected mode owns only the thread approval/reviewer/sandbox fields described by the non-interactive permissions decision.
|
||||
`@deepseek-ai/dsh-subagent-codex` registers a Profile-selected provider name that defaults to `codex`, resolves the `codex` bin declared by its pinned `@openai/codex@0.149.1` package, and starts that wrapper through the current Node executable with `app-server --stdio`. The wrapper selects the private native platform payload; the provider neither resolves nor falls back to a host `codex`. Its public configuration contains a non-empty `providerName`, an optional non-empty `model`, an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a three-value native `permissionMode` that defaults to `never`. Each named instance retains those resolved values for its own runs. An explicit model is passed unchanged on every ephemeral `thread/start`; omission leaves native Codex settings authoritative. Installation, login, `CODEX_HOME`, model discovery or fallback, base URL, and product-session settings remain native Codex or deployment responsibilities; the selected mode owns only the thread approval/reviewer/sandbox fields described by the non-interactive permissions decision.
|
||||
|
||||
Before publication, the provider validates a non-empty text-only task, starts the managed app-server in the parent workspace, completes `initialize` → `initialized`, maps the resolved mode into official `thread/start` fields, and creates an `ephemeral: true` thread. The fixed app-server argv contains no mode or task text. The published run owns exactly one `turn/start`; its thread and turn ids remain private and are never persisted in the parent Session.
|
||||
Before publication, the provider validates a non-empty text-only task, starts the managed app-server in the parent workspace, completes `initialize` → `initialized`, maps the optional model and resolved mode into official `thread/start` fields, and creates an `ephemeral: true` thread. The fixed app-server argv contains no model, mode, or task text. The published run owns exactly one `turn/start`; its thread and turn ids remain private and are never persisted in the parent Session.
|
||||
|
||||
`turn/completed` is the authoritative remote terminal fact. The latest `agentMessage` with `phase: "final_answer"` wins, and that selected message must contain nonblank text. When the product emits no explicit final phase, the latest message with `phase: null` is the compatibility fallback and must likewise be nonblank; commentary never replaces either answer. The [structured failure-facts decision](2026-08-18-product-subagent-failure-facts.md) owns Codex error-info categories, HTTP status, lifecycle stages, process outcomes, and stop-reason preservation. Local cancellation remains `aborted` without a failure diagnostic.
|
||||
`turn/completed` is the authoritative remote terminal fact. The latest `agentMessage` with `phase: "final_answer"` wins, and that selected message must contain nonblank text. When the product emits no explicit final phase, the latest message with `phase: null` is the compatibility fallback and must likewise be nonblank; commentary never replaces either answer. The [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns Codex action categories, HTTP status, lifecycle stages, process outcomes, and stop-reason preservation. Local cancellation remains `aborted` without a failure diagnostic.
|
||||
|
||||
For command and file approvals, the unattended wire selects a non-approval decision offered by the request, preferring `cancel`; the stable 0.147.0 request shape without an offered-decision list falls back to `decline`. It grants no requested permissions for the turn, answers user-input requests with no answers, and declines MCP elicitation. It records safe categories for those requests, declined command/file items, and `sandboxError`. Codex emits some early `never` rejections and sandbox violations only on structured stderr, so the Provider pipes and forwards stderr unchanged while matching two fixed signatures in a bounded per-run tail; raw stderr never enters the diagnostic. A request with no legal unattended response, or any unknown server request, fails the run instead of waiting for a user interface the provider does not supply.
|
||||
For command and file approvals, the unattended wire selects a non-approval decision offered by the request, preferring `cancel`; the stable 0.149.1 request shape without an offered-decision list falls back to `decline`. It grants no requested permissions for the turn, answers user-input requests with no answers, and declines MCP elicitation. It records safe categories for those requests, declined command/file items, and structured `sandboxError` terminals. Product stderr is forwarded unchanged to the Host but is neither classified nor copied into the diagnostic. A request with no legal unattended response, or any unknown server request, fails the run instead of waiting for a user interface the provider does not supply.
|
||||
|
||||
An unpublished startup failure closes the wire, terminates the acquired process tree, waits for exit, detaches the stderr observer, and then rejects `start()` with its fixed operation stage. Published disposal best-effort interrupts a known turn, closes the wire, ends stdin, invokes the shared termination escalation, waits for whole-tree exit, and detaches the observer. Independent cleanup failure reports `teardown`; when startup and rollback both fail, the aggregate's top message retains both safe stage lines while the underlying causes remain internal.
|
||||
|
||||
Codex 0.147.0 speaks the Responses protocol, while DeepSeek's public OpenAI-compatible endpoint speaks Chat Completions. The credentialed Codex e2e therefore uses a loopback-only, test-private bridge for one no-tool nonce request: real Codex sends Responses to the bridge, the bridge forwards the received bearer credential and extracted task to the fixed official DeepSeek endpoint, and it wraps the real text in the minimal Responses SSE lifecycle. The bridge is neither a production proxy nor evidence that Codex connects to DeepSeek Chat Completions natively.
|
||||
Codex 0.149.1 speaks the Responses protocol, while DeepSeek's public OpenAI-compatible endpoint speaks Chat Completions. The credentialed Codex e2e therefore uses a loopback-only, test-private bridge for one no-tool nonce request: real Codex sends Responses to the bridge, the bridge forwards the received bearer credential and extracted task to the fixed official DeepSeek endpoint, and it wraps the real text in the minimal Responses SSE lifecycle. The bridge is neither a production proxy nor evidence that Codex connects to DeepSeek Chat Completions natively.
|
||||
|
||||
## Claude Code provider
|
||||
|
||||
`@deepseek-ai/dsh-subagent-claude-code` registers a Profile-selected provider name that defaults to `claude-code` and invokes `@anthropic-ai/claude-agent-sdk@0.3.220`. The provider omits `pathToClaudeCodeExecutable`, so the SDK selects Claude Code 2.1.220 from the matching OS, CPU, and Linux-libc platform package in its own optional dependency closure. The provider does not resolve or fall back to a host `claude`; an omitted, unsupported, missing, or damaged platform payload fails the first delegation at the SDK startup boundary. The provider uses the official `query()` entrypoint and passes the SDK's native `claude` or `claude.exe` command, arguments, cwd, environment, and forwarded signal from `spawnClaudeCodeProcess` to `dsh-subprocess`; its private `SpawnedProcess` adapter exposes only the stream, event, kill, and exit facts the SDK requires.
|
||||
`@deepseek-ai/dsh-subagent-claude-code` registers a Profile-selected provider name that defaults to `claude-code` and invokes `@anthropic-ai/claude-agent-sdk@0.3.241`. The provider omits `pathToClaudeCodeExecutable`, so the SDK selects Claude Code 2.1.241 from the matching OS, CPU, and Linux-libc platform package in its own optional dependency closure. The provider does not resolve or fall back to a host `claude`; an omitted, unsupported, missing, or damaged platform payload fails the first delegation at the SDK startup boundary. The provider uses the official `query()` entrypoint and passes the SDK's native `claude` or `claude.exe` command, arguments, cwd, environment, and forwarded signal from `spawnClaudeCodeProcess` to `dsh-subprocess`; its private `SpawnedProcess` adapter exposes only the stream, event, kill, and exit facts the SDK requires.
|
||||
|
||||
The public configuration contains a non-empty `providerName`, an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a five-value native `permissionMode` that defaults to `dontAsk`. Each named instance retains those resolved values for its own runs. Each run creates its own `AbortController`, sets `persistSession: false`, disables `AskUserQuestion`, and passes the resolved mode to the SDK; only `bypassPermissions` receives the SDK's explicit dangerous confirmation. The provider deliberately omits `settingSources`, so the SDK reads the host's normal user, project, and local Claude settings relative to the parent Session cwd. It neither copies nor filters those settings and does not create or modify login state. Remaining permission prompts are denied, MCP elicitation is declined, and blocking dialogs fail closed instead of waiting for a user interface the provider does not own.
|
||||
The public configuration contains a non-empty `providerName`, an optional non-empty `model`, an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a five-value native `permissionMode` that defaults to `dontAsk`. Each named instance retains those resolved values for its own runs. An explicit model is passed unchanged through `Options.model`; omission leaves that field absent so native settings choose the model. Each run creates its own `AbortController`, sets `persistSession: false`, disables `AskUserQuestion`, and passes the resolved mode to the SDK; only `bypassPermissions` receives the SDK's explicit dangerous confirmation. The provider deliberately omits `settingSources`, so the SDK reads the host's normal user, project, and local Claude settings relative to the parent Session cwd. It neither copies nor filters those settings and does not create or modify login state. Remaining permission prompts are denied, MCP elicitation is declined, and blocking dialogs fail closed instead of waiting for a user interface the provider does not own.
|
||||
|
||||
The provider publishes only after both the SDK `Query` and a live managed CLI handle exist. It consumes the complete SDK stream and completes only when a `result` message has `subtype: "success"`, `is_error: false`, and a nonblank `result`, and the iterator then ends normally. The [structured failure-facts decision](2026-08-18-product-subagent-failure-facts.md) owns every non-success category, stage, process outcome, and its ordering with a contributing permission decision. Local cancellation wins and becomes `aborted` without either diagnostic fact.
|
||||
The provider publishes only after both the SDK `Query` and a live managed CLI handle exist. It consumes the complete SDK stream and completes only when a `result` message has `subtype: "success"`, `is_error: false`, and a nonblank `result`, and the iterator then ends normally. The [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns every non-success action category, stage, process outcome, and its ordering with a contributing permission decision. Local cancellation wins and becomes `aborted` without either diagnostic fact.
|
||||
|
||||
Startup rollback and published disposal close the SDK query, abort the per-run controller, invoke shared process-tree termination, and wait for whole-tree exit. `Query.close()` expresses graceful protocol intent but does not replace the subprocess owner's exit proof. An unpublished failure exposes only fixed `query-start` facts; a published process failure can expose its independent exit code and signal; an independent cleanup rejection exposes `teardown`. Original SDK, Host, and cleanup errors remain on internal cause chains and logs rather than entering the diagnostic.
|
||||
|
||||
@@ -62,11 +62,11 @@ The credentialed Claude Code e2e uses the official DeepSeek Claude Code contract
|
||||
|
||||
Each product owns branch-complete package tests, a required keyless real-product spec, a Loader composition e2e, and a credentialed DeepSeek e2e. The keyless product tier uses the exact official distribution under test, a non-empty fake product key, an isolated temporary workspace and product home, and a loopback fixed-answer model. Missing product requests, wrong authentication, altered task text, a non-exact answer, a skipped real product, or a surviving managed handle fails the required test. The Codex Loader fixture exposes two named Codex instances and tools; the Claude Code Loader fixture exposes the default Codex tool plus two named Claude Code instances and tools. Both fixtures include generic Job controls and start neither product process. The credentialed tier starts the same production provider and real product with a runtime-only key, requires a unique nonce from the fixed official DeepSeek service, and proves quiescence again; it self-skips only when a local operator supplied no key, while trusted CI preflights the secret.
|
||||
|
||||
The Codex evidence pins `@openai/codex@0.147.0`, `codex-cli 0.147.0`, and all six optional platform aliases. Its real-product spec observes the package-local wrapper argv, exact Bearer key, original task, byte-exact final answer, native permission modes, explicit dangerous-bypass writing in suite-owned temporary storage, and wrapper/native whole-tree exit. An isolated wrapper fixture proves missing-payload failure without host fallback, two named instances retain separate environments and modes, and production never resolves a host `codex` from `PATH`. The [structured failure-facts decision](2026-08-18-product-subagent-failure-facts.md) owns schema, failure, process-outcome, and final presentation evidence.
|
||||
The Codex evidence pins `@openai/codex@0.149.1`, `codex-cli 0.149.1`, and all six optional platform aliases. Its generated schema proves optional `ThreadStartParams.model`; the real-product spec observes omitted-model inheritance, two explicit instance models, the package-local wrapper argv, exact Bearer key, original task, byte-exact final answer, native permission modes, explicit dangerous-bypass writing in suite-owned temporary storage, and wrapper/native whole-tree exit. An isolated wrapper fixture proves missing-payload failure without host fallback, named instances retain separate models, environments, and modes, and production never resolves a host `codex` from `PATH`. The [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns failure, process-outcome, and final presentation evidence.
|
||||
|
||||
The Codex credentialed e2e registers the production provider, starts the same real app-server, and requests one random nonce through the test-private bridge described above. It fixes the external endpoint and model, stores no credential or request payload, requires exactly one completed upstream response, compares the trimmed product answer byte-for-byte with the nonce, and waits for every managed handle to exit.
|
||||
|
||||
The Claude Code evidence pins Agent SDK 0.3.220, Claude Code 2.1.220, and all eight SDK platform packages. Its real-product spec lets the SDK select the installed payload, asserts that the shared subprocess argv begins with that package's native CLI, and observes the exact `x-api-key`, original task, byte-exact final answer, native permission modes, suite-owned denied and bypassed writes, and whole-tree exit. Package tests prove that production never resolves host `PATH`, omits the executable override, and forwards the SDK-selected Windows `claude.exe` without a batch shim. This evidence proves the pinned official SDK/CLI integration rather than compatibility with independently installed Claude versions; the [structured failure-facts decision](2026-08-18-product-subagent-failure-facts.md) owns failure and process-outcome evidence. Loader coverage resolves both products through their optional Bundle patches while starting neither product.
|
||||
The Claude Code evidence pins Agent SDK 0.3.241, Claude Code 2.1.241, and all eight SDK platform packages. Its real-product spec lets the SDK select the installed payload, asserts that the shared subprocess argv begins with that package's native CLI, and observes omitted-model inheritance, two explicit instance models, the exact `x-api-key`, original task, byte-exact final answer, native permission modes, suite-owned denied and bypassed writes, and whole-tree exit. Package tests prove that production never resolves host `PATH`, omits the executable override, and forwards the SDK-selected Windows `claude.exe` without a batch shim. This evidence proves the pinned official SDK/CLI integration rather than compatibility with independently installed Claude versions; the [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns failure and process-outcome evidence. Loader coverage resolves both products through their optional Bundle patches while starting neither product.
|
||||
|
||||
The Claude Code credentialed e2e maps the key and fixed official endpoint only in the provider's in-memory environment, uses the documented `deepseek-v4-pro[1m]` and `deepseek-v4-flash` model variables, and traverses the production provider, official SDK, and real CLI. It compares the trimmed result with a random nonce and proves whole-tree exit without calling the Messages API directly from the test.
|
||||
|
||||
@@ -82,7 +82,7 @@ The project owner's distribution authorization is scoped to the official `@anthr
|
||||
|
||||
**Product doubles as required evidence.** Doubles cover exhaustive private protocol branches but do not prove package exports, official distributions, authentication, or real process behavior. Required evidence drives each official product against a loopback model fixture.
|
||||
|
||||
**Plugin-managed login, product home, models, settings, sandbox rules, or fine-grained permission policy.** Those choices would create another authority beside each product's native configuration and enlarge a one-shot provider into account management. Each product exposes only one native non-interactive mode choice in addition to environment and teardown configuration; neither Provider mirrors product rules or adds a human interaction channel.
|
||||
**Plugin-managed login, product home, model discovery or fallback, settings, sandbox rules, or fine-grained permission policy.** Those choices would create another authority beside each product's native configuration and enlarge a one-shot provider into account management. A Provider may pass one opaque Profile-selected model override where the official product supports it, but it does not discover, validate, alias, or fall back between models. Neither Provider mirrors product rules or adds a human interaction channel.
|
||||
|
||||
**Continuation, progress, product-native background state, and shared parent context.** The provider payload remains one final answer for one self-contained task. The generic Job layer may add its id, status, notice, collection, and cancellation results, but product sessions, resume, follow-up, intermediate messages, parent transcript transfer, structured output, and provider-specific background state need separate user contracts and are not prebuilt.
|
||||
|
||||
@@ -90,6 +90,6 @@ The project owner's distribution authorization is scoped to the official `@anthr
|
||||
|
||||
Users delegate through Profile-configured one-shot tools backed by the official product integrations. Explicit Profile installation and host-plane provider placement are owned by the [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md); named instance identity and tool binding are owned by the [named-instance decision](2026-08-18-product-subagent-named-instances.md); per-Preset tool exposure and foreground-default optional Job scheduling are owned by the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md). This note's provider lifecycle keeps native settings and behavior while shared services retain the sole ownership of job settlement and process-tree quiescence.
|
||||
|
||||
Every delegation pays for a fresh product process and independent model context. Successful product payload remains final assistant text; a failed product run may separately expose the shared safe diagnostic containing provider-owned permission facts or version-pinned structured failure facts. Background scheduling additionally exposes generic Job ids, status, completion notices, and collection or cancellation results. Both products use Bundle-pinned platform CLIs plus native account and workspace settings and the selected Provider permission mode. Credentialed e2e runs also spend external API quota and depend on the official DeepSeek endpoint; deterministic protocol, failure, cancellation, and approval coverage remains in the keyless tier. The providers do not resume sessions, stream progress, accept new human interaction, roll back tool or file side effects, or impose a wall-clock timeout.
|
||||
Every delegation pays for a fresh product process and independent model context. Successful product payload remains final assistant text; a failed product run may separately expose the shared safe diagnostic containing provider-owned permission facts and safe product failure categories. Background scheduling additionally exposes generic Job ids, status, completion notices, and collection or cancellation results. Both products use Bundle-pinned platform CLIs plus native account and workspace settings and the selected Provider permission mode; a supported optional instance model overrides only that run's native model selection. Credentialed e2e runs also spend external API quota and depend on the official DeepSeek endpoint; deterministic protocol, failure, cancellation, and approval coverage remains in the keyless tier. The providers do not resume sessions, stream progress, accept new human interaction, roll back tool or file side effects, or impose a wall-clock timeout.
|
||||
|
||||
Compatibility is pinned by package-level unit coverage, keyless real-product loopback tests, credentialed DeepSeek nonce tests, public Loader composition, built-package and NodeNext consumer checks, generated documentation and notices, and the repository CI matrix. A supported product or DeepSeek endpoint/model baseline change must refresh those facts; production performs no separate runtime version probe.
|
||||
|
||||
+13
-13
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
harness 交付两个同级的一次性提供方包,其默认注册名称分别为 `codex` 与 `claude-code`。本说明负责它们的产品协议、结果映射和进程生命周期;[命名实例决策](2026-08-18-product-subagent-named-instances.zh.md)负责 Profile 选择的提供方身份与静态工具绑定,[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md)负责各自独立的可选 Bundle 与 host plane(宿主平面)放置,[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.zh.md)负责模型可见的调度选择,[非交互权限决策](2026-08-15-product-subagent-noninteractive-permissions.zh.md)负责各产品提供方的 Profile 模式选择与安全权限决定,[结构化失败事实决策](2026-08-18-product-subagent-failure-facts.zh.md)则负责通过同一诊断公开锁定产品版本的类别、生命周期阶段与进程结果。两个包都接受多个命名实例。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品与实例选择仍属于部署配置。
|
||||
harness 交付两个同级的一次性提供方包,其默认注册名称分别为 `codex` 与 `claude-code`。本说明负责它们的产品协议、结果映射和进程生命周期;[命名实例决策](2026-08-18-product-subagent-named-instances.zh.md)负责 Profile 选择的提供方身份、可选实例模型与静态工具绑定;[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md)负责各自独立的可选 Bundle 与 host plane(宿主平面)放置;[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.zh.md)负责模型可见的调度选择;[非交互权限决策](2026-08-15-product-subagent-noninteractive-permissions.zh.md)负责各产品提供方的 Profile 模式选择与安全权限决定;[最小诊断决策](../simplification/2026-08-21-product-subagent-minimal-diagnostics.zh.md)负责粗粒度产品行动类别。两个包都接受多个命名实例。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品与实例选择仍属于部署配置。
|
||||
|
||||
这两个提供方都报告 `inheritsParentContext: false`,不声明任何可选的启动能力,并传递父会话 cwd,但不会复制父级对话。文档所示的工具使用 `backgroundMode: 'one-shot'` 与 `maxDepth: 'provider-managed'`:消费方默认在前台收集结果,也可把同一次运行放入通用 Job 运行时,而递归策略仍由进程外产品负责。每次调用都会创建一个全新的产品进程和一次不可续接的产品对话。`ctx.subagents` 负责具名请求解析与成对生命周期事件;`dsh-tool-subagent` 负责模型可见的调度以及前台与 Job 适配;`ctx.jobs` 和 `dsh-tool-jobs` 负责 Job id、状态、输出、控制、通知与父级 owner 取消;各产品提供方负责原生结果映射,`dsh-subprocess` 则负责凭证清洗、进程树终止以及整棵进程树的退出观测。
|
||||
|
||||
@@ -34,25 +34,25 @@ configured tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> pro
|
||||
|
||||
## Codex 提供方
|
||||
|
||||
`@deepseek-ai/dsh-subagent-codex` 注册由 Profile 选择、默认值为 `codex` 的提供方名称,解析锁定的 `@openai/codex@0.147.0` 包所声明的 `codex` bin,并使用当前 Node 可执行文件加 `app-server --stdio` 启动该 wrapper。Wrapper 会选择私有原生平台载荷;提供方既不解析也不回退宿主 `codex`。其公开配置包含非空的 `providerName`、显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`,以及默认使用 `never` 的三值原生 `permissionMode`。每个命名实例会为自己的运行保留这些已解析值。安装、登录、`CODEX_HOME`、模型选择、基础 URL 和产品会话设置仍由 Codex 原生机制或部署环境负责;所选模式只拥有非交互权限决策中描述的线程 approval/reviewer/sandbox 字段。
|
||||
`@deepseek-ai/dsh-subagent-codex` 注册由 Profile 选择、默认值为 `codex` 的提供方名称,解析锁定的 `@openai/codex@0.149.1` 包所声明的 `codex` bin,并使用当前 Node 可执行文件加 `app-server --stdio` 启动该 wrapper。Wrapper 会选择私有原生平台载荷;提供方既不解析也不回退宿主 `codex`。其公开配置包含非空的 `providerName`、可选的非空 `model`、显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`,以及默认使用 `never` 的三值原生 `permissionMode`。每个命名实例会为自己的运行保留这些已解析值。显式模型会原样传给每个临时 `thread/start`;省略时仍以 Codex 原生设置为权威。安装、登录、`CODEX_HOME`、模型发现或 fallback、基础 URL 和产品会话设置仍由 Codex 原生机制或部署环境负责;所选模式只拥有非交互权限决策中描述的线程 approval/reviewer/sandbox 字段。
|
||||
|
||||
发布前,提供方会验证非空的纯文本任务,在父级工作区中启动受管的 app-server,完成 `initialize` → `initialized` 握手,把已解析模式映射为官方 `thread/start` 字段,并创建一个 `ephemeral: true` 线程。固定 app-server argv 不包含模式或任务文本。已发布的运行只拥有一次 `turn/start`;其线程 ID 与轮次 ID 保持私有,绝不会持久化到父会话。
|
||||
发布前,提供方会验证非空的纯文本任务,在父级工作区中启动受管的 app-server,完成 `initialize` → `initialized` 握手,把可选模型与已解析模式映射为官方 `thread/start` 字段,并创建一个 `ephemeral: true` 线程。固定 app-server argv 不包含模型、模式或任务文本。已发布的运行只拥有一次 `turn/start`;其线程 ID 与轮次 ID 保持私有,绝不会持久化到父会话。
|
||||
|
||||
`turn/completed` 是权威的远端终止事实。以最后一条带有 `phase: "final_answer"` 的 `agentMessage` 为准,且选中的消息必须包含非空白文本。若产品没有发出明确的最终阶段,则以最后一条 `phase: null` 的消息作为兼容性回退,该消息也必须包含非空白文本;过程说明绝不会取代上述任一答案。[结构化失败事实决策](2026-08-18-product-subagent-failure-facts.zh.md)负责 Codex error-info 类别、HTTP status、生命周期阶段、进程结果与终止原因保持。本地取消仍是 `aborted` 且不附带失败诊断。
|
||||
`turn/completed` 是权威的远端终止事实。以最后一条带有 `phase: "final_answer"` 的 `agentMessage` 为准,且选中的消息必须包含非空白文本。若产品没有发出明确的最终阶段,则以最后一条 `phase: null` 的消息作为兼容性回退,该消息也必须包含非空白文本;过程说明绝不会取代上述任一答案。[最小诊断决策](../simplification/2026-08-21-product-subagent-minimal-diagnostics.zh.md)负责 Codex 行动类别、HTTP status、生命周期阶段、进程结果与终止原因保持。本地取消仍是 `aborted` 且不附带失败诊断。
|
||||
|
||||
对于命令与文件审批,无人值守的协议连接会从请求给出的决策选项中选择一项不予批准的决策,并优先选择 `cancel`;稳定的 0.147.0 请求形态没有决策选项列表,因此回退到 `decline`。它不授予该轮次请求的任何权限,不向用户输入请求提供任何答案,并拒绝 MCP elicitation。它会记录这些请求、被拒绝的命令/文件 item 与 `sandboxError` 的安全类别。Codex 的部分早期 `never` 拒绝和 sandbox violation 只写入结构化 stderr,因此提供方会 pipe 并原样转发 stderr,同时在每次运行的有界尾部中匹配两个固定签名;原始 stderr 绝不会进入诊断。若请求在无人值守模式下没有合法响应,或是未知服务器请求,此次运行就会失败,而不会等待本提供方没有提供的用户界面。
|
||||
对于命令与文件审批,无人值守的协议连接会从请求给出的决策选项中选择一项不予批准的决策,并优先选择 `cancel`;稳定的 0.149.1 请求形态没有决策选项列表,因此回退到 `decline`。它不授予该轮次请求的任何权限,不向用户输入请求提供任何答案,并拒绝 MCP elicitation。它会记录这些请求、被拒绝的命令/文件 item 与结构化 `sandboxError` 终态的安全类别。产品 stderr 会原样转发给 Host,但既不会被分类,也不会复制进诊断。若请求在无人值守模式下没有合法响应,或是未知服务器请求,此次运行就会失败,而不会等待本提供方没有提供的用户界面。
|
||||
|
||||
若启动在发布前失败,提供方会关闭协议连接、终止已获取的进程树、等待其退出、移除 stderr observer,然后用固定操作阶段拒绝 `start()`。对已发布的运行执行资源释放时,提供方会尽力中断已知轮次、关闭协议连接、结束标准输入、调用共享的逐级终止机制,等待整棵进程树退出,并移除 observer。独立清理失败会报告 `teardown`;启动与回滚同时失败时,聚合的顶层消息会保留两条安全阶段说明,而底层 cause 仍只在内部可见。
|
||||
|
||||
Codex 0.147.0 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端点使用 Chat Completions。因此,带密钥 Codex e2e 会采用一个仅限回环、仅供测试内部使用的桥接层来处理一次不使用工具的随机数请求:真实 Codex 将 Responses 发送到桥接层,桥接层把收到的 Bearer 凭据与提取出的任务转发到固定的 DeepSeek 官方端点,再将真实文本包装进最小化的 Responses SSE(Server-Sent Events)生命周期。该桥接层既不是生产代理,也不能作为 Codex 原生连接 DeepSeek Chat Completions 的证据。
|
||||
Codex 0.149.1 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端点使用 Chat Completions。因此,带密钥 Codex e2e 会采用一个仅限回环、仅供测试内部使用的桥接层来处理一次不使用工具的随机数请求:真实 Codex 将 Responses 发送到桥接层,桥接层把收到的 Bearer 凭据与提取出的任务转发到固定的 DeepSeek 官方端点,再将真实文本包装进最小化的 Responses SSE(Server-Sent Events)生命周期。该桥接层既不是生产代理,也不能作为 Codex 原生连接 DeepSeek Chat Completions 的证据。
|
||||
|
||||
## Claude Code 提供方
|
||||
|
||||
`@deepseek-ai/dsh-subagent-claude-code` 注册由 Profile 选择、默认值为 `claude-code` 的提供方名称,并调用 `@anthropic-ai/claude-agent-sdk@0.3.220`。提供方会省略 `pathToClaudeCodeExecutable`,因此 SDK 会从自己的 optional dependency 闭包中,按操作系统、CPU 与 Linux libc 选择携带 Claude Code 2.1.220 的匹配平台包。提供方既不会解析也不会回退宿主 `claude`;省略 optional dependency、不受支持的平台,以及缺失或损坏的平台载荷,都会在第一次委派的 SDK 启动边界失败。提供方使用官方 `query()` 入口点,并把 SDK 的 `spawnClaudeCodeProcess` 给出的原生 `claude` 或 `claude.exe` 命令、参数、cwd、环境和转发的信号交给 `dsh-subprocess`;其私有 `SpawnedProcess` 适配器只公开 SDK 所需的流、事件、终止和退出事实。
|
||||
`@deepseek-ai/dsh-subagent-claude-code` 注册由 Profile 选择、默认值为 `claude-code` 的提供方名称,并调用 `@anthropic-ai/claude-agent-sdk@0.3.241`。提供方会省略 `pathToClaudeCodeExecutable`,因此 SDK 会从自己的 optional dependency 闭包中,按操作系统、CPU 与 Linux libc 选择携带 Claude Code 2.1.241 的匹配平台包。提供方既不会解析也不会回退宿主 `claude`;省略 optional dependency、不受支持的平台,以及缺失或损坏的平台载荷,都会在第一次委派的 SDK 启动边界失败。提供方使用官方 `query()` 入口点,并把 SDK 的 `spawnClaudeCodeProcess` 给出的原生 `claude` 或 `claude.exe` 命令、参数、cwd、环境和转发的信号交给 `dsh-subprocess`;其私有 `SpawnedProcess` 适配器只公开 SDK 所需的流、事件、终止和退出事实。
|
||||
|
||||
公开配置包含非空的 `providerName`、显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`,以及默认使用 `dontAsk` 的五值原生 `permissionMode`。每个命名实例会为自己的运行保留这些已解析值。每次运行都会创建自己的 `AbortController`,设置 `persistSession: false`、禁用 `AskUserQuestion`,并把已解析模式传给 SDK;只有 `bypassPermissions` 会取得 SDK 的显式危险确认。提供方故意省略 `settingSources`,因此 SDK 会相对于父会话 cwd 读取宿主机常规的用户、项目和本地 Claude 设置。它既不复制也不过滤这些设置,也不会创建或修改登录状态。其余权限提示会被拒绝,MCP elicitation 会被拒绝,阻塞对话会快速失败,而不会等待本提供方不负责的用户界面。
|
||||
公开配置包含非空的 `providerName`、可选的非空 `model`、显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`,以及默认使用 `dontAsk` 的五值原生 `permissionMode`。每个命名实例会为自己的运行保留这些已解析值。显式模型会原样传入 `Options.model`;省略时不设置该字段,由原生设置选择模型。每次运行都会创建自己的 `AbortController`,设置 `persistSession: false`、禁用 `AskUserQuestion`,并把已解析模式传给 SDK;只有 `bypassPermissions` 会取得 SDK 的显式危险确认。提供方故意省略 `settingSources`,因此 SDK 会相对于父会话 cwd 读取宿主机常规的用户、项目和本地 Claude 设置。它既不复制也不过滤这些设置,也不会创建或修改登录状态。其余权限提示会被拒绝,MCP elicitation 会被拒绝,阻塞对话会快速失败,而不会等待本提供方不负责的用户界面。
|
||||
|
||||
只有在 SDK `Query` 与受管的活动 CLI 句柄都已存在后,提供方才会发布运行。它会消费完整的 SDK 流;只有 `result` 消息具有 `subtype: "success"`、`is_error: false` 和非空白 `result`,且迭代器随后正常结束时,运行才会完成。[结构化失败事实决策](2026-08-18-product-subagent-failure-facts.zh.md)负责所有非成功类别、阶段、进程结果,以及它们与参与失败的权限决定之间的顺序。本地取消会胜出并成为 `aborted`,且不附带这两类诊断事实。
|
||||
只有在 SDK `Query` 与受管的活动 CLI 句柄都已存在后,提供方才会发布运行。它会消费完整的 SDK 流;只有 `result` 消息具有 `subtype: "success"`、`is_error: false` 和非空白 `result`,且迭代器随后正常结束时,运行才会完成。[最小诊断决策](../simplification/2026-08-21-product-subagent-minimal-diagnostics.zh.md)负责所有非成功行动类别、阶段、进程结果,以及它们与参与失败的权限决定之间的顺序。本地取消会胜出并成为 `aborted`,且不附带这两类诊断事实。
|
||||
|
||||
启动回滚和已发布运行的资源释放都会关闭 SDK query、中止该次运行的控制器、调用共享的进程树终止机制,并等待整棵进程树退出。`Query.close()` 表达优雅的协议关闭意图,但不能取代子进程责任方的退出证明。未发布失败只公开固定的 `query-start` 事实;已发布进程失败可以分别公开退出码与信号;独立清理拒绝则公开 `teardown`。原始 SDK、Host 与清理错误只保留在内部 cause 链和日志中,不进入诊断。
|
||||
|
||||
@@ -62,11 +62,11 @@ Codex 0.147.0 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端
|
||||
|
||||
每个产品都负责覆盖所有分支的包测试、一项必跑的无密钥真实产品测试、一项 Loader 组合 e2e 和一项带密钥 DeepSeek e2e。无密钥产品层级使用被测的确切官方发行版、非空的伪产品密钥、隔离的临时工作区与产品主目录,以及能返回固定答案的回环模型。产品请求缺失、身份验证错误、任务文本被改动、答案不完全一致、真实产品被跳过或受管句柄仍存活,都会使这项必跑测试失败。Codex Loader fixture 会公开两个命名 Codex 实例与工具;Claude Code Loader fixture 会公开默认 Codex 工具以及两个命名 Claude Code 实例与工具。两个 fixture 都包含通用 Job 控制工具,而且不会启动任何产品进程。带密钥层级会使用仅在运行时提供的密钥启动同一生产提供方与真实产品,要求从固定的 DeepSeek 官方服务取得唯一随机数,并再次证明完全停稳;仅当本地操作者未提供密钥时才会自行跳过,而受信任的 CI 会预检该 secret。
|
||||
|
||||
Codex 证据会锁定 `@openai/codex@0.147.0`、`codex-cli 0.147.0` 与六个平台 alias。其真实产品测试会观测包内 wrapper argv、确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、原生权限模式、测试拥有临时存储中的显式危险绕过写入,以及 wrapper/原生整棵进程树退出。独立 wrapper fixture 会证明载荷缺失时不回退宿主命令,两个命名实例会保留彼此独立的环境与模式,生产环境也不会从 `PATH` 解析宿主 `codex`。[结构化失败事实决策](2026-08-18-product-subagent-failure-facts.zh.md)负责 schema、失败、进程结果与最终呈现证据。
|
||||
Codex 证据会锁定 `@openai/codex@0.149.1`、`codex-cli 0.149.1` 与六个平台 alias。生成 schema 会证明可选的 `ThreadStartParams.model`;真实产品测试会观测省略模型继承、两个显式实例模型、包内 wrapper argv、确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、原生权限模式、测试拥有临时存储中的显式危险绕过写入,以及 wrapper/原生整棵进程树退出。独立 wrapper fixture 会证明载荷缺失时不回退宿主命令,命名实例会保留彼此独立的模型、环境与模式,生产环境也不会从 `PATH` 解析宿主 `codex`。[最小诊断决策](../simplification/2026-08-21-product-subagent-minimal-diagnostics.zh.md)负责失败、进程结果与最终呈现证据。
|
||||
|
||||
带密钥 Codex e2e 会注册生产提供方,启动同样的真实 app-server,并通过上述测试专用桥接层请求一个随机数。该测试固定外部端点与模型,不存储任何凭据或请求载荷,要求上游恰好完成一次响应,将去除首尾空白后的产品答案与该随机数逐字节比较,并等待所有受管句柄退出。
|
||||
|
||||
Claude Code 证据会锁定 Agent SDK 0.3.220、Claude Code 2.1.220 与八个 SDK 平台包。真实产品测试会让 SDK 选择已安装载荷,断言共享子进程 argv 以该包的原生 CLI 开头,并观测确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、原生权限模式、测试拥有范围内的拒绝写入与 bypass 写入,以及整棵进程树退出。包测试还会证明生产运行从不解析宿主 `PATH`、省略可执行文件覆盖,并直接转发 SDK 所选的 Windows `claude.exe` 而不经过 batch shim。这项证据证明锁定的官方 SDK/CLI 集成,而不证明与独立安装的 Claude 版本兼容;[结构化失败事实决策](2026-08-18-product-subagent-failure-facts.zh.md)负责失败与进程结果证据。Loader 覆盖会通过各自的可选 Bundle patch 解析两个产品,且不会启动任一产品。
|
||||
Claude Code 证据会锁定 Agent SDK 0.3.241、Claude Code 2.1.241 与八个 SDK 平台包。真实产品测试会让 SDK 选择已安装载荷,断言共享子进程 argv 以该包的原生 CLI 开头,并观测省略模型继承、两个显式实例模型、确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、原生权限模式、测试拥有范围内的拒绝写入与 bypass 写入,以及整棵进程树退出。包测试还会证明生产运行从不解析宿主 `PATH`、省略可执行文件覆盖,并直接转发 SDK 所选的 Windows `claude.exe` 而不经过 batch shim。这项证据证明锁定的官方 SDK/CLI 集成,而不证明与独立安装的 Claude 版本兼容;[最小诊断决策](../simplification/2026-08-21-product-subagent-minimal-diagnostics.zh.md)负责失败与进程结果证据。Loader 覆盖会通过各自的可选 Bundle patch 解析两个产品,且不会启动任一产品。
|
||||
|
||||
带密钥 Claude Code e2e 仅在提供方的内存环境中映射密钥与固定的官方端点,把模型变量设为文档所示的 `deepseek-v4-pro[1m]` 与 `deepseek-v4-flash`,并实际经过生产提供方、官方 SDK 与真实 CLI。它将去除首尾空白后的结果与一个随机数比较,并证明整棵进程树退出,且测试不会直接调用 Messages API。
|
||||
|
||||
@@ -82,7 +82,7 @@ Claude Code 证据会锁定 Agent SDK 0.3.220、Claude Code 2.1.220 与八个 SD
|
||||
|
||||
**以产品替身作为强制证据。** 替身可以穷尽覆盖私有协议分支,但无法证明包导出、官方发行版、身份验证或真实进程行为。强制证据会驱动每个官方产品连接回环模型 fixture。
|
||||
|
||||
**由插件管理登录、产品主目录、模型、设置、沙箱规则或细粒度权限策略。** 这些选择会在每个产品的原生配置之外建立另一套权威来源,并将一次性提供方扩张为账户管理功能。两个产品除环境和清理配置外都只公开一个原生非交互模式选择;任一提供方都不会镜像产品规则或增加人工交互通道。
|
||||
**由插件管理登录、产品主目录、模型发现或 fallback、设置、沙箱规则或细粒度权限策略。** 这些选择会在每个产品的原生配置之外建立另一套权威来源,并将一次性提供方扩张为账户管理功能。官方产品支持时,提供方可以传入一个不透明的 Profile 模型覆盖,但不会发现、校验、解释别名或在模型间 fallback。任一提供方都不会镜像产品规则或增加人工交互通道。
|
||||
|
||||
**续接、进度、产品原生后台状态和共享父级上下文。** 提供方载荷仍是一项自包含任务的一个最终回答。通用 Job 层可以额外提供 id、状态、通知、收集与取消结果,但产品会话、恢复、后续交互、中间消息、父级 transcript(文本记录)传递、结构化输出和提供方专属后台状态都需要独立的用户约定,当前实现不会预先构建这些功能。
|
||||
|
||||
@@ -90,6 +90,6 @@ Claude Code 证据会锁定 Agent SDK 0.3.220、Claude Code 2.1.220 与八个 SD
|
||||
|
||||
用户通过由 Profile 配置、并由官方产品集成支持的一次性工具进行委派。显式 Profile 安装与 host plane 提供方放置由[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md)负责;命名实例身份与工具绑定由[命名实例决策](2026-08-18-product-subagent-named-instances.zh.md)负责;按 Preset 暴露工具以及默认前台且可选通用 Job 的调度方式由[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.zh.md)负责。本说明规定的提供方生命周期会保留原生设置与行为,而共享服务继续独占作业结算与进程树完全停稳的责任。
|
||||
|
||||
每次委派都要承担新建产品进程和独立模型上下文的开销。成功的产品载荷仍只有最终 assistant 文本;失败的产品运行可以另行公开共享安全诊断,其中包含由提供方拥有的权限事实,或锁定版本产品提供的结构化失败事实。后台调度还会额外公开通用 Job id、状态、完成通知以及收集或取消结果。两个产品都使用 Bundle 锁定的平台 CLI,并保留原生账户与工作区设置以及所选提供方权限模式。带密钥 e2e 运行还会消耗外部 API 配额,并依赖 DeepSeek 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。
|
||||
每次委派都要承担新建产品进程和独立模型上下文的开销。成功的产品载荷仍只有最终 assistant 文本;失败的产品运行可以另行公开共享安全诊断,其中包含由提供方拥有的权限事实与安全产品失败类别。后台调度还会额外公开通用 Job id、状态、完成通知以及收集或取消结果。两个产品都使用 Bundle 锁定的平台 CLI,并保留原生账户与工作区设置以及所选提供方权限模式;受支持的可选实例模型只覆盖该次运行的原生模型选择。带密钥 e2e 运行还会消耗外部 API 配额,并依赖 DeepSeek 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。
|
||||
|
||||
兼容性由包级单元测试覆盖率、无密钥真实产品回环测试、带密钥 DeepSeek 随机数测试、公开 Loader 组合、已构建包与 NodeNext 消费方检查、生成的文档与声明以及仓库 CI 矩阵共同锁定。更改受支持的产品基线或 DeepSeek 端点/模型基线时必须刷新这些事实;生产环境不会另行执行运行时版本探测。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-05-agent-teams.md
|
||||
2026-08-05-agent-teams.md: d37777a0cca467edcec5d38999aee53aaf14dc36
|
||||
2026-08-05-agent-teams.zh.md: 91571bd28aa44a21bebe1ef9cfc8434c2e8850a3
|
||||
2026-08-05-agent-teams.md: 9924550a04b636535ce1daa329865beb1c9e951d
|
||||
2026-08-05-agent-teams.zh.md: 91ae807f3005c173a61f9fc32661d2b650692a04
|
||||
|
||||
@@ -10,7 +10,7 @@ The subagent seam supplies fresh/fork providers, durable child Sessions, FIFO fo
|
||||
|
||||
All same-process Agents also share one checkout. Filesystem edit tools can reject an observed stale version, but Bash, formatters, generators, and external writers bypass that fence. Treating a teammate name or task owner as a file lock would hide rather than solve this concurrency boundary.
|
||||
|
||||
The model-visible Team tools remain opt-in so the default tool catalog and simple-task behavior do not change. An explicitly requested Team must survive child Activation settlement and mailbox delivery races long enough for the Lead to aggregate the result before process teardown.
|
||||
Agent Teams needs an explicit source-checkout composition before its public contracts are stable enough for released CLI or Web bundles. The default tool catalog and simple-task behavior must remain unchanged, while an explicitly requested Team must survive child Activation settlement and mailbox delivery races long enough for the Lead to aggregate the result before process teardown.
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -54,7 +54,7 @@ Worktree isolation is not a harness runtime behavior. A deployment or prompt may
|
||||
|
||||
**Create isolated worktrees automatically.** Rejected because worktree creation, branch naming, merge policy, ignored files, build artifacts, and cleanup are deployment choices. It also changes the same-world behavior existing subagents and sandboxes expose.
|
||||
|
||||
**Enable Teams in the default catalog.** Rejected because scoped Team controls would shadow same-named legacy globals and unsolicited delegation would add latency and token cost to simple tasks. Explicit composition keeps model-visible ownership unambiguous without changing shipped requests.
|
||||
**Enable Teams in the default catalog.** Rejected because scoped Team controls would shadow same-named legacy globals and unsolicited delegation would add latency and token cost to simple tasks. A private profile bundle inserts Team and disables the legacy controls without adding Team packages to shipped dependency graphs.
|
||||
|
||||
**Use an in-memory board and mailbox.** Rejected because child settlement, HMR, and process interruption would lose accepted coordination state and make retries ambiguous.
|
||||
|
||||
@@ -62,7 +62,7 @@ Worktree isolation is not a harness runtime behavior. A deployment or prompt may
|
||||
|
||||
## Testing
|
||||
|
||||
Package tests cover identity, name and authority checks, provider selection, reserved-id persistence collisions, child-before-Lead flush ordering, durable provisioning failure and pending-inbox JSONL/SQLite reconciliation, concurrent target-local ordering, pending/history de-duplication, mailbox limits, post-flush notification, bounded disposal with in-flight creation and dispatch cancellation, failed-member cleanup, task CAS and DAG validation, write-scope warnings, wait cancellation/timeout, inbox-preserving interruption, ordinary-fork isolation, legacy-control shadowing, compact declared-schema result rendering, and scoped registration HMR at per-file 100% coverage. A keyless headless Loader snapshot assembles the real Team plugins and records teammate creation, peer mail, dependent tasks, waiting, and Lead aggregation.
|
||||
Package tests cover identity, name and authority checks, provider selection, reserved-id persistence collisions, child-before-Lead flush ordering, durable provisioning failure and pending-inbox JSONL/SQLite reconciliation, concurrent target-local ordering, pending/history de-duplication, mailbox limits, post-flush notification, bounded disposal with in-flight creation and dispatch cancellation, failed-member cleanup, task CAS and DAG validation, write-scope warnings, wait cancellation/timeout, inbox-preserving interruption, ordinary-fork isolation, legacy-control shadowing, compact declared-schema result rendering, and scoped registration HMR at per-file 100% coverage. A keyless product snapshot loads the private Agent Teams profile bundle through `dsh --profile headless` and pins its complete model-visible tool list, Team policy, and durable workflow projection for two teammates, dependent tasks, peer delivery, waiting, completion, and aggregation. A CLI e2e reuses the same deterministic adapter and verifies normal process exit with persisted Team and child logs.
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ subagent seam 已提供 fresh/fork provider、持久 child Session、FIFO foll
|
||||
|
||||
同进程 Agent 还共享一个 checkout。文件系统 edit 工具可以拒绝已观察到的陈旧版本,但 Bash、formatter、generator 与外部 writer 会绕过该屏障。把 teammate name 或 task owner 当作文件锁只会掩盖而不是解决该并发边界。
|
||||
|
||||
面向模型的 Team 工具保持显式启用,使默认工具目录与简单任务行为不变。显式请求的 Team 必须能跨越 child Activation settlement 与 mailbox 投递竞争,使 Lead 在进程 teardown 前汇总结果。
|
||||
在公开约定稳定到足以进入已发布 CLI 或 Web bundle 前,Agent Teams 需要显式的源码 checkout 组合。默认工具目录与简单任务行为必须保持不变;而显式请求的 Team 必须能跨越 child Activation settlement 与 mailbox 投递竞争,使 Lead 在进程 teardown 前汇总结果。
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -54,7 +54,7 @@ Worktree isolation 不是 harness runtime 行为。deployment 或 prompt 可以
|
||||
|
||||
**自动创建隔离 worktree。** 拒绝,因为 worktree 创建、branch 命名、merge 策略、ignored file、构建产物与 cleanup 都是 deployment 选择;它也会改变既有 subagent 与 sandbox 暴露的 same-world 行为。
|
||||
|
||||
**在默认工具目录中启用 Team。** 拒绝,因为 scoped Team control 会覆盖同名旧全局工具,主动 delegation 也会给简单任务增加延迟和 token 成本。显式组合可以保持面向模型的归属明确,同时不改变默认 request。
|
||||
**在默认工具目录中启用 Team。** 拒绝,因为 scoped Team control 会覆盖同名旧全局工具,主动 delegation 也会给简单任务增加延迟和 token 成本。私有 profile bundle 会插入 Team 并禁用旧 control,同时不向已发布依赖图添加 Team 包。
|
||||
|
||||
**使用内存 task board 与 mailbox。** 拒绝,因为 child settlement、HMR 与进程中断会丢失已接受协调状态,并让重试变得含糊。
|
||||
|
||||
@@ -62,7 +62,7 @@ Worktree isolation 不是 harness runtime 行为。deployment 或 prompt 可以
|
||||
|
||||
## Testing
|
||||
|
||||
Package test 以逐文件 100% coverage 覆盖身份、名字与权限检查、provider 选择、预留 id 持久化冲突、child-before-Lead flush 顺序、持久 provisioning 失败与 pending-inbox JSONL/SQLite 对账、target-local 并发顺序、pending/history 去重、mailbox 限额、flush 后 notification、取消在途创建与 dispatch 的有界 dispose、failed member cleanup、task CAS 与 DAG 校验、write-scope warning、wait cancel/timeout、保留 inbox 的 interrupt、普通 fork 隔离、旧 control shadowing、声明 schema 的紧凑结果渲染与 scoped registration HMR。一条 keyless headless Loader 快照会组合真实 Team 插件,并记录 teammate 创建、peer mail、依赖任务、等待与 Lead 汇总。
|
||||
Package test 以逐文件 100% coverage 覆盖身份、名字与权限检查、provider 选择、预留 id 持久化冲突、child-before-Lead flush 顺序、持久 provisioning 失败与 pending-inbox JSONL/SQLite 对账、target-local 并发顺序、pending/history 去重、mailbox 限额、flush 后 notification、取消在途创建与 dispatch 的有界 dispose、failed member cleanup、task CAS 与 DAG 校验、write-scope warning、wait cancel/timeout、保留 inbox 的 interrupt、普通 fork 隔离、旧 control shadowing、声明 schema 的紧凑结果渲染与 scoped registration HMR。一条 keyless 产品快照会通过 `dsh --profile headless` 加载私有 Agent Teams profile bundle,并为两个 teammate、依赖任务、peer 投递、等待、完成和汇总固定完整的面向模型工具列表、Team policy 与持久 workflow 投影。CLI e2e 会复用同一个确定性 adapter,并验证带持久 Team 与 child 日志的正常退出。
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.md
|
||||
2026-08-15-product-subagent-noninteractive-permissions.md: 8788fba3492e08090dd038fc3e7377f6bd1e29cd
|
||||
2026-08-15-product-subagent-noninteractive-permissions.zh.md: cbf3c3cd14fcecd2c24e335a8b71cc3b5370e247
|
||||
2026-08-15-product-subagent-noninteractive-permissions.md: 8ce71cbf156d3e22a22fa8ad526b838ec1a5effe
|
||||
2026-08-15-product-subagent-noninteractive-permissions.zh.md: e6471b2ce1c8ebbb767254df2426969a3da36f78
|
||||
|
||||
+6
-6
@@ -26,13 +26,13 @@ Claude Code defaults to `dontAsk` and accepts only the native non-interactive mo
|
||||
| `plan` | Use planning mode, deny execution approval, and return the completed plan as the final answer. |
|
||||
| `bypassPermissions` | Set the SDK's explicit dangerous confirmation and bypass permission checks. |
|
||||
|
||||
The Provider continues to omit `settingSources`: Claude Code remains the owner of user, project, and local settings, authentication, tools, and sandbox behavior outside the selected mode.
|
||||
The Provider continues to omit `settingSources`: an optional instance-level model is a separate direct SDK override, while Claude Code remains the owner of user, project, and local settings, authentication, tools, and sandbox behavior outside the selected mode.
|
||||
|
||||
Every query disables `AskUserQuestion`. Non-bypass permission callbacks deny instead of returning the SDK's indefinitely blocking `null`; plan mode also places `ExitPlanMode` in `disallowedTools`, so native allow rules cannot switch the unattended query back to execution. MCP elicitation is declined; the supported refusal dialog is cancelled; undeclared dialog kinds use the SDK's no-dialog failure behavior. A native `permission_denied` message records the same operation-local fact. These paths do not create an approval session, queue, cache, or retry loop.
|
||||
|
||||
### Codex
|
||||
|
||||
Codex defaults to `never` and accepts the three native non-interactive modes exposed by Codex 0.147.0. The Provider starts the fixed app-server command, then maps the selected mode into official `thread/start` fields because CLI-global permission flags do not configure threads created later by an app-server client:
|
||||
Codex defaults to `never` and accepts the three native non-interactive modes exposed by Codex 0.149.1. The Provider starts the fixed app-server command, then maps the selected mode into official `thread/start` fields because CLI-global permission flags do not configure threads created later by an app-server client:
|
||||
|
||||
| Value | `thread/start` fields | Native behavior |
|
||||
| --- | --- | --- |
|
||||
@@ -40,13 +40,13 @@ Codex defaults to `never` and accepts the three native non-interactive modes exp
|
||||
| `approve-for-me` | `approvalPolicy: on-request`, `approvalsReviewer: auto_review`, `sandbox: workspace-write` | Route permission requests through Codex automatic review. |
|
||||
| `dangerously-bypass-approvals-and-sandbox` | `approvalPolicy: never`, `sandbox: danger-full-access` | Skip approval and sandbox enforcement. |
|
||||
|
||||
The Provider overrides only those thread fields. `CODEX_HOME`, project configuration, model/provider selection, MCP, hooks, skills, authentication, and sandbox facts not selected by the mode remain native Codex state. The wire still denies any unexpected approval, permission, user-input, or MCP request rather than opening a dynamic allow path.
|
||||
The Provider overrides only those permission and sandbox fields. An optional instance-level model is a separate direct `thread/start` override; `CODEX_HOME`, project configuration, model-provider selection, MCP, hooks, skills, authentication, and sandbox facts not selected by the mode remain native Codex state. The wire still denies any unexpected approval, permission, user-input, or MCP request rather than opening a dynamic allow path.
|
||||
|
||||
### Failure diagnostic
|
||||
|
||||
`SubagentResult` carries an optional `diagnostic` for provider-authored, non-assistant failure detail. A Provider removes tool inputs, file contents, environment values, credentials, and raw protocol payloads before producing it. The shared out-of-process result boundary limits the complete text to 4096 UTF-8 bytes and marks truncation without splitting a character. The [structured failure-facts decision](2026-08-18-product-subagent-failure-facts.md) owns non-permission product categories, lifecycle stages, and process outcomes carried by the same field.
|
||||
`SubagentResult` carries an optional `diagnostic` for provider-authored, non-assistant failure detail. A Provider removes tool inputs, file contents, environment values, credentials, and raw protocol payloads before producing it. The shared out-of-process result boundary limits the complete text to 4096 UTF-8 bytes and marks truncation without splitting a character. The [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns both products' non-permission action categories, lifecycle stages, HTTP facts, and process outcomes carried by the same field.
|
||||
|
||||
Each product's permission fact contains only the effective mode, request category, unattended decision, and a fixed safe reason. Claude Code derives those facts from SDK callbacks and `permission_denied` messages. Codex derives them from app-server requests, declined items, `sandboxError`, and two fixed permission signatures in a bounded stderr tail; raw stderr is still forwarded to the Host but never copied into the diagnostic. Both Providers place their structured failure line before the latest contributing permission fact. A successful result returns only the strict final answer; local cancellation remains `aborted` without permission detail; an unpublished startup failure still rejects `start()`. The Provider never adds either diagnostic fact to assistant output, structured output, or `subagent/end.lastAssistantMessage`.
|
||||
Each product's permission fact contains only the effective mode, request category, unattended decision, and a fixed safe reason. Claude Code derives those facts from SDK callbacks and `permission_denied` messages. Codex derives them from app-server requests, declined items, and structured `sandboxError` terminals. Raw stderr is forwarded to the Host but is neither classified nor copied into the diagnostic. Both Providers place their failure line before the latest contributing permission fact. A successful result returns only the strict final answer; local cancellation remains `aborted` without permission detail; an unpublished startup failure still rejects `start()`. The Provider never adds either diagnostic fact to assistant output, structured output, or `subagent/end.lastAssistantMessage`.
|
||||
|
||||
The foreground consumer presents the stop-reason headline, then the optional diagnostic, then any partial assistant output. The one-shot background adapter stores the same diagnostic beside the stop reason in the failed Job detail. Providers that omit the field retain their previous behavior.
|
||||
|
||||
@@ -63,7 +63,7 @@ The foreground consumer presents the stop-reason headline, then the optional dia
|
||||
|
||||
## Verification
|
||||
|
||||
Package tests pin every allowed and rejected Config value, the exact SDK and app-server field mappings, dangerous confirmations, unattended terminal responses, diagnostic sanitization and UTF-8 bound, successful-result omission, concurrent-run isolation, foreground ordering, Job detail, stderr observer disposal, and process cleanup. The real Claude Agent SDK/CLI fixture proves its safe default, restricted denial, explicit bypass, and whole-tree quiescence. The real Codex app-server fixture proves that thread-level `never` overrides ambient `on-request`, automatic review starts, dangerous bypass writes only inside suite-owned temporary storage, fixed stderr signatures produce safe diagnostics, and the wrapper/native tree exits. Loader composition proves non-default modes can be published without starting either product, and the keyless ACP snapshot records each product's failure diagnostic through foreground and Job presentation while the model-facing product tool schemas contain no permission parameter.
|
||||
Package tests pin every allowed and rejected Config value, the exact SDK and app-server field mappings, dangerous confirmations, unattended terminal responses, diagnostic sanitization and UTF-8 bound, successful-result omission, concurrent-run isolation, foreground ordering, Job detail, stderr observer disposal, and process cleanup. The real Claude Agent SDK 0.3.241 and Claude Code 2.1.241 fixture proves its safe default, restricted denial, explicit bypass, and whole-tree quiescence. The real Codex 0.149.1 app-server fixture proves that thread-level `never` overrides ambient `on-request`, automatic review starts, dangerous bypass writes only inside suite-owned temporary storage, a rejected escalation leaves no side effect or raw command or path in the diagnostic, stderr remains Host-only, and the wrapper/native tree exits. Loader composition proves non-default modes can be published without starting either product, and the keyless ACP snapshot records each product's failure diagnostic through foreground and Job presentation while the model-facing product tool schemas contain no permission parameter.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
+6
-6
@@ -26,13 +26,13 @@ Claude Code 默认使用 `dontAsk`,而且只接受锁定版本 Agent SDK 支
|
||||
| `plan` | 使用规划模式,拒绝执行审批,并把完整计划作为最终答案返回。 |
|
||||
| `bypassPermissions` | 设置 SDK 的显式危险确认并跳过权限检查。 |
|
||||
|
||||
提供方继续省略 `settingSources`:除所选模式以外,用户、项目和本地设置、身份验证、工具与沙箱行为仍由 Claude Code 拥有。
|
||||
提供方继续省略 `settingSources`:可选的实例级模型是独立的直接 SDK 覆盖;除所选模式以外,用户、项目和本地设置、身份验证、工具与沙箱行为仍由 Claude Code 拥有。
|
||||
|
||||
每次 query 都禁用 `AskUserQuestion`。非 bypass 模式的权限回调会拒绝请求,而不会返回 SDK 中会无限阻塞的 `null`;plan 模式还会把 `ExitPlanMode` 放入 `disallowedTools`,因此原生 allow 规则无法把无人值守 query 切回执行模式。MCP elicitation 会被拒绝;已支持的拒绝对话会被取消;未声明的对话类型使用 SDK 的无对话失败行为。原生 `permission_denied` 消息会记录同一份当前运行事实。这些路径不会创建审批会话、队列、缓存或重试循环。
|
||||
|
||||
### Codex
|
||||
|
||||
Codex 默认使用 `never`,并接受 Codex 0.147.0 公开的三种原生非交互模式。提供方启动固定的 app-server 命令,再把所选模式映射为官方 `thread/start` 字段,因为 CLI 全局权限 flag 不会配置之后由 app-server 客户端创建的线程:
|
||||
Codex 默认使用 `never`,并接受 Codex 0.149.1 公开的三种原生非交互模式。提供方启动固定的 app-server 命令,再把所选模式映射为官方 `thread/start` 字段,因为 CLI 全局权限 flag 不会配置之后由 app-server 客户端创建的线程:
|
||||
|
||||
| 值 | `thread/start` 字段 | 原生行为 |
|
||||
| --- | --- | --- |
|
||||
@@ -40,13 +40,13 @@ Codex 默认使用 `never`,并接受 Codex 0.147.0 公开的三种原生非交
|
||||
| `approve-for-me` | `approvalPolicy: on-request`、`approvalsReviewer: auto_review`、`sandbox: workspace-write` | 由 Codex 自动评审权限请求。 |
|
||||
| `dangerously-bypass-approvals-and-sandbox` | `approvalPolicy: never`、`sandbox: danger-full-access` | 跳过审批与 sandbox。 |
|
||||
|
||||
提供方只覆盖这些线程字段。`CODEX_HOME`、项目配置、模型/provider 选择、MCP、hook、skill、身份验证,以及模式未选择的 sandbox 事实仍属于 Codex 原生状态。wire 仍会拒绝任何意外到达的审批、权限、用户输入或 MCP 请求,而不会开放动态 allow 通道。
|
||||
提供方只覆盖这些权限与 sandbox 字段。可选的实例级模型是独立的直接 `thread/start` 覆盖;`CODEX_HOME`、项目配置、模型 provider 选择、MCP、hook、skill、身份验证,以及模式未选择的 sandbox 事实仍属于 Codex 原生状态。wire 仍会拒绝任何意外到达的审批、权限、用户输入或 MCP 请求,而不会开放动态 allow 通道。
|
||||
|
||||
### 失败诊断
|
||||
|
||||
`SubagentResult` 携带可选的 `diagnostic`,用于提供方产生且不属于 assistant 内容的失败说明。提供方在生成它之前会排除工具输入、文件内容、环境值、凭证与原始协议载荷。共享的进程外结果边界会把完整文本限制在 4096 个 UTF-8 字节以内,并在不切断字符的前提下标记截断。[结构化失败事实决策](2026-08-18-product-subagent-failure-facts.zh.md)负责由同一字段承载的非权限产品类别、生命周期阶段与进程结果。
|
||||
`SubagentResult` 携带可选的 `diagnostic`,用于提供方产生且不属于 assistant 内容的失败说明。提供方在生成它之前会排除工具输入、文件内容、环境值、凭证与原始协议载荷。共享的进程外结果边界会把完整文本限制在 4096 个 UTF-8 字节以内,并在不切断字符的前提下标记截断。[最小诊断决策](../simplification/2026-08-21-product-subagent-minimal-diagnostics.zh.md)负责两个产品通过同一字段承载的非权限行动类别、生命周期阶段、HTTP 事实与进程结果。
|
||||
|
||||
每个产品的权限事实都只包含有效模式、请求类别、无人值守决定与固定的安全原因。Claude Code 从 SDK 回调和 `permission_denied` 消息取得这些事实。Codex 从 app-server 请求、被拒绝的 item、`sandboxError` 与每次运行有界 stderr 尾部中的两个固定权限签名取得事实;原始 stderr 仍会转发给 Host,但绝不会复制进诊断。两个提供方都会把结构化失败行放在最新参与失败的权限事实之前。成功结果只返回严格的最终答案;本地取消仍以 `aborted` 结算且不附带权限说明;未发布的启动失败仍会拒绝 `start()`。提供方绝不会把任一诊断事实写入 assistant 输出、结构化输出或 `subagent/end.lastAssistantMessage`。
|
||||
每个产品的权限事实都只包含有效模式、请求类别、无人值守决定与固定的安全原因。Claude Code 从 SDK 回调和 `permission_denied` 消息取得这些事实。Codex 从 app-server 请求、被拒绝的 item 与结构化 `sandboxError` 终态取得事实。原始 stderr 仍会转发给 Host,但既不会被分类,也绝不会复制进诊断。两个提供方都会把失败行放在最新参与失败的权限事实之前。成功结果只返回严格的最终答案;本地取消仍以 `aborted` 结算且不附带权限说明;未发布的启动失败仍会拒绝 `start()`。提供方绝不会把任一诊断事实写入 assistant 输出、结构化输出或 `subagent/end.lastAssistantMessage`。
|
||||
|
||||
前台消费方依次呈现终止原因标题、可选诊断和任何部分 assistant 输出。一次性后台适配器会在失败 Job 的 detail 中,把同一诊断与终止原因一起保存。没有填写该字段的提供方保持原有行为。
|
||||
|
||||
@@ -63,7 +63,7 @@ Codex 默认使用 `never`,并接受 Codex 0.147.0 公开的三种原生非交
|
||||
|
||||
## Verification
|
||||
|
||||
包测试固定所有允许与拒绝的 Config 值、准确的 SDK 与 app-server 字段映射、危险确认、无人值守终态、诊断脱敏与 UTF-8 上限、成功结果不携带诊断、并发运行隔离、前台顺序、Job detail、stderr observer 释放和进程清理。真实 Claude Agent SDK/CLI fixture 证明其安全默认、受限拒绝、显式 bypass 与整棵进程树完全停稳。真实 Codex app-server fixture 证明线程级 `never` 覆盖环境中的 `on-request`、自动评审可以启动、危险绕过只在测试拥有的临时存储中写入、固定 stderr 签名产生安全诊断,而且 wrapper/native 进程树会退出。Loader 组装证明非默认模式可以在不启动任一产品的情况下发布;无密钥 ACP snapshot 则记录每个产品的失败诊断如何经过前台与 Job 呈现,同时面向模型的产品工具 schema 不包含权限参数。
|
||||
包测试固定所有允许与拒绝的 Config 值、准确的 SDK 与 app-server 字段映射、危险确认、无人值守终态、诊断脱敏与 UTF-8 上限、成功结果不携带诊断、并发运行隔离、前台顺序、Job detail、stderr observer 释放和进程清理。真实 Claude Agent SDK 0.3.241 与 Claude Code 2.1.241 fixture 证明其安全默认、受限拒绝、显式 bypass 与整棵进程树完全停稳。真实 Codex 0.149.1 app-server fixture 证明线程级 `never` 覆盖环境中的 `on-request`、自动评审可以启动、危险绕过只在测试拥有的临时存储中写入、被拒绝的提权不会留下副作用且诊断不含原始命令或路径、stderr 只供 Host 观测,而且 wrapper/native 进程树会退出。Loader 组装证明非默认模式可以在不启动任一产品的情况下发布;无密钥 ACP snapshot 则记录每个产品的失败诊断如何经过前台与 Job 呈现,同时面向模型的产品工具 schema 不包含权限参数。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-18-conversation-adaptive-content-width.md
|
||||
2026-08-18-conversation-adaptive-content-width.md: 9b0e7afbded296b60b7c283030a537321b36684e
|
||||
2026-08-18-conversation-adaptive-content-width.zh.md: f1f424cf9d4ac087518e9f79a365d0052069b999
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Adaptive and drag-resizable conversation content width
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-18-conversation-adaptive-content-width.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The conversation column's shared width axis (`--dsh-chat-content-width`) was the fixed figma constant 748px. On wide monitors (a 4000px display leaves a ~3500px column) the transcript occupied under a quarter of the column with dead margins on both sides. Every derived surface — the input card (W + 32px), dock cards, takeover panels, StatsLine, the back-to-bottom padding formula — rides this one variable, so any change had to keep the whole column's alignment relations intact. Alongside the adaptive default, users asked for direct control: hover the transcript's side margins to get a col-resize cursor and drag either edge, with both edges moving symmetrically.
|
||||
|
||||
## Decision
|
||||
|
||||
**The axis becomes a user override over an adaptive clamp.** `ConversationRoot.module.css` declares `--dsh-chat-content-width: var(--dsh-chat-user-width, clamp(680px, calc(var(--dsh-conversation-column-width, 0px) * 0.64), 920px))`. The floor is 680px — one step under the figma 748px, after full-width reading felt wide on every screen — wider columns take 64% of the column, and 920px caps line length for readability (~113 characters at the base font). A dragged preference replaces the adaptive term wholesale.
|
||||
|
||||
**The column width is published by a ResizeObserver, not container queries.** The component publishes the root's `offsetWidth` as `--dsh-conversation-column-width` in px (the same callback-ref pattern as the existing composer seat height observer). `container-type: inline-size` was rejected: the conversation subtree contains portal-free `position: fixed` descendants (Tooltip, Menu, JsonTree copy anchors) whose viewport anchoring a size container would capture — the same class of trap the `.composerHero` comment records for transforms. A bare `%` in the variable was rejected because custom-property percentages resolve per consumer against different containing blocks, breaking the input-card = W + 32px invariant; `vw` was rejected because the column is not the viewport (sidebar fold changes the column only).
|
||||
|
||||
**Drag handles are 40px strips beside the transcript, symmetric by construction.** Each strip's inner edge sits 24px outside the content column and extends 40px outward, with the outer edge clamped to keep a 24px safe zone from the column edges (24 + 40 + 24 = the 88px-per-side budget below); when the margin cannot fit inset + strip + safe zone the computed width goes negative and the strip resolves to zero. Both handles write the one centered width — outward travel widens by 2× the pointer distance — reusing AppFrame's DragHandle capture model (pointer capture + rAF throttle + drag-start snapshot); only a gesture with actual pointer travel commits to storage, so a bare press-and-release on a window-clamped width cannot overwrite the wider stored preference. The hover indicator is a 3px glow riding the pointer's Y (published as `--dsh-width-handle-pointer-y` on pointermove): a 24px solid core fading over 40px each side, in the scrollbar hover tint because border-token alphas disappear against the base fill. Handles render only in the active phase; views that elect a composer overlay (trajectory) hide them, and the header lifts above them (z-index 9) to stay clickable.
|
||||
|
||||
**The preference persists in `localStorage` (`dsh.conversation.contentWidth`) and clamps without rewriting.** The displayed width re-clamps to `[640px, column − 176px]` when the column shrinks (88px per side keeps the handles fully placeable — a wider drag would push its own handles off the column), but the stored preference survives — widening the window restores it, the same rule AppFrame's sidebar drag follows. The handle carries no reset affordance and no tooltip; a stored preference is only ever replaced by another drag.
|
||||
|
||||
**The user bubble cap follows the axis.** `min(525px, 82%)` becomes `min(calc(var(--dsh-chat-content-width, 748px) * 0.702), 82%)` (0.702 = 525/748, the figma bubble share of the figma column) in both `ui-conversation` MessageItem and the symmetric `ui-goal` command bubble, so bubbles scale with the column. The 748px fallback covers mounts outside the conversation column.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Raise the constant (748 → ~850).** Rejected: every mid-width window's line length grows too, hurting readability where most users live.
|
||||
|
||||
**Wide-content bleed (code blocks and tool cards break out of the prose column).** Best reading ergonomics but touches MarkdownText and every tool card's layout; deferred as a possible second phase.
|
||||
|
||||
**A settings-backed "wide mode" toggle.** Adds a persistent settings surface for what drag already covers; not needed.
|
||||
|
||||
**A 12px handle strip beside the input card.** Shipped first and unusable in practice: on a wide screen the strip was a sliver in a thousand-plus pixels of margin, and the sticky input card overlapped it. Replaced by the 40px strip anchored to the glow line's position.
|
||||
|
||||
## Consequences
|
||||
|
||||
Ordinary windows read slightly narrower than the figma baseline (680px floor). Wide columns widen the transcript to at most 920px, and a drag can take it anywhere in `[640px, column − 176px]`, both without touching any derived surface: input card, dock cards, takeover panels, and the back-to-bottom formula follow the axis they already consumed. A known ~4px centering offset between the handle (column-centered) and the content box (centered after scrollbar-gutter reservation) stays well inside the 40px strip. The 680px / 64% / 920px numbers are one declaration in `ConversationRoot.module.css` mirrored by `resolveContentWidth` in the component; retuning them touches nothing else.
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# Agent Note:会话正文宽度自适应与拖拽调宽
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-18-conversation-adaptive-content-width.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
会话列的共享宽度轴(`--dsh-chat-content-width`)是 figma 定值 748px。在宽显示器上(4000px 屏幕的会话列约 3500px)正文只占列宽不到四分之一,两侧是大片空白边距。所有派生表面——输入卡(W + 32px)、dock 卡片、takeover 面板、StatsLine、回底按钮的 padding 公式——都由这一个变量推导,任何改动都必须保持整列的对齐关系。在自适应默认值之外,用户还要求直接控制:hover 正文两侧边距出现 col-resize 光标,拖任一侧、两侧对称联动。
|
||||
|
||||
## 决策
|
||||
|
||||
**宽度轴变为"用户覆盖 + 自适应 clamp"。** `ConversationRoot.module.css` 声明 `--dsh-chat-content-width: var(--dsh-chat-user-width, clamp(680px, calc(var(--dsh-conversation-column-width, 0px) * 0.64), 920px))`。下限 680px——比 figma 的 748px 低一档,因为满宽阅读在各种屏幕上都显宽——更宽的列取列宽的 64%,920px 封顶保证行长可读性(基准字号下约 113 字符)。拖拽偏好存在时整体替换自适应项。
|
||||
|
||||
**列宽由 ResizeObserver 发布,不用容器查询。** 组件把根节点的 `offsetWidth` 以 px 发布为 `--dsh-conversation-column-width`(与既有 composer seat 高度 observer 相同的 callback-ref 模式)。拒绝 `container-type: inline-size`:会话子树内有不经 portal 的 `position: fixed` 后代(Tooltip、Menu、JsonTree 复制锚点),尺寸容器会捕获它们的视口定位——与 `.composerHero` 注释记录的 transform 陷阱同类。拒绝变量里的裸 `%`:自定义属性百分比在各消费点按不同包含块解析,破坏输入卡 = W + 32px 不变量;拒绝 `vw`:列不等于视口(侧栏折叠只改列宽)。
|
||||
|
||||
**拖拽手柄是正文两侧 40px 宽的条,对称是构造性的。** 每条内边缘位于内容列外 24px、向外延伸 40px,外边缘被钳制在距列缘至少 24px 的安全区(24 + 40 + 24 = 下文每侧 88px 的预算);边距装不下"内偏移 + 热区 + 安全区"时计算宽度为负、热区解析为零。两个手柄写同一个居中宽度——向外拖按指针位移 2 倍变宽——复用 AppFrame DragHandle 的捕获模型(指针捕获 + rAF 节流 + 拖拽起点快照);只有指针确实产生位移的手势才提交存储,因此在被窗口钳制的宽度上按下即松开不会用钳制后的显示值覆盖更宽的已存偏好。hover 提示是跟随指针 Y 的 3px 光带(pointermove 发布 `--dsh-width-handle-pointer-y`):24px 实色核心、两侧各 40px 渐变,用滚动条 hover 色——border token 的透明度在底色上几乎不可见。手柄只在 active 阶段渲染;选举了 composer overlay 的视图(trajectory)隐藏手柄,header 提升到手柄之上(z-index 9)保持可点。
|
||||
|
||||
**偏好持久化在 `localStorage`(`dsh.conversation.contentWidth`),钳制不改写。** 列收窄时显示宽度重新钳制到 `[640px, 列宽 − 176px]`(每侧预留 88px 保证手柄永远放得下),但存储的偏好保留——拉宽窗口自动恢复,与 AppFrame 侧栏拖拽同规则。手柄不带重置操作也不带 tooltip;已存储的偏好只会被下一次拖拽替换。
|
||||
|
||||
**用户气泡上限跟随宽度轴。** `min(525px, 82%)` 改为 `min(calc(var(--dsh-chat-content-width, 748px) * 0.702), 82%)`(0.702 = 525/748,即 figma 气泡占 figma 列宽的比例),`ui-conversation` MessageItem 与对称的 `ui-goal` 命令气泡同步,气泡随列缩放。748px 缺省值覆盖会话列之外的挂载。
|
||||
|
||||
## 备选方案
|
||||
|
||||
**调大常量(748 → 约 850)。** 拒绝:所有中等宽度窗口的行长一起变长,伤及多数用户的可读性。
|
||||
|
||||
**宽内容出血(代码块、工具卡片突破散文列)。** 阅读工效最佳但涉及 MarkdownText 和所有工具卡片布局;作为可能的二期推迟。
|
||||
|
||||
**settings 支持的"宽屏模式"开关。** 为拖拽已覆盖的能力增加持久设置面;不需要。
|
||||
|
||||
**输入卡旁 12px 手柄条。** 首版实现,实践中不可用:宽屏上千余像素的边距里只有一条细缝,且 sticky 输入卡遮挡它。改为锚定在光带位置的 40px 条。
|
||||
|
||||
## 影响
|
||||
|
||||
普通窗口的阅读宽度比 figma 基线略窄(下限 680px)。宽列正文最多放宽到 920px,拖拽可取 `[640px, 列宽 − 176px]` 内任意值,两者都不触碰任何派生表面:输入卡、dock 卡片、takeover 面板和回底公式沿用它们本就消费的宽度轴。手柄(按列居中)与内容盒(按滚动条预留后居中)之间约 4px 的已知偏差完全落在 40px 热区内。680px / 64% / 920px 三个数值在 `ConversationRoot.module.css` 一处声明、由组件内 `resolveContentWidth` 镜像;重调它们不影响其他代码。
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md
|
||||
2026-08-18-model-selected-subagent-routes.md: 1602e3ac90870edbd0206cd87fdf97ecc34cad41
|
||||
2026-08-18-model-selected-subagent-routes.zh.md: 0dad8b9d030e6de65cb3fa1e0e93ad7c28bcc5c1
|
||||
2026-08-18-model-selected-subagent-routes.md: bf4788b141370933197d9ec1a1ad3c8e76a6740c
|
||||
2026-08-18-model-selected-subagent-routes.zh.md: 1d83e2e91ffe87fff7f8e9d1988320cb2bb8f2f7
|
||||
|
||||
@@ -14,9 +14,9 @@ The model also needs a bounded way to discover live providers and model-owned ef
|
||||
|
||||
`dsh-tool-subagent` exposes optional `provider`, `model`, and `reasoning_effort` fields only when its instance enables `enableModelSelection`, or its Agent-scoped `modelSelectionSettings` instance resolves an enabled Session decision, and the bound subagent provider advertises `SubagentCapabilities.agentOptions`. No route allowlist is required. Registered LLM provider routes are available for child selection; this tool does not add a second authorization policy over the deployment's LLM registry. Disabled instances omit and reject model-facing selection, while configured `Config.agentOptions` remain deployment-owned defaults. Either selection mode against a provider without the capability fails the plugin mount.
|
||||
|
||||
Provider and model form one route and must be supplied together. An effort may be supplied alone when configured or parent values provide the effective route. Model arguments override `Config.agentOptions`, and configured fields override the parent Agent's latest logged request selection; creation options supply the fallback before its first request and retain the configured output-token limit. Reasoning-effort identifiers remain adapter-owned. An unchanged route inherits an omitted effort, while changing provider or model without naming an effort clears the lower layer's route-owned value so the selected model resolves its own default. `AgentOptions` carries the resulting effort into the child loop, whose request header logs the effective value. A continuable descriptor records it with the resolved provider and model so a child that has not logged its first request can cold-resume with the same selection.
|
||||
Provider and model form one route and must be supplied together. An effort may be supplied alone when configured, parent, or provider-owned route defaults provide the effective route. Static `provider.agentRouteDefaults`, when present, establish the provider/model baseline; `Config.agentOptions` and model arguments overlay it before route-aware effort clearing. Providers without static defaults use compatible fields from the parent Agent's latest logged request selection, with creation options supplying the fallback before its first request and retaining the configured output-token limit. Reasoning-effort identifiers remain adapter-owned. An unchanged route inherits an omitted effort only from the selected baseline; changing provider or model without naming an effort clears the lower layer's route-owned value so the selected model resolves its own default. `AgentOptions` carries the resulting effort into the child loop, whose request header logs the effective value. A continuable descriptor records it with the resolved provider and model so a child that has not logged its first request can cold-resume with the same selection.
|
||||
|
||||
An explicit or configured provider, model, or effort resolves through `ctx.llm.resolveCallConfig()` before child creation. That lookup owns provider registration, exact-model metadata, reasoning-effort validation, and adapter defaults. The tool checks cancellation again after the asynchronous lookup and before creating a child or background job. Calls with no model-facing selection and no configured route fields preserve the existing provider path without requiring the optional LLM service.
|
||||
An explicit or configured provider, model, or effort resolves through `ctx.llm.resolveCallConfig()` after the provider baseline and request precedence are complete. Providers with static route defaults suppress parent-effort inheritance when the request omits effort, preserving the selected model's default. The LLM lookup owns provider registration, exact-model metadata, reasoning-effort validation, and adapter defaults. After the asynchronous lookup, the tool checks cancellation and confirms the same provider instance remains registered before creating a child or background job, so HMR cannot combine one provider's defaults with another provider's process. Calls with no model-facing selection and no configured route fields preserve the existing provider path without requiring the optional LLM service.
|
||||
|
||||
An enabled definition registers `list_subagent_models`. With no arguments the tool lists registered providers; with `provider` it calls that adapter's advisory model catalog; with `provider` and `model` it resolves the exact model and returns its reasoning efforts and default. At most one instance in a tool scope enables selection because the discovery name is global. Shipped product compositions put `modelSelectionSettings: true` on the primary Agent-scoped `subagent` instance and register the Host-owned `subagent-model-selection` settings namespace with `enabled: false`. A new top-level Session samples that preference during composition and logs an enabled decision as `subagent/model-selection-enabled` before any model request. A child Session inherits the live parent's decision, and a resumed Session uses its existing marker instead of the current preference. Therefore a settings edit affects only subsequently composed top-level Sessions. The fixed discovery definition remains available without the optional LLM service, while discovery and selected-route calls fail until that service is present. An unlisted model remains selectable when the adapter accepts its id.
|
||||
|
||||
@@ -24,7 +24,7 @@ Shipped `subagent_fork` instances leave `enableModelSelection` disabled even tho
|
||||
|
||||
The delegation definition is static across adapter registration and catalog changes, so live topology neither expands every parent request nor invalidates its cache prefix. The discovery result enters the transcript only when called. A custom inheritance-capable instance that enables selection warns that changing provider or model can prevent provider-side reuse of the inherited conversation prefix.
|
||||
|
||||
`SubagentCapabilities.agentOptions` remains the transport truth. The service rejects a request carrying those options before calling a provider that advertises `false`. Both in-process providers advertise `true`; the current ACP, Codex, Claude Code, and DSH SDK transports advertise `false`. Tool configuration that supplies `agentOptions`, statically enables model selection, or makes it settings-controlled also fails when its bound provider lacks the capability.
|
||||
`SubagentCapabilities.agentOptions` remains the transport truth. The service rejects a request carrying those options before calling a provider that advertises `false`. Both in-process providers and the DSH SDK transport advertise `true`; DSH SDK publishes its provider/model defaults as detached immutable data for Consumer preflight, while `start()` independently applies the same Config defaults plus maxTokens for direct callers and child initialization. ACP, Codex, and Claude Code advertise `false`. Tool configuration that supplies `agentOptions`, statically enables model selection, or makes it settings-controlled also fails when its bound provider lacks the capability.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -51,10 +51,10 @@ The delegation definition is static across adapter registration and catalog chan
|
||||
- An enabled delegation tool can select any live child LLM route without deployment selector configuration; disabled instances omit and reject model-facing route fields.
|
||||
- The primary delegation-tool instance defaults selection off, exposes a Models-page opt-in for new Sessions, and registers `list_subagent_models` only in Sessions whose durable decision is enabled; its catalog rows do not restrict delegation.
|
||||
- Shipped fork tools inherit the parent's provider and model and omit model-facing route fields so the inherited conversation prefix remains eligible for KV Cache reuse.
|
||||
- Omission retains configured defaults and compatible inheritance from the parent's latest logged request; a route change without an explicit effort uses the selected model's default.
|
||||
- Omission retains configured defaults plus static provider route defaults or compatible parent inheritance; a route change without an explicit effort uses the selected model's default.
|
||||
- Adapter catalog and topology changes leave the delegation definition and its prompt-cache prefix unchanged.
|
||||
- Out-of-process subagent providers reject configured and model-selected Agent options until they implement and advertise the capability.
|
||||
- Unit coverage owns the default-off Host preference, new-Session sampling, child inheritance, resumed decisions, opt-in schema and execution enforcement, merge precedence, route-aware effort inheritance, preflight cancellation, live discovery, diagnostics, definition stability, capability rejection, and optional-service behavior. A shipped headless snapshot pins inheritance from a logged parent selection; the shipped examples also own the assembled keyless model-visible schemas.
|
||||
- DSH SDK children accept configured and model-selected Agent routes; ACP, Codex, and Claude Code reject them until they implement and advertise the capability.
|
||||
- Unit coverage owns the default-off Host preference, new-Session sampling, child inheritance, resumed decisions, opt-in schema and execution enforcement, merge precedence, route-aware effort inheritance, preflight cancellation, live discovery, diagnostics, definition stability, capability rejection, and optional-service behavior. A shipped headless snapshot pins inheritance from a logged parent selection; the shipped examples own the assembled keyless model-visible schemas, and the SDK Loader and snapshot evidence pin the complete route through a separate child runtime.
|
||||
|
||||
## Related decisions
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ Status: implemented
|
||||
|
||||
只有实例启用 `enableModelSelection`,或其 Agent 作用域的 `modelSelectionSettings` 实例解析出已启用的 Session 决定,且绑定的 subagent 提供方声明 `SubagentCapabilities.agentOptions` 时,`dsh-tool-subagent` 才公开可选的 `provider`、`model` 与 `reasoning_effort` 字段,不要求配置路由允许列表。已注册的 LLM 提供方路由都可供子级选择;本工具不会在部署的 LLM 注册表之上增加第二套授权策略。禁用的实例会省略并拒绝面向模型的选择,而配置的 `Config.agentOptions` 仍是部署方所有的默认值。如果提供方缺少该能力,任一种选择模式都会使插件挂载失败。
|
||||
|
||||
提供方与模型共同组成一条路由,必须一起提供。如果配置值或父级值能够提供生效路由,则可以只提供推理强度。模型参数覆盖 `Config.agentOptions`,配置字段覆盖父 Agent 最新记录的请求选择;首个请求之前由创建选项提供回退,并保留其中配置的输出 token 上限。推理强度 ID 仍由 adapter 所有。路由不变时会继承省略的强度;更换提供方或模型但没有指定强度时,会清除下层路由自有的值,使所选模型解析自己的默认值。`AgentOptions` 把结果强度传入子级循环,其请求 header 会记录生效值。可继续描述符会把它与解析后的提供方和模型一同记录,使尚未写入首个请求的子级能以相同选择冷恢复。
|
||||
提供方与模型共同组成一条路由,必须一起提供。如果配置值、父级值或提供方持有的路由默认值能够提供生效路由,则可以只提供推理强度。静态的 `provider.agentRouteDefaults` 在存在时构成 provider/model 基线;`Config.agentOptions` 与模型参数会在路由相关强度清除之前覆盖它。没有静态默认值的提供方会使用父 Agent 最新记录请求中的兼容字段,首个请求之前由创建选项提供回退,并保留其中配置的输出 token 上限。推理强度 ID 仍由 adapter 所有。只有所选基线的路由不变时才会继承省略的强度;更换提供方或模型但没有指定强度时,会清除下层路由自有的值,使所选模型解析自己的默认值。`AgentOptions` 把结果强度传入子级循环,其请求 header 会记录生效值。可继续描述符会把它与解析后的提供方和模型一同记录,使尚未写入首个请求的子级能以相同选择冷恢复。
|
||||
|
||||
显式或配置的提供方、模型或强度会在创建子级前通过 `ctx.llm.resolveCallConfig()` 解析。该查询负责提供方注册、精确模型元数据、推理强度校验和 adapter 默认值。异步查询完成后、创建子级或后台 job 之前,工具会再次检查取消状态。既没有面向模型的选择、也没有配置路由字段的调用会保留原有提供方路径,不要求可选 LLM 服务存在。
|
||||
显式或配置的提供方、模型或强度会在提供方基线与请求优先级完成后,通过 `ctx.llm.resolveCallConfig()` 解析。具有静态路由默认值的提供方会在请求省略强度时禁止继承父级强度,从而保留所选模型的默认值。LLM 查询负责提供方注册、精确模型元数据、推理强度校验和 adapter 默认值。异步查询完成后、创建子级或后台 job 之前,工具会再次检查取消状态,并确认同一个提供方实例仍处于注册状态,因此 HMR 不会把一个提供方的默认值与另一个提供方的进程组合。既没有面向模型的选择、也没有配置路由字段的调用会保留原有提供方路径,不要求可选 LLM 服务存在。
|
||||
|
||||
启用的定义会注册 `list_subagent_models`。无参数调用列出已注册提供方;提供 `provider` 时调用该适配器的建议性模型目录;同时提供 `provider` 与 `model` 时解析精确模型,并返回其推理强度和默认值。因为发现工具使用全局名称,一个工具作用域最多由一个实例启用选择。随附产品组合在 Agent 作用域的主 `subagent` 实例上设置 `modelSelectionSettings: true`,并注册默认 `enabled: false` 的 Host 自有 `subagent-model-selection` settings namespace。新的顶层 Session 会在组合期间读取该偏好,并在任何模型请求之前把启用决定记录为 `subagent/model-selection-enabled`。子 Session 继承在线父级的决定;恢复的 Session 使用已有标记,而不是当前偏好。因此,设置修改只影响之后组合的顶层 Session。即使缺少可选 LLM 服务,固定发现定义仍保持可用;发现调用和所选路由调用会在该服务出现前失败。只要适配器接受某个未列出的模型 ID,仍可选择该模型。
|
||||
|
||||
@@ -24,7 +24,7 @@ Status: implemented
|
||||
|
||||
委派定义不会随 adapter 注册和目录变化而改变,因此实时拓扑既不会扩大每个父级请求,也不会使缓存前缀失效。只有调用发现工具时,目录结果才进入 transcript。自定义的上下文继承实例如果启用选择,其描述会警告,更改提供方或模型可能阻止提供方复用继承的对话前缀。
|
||||
|
||||
`SubagentCapabilities.agentOptions` 仍是传输事实。如果请求携带这些选项,而提供方声明为 `false`,服务会在调用提供方前拒绝。两个进程内提供方声明为 `true`;当前 ACP、Codex、Claude Code 与 DSH SDK 传输声明为 `false`。工具配置提供 `agentOptions`、静态启用模型选择或让它受 settings 控制时,如果绑定的提供方缺少该能力,也会失败。
|
||||
`SubagentCapabilities.agentOptions` 仍是传输事实。如果请求携带这些选项,而提供方声明为 `false`,服务会在调用提供方前拒绝。两个进程内提供方与 DSH SDK 传输声明为 `true`;DSH SDK 会把 provider/model 默认值作为分离且不可变的数据公开给 Consumer 预检,而 `start()` 会为直接调用方与子运行时初始化独立应用同一份 Config 默认值及 maxTokens。ACP、Codex 与 Claude Code 声明为 `false`。工具配置提供 `agentOptions`、静态启用模型选择或让它受 settings 控制时,如果绑定的提供方缺少该能力,也会失败。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -51,10 +51,10 @@ Status: implemented
|
||||
- 启用的委派工具无需部署选择器配置,即可选择任意实时子级 LLM 路由;禁用的实例会省略并拒绝面向模型的路由字段。
|
||||
- 主委派工具实例默认关闭选择,为新 Session 提供 Models 页面 opt-in,并且只在持久决定已启用的 Session 中注册 `list_subagent_models`;其目录条目不会限制委派。
|
||||
- 随附 fork 工具会继承父级的提供方与模型,并省略面向模型的路由字段,使继承的对话前缀仍可供 KV Cache 复用。
|
||||
- 省略选择时保留配置默认值,并从父级最新记录的请求中进行兼容继承;改变路由但不显式指定强度时,使用所选模型的默认值。
|
||||
- 省略选择时保留配置默认值,并使用静态提供方路由默认值或来自父级最新记录请求的兼容继承;改变路由但不显式指定强度时,使用所选模型的默认值。
|
||||
- adapter 目录和拓扑变化不会改变委派定义及其 prompt 缓存前缀。
|
||||
- 进程外 subagent 提供方在实现并声明该能力前,会拒绝配置和模型选择的 Agent 选项。
|
||||
- 单元测试覆盖默认关闭的 Host 偏好、新 Session 读取、子级继承、恢复决定、选择启用时的 schema 与执行强制、合并优先级、路由相关强度继承、预检取消、实时发现、诊断、定义稳定性、能力拒绝与可选服务行为。随附的 headless 快照固定从父级已记录选择继承的行为;随附示例还覆盖组装后无密钥、模型可见的 schema。
|
||||
- DSH SDK 子级接受配置和模型选择的 Agent 路由;ACP、Codex 与 Claude Code 在实现并声明该能力前仍会拒绝。
|
||||
- 单元测试覆盖默认关闭的 Host 偏好、新 Session 读取、子级继承、恢复决定、选择启用时的 schema 与执行强制、合并优先级、路由相关强度继承、预检取消、实时发现、诊断、定义稳定性、能力拒绝与可选服务行为。随附的 headless 快照固定从父级已记录选择继承的行为;随附示例覆盖组装后无密钥、模型可见的 schema,SDK Loader 与快照证据固定完整路由经过独立子运行时的链路。
|
||||
|
||||
## 相关决策
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-18-product-subagent-named-instances.md
|
||||
2026-08-18-product-subagent-named-instances.md: 759d3941ff8404138954c409f0fd4949e357200e
|
||||
2026-08-18-product-subagent-named-instances.zh.md: 6faf0e70f639cbc6528e27b800b8e5f99f0d6c86
|
||||
2026-08-18-product-subagent-named-instances.md: 749267e311cbcdebc7a6b8ad84e046fe21acf6fd
|
||||
2026-08-18-product-subagent-named-instances.zh.md: d39fdd46a30aa665715d782162ddaaaac3a31f3e
|
||||
|
||||
@@ -6,17 +6,17 @@ English | [中文](2026-08-18-product-subagent-named-instances.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A Profile can mount one Cordis plugin package in multiple rows, but the Codex and Claude Code product providers previously registered every row under one fixed product name. A second row therefore failed as a duplicate before its distinct permission mode, environment, or process-release settings could become usable. Deriving an implicit name from those settings would create a second identity rule, while choosing a provider during a tool call would let model input select deployment authority.
|
||||
A Profile can mount one Cordis plugin package in multiple rows, but the Codex and Claude Code product providers previously registered every row under one fixed product name. A second row therefore failed as a duplicate before its distinct model, permission mode, environment, or process-release settings could become usable. Deriving an implicit name from those settings would create a second identity rule, while choosing a provider during a tool call would let model input select deployment authority.
|
||||
|
||||
The existing subagent registry already owns unique provider names, reversible registration, lifecycle events, and holder-owned published runs. The existing `dsh-tool-subagent` configuration already binds one provider name to one model-visible tool name. Product providers need to expose the missing Profile-owned identity without adding another registry or selection protocol.
|
||||
|
||||
## Decision
|
||||
|
||||
Each product provider Config owns a non-empty `providerName`; the defaults remain `codex` and `claude-code`. The resolved name is fixed when the plugin row loads and becomes the Provider object's `name`; registration, lookup, lifecycle events, run logs, and HMR removal therefore use the same value. Each mounted row retains its own `permissionMode`, `env`, `disposeGraceMs`, and run resources.
|
||||
Each product provider Config owns a non-empty `providerName`; the defaults remain `codex` and `claude-code`. The resolved name is fixed when the plugin row loads and becomes the Provider object's `name`; registration, lookup, lifecycle events, run logs, and HMR removal therefore use the same value. Each mounted row retains its own optional non-empty `model`, `permissionMode`, `env`, `disposeGraceMs`, and run resources. An explicit model is fixed for every query or thread from that instance, while omission leaves the native product settings authoritative.
|
||||
|
||||
Profiles may mount multiple Codex or Claude Code rows when every row uses a distinct `providerName`. Each `dsh-tool-subagent` row continues to bind its existing `provider` field to that exact name and exposes an independently configured `toolName`. Tool calls carry no provider selector, alias, or permission input. A duplicate provider name fails through the existing `DUPLICATE_PROVIDER` path and leaves the first registration intact.
|
||||
|
||||
Removing one provider row blocks new starts and removes only tools bound to that name. Runs already published by the removed instance remain owned by their holders and settle or dispose independently. Sibling instances remain registered and keep their own environment, native permission mode, cancellation controller, product process, and cleanup grace.
|
||||
Removing one provider row blocks new starts and removes only tools bound to that name. Runs already published by the removed instance remain owned by their holders and settle or dispose independently. Sibling instances remain registered and keep their own configured model, environment, native permission mode, cancellation controller, product process, and cleanup grace.
|
||||
|
||||
### Ownership and lifecycle
|
||||
|
||||
@@ -25,11 +25,11 @@ Removing one provider row blocks new starts and removes only tools bound to that
|
||||
| Provider instance name | Product Provider Config | One immutable registry name per mounted row, with the existing default when omitted |
|
||||
| Name uniqueness and lifecycle events | `ctx.subagents` | Duplicate registration fails; disposal removes only the matching name |
|
||||
| Model-visible tool name and binding | `dsh-tool-subagent` Config | One static tool resolves one configured provider name |
|
||||
| Permission, environment, and process cleanup | One Provider instance | Concurrent runs and sibling instances do not share deployment configuration or run resources |
|
||||
| Model, permission, environment, and process cleanup | One Provider instance | Concurrent runs and sibling instances do not share deployment configuration or run resources |
|
||||
|
||||
## Verification
|
||||
|
||||
Both product packages pin their default and custom names, empty-name rejection, duplicate rollback, actual-name diagnostics, two concurrent instances with different permission modes, environments, and cleanup grace, cancellation isolation, and removal of one instance while its published run remains valid. The official product loopback tests run two named instances in one Host against separate model fixtures and prove independent unload and process-tree quiescence. Public Loader compositions mount two rows and two distinct tools for each product without starting either product, while keyless ACP snapshots pin the four-tool combined roster and the absence of a dynamic provider parameter.
|
||||
Both product packages pin their default and custom names, empty-name and empty-model rejection, duplicate rollback, actual-name diagnostics, two concurrent instances with different models, permission modes, environments, and cleanup grace, cancellation isolation, and removal of one instance while its published run remains valid. Official product fixtures prove omitted-model inheritance and exact per-query or per-thread model isolation. Public Loader compositions mount two rows and two distinct tools for each product without starting either product, while keyless ACP snapshots pin the four-tool combined roster and the absence of dynamic provider or model parameters.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -43,6 +43,6 @@ Both product packages pin their default and custom names, empty-name rejection,
|
||||
|
||||
## Consequences
|
||||
|
||||
A Profile can expose several Codex and Claude Code tools backed by separate native permission modes and environments while existing configurations continue to resolve `codex` and `claude-code`. Provider and tool names remain independent configuration facts, so changing one requires updating the binding that refers to it.
|
||||
A Profile can expose several Codex and Claude Code tools backed by separate models, native permission modes, and environments while existing configurations continue to resolve `codex` and `claude-code`. Provider and tool names remain independent configuration facts, so changing one requires updating the binding that refers to it.
|
||||
|
||||
The design adds no runtime renaming, model-visible selector, generated tool name, persistent instance directory, shared process pool, or compatibility alias. Correct multi-instance configurations require unique provider names and unique tool names; duplicate tool-name waiting remains a separate limitation.
|
||||
The design adds no runtime renaming, model-visible provider or model selector, generated tool name, persistent instance directory, shared process pool, model discovery, fallback, or compatibility alias. Correct multi-instance configurations require unique provider names and unique tool names; duplicate tool-name waiting remains a separate limitation.
|
||||
|
||||
@@ -6,17 +6,17 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
Profile 可以用多个配置项挂载同一个 Cordis 插件包,但 Codex 与 Claude Code 产品提供方此前会把每个配置项都注册到一个固定产品名称下。因此,第二个配置项会在其独立权限模式、环境或进程释放设置可用前因名称重复而失败。根据这些设置隐式派生名称会建立第二套身份规则,而在工具调用期间选择提供方会让模型输入决定部署权限。
|
||||
Profile 可以用多个配置项挂载同一个 Cordis 插件包,但 Codex 与 Claude Code 产品提供方此前会把每个配置项都注册到一个固定产品名称下。因此,第二个配置项会在其独立模型、权限模式、环境或进程释放设置可用前因名称重复而失败。根据这些设置隐式派生名称会建立第二套身份规则,而在工具调用期间选择提供方会让模型输入决定部署权限。
|
||||
|
||||
现有 subagent 注册表已经拥有提供方名称唯一性、可逆注册、生命周期事件和由持有方拥有的已发布运行。现有 `dsh-tool-subagent` 配置也已经把一个提供方名称绑定到一个模型可见工具名称。产品提供方只需公开缺失的 Profile 所有身份,无需增加另一套注册表或选择协议。
|
||||
|
||||
## 决策
|
||||
|
||||
每个产品提供方 Config 都拥有非空的 `providerName`;默认值仍分别为 `codex` 与 `claude-code`。插件配置项加载时会固定解析后的名称,并把它作为 Provider 对象的 `name`;注册、查找、生命周期事件、运行日志和 HMR(热模块替换)移除因此使用同一个值。每个已挂载配置项保留自己的 `permissionMode`、`env`、`disposeGraceMs` 和运行资源。
|
||||
每个产品提供方 Config 都拥有非空的 `providerName`;默认值仍分别为 `codex` 与 `claude-code`。插件配置项加载时会固定解析后的名称,并把它作为 Provider 对象的 `name`;注册、查找、生命周期事件、运行日志和 HMR(热模块替换)移除因此使用同一个值。每个已挂载配置项保留自己的可选非空 `model`、`permissionMode`、`env`、`disposeGraceMs` 和运行资源。显式模型会为该实例的每次 query 或线程固定,省略时仍以原生产品设置为权威。
|
||||
|
||||
当每个配置项使用不同的 `providerName` 时,Profile 可以挂载多个 Codex 或 Claude Code 配置项。每个 `dsh-tool-subagent` 配置项继续用已有的 `provider` 字段绑定这个准确名称,并公开独立配置的 `toolName`。工具调用不携带提供方选择器、别名或权限输入。重复提供方名称沿用现有 `DUPLICATE_PROVIDER` 路径失败,而且不会替换第一个注册项。
|
||||
|
||||
移除一个提供方配置项会阻止新的启动,并且只移除绑定到该名称的工具。该实例已经发布的运行仍由其持有方拥有,并会独立结算或 dispose(资源释放)。兄弟实例继续保持注册,并保留各自的环境、原生权限模式、取消控制器、产品进程和清理宽限期。
|
||||
移除一个提供方配置项会阻止新的启动,并且只移除绑定到该名称的工具。该实例已经发布的运行仍由其持有方拥有,并会独立结算或 dispose(资源释放)。兄弟实例继续保持注册,并保留各自配置的模型、环境、原生权限模式、取消控制器、产品进程和清理宽限期。
|
||||
|
||||
### 所有权与生命周期
|
||||
|
||||
@@ -25,11 +25,11 @@ Profile 可以用多个配置项挂载同一个 Cordis 插件包,但 Codex 与
|
||||
| 提供方实例名称 | 产品提供方 Config | 每个已挂载配置项拥有一个不可变注册名称;省略时使用现有默认值 |
|
||||
| 名称唯一性与生命周期事件 | `ctx.subagents` | 重复注册失败;资源释放只移除匹配名称 |
|
||||
| 模型可见工具名称与绑定 | `dsh-tool-subagent` Config | 一个静态工具解析一个已配置的提供方名称 |
|
||||
| 权限、环境与进程清理 | 一个提供方实例 | 并发运行与兄弟实例不共享部署配置或运行资源 |
|
||||
| 模型、权限、环境与进程清理 | 一个提供方实例 | 并发运行与兄弟实例不共享部署配置或运行资源 |
|
||||
|
||||
## 验证
|
||||
|
||||
两个产品包测试都会固定默认与自定义名称、空名称拒绝、重复注册回滚、实际名称诊断、使用不同权限模式、环境与清理宽限期的两个并发实例、取消隔离,以及移除一个实例后其已发布运行仍然有效。官方产品回环测试会在同一个 Host 中针对独立模型 fixture(测试前置数据)运行两个命名实例,并证明独立卸载与进程树完全停稳。公共 Loader 组合会为每个产品挂载两个配置项与两个不同工具,而且不启动任一产品;无密钥 ACP 快照固定最终四工具组合,并证明没有动态提供方参数。
|
||||
两个产品包测试都会固定默认与自定义名称、空名称和空模型拒绝、重复注册回滚、实际名称诊断、使用不同模型、权限模式、环境与清理宽限期的两个并发实例、取消隔离,以及移除一个实例后其已发布运行仍然有效。官方产品 fixture 会证明省略模型继承,以及逐 query 或逐线程的准确模型隔离。公共 Loader 组合会为每个产品挂载两个配置项与两个不同工具,而且不启动任一产品;无密钥 ACP 快照固定最终四工具组合,并证明没有动态提供方或模型参数。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -43,6 +43,6 @@ Profile 可以用多个配置项挂载同一个 Cordis 插件包,但 Codex 与
|
||||
|
||||
## 结果
|
||||
|
||||
Profile 可以公开多个由不同原生权限模式与环境支持的 Codex 与 Claude Code 工具,而现有配置仍会解析为 `codex` 与 `claude-code`。提供方名称与工具名称继续是彼此独立的配置事实,因此修改其中一项时必须同时更新引用它的绑定。
|
||||
Profile 可以公开多个由不同模型、原生权限模式与环境支持的 Codex 与 Claude Code 工具,而现有配置仍会解析为 `codex` 与 `claude-code`。提供方名称与工具名称继续是彼此独立的配置事实,因此修改其中一项时必须同时更新引用它的绑定。
|
||||
|
||||
本设计不增加运行时改名、模型可见选择器、自动生成的工具名称、持久实例目录、共享进程池或兼容别名。正确的多实例配置要求提供方名称与工具名称都保持唯一;重复工具名称的等待问题仍是独立限制。
|
||||
本设计不增加运行时改名、模型可见的提供方或模型选择器、自动生成的工具名称、持久实例目录、共享进程池、模型发现、fallback 或兼容别名。正确的多实例配置要求提供方名称与工具名称都保持唯一;重复工具名称的等待问题仍是独立限制。
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-18-product-subagent-failure-facts.md
|
||||
2026-08-18-product-subagent-failure-facts.md: 50d8e918f288a6b8a9b90474499b2ed20731643f
|
||||
2026-08-18-product-subagent-failure-facts.zh.md: abdbf8c0ebddb1fb30cef3c7e80fcf7040e45d2d
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-18-settings-font-size-control.md
|
||||
2026-08-18-settings-font-size-control.md: 34ee7f28f58a0a5a5dc8cd39bc4cecf5e081359f
|
||||
2026-08-18-settings-font-size-control.zh.md: 2c2ea714d50ca1f934d78ede6175935338488331
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: Settings-backed conversation content font size
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-18-settings-font-size-control.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The conversation's body text size was fixed (14px after the 0.875 markdown-ladder rescale). Users asked for a Settings control: a "字号大小" row under General → Appearance with a stepper, range 12–17, default 14, that resizes the transcript body text and the composer input text together.
|
||||
|
||||
## Decision
|
||||
|
||||
**The theme plugin owns the setting.** `ThemeSettingsSchema` gains `fontSize` (`z.number().step(1).min(12).max(17).default(14)`) beside `preference` in the existing `ui-theme` namespace — one durable section, one settings scope, one adoption path. `ThemeRuntime` carries `fontSize` in `ThemeSnapshot`, exposes `setFontSize(px)` (integer-and-range validated, throws a teaching error), and republishes on `theme/change`. The same plugin registers the FontSizeRow into `settings.general.item` at order 11, directly under the Appearance cubes (order 10).
|
||||
|
||||
**Presentation rides the existing snapshot pipeline.** The service never touches the DOM: ui-layout's `ThemePresenter` writes `--dsh-content-font-size` on `body` from each snapshot (and retracts it on dispose), and the Host boot script embeds the durable value in the index response so first paint uses the chosen size — the same pre-plugin path the dark-mode attribute takes, avoiding a font-size flash.
|
||||
|
||||
**One CSS delta variable moves the ladder.** `gradient-shadow-text.css` derives `--dsh-content-font-delta: calc(var(--dsh-content-font-size, 14px) - 14px)` and shifts the markdown h1–h4 and base variants (size and line height) by that same px increment, preserving the heading hierarchy and each variant's leading. Table, small, and code variants stay fixed — as does the interrupted-turn `.stopped` tag (11px): they are dense secondary text whose defaults would fall below legibility when stepped down. Consumers outside the token ladder read `var(--dsh-content-font-size, 14px)` (or `calc(<own default> + var(--dsh-content-font-delta, 0px))` for smaller steps) and `calc(<default line-height> + var(--dsh-content-font-delta, 0px))` directly: the assistant narration root, the user bubble (reference summaries and their inline glyphs included), the composer card (whose textarea/mirror/backdrop stack inherits font metrics from the card by design), and the flow chrome around them — the shared DisclosureRow header (tool calls, think, commands; row height, title, and leading box all move) with its expanded bodies' `22px + delta` indent keeping content aligned under the shifted title start, ToolRow/bash-row summaries and file links, think text (12px keeping its 2px step under the body), compaction/context/retry/error rows, StatsLine, the chat hint and open-error strips, the workflow-run panel (run/phase headers and expanded member rows), the message clock and icon actions (slot-injected message-feedback actions match through the same variables), and the turn status line. Flow icons scale through each leading box's CSS edge (`svg` width/height overriding the glyph attributes); StateDot is exempt via its `data-state` attribute — a status mark, not text furniture. The 14px fallbacks keep every surface pixel-identical when the variable is absent (tests, storybook-like mounts, remote compositions before adoption).
|
||||
|
||||
**The stepper is a pill, not a menu.** The row reuses the selector-pill geometry (h36 r18 module fill) with the value centered in the pill, the up/down arrow column revealed on hover/focus-within and absolutely anchored to the pill's right edge (so revealing never moves the value), and a `px` unit label after the pill. A tertiary description line under the title states the scope — the size only affects conversation content, not the application chrome. Arrows disable at the bounds; the display follows the store mirror, never the click echo — the same store/face pattern as the Appearance row.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**A separate settings namespace or plugin.** Rejected: the font size is an appearance preference with the same persistence, adoption, and remote-browser semantics as the theme preference; a second namespace duplicates the scope machinery for one integer.
|
||||
|
||||
**Scaling via a multiplier (`em`/percentage) instead of a px delta.** Rejected: multiplying spreads the 12–17px range disproportionately across the ladder (21px h1 would swing ~18–25.5px) and produces fractional line heights; the fixed px shift keeps every step integer and the hierarchy's px gaps intact.
|
||||
|
||||
**Scaling every font token (tables, code, small).** Rejected: those variants are secondary/dense by design; at −2 the small ladder would hit 10px and code 9px, below legibility.
|
||||
|
||||
## Consequences
|
||||
|
||||
The 0.875 markdown-ladder rescale (body 16 → 14) ships with this change as the new default rendering; at delta 0 every axis consumer is pixel-identical to that rescaled baseline, and surfaces without the variable fall back to the same 14px. A changed size persists in `$DSH_HOME/settings.yaml`, survives reloads without flashing (the boot script writes the durable value pre-hydration and `ThemeRuntime` seeds its initial snapshot from it), applies live across transcript and composer, and remote browsers keep the process-local-selection rule the theme preference already has. `setFontSize` joins the model-visible cordis client API catalog beside `setTheme`.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note:Settings 支撑的会话正文字号
|
||||
|
||||
状态:已实现
|
||||
|
||||
[English](2026-08-18-settings-font-size-control.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
会话正文字号是固定的(markdown 阶梯按 0.875 重缩放后为 14px)。用户需要一个设置项:在 General → Appearance 下加一行「字号大小」,用步进器交互,范围 12–17,默认 14,同时调整转录正文与 composer 输入框的文字大小。
|
||||
|
||||
## 决策
|
||||
|
||||
**主题插件拥有该设置。**`ThemeSettingsSchema` 在既有 `ui-theme` namespace 的 `preference` 旁新增 `fontSize`(`z.number().step(1).min(12).max(17).default(14)`)——一个持久化 section、一个 settings scope、一条采纳路径。`ThemeRuntime` 在 `ThemeSnapshot` 中携带 `fontSize`,暴露 `setFontSize(px)`(校验整数与范围,越界抛教学式错误),并通过 `theme/change` 重新发布。同一插件把 FontSizeRow 注册进 `settings.general.item`,order 11,紧挨外观方块(order 10)之下。
|
||||
|
||||
**呈现走既有快照管线。**服务绝不接触 DOM:ui-layout 的 `ThemePresenter` 依据每份快照在 `body` 上写 `--dsh-content-font-size`(dispose 时收回),Host 引导脚本把持久化值嵌入 index 响应,让首帧就使用所选字号——与暗色属性同一条插件前路径,避免字号闪变。
|
||||
|
||||
**一个 CSS 增量变量平移阶梯。**`gradient-shadow-text.css` 派生 `--dsh-content-font-delta: calc(var(--dsh-content-font-size, 14px) - 14px)`,把 markdown h1–h4 与 base 各变体(字号与行高)按同一像素增量平移,保持标题层级与各变体的行距。表格、small 与 code 变体保持固定——中断回合的 `.stopped` 标签(11px)同样固定:它们是密集次级文本,其默认值在字号下调时会低于可读下限。token 阶梯之外的消费方直接读取 `var(--dsh-content-font-size, 14px)`(较小档位则用 `calc(<自身默认> + var(--dsh-content-font-delta, 0px))`)与 `calc(<默认行高> + var(--dsh-content-font-delta, 0px))`:助手正文根节点、用户气泡(含引用摘要及其行内字形)、composer 卡片(其 textarea/mirror/backdrop 三层按设计从卡片继承字体度量),以及围绕它们的流内 chrome——共享的 DisclosureRow 头部(工具调用、think、命令;行高、标题与 leading 盒同步移动),其展开内容以 `22px + delta` 缩进跟随平移后的标题起点保持对齐、ToolRow/bash 行的 summary 与文件链接、think 正文(12px,保持比正文小 2px 的层级)、compaction/context/retry/错误行、StatsLine、chat 提示与打开失败条、workflow-run 面板(run/phase 头部与展开的成员行)、消息时钟与图标操作(slot 注入的消息反馈操作经同一对变量同步缩放),以及回合状态行。流内图标经由各 leading 盒的 CSS 边长缩放(`svg` width/height 覆盖字形自身属性);StateDot 通过其 `data-state` 属性豁免——它是状态标记,不是文字组件。14px 回退让变量缺席时(测试、独立挂载、采纳前的远程组合)所有表面逐像素不变。
|
||||
|
||||
**步进器是药丸控件,不是菜单。**该行复用选择器药丸几何(h36 r18 模块填充),数值在药丸内居中,上下箭头列在 hover/focus-within 时显示并绝对定位在药丸右缘(显示时数值不移动),药丸后带 `px` 单位标签。标题下方的三级说明行标明作用范围——字号仅影响会话内容,不影响应用外框。到达边界时对应箭头禁用;显示跟随 store 镜像,绝不跟随点击回声——与外观行相同的 store/face 模式。
|
||||
|
||||
## 已考虑的替代方案
|
||||
|
||||
**独立 settings namespace 或独立插件。**否决:字号与主题偏好具有相同的持久化、采纳与远程浏览器语义,属外观偏好;为一个整数复制一套 scope 机制不值得。
|
||||
|
||||
**用倍率(`em`/百分比)而非像素增量缩放。**否决:乘法会让 12–17px 的范围在阶梯上不成比例地放大(21px 的 h1 会摆动到约 18–25.5px),并产生小数行高;固定像素平移让每一档都是整数,层级间的像素差保持不变。
|
||||
|
||||
**缩放全部字体 token(表格、code、small)。**否决:这些变体按设计就是次级/密集文本;−2 档时 small 阶梯会降到 10px、code 降到 9px,低于可读下限。
|
||||
|
||||
## 后果
|
||||
|
||||
0.875 的 markdown 阶梯重缩放(正文 16 → 14)作为新的默认渲染随本变更一同交付;增量为 0 时所有轴消费方与该重缩放基线逐像素一致,无变量的表面回退到同样的 14px。修改后的字号持久化在 `$DSH_HOME/settings.yaml`,重载不闪变(引导脚本在 hydration 前写入持久化值,`ThemeRuntime` 以它为初始快照种子),在转录与 composer 上实时生效;远程浏览器沿用主题偏好既有的进程内选择规则。`setFontSize` 与 `setTheme` 一同进入模型可见的 cordis 客户端 API 目录。
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-21-out-of-process-subagent-minimal-diagnostics.md
|
||||
2026-08-21-out-of-process-subagent-minimal-diagnostics.md: 533ace5a13df75fb594e0cecc65a743df27b6baf
|
||||
2026-08-21-out-of-process-subagent-minimal-diagnostics.zh.md: fe8adf764b240d77cfcde95999ee6689bf11b4a4
|
||||
2026-08-21-out-of-process-subagent-minimal-diagnostics.md: 82f2d2b131c5e991891b030d7c48873aa803386b
|
||||
2026-08-21-out-of-process-subagent-minimal-diagnostics.zh.md: 4cf5a1ebef0ba83a241dec5a9d8cd21ee2010671
|
||||
|
||||
+21
-8
@@ -6,13 +6,13 @@ English | [中文](2026-08-21-out-of-process-subagent-minimal-diagnostics.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
An ACP child can stop because it reached a remote limit, denied a required permission, lost its protocol transport, or exited as a process. The shared result historically reduced these outcomes to a stop reason such as `error`, while startup and cleanup rejection messages could expose the original exception. A parent could not choose between narrowing the task, adjusting permission policy, or repairing the child deployment without Host logs.
|
||||
An ACP or DSH SDK child can stop because it reached a remote limit, denied a required permission, ended with a non-completed child turn, lost its protocol transport, or exited as a process. The shared result historically reduced these outcomes to a stop reason such as `error`, while startup and cleanup rejection messages could expose the original exception. A parent could not choose between narrowing the task, adjusting permission policy, or repairing the child deployment without Host logs.
|
||||
|
||||
Copying exceptions, stderr, task content, tool input, paths, environment values, credentials, or protocol payloads into `SubagentResult.diagnostic` would make untrusted child text model-visible. Reusing a complete product-specific error union would also duplicate independently versioned authorities in the provider-neutral [subagent seam](2026-06-21-subagent-capability-seam.md).
|
||||
|
||||
## Decision
|
||||
|
||||
Each out-of-process provider owns a small mapping from facts it already receives at its protocol and process lifecycle points to fixed safe display text. The ACP provider implements that rule from its closed stop reasons, current operation, closed tool kind, configured permission policy, selected permission outcome, and the managed subprocess exit code or signal. Consumers continue to use the existing optional `SubagentResult.diagnostic`; they do not parse its punctuation or provider-private category names.
|
||||
Each out-of-process provider owns a small mapping from facts it already receives at its protocol and process lifecycle points to fixed safe display text. The ACP provider derives it from closed stop reasons, current operation, closed tool kind, configured permission policy, selected permission outcome, and the managed subprocess exit code or signal. The DSH SDK provider derives it from the child `turn/end` reason, current SDK operation, and exported SDK error class. Consumers continue to use the existing optional `SubagentResult.diagnostic`; they do not parse its punctuation or provider-private category names.
|
||||
|
||||
### Safe failure text
|
||||
|
||||
@@ -38,21 +38,34 @@ When an ACP permission request contributes to a non-completed result, a fixed li
|
||||
|
||||
`max_turn_requests` remains the shared `error` stop reason and adds `remote-limit`. An unknown stop reason remains `error` and becomes the fixed `unknown` category without copying the value. `max_tokens`, `refusal`, and `cancelled` keep their existing shared stop reasons; they add a diagnostic only when a permission decision must be explained.
|
||||
|
||||
### DSH SDK facts
|
||||
|
||||
| Stage | Owned operation | Safe categories and facts |
|
||||
| --- | --- | --- |
|
||||
| `initialize` | Parent workspace resolution, SDK runtime spawn, and initialize handshake | `configuration`, `protocol`, `transport`, or `unknown` |
|
||||
| `session-run` | Prompt acceptance, session notifications, and final child reason | `child-error`, `child-disposed`, `child-unknown`, `missing-terminal`, `protocol`, `transport`, or `unknown` |
|
||||
| `shutdown` | Bounded SDK shutdown and runtime process release | `unknown`; protocol-shutdown failures remain Host-only in the SDK client |
|
||||
|
||||
Child `completed`, `max-tokens`, and ordinary `aborted` results keep their existing shared stop reasons without extra text. An `aborted` turn whose closed cause is `disposed` keeps `aborted` and adds `child-disposed`. `blocked` reuses `refusal`; `error` adds `child-error`. Persistence repair alone produces `interrupted`, so this fresh-session provider leaves it as generic `error` without a diagnostic. A missing terminal event adds `missing-terminal`; an unknown reason uses `child-unknown` without copying the value or the child's structured failure message.
|
||||
|
||||
During initialize or session run, `SdkProtocolError` and JSON-RPC error responses map to `protocol`, and `TransportClosedError` maps to `transport`; the provider never reads their messages. Other exceptions and shutdown rejection use `unknown`. Request timeout classification remains deferred because this provider does not configure or propagate a request timeout.
|
||||
|
||||
### Ownership and lifecycle
|
||||
|
||||
| Fact or resource | Owner | Consumer behavior |
|
||||
| --- | --- | --- |
|
||||
| ACP stop reason and tool kind | ACP server and SDK | The provider maps only closed values and uses fixed unknown fallbacks |
|
||||
| Current failure stage and latest permission decision | One ACP run | Derived at the failure point and discarded with the run; concurrent runs share no diagnostic state |
|
||||
| Exit code and signal | `dsh-subprocess` handle | Displayed only after the managed outcome is observed; stderr is never parsed |
|
||||
| Protocol terminal fact | ACP server or child Harness Session | Each provider maps only its owned closed values and uses fixed unknown fallbacks |
|
||||
| Current failure stage and operation-local detail | One provider run | Derived at the failure point and discarded with the run; concurrent runs share no diagnostic state |
|
||||
| Exit code and signal | ACP's `dsh-subprocess` handle | Displayed only after the managed outcome is observed; stderr is never parsed |
|
||||
| SDK error category | TypeScript SDK client error class | Classified with `instanceof`; the Error message and stderr tail remain internal |
|
||||
| Diagnostic bytes and presentation | `dsh-subagent`, foreground tool, and Job runtime | The same bounded text stays separate from assistant output in foreground and one-shot background modes |
|
||||
| Raw failure | Child runtime, Error cause chain, and Host logger | Available for Host diagnosis only, never copied into the parent model result |
|
||||
|
||||
Startup publishes no run until initialize and new-session succeed. A startup failure rolls the private child back to quiescence before rejecting with safe facts. A published run settles its result without rejection, and `dispose()` independently reports a safe teardown failure while still using the backend's existing whole-tree cleanup ladder.
|
||||
Startup publishes no run until the provider's handshake completes. Successful startup cleanup rolls the private child back to quiescence before rejection. Cleanup failure preserves startup plus teardown/shutdown for an ordinary failure, or cleanup alone after cancellation, without claiming complete managed-process quiescence. A published run settles its result without rejection, and `dispose()` independently reports safe teardown or shutdown facts while still using the backend's existing process cleanup ladder.
|
||||
|
||||
## Verification
|
||||
|
||||
ACP package tests drive a real stdio protocol child and pin every stop-reason mapping, remote-limit and unknown fallbacks, permission allow/deny facts, configuration, initialize, new-session, prompt, process, and teardown stages, startup rollback, successful-result and local-cancellation omission, partial output, concurrent-run isolation, Host-only raw errors, process quiescence, and the shared multibyte diagnostic limit. A Loader composition proves the real configured provider reaches the model-visible foreground result. The keyless ACP snapshot pins the same diagnostic and permission fact in foreground error output and one-shot background `job_output` detail.
|
||||
ACP package tests drive a real stdio protocol child and pin every stop-reason mapping, remote-limit and unknown fallbacks, permission allow/deny facts, configuration, initialize, new-session, prompt, process, and teardown stages, startup rollback, successful-result and local-cancellation omission, partial output, concurrent-run isolation, Host-only raw errors, process quiescence, and the shared multibyte diagnostic limit. DSH SDK package tests drive the real SDK client against its stdio fake runtime and pin every reachable child reason, current typed SDK category, initialize/session-run/shutdown stages, SDK-owned failed-start cleanup, cancellation cleanup, partial output, concurrency, sanitization, and quiescence. Loader compositions prove each real configured provider reaches the model-visible foreground result. Keyless ACP and JSON-RPC snapshots pin each provider's exact foreground and one-shot background diagnostic text.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -68,6 +81,6 @@ ACP package tests drive a real stdio protocol child and pin every stop-reason ma
|
||||
|
||||
## Consequences
|
||||
|
||||
The parent can distinguish an ACP remote limit, permission involvement, protocol or transport failure, deployment/process failure, and teardown failure without receiving child-controlled text. Startup and cleanup errors use the same safe facts as published results, while Host observation retains the original cause.
|
||||
The parent can distinguish an ACP remote limit or permission decision and a DSH child-turn, protocol, transport, or shutdown failure without receiving child-controlled text. Startup and cleanup errors use the same safe facts as published results, while Host observation retains the original cause.
|
||||
|
||||
The diagnostic remains display text rather than a public protocol. Consumers may present it but must not branch on its format. This decision adds no retry policy, recovery controller, shared provider-error enum, stderr classifier, authentication taxonomy, session persistence, progress stream, or new ACP capability.
|
||||
|
||||
+21
-8
@@ -6,13 +6,13 @@ Status: implemented
|
||||
|
||||
## Problem
|
||||
|
||||
ACP 子进程可能因为达到远端限制、拒绝必需权限、失去协议传输或进程退出而停止。共享结果以往只把这些结果压成 `error` 等结束原因,而启动和清理拒绝的消息还可能暴露原始异常。父 agent 若不读取 Host 日志,就无法决定应缩小任务、调整权限策略还是修复子运行时部署。
|
||||
ACP 或 DSH SDK 子进程可能因为达到远端限制、拒绝必需权限、以非完成子轮次结束、失去协议传输或进程退出而停止。共享结果以往只把这些结果压成 `error` 等结束原因,而启动和清理拒绝的消息还可能暴露原始异常。父 agent 若不读取 Host 日志,就无法决定应缩小任务、调整权限策略还是修复子运行时部署。
|
||||
|
||||
若把异常、stderr、任务内容、工具输入、路径、环境值、凭证或协议 payload 复制进 `SubagentResult.diagnostic`,不受信任的子进程文本就会变成模型可见内容。若复用完整的产品专属错误联合,又会在提供方无关的 [subagent seam](2026-06-21-subagent-capability-seam.zh.md) 中复制彼此独立版本化的权威。
|
||||
|
||||
## Decision
|
||||
|
||||
每个进程外提供方分别拥有一份小型映射,把其协议与进程生命周期位置已经收到的事实转换成固定安全展示文本。ACP 提供方使用闭集结束原因、当前操作、闭集工具种类、已配置权限策略、选中的权限结果,以及受管子进程退出码或信号来实现该规则。消费方继续使用现有可选 `SubagentResult.diagnostic`,且不解析其标点或提供方私有 category 名称。
|
||||
每个进程外提供方分别拥有一份小型映射,把其协议与进程生命周期位置已经收到的事实转换成固定安全展示文本。ACP 提供方使用闭集结束原因、当前操作、闭集工具种类、已配置权限策略、选中的权限结果,以及受管子进程退出码或信号来派生。DSH SDK 提供方使用子 `turn/end` 原因、当前 SDK 操作与导出的 SDK 错误 class 来派生。消费方继续使用现有可选 `SubagentResult.diagnostic`,且不解析其标点或提供方私有 category 名称。
|
||||
|
||||
### 安全失败文本
|
||||
|
||||
@@ -38,21 +38,34 @@ Subagent failure (provider: <provider>; stage: <stage>; category: <category>; st
|
||||
|
||||
`max_turn_requests` 继续映射到共享 `error`,并附加 `remote-limit`。未知结束原因继续映射到 `error`,category 固定为 `unknown`,不会复制原值。`max_tokens`、`refusal` 与 `cancelled` 保持既有共享结束原因;只有需要解释权限决定时才会附加诊断。
|
||||
|
||||
### DSH SDK 事实
|
||||
|
||||
| Stage | 归属操作 | 安全 category 与事实 |
|
||||
| --- | --- | --- |
|
||||
| `initialize` | 父工作区解析、SDK 运行时 spawn 与 initialize 握手 | `configuration`、`protocol`、`transport` 或 `unknown` |
|
||||
| `session-run` | prompt 接受、会话通知与最终子轮次原因 | `child-error`、`child-disposed`、`child-unknown`、`missing-terminal`、`protocol`、`transport` 或 `unknown` |
|
||||
| `shutdown` | 有界 SDK shutdown 与运行时进程释放 | `unknown`;协议 shutdown 失败仍留在 SDK 客户端的 Host 诊断中 |
|
||||
|
||||
子 `completed`、`max-tokens` 与普通 `aborted` 结果保持既有共享结束原因,不附加文本。闭集原因是 `disposed` 的 `aborted` 轮次仍保持 `aborted`,并附加 `child-disposed`。`blocked` 复用 `refusal`;`error` 附加 `child-error`。只有持久化修复会产生 `interrupted`,因此本全新会话提供方把它保留为不带诊断的通用 `error`。缺失终态事件会附加 `missing-terminal`;未知原因使用 `child-unknown`,且不复制原值或子进程结构化失败消息。
|
||||
|
||||
在 initialize 或 session run 期间,`SdkProtocolError` 与 JSON-RPC 错误响应映射为 `protocol`,`TransportClosedError` 映射为 `transport`;提供方绝不读取其消息。其他异常和 shutdown 拒绝使用 `unknown`。由于本提供方没有配置或传播 request timeout,请求超时分类继续推迟。
|
||||
|
||||
### 所有权与生命周期
|
||||
|
||||
| 事实或资源 | Owner | 消费方行为 |
|
||||
| --- | --- | --- |
|
||||
| ACP 结束原因与工具种类 | ACP server 与 SDK | 提供方只映射闭集值,并对闭集外值使用固定 unknown 回退 |
|
||||
| 当前失败 stage 与最新权限决定 | 单次 ACP 运行 | 只在失败点派生,并随运行丢弃;并发运行不共享诊断状态 |
|
||||
| 退出码与信号 | `dsh-subprocess` 句柄 | 仅在观测到受管结果后展示;绝不解析 stderr |
|
||||
| 协议终态事实 | ACP server 或子 Harness Session | 每个提供方只映射自身拥有的闭集值,并使用固定 unknown 回退 |
|
||||
| 当前失败 stage 与 operation-local 细节 | 单次提供方运行 | 只在失败点派生,并随运行丢弃;并发运行不共享诊断状态 |
|
||||
| 退出码与信号 | ACP 的 `dsh-subprocess` 句柄 | 仅在观测到受管结果后展示;绝不解析 stderr |
|
||||
| SDK 错误 category | TypeScript SDK 客户端错误 class | 仅通过 `instanceof` 分类;Error 消息和 stderr tail 留在内部 |
|
||||
| 诊断字节与呈现 | `dsh-subagent`、前台工具与 Job 运行时 | 前台和一次性后台模式都把同一份有界文本与 assistant 输出分开 |
|
||||
| 原始失败 | 子运行时、Error cause 链与 Host logger | 只供 Host 排障,绝不复制进父模型结果 |
|
||||
|
||||
启动只有在 initialize 与 new-session 成功后才发布运行。启动失败会先把私有子进程回滚到完全停稳,再以安全事实拒绝。已发布运行的结果不会拒绝,而 `dispose()` 会独立报告安全 teardown 失败,并继续使用后端既有的整棵进程树清理阶梯。
|
||||
启动只有在提供方握手完成后才发布运行。启动清理成功时,私有子进程会先回滚到完全停稳再拒绝。清理失败时,普通失败会保留启动与 teardown/shutdown,取消后只保留清理事实,且不会宣称受管进程已经完全停稳。已发布运行的结果不会拒绝,而 `dispose()` 会独立报告安全 teardown 或 shutdown 事实,并继续使用后端既有的进程清理阶梯。
|
||||
|
||||
## Verification
|
||||
|
||||
ACP 包测试通过真实 stdio 协议子进程固定全部结束原因映射、远端限制与 unknown 回退、权限 allow/deny 事实、configuration、initialize、new-session、prompt、process 与 teardown stage、启动回滚、成功结果与本地取消省略、部分输出、并发运行隔离、仅 Host 可见的原始错误、进程完全停稳,以及共享多字节诊断限制。Loader 组合证明真实配置的提供方会到达模型可见前台结果。无密钥 ACP snapshot 会在前台错误输出与一次性后台 `job_output` detail 中固定同一份诊断与权限事实。
|
||||
ACP 包测试通过真实 stdio 协议子进程固定全部结束原因映射、远端限制与 unknown 回退、权限 allow/deny 事实、configuration、initialize、new-session、prompt、process 与 teardown stage、启动回滚、成功结果与本地取消省略、部分输出、并发运行隔离、仅 Host 可见的原始错误、进程完全停稳,以及共享多字节诊断限制。DSH SDK 包测试通过真实 SDK 客户端驱动其 stdio 伪运行时,固定全部可达子轮次原因、当前 typed SDK category、initialize/session-run/shutdown stage、SDK 自有失败启动清理、本地取消清理、部分输出、并发、脱敏与停稳。Loader 组合证明两个真实配置的提供方都能到达模型可见前台结果。无密钥 ACP 与 JSON-RPC snapshot 会固定各自提供方的准确前台与一次性后台诊断文本。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -68,6 +81,6 @@ ACP 包测试通过真实 stdio 协议子进程固定全部结束原因映射、
|
||||
|
||||
## Consequences
|
||||
|
||||
父 agent 可以区分 ACP 远端限制、权限参与、协议或传输失败、部署/进程失败与 teardown 失败,同时不会接收子进程控制的文本。启动和清理错误与已发布结果使用同一套安全事实,而 Host 观测仍保留原始 cause。
|
||||
父 agent 可以区分 ACP 远端限制或权限决定,以及 DSH 子轮次、协议、传输或 shutdown 失败,同时不会接收子进程控制的文本。启动和清理错误与已发布结果使用同一套安全事实,而 Host 观测仍保留原始 cause。
|
||||
|
||||
诊断仍是展示文本,不是公共协议。消费方可以呈现它,但不得按格式分支。本决策不增加重试策略、恢复控制器、共享提供方错误 enum、stderr 分类器、认证分类、会话持久化、进度流或新的 ACP 能力。
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-24-route-priced-image-request-pressure.md
|
||||
2026-08-24-route-priced-image-request-pressure.md: 45a29211730474369607ed5fb933f380d640bf27
|
||||
2026-08-24-route-priced-image-request-pressure.zh.md: cf005a3ee343edf5774d554a4ec78cb876703774
|
||||
@@ -0,0 +1,39 @@
|
||||
# Agent Note: Route-priced image request pressure
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-24-route-priced-image-request-pressure.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The token meter priced an `ImageBlock` as the structural JSON of its durable reference — roughly forty tokens — while a DeepSeek request image costs up to 384 visual tokens, so an image-dense session could carry hundreds of thousands of unbilled estimated tokens. Provider usage anchors only completed requests: the first multimodal request, images added after the anchor, and offload-set changes all fed automatic compaction a pressure figure that was wrong by orders of magnitude, triggering it far too late (context overflow) or, after a route change, too early. The [version-one simplification](../simplification/2026-07-29-simplify-web-image-input-v1.md) had deliberately rejected a provider-neutral tile formula and deferred visual pricing until a provider-aware estimator had a concrete consumer.
|
||||
|
||||
## Decision
|
||||
|
||||
Compaction pressure is now priced by the routed model's own request projection. `LlmAdapter.imageRequestPricing(provider, model)` is an optional synchronous hook returning an `LlmImageRequestPricing` for one exact route, resolved through `ctx.llm.imageRequestPricing()`; the base adapter declares none and unknown providers degrade to `undefined`, never throw. Each ordered image occurrence resolves to an `LlmImageRequestPrice`: the provider's visual tokens for a retained image plus the model-visible text the wire actually carries (request-preview handle, offload placeholder, or text-only substitution), with the text left to the caller's own estimator so no provider fixes a text tokenization.
|
||||
|
||||
The DeepSeek adapter implements the hook from its connection snapshot (`request-pricing.ts`): uncatalogued and text-only models price every occurrence as its `textOnlyImageText` substitution; image-capable models reproduce the serializer's first-stage oldest-first offload through the shared `offloadedImagePrefixCount()`, build handle and placeholder text through the same execution-world access resolution the serializer uses, and price retained images at their `requestImageDimensions` projection with `deepSeekImageTokens()` — a verbatim port of the provider's published v4 vision calculator (14px patches, 3:1 downsampling, 384-token cap, minimum-pixel scale-up, 8:1 width clamp), priced at the worst-case pad-to-4 alignment. The pure geometry moved from `attachment-local` to `dsh-attachment` so provider and pricing share it.
|
||||
|
||||
The token meter's surface fold stores route-neutral facts per node — the fixed-heuristic price, the image-free price, and the durable image occurrences — and `measure()` prices the surface under the effective envelope's route on every call. The anchor holds its raw materials (surface snapshot, provider-output price, usage) instead of a precomputed baseline, so a matching header reprices both the anchor and the current surface under one route and the signed delta compares like with like; the usage-versus-estimated choice happens per measurement against the route-priced anchor. Public `TokenSurfaceNode` carries both `tokens` (route-priced; read by trigger, retention, range selection, and the summary-shrink comparison) and `heuristicTokens` (fixed; the shadow-price protocol's unit, so `compaction/summary` and `compaction/prune` stay consistent with the O(1) projection fold's own appends). The `contextPressure` and `contextBreakdown` projections deliberately stay on the fixed heuristic.
|
||||
|
||||
The test-support replay adapter declares a flat per-model `imageRequestTokens` so keyless assembled scenarios exercise the seam; the `image-compaction` ACP snapshot proves six inline images push the second turn's pre-step measurement over an automatic threshold that the text-only heuristic stays under, and that the triggered compaction shadows the image message at its heuristic price.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Price images inside the provider-neutral estimator.** Rejected by the [version-one note](../simplification/2026-07-29-simplify-web-image-input-v1.md) and still wrong: visual pricing varies by provider, model, detail mode, and preprocessing, and a hard-coded figure would look authoritative on routes it does not describe. The hook keeps every constant in the adapter that owns the route.
|
||||
|
||||
**Correct pressure only from provider usage.** Usage cannot price the first multimodal request, an image added after the anchor, or a changed offload set — exactly the cases that made compaction fire too late. Usage stays the anchor for completed requests; the route projection prices the increment.
|
||||
|
||||
**Reproduce the full serialization pipeline, including prepared-version bytes and the base64 fallback budgets.** The second-stage offload depends on encoded request bytes that only exist after asynchronous image preparation. The pricing reproduces the deterministic first stage from durable byte lengths; a fallback request can only offload more and cost less, so the estimate stays conservative without I/O in a synchronous hook.
|
||||
|
||||
**Route-price the shadow-price protocol too.** Logged `shadowedTokenCount` feeds the O(1) projection fold, whose appends are priced by the fixed heuristic; pricing replacements by route would make the persisted running total drift. Keeping the protocol on `heuristicTokens` preserves the fold's by-construction agreement.
|
||||
|
||||
**Fold route pricing into the meter's replay state.** A fold keyed to one route would have to replay on every route change and could not answer a `requestHeader` override for a different model. Storing route-neutral node facts and pricing at `measure()` keeps replay single-pass and measurement O(surface), which the contract already promises.
|
||||
|
||||
## Consequences
|
||||
|
||||
Automatic compaction now triggers on the pressure the routed model's next request will actually carry: image-dense DeepSeek sessions compact before overflow instead of after it, text-only routes charge substitution text instead of phantom visual tokens, and offloaded images cost their placeholder. The worst-case alignment pad overprices an image by at most three tokens, and the unreproduced base64-fallback budgets can only overprice — both errors are conservative; an execution-world access path that changes between pricing and the request shifts a descriptor's text price by its own length, and provider usage remains the authoritative anchor once a request completes. The published v4 calculator constants live in `llm-deepseek` alone; if the provider revises its vision projection, that one module and its pinned vectors are the change site. Measurement cost gains one pricing resolution and one image-occurrence walk per call, still O(surface).
|
||||
|
||||
## Testing
|
||||
|
||||
Formula vectors in `image-tokens.spec.ts` pin the published calculator's outputs, including the aspect-clamp, scale-up floor, one-column solver, odd-grid trim, and second-pass convergence cases, cross-checked against the reference implementation over a dimension grid and 50,000-point fuzz during development. `request-pricing.spec.ts` covers text-only substitution, the low-detail preset, and count- and byte-driven offload boundaries. Token-meter specs cover the first multimodal estimate, post-anchor image deltas over usage, text-only repricing under a header override, pricer-less neutrality, occurrence-count mismatch, and nested tool-result images. Compaction specs prove trigger, retention, range selection, and the summary-shrink comparison read the route price while the logged shadow price stays heuristic, including a summary that only route-priced shrink accepts. Access-resolution threading is covered at the pricing function and the adapter override. The keyless `image-compaction` ACP snapshot exercises the assembled application end to end.
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
# Agent Note: 按路由定价的图片请求压力
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-24-route-priced-image-request-pressure.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
token 计量服务把 `ImageBlock` 按其持久引用的 JSON 结构计价,约四十个 token,而一张 DeepSeek 请求图片最多消耗 384 个视觉 token,因此图片密集的会话可能携带数十万个未计入估算的 token。provider usage 只锚定已完成的请求:首次多模态请求、锚点之后新增的图片、offload 集合的变化,都会让自动 compaction 拿到数量级错误的压力值,触发得过晚(上下文溢出)或在路由切换后过早。[版本一简化](../simplification/2026-07-29-simplify-web-image-input-v1.zh.md)曾有意否决 provider-neutral 的 tile 公式,把视觉定价推迟到 provider-aware 估算器出现具体消费方之时。
|
||||
|
||||
## Decision
|
||||
|
||||
compaction 压力现在按路由模型自身的请求投影定价。`LlmAdapter.imageRequestPricing(provider, model)` 是可选的同步钩子,为一条确切路由返回 `LlmImageRequestPricing`,经 `ctx.llm.imageRequestPricing()` 解析;基类不声明定价,未注册的 provider 降级为 `undefined` 而绝不抛出。每个按序的图片出现处解析为一个 `LlmImageRequestPrice`:保留图片的提供方视觉 token,加上线上实际携带的模型可见文本(请求预览句柄、offload 占位文本或纯文本替换),文本交由调用方自己的估算器计价,避免任何提供方固定一种文本 token 化。
|
||||
|
||||
DeepSeek 适配器基于连接快照实现该钩子(`request-pricing.ts`):未编目和纯文本模型把每个出现处按其 `textOnlyImageText` 替换计价;支持图片的模型通过共享的 `offloadedImagePrefixCount()` 复现序列化器第一阶段的最旧优先 offload,经序列化器同一套执行环境访问解析构建句柄与占位文本,并按 `requestImageDimensions` 投影尺寸用 `deepSeekImageTokens()` 为保留图片计价,后者是提供方公布的 v4 视觉计算器的逐句移植(14px patch、3:1 降采样、384 token 上限、最小像素放大、8:1 宽度钳制),按最坏的 pad-to-4 对齐计价。纯几何函数从 `attachment-local` 上移到 `dsh-attachment`,供提供方与定价共享。
|
||||
|
||||
token 计量服务的表层 fold 为每个节点存储与路由无关的事实:固定启发式价格、去图价格与持久图片出现处;`measure()` 在每次调用时按生效 envelope 的路由为表层定价。锚点保存原始材料(表层快照、提供方输出价格、usage)而非预先计算的基线,因此匹配的标头会把锚点与当前表层放在同一路由下重新定价,带符号 delta 的比较口径一致;usage 与估算的选择在每次计量时针对路由定价锚点做出。公开的 `TokenSurfaceNode` 同时携带 `tokens`(路由定价;触发、保留、选段与摘要收缩比较读取它)和 `heuristicTokens`(固定值;影子价协议的计量单位,使 `compaction/summary` 与 `compaction/prune` 与 O(1) 投影 fold 自身的追加保持一致)。`contextPressure` 与 `contextBreakdown` 投影有意保持固定启发式规则。
|
||||
|
||||
test-support 的回放适配器按模型声明固定的 `imageRequestTokens`,让 keyless 装配场景走通这条 seam;`image-compaction` ACP 快照证明六张内联图片把第二轮 pre-step 计量推过自动阈值,而纯文本启发式保持在阈值之下,且被触发的 compaction 按启发式价格遮蔽了图片消息。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**在 provider-neutral 估算器里为图片定价。** 已被[版本一 note](../simplification/2026-07-29-simplify-web-image-input-v1.zh.md)否决且依然错误:视觉定价随提供方、模型、细节档位与预处理而不同,写死的数字在它不描述的路由上会显得权威却错误。钩子把每个常量留在拥有该路由的适配器里。
|
||||
|
||||
**只用 provider usage 校正压力。** usage 无法为首次多模态请求、锚点后新增图片或变化的 offload 集合定价,而这些正是让 compaction 触发过晚的情形。usage 仍是已完成请求的锚点;增量由路由投影定价。
|
||||
|
||||
**复现完整序列化管线,包括请求版本字节与 base64 回退预算。** 第二阶段 offload 依赖异步图片准备之后才存在的编码字节。定价复现由持久字节长度决定的确定性第一阶段;回退请求只会 offload 更多、花费更少,因此估算在同步无 I/O 的钩子里保持保守。
|
||||
|
||||
**让影子价协议也按路由定价。** 记录的 `shadowedTokenCount` 供 O(1) 投影 fold 消费,而该 fold 的追加按固定启发式计价;替换若按路由定价会让持久化的累计值漂移。协议保持在 `heuristicTokens` 上,维持 fold 的构造性一致。
|
||||
|
||||
**把路由定价并入计量服务的回放状态。** 绑定单一路由的 fold 在路由每次变化时都得重放,也无法回答指向另一模型的 `requestHeader` 覆盖。存储与路由无关的节点事实并在 `measure()` 时定价,保持单遍回放与契约已承诺的 O(surface) 计量。
|
||||
|
||||
## Consequences
|
||||
|
||||
自动 compaction 现在按路由模型下一次请求实际携带的压力触发:图片密集的 DeepSeek 会话在溢出之前而非之后压缩,纯文本路由收取替换文本而非幻影视觉 token,被 offload 的图片按占位文本计费。最坏对齐 pad 对单图最多多计三个 token,未复现的 base64 回退预算只会多计——两种误差都偏保守;执行环境访问路径若在定价与请求之间变化,只会按其自身长度改变描述文本的价格,请求完成后 provider usage 仍是权威锚点。公布的 v4 计算器常量只存在于 `llm-deepseek`;提供方若修订其视觉投影,改动点就是这一个模块与其钉死的向量。每次计量多一次定价解析与一次图片出现处遍历,仍为 O(surface)。
|
||||
|
||||
## Testing
|
||||
|
||||
`image-tokens.spec.ts` 的公式向量钉死公布计算器的输出,覆盖宽高比钳制、放大下限、单列求解、奇数网格裁剪与第二遍收敛的用例,开发期间与参考实现在尺寸网格及五万点模糊测试上对拍。`request-pricing.spec.ts` 覆盖纯文本替换、低细节预设以及数量与字节驱动的 offload 边界。token-meter 测试覆盖首次多模态估算、usage 之上的锚后图片 delta、标头覆盖下的纯文本重定价、无定价器时的中性行为、出现处数量不匹配与嵌套工具结果图片。compaction 测试证明触发、保留、选段与摘要收缩比较读取路由价格而记录的影子价保持启发式,包括一个只有路由定价收缩才接受的摘要。访问解析的传递在定价函数与适配器覆写两处都有覆盖。keyless 的 `image-compaction` ACP 快照端到端验证装配后的应用。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.md
|
||||
2026-07-04-doc-tiers-and-budgets.md: 3f263864b9b6ee9479d1133b908617f10073dd66
|
||||
2026-07-04-doc-tiers-and-budgets.zh.md: d0745d2ac7aacea0f61ea6b699fb86fa39326881
|
||||
2026-07-04-doc-tiers-and-budgets.md: 378da8f8fddafa32dc7450bfac1c5376f2c7a065
|
||||
2026-07-04-doc-tiers-and-budgets.zh.md: 1d92ed7fbbec8a9a15bf94a2d320ee88f65a9fa8
|
||||
|
||||
@@ -15,7 +15,7 @@ Standing docs accumulated repeated rules, retold incidents, duplicated package m
|
||||
- **One product onboarding path.** The root README owns the recommended package-run path, the source-run alternative, and compact `dsh plugin --profile` usage. The published user guide starts with tasks inside the running Web UI, then links to distinct tutorials or reference owners for other interfaces, plugin development, and advanced configuration instead of repeating Web startup.
|
||||
- **A narrow, hard budget gate.** [scripts/verify-doc-budgets.ts](../../../../scripts/verify-doc-budgets.ts) joins `doc-sync`: every doc listed in [scripts/doc-budgets.manifest.json](../../../../scripts/doc-budgets.manifest.json) must stay under its word ceiling (`wc -w` semantics, whole file), and a budgeted file that is missing fails the gate so a rename cannot silently orphan its budget. Scope is deliberately only the accretion-prone standing docs — the root and subtree `AGENTS.md` files, `architecture.md`, `packages/README.md`, and the standing policy docs they evict content into (`docs/testing.md`, `docs/defensive-patterns.md`). Reference docs, Agent Notes, and package READMEs are unbudgeted: length is legitimate there when every row is a fact, and review plus the slop checklist govern them.
|
||||
- **Ceilings are an enforcement frontier that ratchets.** A doc at or below its target keeps at least 5% headroom as its ceiling ratchets down; a doc above target keeps a frozen ceiling that prevents growth until it reaches the target (root `AGENTS.md` ≤ 1,600 words; `architecture.md` ≤ 1,800; subtree `AGENTS.md` ≤ 600 except `packages/AGENTS.md` ≤ 650 and `docs/AGENTS.md` ≤ 1,250; `packages/README.md` ≤ 600). When the gate goes red, relocate or condense; raise a ceiling only with explicit PR justification.
|
||||
- **A thin workflow skill, contracts in docs.** [.agents/skills/dsh-doc-standards](../../../skills/dsh-doc-standards/SKILL.md) carries the placement/audit/red-gate workflow and defers to the standard as its source of truth, the same split as [dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md) over the i18n contract.
|
||||
- **A thin workflow skill, contracts in docs.** [.agents/skills/dsh-doc](../../../skills/dsh-doc/SKILL.md) carries the placement, audit, budget, and website workflow and defers to the standard as its source of truth, the same split as [dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md) over the i18n contract.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Status: implemented
|
||||
- **单一产品入门路径。**根 README 负责推荐的包运行路径、从源码运行的备选路径和简要的 `dsh plugin --profile` 用法。已发布的用户指南从运行中的 Web UI 内部任务开始,再链接到其他界面的独立教程或插件开发与进阶配置的参考文档归属处,而不会重复介绍 Web 启动步骤。
|
||||
- **范围窄且严格的预算门禁。**[scripts/verify-doc-budgets.ts](../../../../scripts/verify-doc-budgets.ts) 接入 `doc-sync`:[scripts/doc-budgets.manifest.json](../../../../scripts/doc-budgets.manifest.json) 列出的每份文档都必须低于其词数上限(采用 `wc -w` 语义,统计整个文件);预算内文件缺失也会使门禁失败,使重命名无法悄然遗落其预算。范围刻意只涵盖容易膨胀的常设文档——根目录和子树中的 `AGENTS.md` 文件、`architecture.md`、`packages/README.md`,以及它们将内容移入的常设策略文档(`docs/testing.md`、`docs/defensive-patterns.md`)。参考文档、Agent Note 和包 README 不设预算:只要每一行都是事实,长度在这些位置就是合理的;评审和赘余检查清单负责约束它们。
|
||||
- **上限是只进不退的执行红线。** 达到或低于目标的文档在上限逐步下调时保留至少 5% 的余量;高于目标的文档则维持冻结的上限,在达到目标之前不得增长(根 `AGENTS.md` ≤ 1,600 词;`architecture.md` ≤ 1,800;子树 `AGENTS.md` ≤ 600,但 `packages/AGENTS.md` ≤ 650、`docs/AGENTS.md` ≤ 1,250;`packages/README.md` ≤ 600)。门禁变红时,迁移或压缩内容;只有在 PR(Pull Request)描述中给出明确理由时才提高上限。
|
||||
- **精简的工作流 skill(技能),约定归文档。**[.agents/skills/dsh-doc-standards](../../../skills/dsh-doc-standards/SKILL.md) 承载文档放置、审计和门禁失败处理工作流,并以文档标准为真源,与 [dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md) 和 i18n 约定之间的分工相同。
|
||||
- **精简的工作流 skill(技能),约定归文档。**[.agents/skills/dsh-doc](../../../skills/dsh-doc/SKILL.md) 承载文档放置、审计、预算与站点发布工作流,并以文档标准为真源,与 [dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md) 和 i18n 约定之间的分工相同。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-22-product-first-root-readme.md
|
||||
2026-07-22-product-first-root-readme.md: 24c6dc04f24bd758d8955824a17b1d99801ecd16
|
||||
2026-07-22-product-first-root-readme.zh.md: f5c0d821e916423c92258649c22ceed222c06439
|
||||
2026-07-22-product-first-root-readme.md: 662ff334536227e7eeac4c8328936bb57e0aecc9
|
||||
2026-07-22-product-first-root-readme.zh.md: 3295794339d6348219ab8db68bad1077d3e54ee2
|
||||
|
||||
@@ -10,13 +10,11 @@ The root README is the repository's product entry point. Its product-first struc
|
||||
|
||||
## Decision
|
||||
|
||||
The root README preserves its existing structure, order, and wording wherever the underlying fact remains correct. A refresh changes only stale claims and adds material needed to represent shipped surfaces; it does not use repository growth as a reason to reframe the whole page.
|
||||
The root README is a compact product and contributor entry point. It states the product identity and plugin architecture, links the documentation site, marks the developer-preview and safety status, and then gives the supported npm and source launch paths.
|
||||
|
||||
A note before installation thanks internal testers, states that features and experience remain unfinished, and asks for direct reports of failures, confusion, and friction through the WeCom group. The existing development-stage statement identifies DeepSeek Harness as being in internal testing.
|
||||
Both launch paths start the Web UI through the `dsh` profile entry point. The source path builds the checkout before it runs `pnpm dsh web`. Detailed ACP, TUI, SDK, capability, and package guidance stays in the user guide, architecture documentation, and package map instead of being repeated on the landing page.
|
||||
|
||||
The user-surface section adds the ACP automation server and Python/JSON-RPC SDK beside the existing Web, TUI, and headless entries. The installed TUI remains the single `dsh` command; the Web instructions build the active checkout before running `dsh web`, and custom or reused checkout paths stay explicit. These launch paths must remain executable through a real PTY and a production build/HTTP smoke, respectively. The capability paragraph keeps its compact inventory style while adding the shipped PTY, LSP, web, goal, planning, task, sandbox, approval, settings, credentials, session-query, and telemetry families and stating that compositions select subsets. One adjacent bullet records the authoritative-session-log rule because persistence, replay, queries, telemetry, and interfaces depend on it.
|
||||
|
||||
Detailed package and service inventories remain at their owning documentation. The English and Chinese README sides share the same technical structure, while their community sections continue to point to the primary channel for each language audience. The documentation website keeps a separate [quick-start entry route](../../../../docs/user/index.md) instead of presenting another product landing page.
|
||||
The remaining sections link community support, contribution guidance, development documentation, agent instructions, the license, and third-party notices. The English and Chinese README sides keep the same technical structure while their community links serve their language audiences. The documentation website keeps a separate [quick-start entry route](../../../../docs/user/index.md).
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -10,13 +10,11 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
只要背后的事实仍然正确,根 README 就保留既有结构、顺序和措辞。刷新时只修正陈旧声明,并补充呈现已交付内容所需的信息;不会因为仓库规模增长就重构整篇叙事。
|
||||
根 README 是简短的产品和贡献者入口。它说明产品定位与插件架构,链接文档站,标明开发者预览与安全状态,然后给出受支持的 npm 和源码启动路径。
|
||||
|
||||
安装说明之前的一则文字感谢内测用户,说明功能和体验仍待完善,并邀请大家通过企业微信群直接反馈失败、困惑和不顺手之处。既有的开发阶段声明明确说明 DeepSeek Harness 处于内测阶段。
|
||||
两条启动路径都通过 `dsh` profile 入口启动 Web UI。源码路径先构建当前检出,再运行 `pnpm dsh web`。ACP、TUI、SDK、能力和包的详细说明由用户指南、架构文档与包索引维护,不在入口页重复。
|
||||
|
||||
用户入口章节在已有的 Web、TUI 和 Headless 入口旁补充 ACP(Agent Client Protocol)自动化服务器和 Python/JSON-RPC SDK。安装后的 TUI 仍只需执行一条 `dsh` 命令;Web 说明要求先构建当前检出,再运行 `dsh web`,并明确处理自定义或复用的检出路径。这两条启动路径必须分别能在真实 PTY 与生产构建/HTTP 冒烟中原样执行。能力段落沿用简洁清单的写法,补充已经交付的 PTY、LSP、Web、目标、规划、任务、沙箱、审批、设置、凭据、会话查询和遥测等能力类别,并说明不同组合只选用其中一部分。相邻的一条列表项说明权威会话日志规则,因为持久化、回放、查询、遥测和各类接口都依赖它。
|
||||
|
||||
包与服务的完整清单仍由各自的归属文档维护。中英文 README 采用相同的技术结构,但社区章节仍分别指向各自语言受众的主要交流渠道。文档网站保留独立的[快速开始入口路由](../../../../docs/user/index.zh.md),不另行呈现产品首页。
|
||||
其余章节链接社区支持、贡献指南、开发文档、agent 指令、许可证与第三方声明。中英文 README 保持相同技术结构,社区链接分别服务各自语言受众。文档网站保留独立的[快速开始入口路由](../../../../docs/user/index.zh.md)。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-27-explicit-change-scope-report.md
|
||||
2026-07-27-explicit-change-scope-report.md: ed09ffc44252e1e571d50537b3471cad8d68d8f9
|
||||
2026-07-27-explicit-change-scope-report.zh.md: 783f89b9b42f1706da218997a06bfe7c28a0936b
|
||||
2026-07-27-explicit-change-scope-report.md: 51f79039f23408f4463d08c08774089a96ce15ab
|
||||
2026-07-27-explicit-change-scope-report.zh.md: 4ea99f15c05123ae774b5bc758abe5b79b54c4c1
|
||||
|
||||
@@ -6,7 +6,7 @@ English | [中文](2026-07-27-explicit-change-scope-report.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The [pre-push workflow](../../../skills/dsh-pre-push-checks/SKILL.md) needs the diff against the actual base, but constructing `origin/<current-branch>` fails for a new worktree branch that tracks `origin/master` before its first push and misstates a stacked branch whose PR targets another feature branch. The [code-review](../../../skills/dsh-code-review/SKILL.md) and [documentation-audit](../../../skills/dsh-doc-standards/SKILL.md) workflows need the same current-base judgment.
|
||||
The [pre-push workflow](../../../skills/dsh-pre-push-checks/SKILL.md) needs the diff against the actual base, but constructing `origin/<current-branch>` fails for a new worktree branch that tracks `origin/master` before its first push and misstates a stacked branch whose PR targets another feature branch. The [code-review](../../../skills/dsh-code-review/SKILL.md) and [documentation-audit](../../../skills/dsh-doc/SKILL.md) workflows need the same current-base judgment.
|
||||
|
||||
An incorrect range undermines evidence selection because it can omit affected paths. A three-dot committed diff also says nothing about Git's separate staged, unstaged, and untracked layers.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
[pre-push 工作流](../../../skills/dsh-pre-push-checks/SKILL.md)需要取得相对于实际基准的 diff,但按 `origin/<current-branch>` 构造引用存在两类问题:对于第一次推送前跟踪 `origin/master`、尚无同名远端分支的新 worktree 分支,该引用无法解析;对于 PR(Pull Request)以另一功能分支为基准的堆叠分支,该引用会错误描述基准。[代码评审](../../../skills/dsh-code-review/SKILL.md)与[文档审计](../../../skills/dsh-doc-standards/SKILL.md)工作流同样需要判断当前基准。
|
||||
[pre-push 工作流](../../../skills/dsh-pre-push-checks/SKILL.md)需要取得相对于实际基准的 diff,但按 `origin/<current-branch>` 构造引用存在两类问题:对于第一次推送前跟踪 `origin/master`、尚无同名远端分支的新 worktree 分支,该引用无法解析;对于 PR(Pull Request)以另一功能分支为基准的堆叠分支,该引用会错误描述基准。[代码评审](../../../skills/dsh-code-review/SKILL.md)与[文档审计](../../../skills/dsh-doc/SKILL.md)工作流同样需要判断当前基准。
|
||||
|
||||
错误的范围可能遗漏受影响的路径,从而削弱证据选择。三点范围产生的已提交 diff 也完全无法说明 Git 中彼此独立的已暂存、未暂存与未跟踪层。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-31-coverage-exempt-heavy-suites.md
|
||||
2026-07-31-coverage-exempt-heavy-suites.md: 1f468a69321b451593a9279cfebc1b457fb08a47
|
||||
2026-07-31-coverage-exempt-heavy-suites.zh.md: 7e519f44c8321b6b99c04c6af56c4cfa5b641663
|
||||
2026-07-31-coverage-exempt-heavy-suites.md: fe33308cfcbd709a563e697a5e62585be58f214d
|
||||
2026-07-31-coverage-exempt-heavy-suites.zh.md: 233fdc403fe734b21d116240b8067eaef119cd45
|
||||
|
||||
@@ -21,6 +21,8 @@ Linux coverage CI and native Windows CI use [in-job partitioned coverage](2026-0
|
||||
|
||||
`scripts/coverage-exempt.ts` is the single roster point, holding the membership contract and the filter/exclude pairs so the two sides cannot drift.
|
||||
|
||||
The roster also contains the packed-image loadability suite. That suite reads built workspace artifacts while the packer and Web Worker runtime sources it imports are threshold-excluded. Native Windows makes the uninstrumented gate wait for `build`, so the suite cannot observe a partially emitted dependency closure.
|
||||
|
||||
### The roster, reconciled entry by entry
|
||||
|
||||
A suite contributes to coverage exactly when it executes measured files in-process (`coverage.include` spans the package src trees). The current roster, audited:
|
||||
@@ -30,6 +32,8 @@ A suite contributes to coverage exactly when it executes measured files in-proce
|
||||
| All 6 typert generator specs | The generator's own src | Generator src is threshold-excluded as a package (`vitest.config.ts`) — outside the threshold scope to begin with |
|
||||
| tools-catalog.spec additionally imports | `typert-registry` and `tool-cordis` src | Each package's own tests cover them fully (verified with focused coverage runs, zero threshold errors) |
|
||||
| `scripts/install-lefthook.spec.ts`, `scripts/oxlint-contract.spec.ts`, `scripts/change-scope.spec.ts`, `scripts/translation-pairing-merge.spec.ts` | None — they test `scripts/` sources (never in `coverage.include`) and work by spawning child processes | Nothing to carry |
|
||||
| `packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts` | None — it spawns a child process that transforms and imports every built bundle (Node's ESM loader is the oracle) | webworker-runtime src is threshold-excluded as a package (`vitest.config.ts`) — outside the threshold scope to begin with |
|
||||
| `packages/experimental/webworker-packer/tests/image-loadable.spec.ts` | Packer and Web Worker runtime src, both threshold-excluded in `vitest.config.ts` | The suite is correctness evidence over built artifacts; native Windows runs it after build in the uninstrumented gate |
|
||||
|
||||
### Membership contract
|
||||
|
||||
@@ -58,6 +62,7 @@ Measured on CI (16-core runner): the gate segment went from 424 seconds to the t
|
||||
## Consequences
|
||||
|
||||
- The exempt suites execute without adding instrumentation cost to the thresholded gate; partitioned wall-clock measurements belong to the [in-job partitioning decision](2026-08-18-in-job-partitioned-coverage.md).
|
||||
- Native Windows makes the exempt gate wait for build, so the packed-image suite reads a complete workspace artifact tree.
|
||||
- `DSH_GATE_CONCURRENCY` has two schedulable gates in this lane again, so the aggregate scheduler is no longer a pass-through.
|
||||
- Adding a heavy suite to the roster requires the membership audit above; a wrong entry fails the instrumented gate loudly rather than eroding coverage silently.
|
||||
- The exempt suites no longer appear in the coverage report's file list of contributors; their correctness signal lives solely in the uninstrumented gate's pass/fail.
|
||||
|
||||
@@ -21,6 +21,8 @@ Linux 覆盖率 CI 与原生 Windows CI 在插桩门禁内部使用 [job 内分
|
||||
|
||||
`scripts/coverage-exempt.ts` 是唯一名单点,集中持有成员资格约定与 filter/exclude 配对,防止两侧漂移。
|
||||
|
||||
该名单还包含构建镜像可加载性套件。这个套件读取工作区构建产物,而它导入的 packer 与 Web Worker runtime 源码已排除在阈值外。原生 Windows 会让无插桩门禁等待 `build`,因此该套件不会观察到只完成部分输出的依赖闭包。
|
||||
|
||||
### 豁免名单与逐项对账
|
||||
|
||||
一个套件对覆盖率有贡献,当且仅当它在进程内执行了被度量的文件(`coverage.include` = 包 src 树)。现行名单逐项核对:
|
||||
@@ -30,6 +32,8 @@ Linux 覆盖率 CI 与原生 Windows CI 在插桩门禁内部使用 [job 内分
|
||||
| typert generator 全部 6 个 spec | generator 自身 src | generator src 已整包 threshold-excluded(`vitest.config.ts`),本不在阈值口径内 |
|
||||
| 其中 tools-catalog.spec 额外 import | `typert-registry`、`tool-cordis` 的 src | 两包各自的测试独立满覆盖(focused coverage 实测无阈值错误) |
|
||||
| `scripts/install-lefthook.spec.ts`、`scripts/oxlint-contract.spec.ts`、`scripts/change-scope.spec.ts`、`scripts/translation-pairing-merge.spec.ts` | 无——被测对象是 `scripts/` 源码(从不在 coverage.include),执行方式是 spawn 子进程 | 无需接 |
|
||||
| `packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts` | 无——spawn 子进程对全部已构建 bundle 做 transform 并 import(oracle 是 Node ESM loader) | webworker-runtime src 已整包 threshold-excluded(`vitest.config.ts`),本不在阈值口径内 |
|
||||
| `packages/experimental/webworker-packer/tests/image-loadable.spec.ts` | packer 与 Web Worker runtime 源码,两者都在 `vitest.config.ts` 中排除阈值 | 该套件为构建产物提供正确性证据;原生 Windows 在构建后通过无插桩门禁运行它 |
|
||||
|
||||
### 成员资格约定
|
||||
|
||||
@@ -58,6 +62,7 @@ CI 实测(16 核 runner):拆分前 gate 段 424 秒,拆分后两 gate
|
||||
## Consequences
|
||||
|
||||
- 豁免套件在执行时不会向阈值门禁叠加插桩开销;分区墙钟数据由 [job 内分区决策](2026-08-18-in-job-partitioned-coverage.zh.md)负责记录。
|
||||
- 原生 Windows 让豁免门禁等待构建,因此构建镜像套件会读取完整的工作区产物树。
|
||||
- `DSH_GATE_CONCURRENCY` 在本 lane 重新拥有两个可调度对象,聚合调度器不再是直通。
|
||||
- 向名单新增重型套件必须完成上述成员资格对账;错误条目会让插桩 gate 大声失败,而不是静默侵蚀覆盖率。
|
||||
- 豁免套件不再出现在覆盖率报告的贡献文件列表中;其正确性信号完全由无插桩 gate 的红绿承载。
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md
|
||||
2026-08-08-native-windows-pull-request-ci.md: 24f1a5fa0c72255f678b837c910eb779445dee27
|
||||
2026-08-08-native-windows-pull-request-ci.zh.md: 805974192577b835b565bd42688c574473ac63bf
|
||||
2026-08-08-native-windows-pull-request-ci.md: 98f48029a86a8b07e53cd4498b27d637508e450b
|
||||
2026-08-08-native-windows-pull-request-ci.zh.md: b2dba91e4a88d0da637521aae2de937672b869a3
|
||||
|
||||
@@ -18,7 +18,7 @@ Every pull request also starts an ordinary independent `windows-native` job name
|
||||
|
||||
The native job is deliberately absent from `all-checks-passed.needs` and does not use `continue-on-error`: the aggregate neither waits for it nor changes conclusion because of it, while the job retains its own unmasked result. Workspace build, production-site, and 100%-per-file coverage failures make the native job fail. Static, documentation, package, built-artifact, lint, and snapshot inventories run in the same job as observational gates: their failures remain visible without changing the native aggregate result because Linux owns their blocking verdict.
|
||||
|
||||
The 16-core lane admits four concurrent outer gates. Workspace build and production-site validation start immediately. Instrumented and exempt-heavy coverage both wait for the complete build: the instrumented corpus includes packer assertions over built `lib/` output, while the exempt gate's temporary Oxlint contract probes must not race source compilation. Every observational gate waits for both coverage gates to settle, regardless of outcome, before entering an available slot; its own `needs` edges still require their predecessors to pass. This also keeps later static gates that create temporary contract files from racing either coverage scan. [In-job partitioned coverage](2026-08-18-in-job-partitioned-coverage.md) uses four single-worker shards, while the exempt-heavy gate receives two workers from `DSH_COVERAGE_MAX_WORKERS=6`, for about six active coverage execution units after build. `publint` is capped at eight workers when the observational inventory starts. Every Vitest project uses forked workers because Node 24's CJS lexer fatal reproduced in shared worker threads on Windows and POSIX. Both coverage gates set Vitest's default per-test and polling budgets to 30 seconds because unrelated process, Git, SQLite, watcher, grammar, and static-gate fixtures can exceed 15 seconds only under the complete lane's concurrent Windows instrumentation. The SQLite busy-journal pacing fixture injects two busy results followed by success under the normal busy budget and observes each inter-attempt delay, keeping schema-setup scheduling outside its timing assertion. The script-only translation-pairing merge suite runs in the exempt-heavy gate because it imports only `scripts/` sources and child processes; V8 instrumentation contributes no threshold coverage there but magnifies Git-process latency. Lefthook concurrency fixtures retain their outcomes with 30-second case budgets and a 10-second process-ready probe, while the installer allows five seconds for a preempted lock owner to publish its record after exclusive creation. Directory-picker composition gives its debounced config write an explicit 15-second poll budget; workspace-context composition fixtures use a test-owned signal without an unrelated one-second deadline. These lane-scoped budgets preserve asserted outcomes, while the 120-minute job deadline still bounds a stuck run. The LSP sources and the ACL-sandbox sources remain in the Windows denominator: stub-based failure-path suites carry every in-process ACL-sandbox file to 100%, and only the runner entry stays excluded — it executes exclusively as a spawned child outside the instrumented run, its behavior pinned end-to-end by the runner suite. Narrow annotated V8 ignores cover only unreachable branches (peer-platform arms and lifecycle-unreachable guards), with their behavior tests retained on the owning platform.
|
||||
The 16-core lane admits four concurrent outer gates. Workspace build and production-site validation start immediately. Instrumented and exempt-heavy coverage both wait for the complete build: the instrumented corpus includes packer assertions over built `lib/` output, while the exempt gate's temporary Oxlint contract probes must not race source compilation and its packed-image suite must read a complete artifact tree. Every observational gate waits for both coverage gates to settle, regardless of outcome, before entering an available slot; its own `needs` edges still require their predecessors to pass. This also keeps later static gates that create temporary contract files from racing either coverage scan. [In-job partitioned coverage](2026-08-18-in-job-partitioned-coverage.md) uses four single-worker shards, while the exempt-heavy gate receives two workers from `DSH_COVERAGE_MAX_WORKERS=6`, for about six active coverage execution units after build. `publint` is capped at eight workers when the observational inventory starts. Every Vitest project uses forked workers because Node 24's CJS lexer fatal reproduced in shared worker threads on Windows and POSIX. Both coverage gates set Vitest's default per-test and polling budgets to 30 seconds because unrelated process, Git, SQLite, watcher, grammar, and static-gate fixtures can exceed 15 seconds only under the complete lane's concurrent Windows instrumentation. The SQLite busy-journal pacing fixture injects two busy results followed by success under the normal busy budget and observes each inter-attempt delay, keeping schema-setup scheduling outside its timing assertion. The script-only translation-pairing merge suite runs in the exempt-heavy gate because it imports only `scripts/` sources and child processes; V8 instrumentation contributes no threshold coverage there but magnifies Git-process latency. Lefthook concurrency fixtures retain their outcomes with 30-second case budgets and a 10-second process-ready probe, while the installer allows five seconds for a preempted lock owner to publish its record after exclusive creation. Directory-picker composition gives its debounced config write an explicit 15-second poll budget; workspace-context composition fixtures use a test-owned signal without an unrelated one-second deadline. These lane-scoped budgets preserve asserted outcomes, while the 120-minute job deadline still bounds a stuck run. The LSP sources and the ACL-sandbox sources remain in the Windows denominator: stub-based failure-path suites carry every in-process ACL-sandbox file to 100%, and only the runner entry stays excluded — it executes exclusively as a spawned child outside the instrumented run, its behavior pinned end-to-end by the runner suite. Narrow annotated V8 ignores cover only unreachable branches (peer-platform arms and lifecycle-unreachable guards), with their behavior tests retained on the owning platform.
|
||||
|
||||
The 16-core allocation is the measured capacity point for this inventory. Six-worker coverage trials produced complete passes in 6 minutes 27 seconds and 7 minutes 50 seconds, while exact-head trials with four, three, and two concurrent workers inside one instrumented Vitest process exposed unreliable fixtures and worker exits. Separate single-worker child processes retain process isolation. Historical sixteen-shard samples reduced instrumented coverage to 112.66–122.01 seconds. Under the current post-build graph, sixteen instrumented shards plus two exempt workers would schedule eighteen coverage execution units on a 16-core runner before any production-site tail or system overhead; four shards plus two exempt workers schedule six. Four deliberately trades some single-job latency for lower process-creation pressure under high self-hosted concurrency. A 32-core comparison reduced aggregate gate time by only 1.47 seconds and still triggered the CJS-lexer fatal inside a fork worker, so additional cores did not provide a reliable wall-clock improvement.
|
||||
|
||||
@@ -32,7 +32,7 @@ Windows durable JSONL paths keep drive roots in native spelling and apply the ex
|
||||
|
||||
Post-boot profile watcher setup proceeds only while the root fiber and Loader are both live. A concurrent setup error is contained only when the same invocation's recorded signal already owns shutdown; unrelated HMR failures remain loud. The [process-shutdown controller](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md) lets a successful one-shot completion drain Node's remaining handles after root disposal, while teardown failure, deadline, and signal escalation retain forced exit. The vendored Include serializes debounced writes, retries only transient access or busy failures with bounded backoff, and observes every timer rejection. A terminal persistence failure remains on the queue and is rethrown to the teardown owner, while successful teardown drains the latest write.
|
||||
|
||||
Shiki disables lazy TextMate-regex compilation and warms each boot grammar before user content enters the unchanged per-line tokenization budget, so scheduler contention cannot publish a partial highlighted stream. The Codex real-product fixture is pinned to stable 0.147.0 schemas and selects an actually advertised command tool and argument shape, preserving the provider-owned protocol while proving unattended rejection and whole-tree exit on each host.
|
||||
Shiki disables lazy TextMate-regex compilation and warms each boot grammar before user content enters the unchanged per-line tokenization budget, so scheduler contention cannot publish a partial highlighted stream. The Codex real-product fixture is pinned to stable 0.149.1 schemas and selects an actually advertised command tool and argument shape, preserving the provider-owned protocol while proving unattended rejection and whole-tree exit on each host.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Status: implemented
|
||||
|
||||
原生作业被刻意排除在 `all-checks-passed.needs` 之外,且不使用 `continue-on-error`:聚合流程既不等待它,也不会因它改变结论;该作业则保留自身未被掩盖的结果。工作区构建、生产网站和逐文件 100% 覆盖率检查失败会使原生作业失败。静态检查、文档、包、构建产物、lint 与快照清单在同一作业内作为观测性门禁运行;其失败保持可见,但不会改变原生聚合结果,因为这些检查的阻断性判定由 Linux 负责。
|
||||
|
||||
16 核通道最多同时运行 4 道外层门禁。工作区构建与生产网站验证会立即启动。插桩覆盖率与豁免重型覆盖率都等待完整构建:插桩语料包含针对已构建 `lib/` 输出的打包器断言,豁免门禁的临时 Oxlint 约定探针则不得与源码编译竞态。每道观测性门禁只等待两道覆盖率门禁以任意结果结算后再进入可用槽位;各门禁自身的 `needs` 边仍要求前置门禁通过。这也使随后创建临时约定文件的静态门禁不会与任一覆盖率扫描竞态。[job 内分区覆盖率](2026-08-18-in-job-partitioned-coverage.zh.md)使用 4 个单 worker 分片,豁免重型门禁则从 `DSH_COVERAGE_MAX_WORKERS=6` 获得 2 个 worker,因此构建完成后约有 6 个活动覆盖率执行单元。观测性清单启动时,`publint` 最多使用 8 个 worker。每个 Vitest 项目都使用 fork worker,因为 Node 24 的 CJS lexer 致命故障可在 Windows 与 POSIX 的共享 worker 中复现。两项覆盖率门禁都将 Vitest 默认的单测试和轮询时间预算设为 30 秒,因为在完整通道并发的 Windows 插桩下,多个互不相关的进程、Git、SQLite、watcher、语法和静态门禁 fixture(测试前置数据)可能超过 15 秒。SQLite busy-journal 节奏 fixture 会在普通 busy 预算内先注入两次 busy 结果,再返回成功,并观察每次尝试之间的延迟,使 schema 设置的调度时间不进入该断言。translation-pairing 合并套件只导入 `scripts/` 源码和子进程,因此放入豁免重型套件门禁;V8 插桩不会为它贡献任何阈值覆盖率,却会放大 Git 进程延迟。Lefthook 并发 fixture 保留原有结果,采用 30 秒单用例预算与 10 秒进程就绪探测;安装器则允许被抢占的 lock 持有者在独占创建后用 5 秒发布记录。directory-picker 组合为防抖配置写入提供显式的 15 秒轮询预算;workspace-context 组合 fixture 使用测试自有、没有无关 1 秒截止时间的信号。这些只属于该通道的预算保留了原有断言结果,120 分钟的 job 截止时间仍会约束卡死的运行。LSP 源码与 ACL 沙箱源码仍计入 Windows 分母:基于 stub 的失败路径套件把每个进程内 ACL 沙箱文件都带到 100%,只有 runner 入口保持排除——它只作为 spawn 出的子进程在插桩运行之外执行,其行为由 runner 套件端到端钉住。窄范围且带注释的 V8 ignore 只覆盖不可达分支(另一平台专属分支、生命周期内不可达的防御守卫),其行为测试仍保留在所属平台。
|
||||
16 核通道最多同时运行 4 道外层门禁。工作区构建与生产网站验证会立即启动。插桩覆盖率与豁免重型覆盖率都等待完整构建:插桩语料包含针对已构建 `lib/` 输出的打包器断言,豁免门禁的临时 Oxlint 约定探针则不得与源码编译竞态,并且其 packed-image 套件必须读取完整的产物树。每道观测性门禁只等待两道覆盖率门禁以任意结果结算后再进入可用槽位;各门禁自身的 `needs` 边仍要求前置门禁通过。这也使随后创建临时约定文件的静态门禁不会与任一覆盖率扫描竞态。[job 内分区覆盖率](2026-08-18-in-job-partitioned-coverage.zh.md)使用 4 个单 worker 分片,豁免重型门禁则从 `DSH_COVERAGE_MAX_WORKERS=6` 获得 2 个 worker,因此构建完成后约有 6 个活动覆盖率执行单元。观测性清单启动时,`publint` 最多使用 8 个 worker。每个 Vitest 项目都使用 fork worker,因为 Node 24 的 CJS lexer 致命故障可在 Windows 与 POSIX 的共享 worker 中复现。两项覆盖率门禁都将 Vitest 默认的单测试和轮询时间预算设为 30 秒,因为在完整通道并发的 Windows 插桩下,多个互不相关的进程、Git、SQLite、watcher、语法和静态门禁 fixture(测试前置数据)可能超过 15 秒。SQLite busy-journal 节奏 fixture 会在普通 busy 预算内先注入两次 busy 结果,再返回成功,并观察每次尝试之间的延迟,使 schema 设置的调度时间不进入该断言。translation-pairing 合并套件只导入 `scripts/` 源码和子进程,因此放入豁免重型套件门禁;V8 插桩不会为它贡献任何阈值覆盖率,却会放大 Git 进程延迟。Lefthook 并发 fixture 保留原有结果,采用 30 秒单用例预算与 10 秒进程就绪探测;安装器则允许被抢占的 lock 持有者在独占创建后用 5 秒发布记录。directory-picker 组合为防抖配置写入提供显式的 15 秒轮询预算;workspace-context 组合 fixture 使用测试自有、没有无关 1 秒截止时间的信号。这些只属于该通道的预算保留了原有断言结果,120 分钟的 job 截止时间仍会约束卡死的运行。LSP 源码与 ACL 沙箱源码仍计入 Windows 分母:基于 stub 的失败路径套件把每个进程内 ACL 沙箱文件都带到 100%,只有 runner 入口保持排除——它只作为 spawn 出的子进程在插桩运行之外执行,其行为由 runner 套件端到端钉住。窄范围且带注释的 V8 ignore 只覆盖不可达分支(另一平台专属分支、生命周期内不可达的防御守卫),其行为测试仍保留在所属平台。
|
||||
|
||||
16 核配置是这项清单经实测选定的容量规格。使用 6 个 coverage worker 的试验分别以 6 分 27 秒和 7 分 50 秒跑出完整通过结果,而在单个插桩 Vitest 进程内使用 4 个、3 个和 2 个并发 worker 的分支头精确试验暴露出不稳定的 fixture 与 worker 退出。相互独立的单 worker 子进程保留进程隔离。历史上的 16 分片样本把插桩覆盖率缩短到 112.66–122.01 秒。在当前的构建后拓扑中,16 个插桩分片加 2 个豁免 worker 会在 16 核运行器上调度 18 个覆盖率执行单元,且尚未计入生产网站的尾部工作或系统开销;4 个分片加 2 个豁免 worker 则调度 6 个。4 个分片刻意用部分单 job 延迟换取自托管高并发下更低的进程创建压力。32 核对比仅将聚合门禁时间缩短 1.47 秒,且仍在 fork worker 内触发 CJS lexer 致命故障,因此增加核心数没有带来可靠的墙钟时间改善。
|
||||
|
||||
@@ -32,7 +32,7 @@ Windows 的持久 JSONL 路径会保留驱动器根目录的原生写法,并
|
||||
|
||||
启动后,只有根 fiber 与 Loader 均处于活跃状态时,系统才会继续设置 profile watcher。只有当同一次调用所记录的信号已取得关闭流程所有权时,系统才会隔离并发设置错误;无关 HMR 故障仍会响亮失败。[进程关闭控制器](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.zh.md)会在根级 dispose 成功后让单次任务的正常完成流程排空 Node 剩余句柄,同时让拆卸失败、截止时间到期和信号升级继续强制退出。vendored Include 会串行化防抖写入,只对瞬时访问或忙碌故障执行有界退避重试,并确保每个由计时器触发的拒绝都得到观察。持久化最终失败后,该故障会保留在队列中,并重新抛给拆卸责任方;成功拆卸则会排空最新写入。
|
||||
|
||||
Shiki 会禁用 TextMate 正则的延迟编译,并在用户内容进入保持不变的逐行 tokenization(词元化)预算前预热每种启动语法,从而避免调度器争用发布不完整的高亮流。Codex 真实产品 fixture 固定使用稳定版 0.147.0 schema,并选择实际提供的命令工具与对应参数形态;这样既保留由提供方负责的协议,也能在每种宿主上证明无人值守拒绝和整棵进程树退出。
|
||||
Shiki 会禁用 TextMate 正则的延迟编译,并在用户内容进入保持不变的逐行 tokenization(词元化)预算前预热每种启动语法,从而避免调度器争用发布不完整的高亮流。Codex 真实产品 fixture 固定使用稳定版 0.149.1 schema,并选择实际提供的命令工具与对应参数形态;这样既保留由提供方负责的协议,也能在每种宿主上证明无人值守拒绝和整棵进程树退出。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-09-md-fragment-anchor-gate.md
|
||||
2026-08-09-md-fragment-anchor-gate.md: a5c4029f5b11f464e09356915d5d2f9134bee616
|
||||
2026-08-09-md-fragment-anchor-gate.zh.md: 50c941b3e2d43e4991ee3a748b1b50b36611e34d
|
||||
2026-08-09-md-fragment-anchor-gate.md: de0d02318b63c4892233c8c2ebe8457152def10c
|
||||
2026-08-09-md-fragment-anchor-gate.zh.md: 54ac751900b508fb0dc6fa0e20f0b50b11e252a9
|
||||
|
||||
@@ -14,7 +14,7 @@ English | [中文](2026-08-09-md-fragment-anchor-gate.zh.md)
|
||||
|
||||
The slug function differs from `gen-cordis-catalog`'s region-anchor slugger (which drops underscores): the generator's headings are always reachable through its explicit `<a id>` anchors, so the two need not share one rule. Chinese pair sides follow the existing repository convention (`docs/glossary.zh.md`, `docs/cordis-primer.zh.md`): keep the English fragment in the link and place an explicit `<a id>` before the Chinese heading, so both language sides expose identical anchors.
|
||||
|
||||
The 15 broken fragments are fixed in the same change: stale slugs retargeted to the current headings, the relocated no-timeout contract now linked at its owning group README, and four zh documents given explicit anchors. `docs/AGENTS.md` and the `dsh-doc-standards` skill no longer prescribe the manual anchor grep for Markdown links; it survives only for anchors cited from TypeScript strings whose output never reaches gate-scanned Markdown (the three scanned references all render into scanned pages, so the gate covers them through the committed output).
|
||||
The 15 broken fragments are fixed in the same change: stale slugs retargeted to the current headings, the relocated no-timeout contract now linked at its owning group README, and four zh documents given explicit anchors. `docs/AGENTS.md` and the `dsh-doc` skill no longer prescribe the manual anchor grep for Markdown links; it survives only for anchors cited from TypeScript strings whose output never reaches gate-scanned Markdown (the three scanned references all render into scanned pages, so the gate covers them through the committed output).
|
||||
|
||||
## Verification
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
slug 函数与 `gen-cordis-catalog` 的区块锚点 slugger 不同(后者丢弃下划线):生成器的标题总能通过其显式 `<a id>` 锚点到达,两者无需共享一条规则。中文侧沿用既有语料惯例(`docs/glossary.zh.md`、`docs/cordis-primer.zh.md`):链接保留英文 fragment,在中文标题前放置显式 `<a id>`,使两个语言侧暴露相同的锚点。
|
||||
|
||||
15 条坏 fragment 在同一变更中修复:陈旧 slug 重定向到当前标题,搬迁的无超时约定改链其属主 group README,四份中文文档补上显式锚点。`docs/AGENTS.md` 与 `dsh-doc-standards` skill 不再要求为 Markdown 链接手工 grep 锚点;人工 grep 只对输出从不进入受检 Markdown 的 TypeScript 字符串锚点保留(扫描到的三处全部渲染进受检页面,gate 经由提交的产物覆盖它们)。
|
||||
15 条坏 fragment 在同一变更中修复:陈旧 slug 重定向到当前标题,搬迁的无超时约定改链其属主 group README,四份中文文档补上显式锚点。`docs/AGENTS.md` 与 `dsh-doc` skill 不再要求为 Markdown 链接手工 grep 锚点;人工 grep 只对输出从不进入受检 Markdown 的 TypeScript 字符串锚点保留(三处受扫描引用全部渲染进受检页面,因此 gate 经由提交的产物覆盖它们)。
|
||||
|
||||
## 验证
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-07-29-simplify-web-image-input-v1.md
|
||||
2026-07-29-simplify-web-image-input-v1.md: e7847dc2ae18f48146cb2b686dcda64beebf2350
|
||||
2026-07-29-simplify-web-image-input-v1.zh.md: 47f6bb5face5b896b5184f7198eef7d99ee25afe
|
||||
2026-07-29-simplify-web-image-input-v1.md: f13abfda3be890f80a8ff852acbc68118b91941c
|
||||
2026-07-29-simplify-web-image-input-v1.zh.md: c92974efe5f84da5b91c09ea4acf820e7853ca7d
|
||||
|
||||
@@ -32,6 +32,6 @@ The attachment seam exposes its limits plus storage-free `validateImage`, `saveI
|
||||
|
||||
## Consequences
|
||||
|
||||
The feature retains the two batch limits and one storage-free validation method required by multi-image prompts, while removing unrelated public fields, lifecycle operations, policy snapshots, and route-assembly branches. Provider/model selection remains composition or profile configuration. Pre-request token pressure may undercount visual input until a provider-aware estimator is designed, while reported usage remains exact.
|
||||
The feature retains the two batch limits and one storage-free validation method required by multi-image prompts, while removing unrelated public fields, lifecycle operations, policy snapshots, and route-assembly branches. Provider/model selection remains composition or profile configuration. Pre-request token pressure keeps the structural heuristic only on routes without declared image pricing; the [route-priced estimator](../feature/2026-08-24-route-priced-image-request-pressure.md) supplies the provider-aware figure, and reported usage remains exact.
|
||||
|
||||
Reintroducing any removed surface requires a concrete consumer and its failure, lifecycle, replay, and testing contract rather than compatibility with this pre-release shape.
|
||||
|
||||
+1
-1
@@ -32,6 +32,6 @@ Status: implemented
|
||||
|
||||
## 后果
|
||||
|
||||
该功能保留了多图片提示词所需的两个批次上限和一个不触碰存储的校验方法,同时移除了无关的公开字段、生命周期操作、策略快照和路由组装分支。提供方/模型选择仍属于组合或 profile 配置。在设计出提供方感知型估算器之前,请求前的 token 压力计算可能少计视觉输入,而上报的用量仍保持精确。
|
||||
该功能保留了多图片提示词所需的两个批次上限和一个不触碰存储的校验方法,同时移除了无关的公开字段、生命周期操作、策略快照和路由组装分支。提供方/模型选择仍属于组合或 profile 配置。请求前的 token 压力只在未声明图片定价的路由上保留结构启发式;[按路由定价的估算器](../feature/2026-08-24-route-priced-image-request-pressure.zh.md)提供提供方感知的数值,上报的用量仍保持精确。
|
||||
|
||||
重新引入任何已移除表面时,都必须有具体消费方,并为其定义失败、生命周期、回放和测试契约,而不是为了兼容这一预发布形态。
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
|
||||
2026-08-12-production-dsh-excludes-product-subagent-providers.md: faa14a5848a421389d8c427f844c4dedc118a0fd
|
||||
2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md: 0b99cf8aebba4aa44fb42e635c94cc64b4d3b458
|
||||
2026-08-12-production-dsh-excludes-product-subagent-providers.md: 551cf86bd0d1901e5ad8e33588b7bf274cb2c8fe
|
||||
2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md: 611c6192c501f6289911c18c36ca21a31c57e2cd
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ The two Bundles remain independent. The Codex Bundle owns the pinned official wr
|
||||
|
||||
## Verification
|
||||
|
||||
Package tests pin both Bundle manifests, published patches, exact self-provider rows, and product runtime dependencies. Claude coverage pins Agent SDK 0.3.220, Claude Code 2.1.220, all eight platform packages, SDK-selected execution, and missing-payload failure without host fallback. Codex coverage pins wrapper 0.147.0, all six platform aliases, package-declared execution, native descendant quiescence, and the same missing-payload behavior. Workspace validation derives each published patch from its Bundle declaration rather than a package catalog. Package/base assertions plus actual pnpm production evidence prove the default and selected-product dependency boundaries, while real Bundle-patch and Agent-Preset composition covers none, either product, both, the tool-grant intersection, later-Session adoption, and zero startup processes.
|
||||
Package tests pin both Bundle manifests, published patches, exact self-provider rows, and product runtime dependencies. Claude coverage pins Agent SDK 0.3.241, Claude Code 2.1.241, all eight platform packages, SDK-selected execution, and missing-payload failure without host fallback. Codex coverage pins wrapper 0.149.1, all six platform aliases, package-declared execution, native descendant quiescence, and the same missing-payload behavior. Workspace validation derives each published patch from its Bundle declaration rather than a package catalog. Package/base assertions plus actual pnpm production evidence prove the default and selected-product dependency boundaries, while real Bundle-patch and Agent-Preset composition covers none, either product, both, the tool-grant intersection, later-Session adoption, and zero startup processes.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ Status: implemented
|
||||
|
||||
## 验证
|
||||
|
||||
包测试会固定两个 Bundle 的 manifest、发布 patch、准确的自身提供方行与产品运行时依赖。Claude 覆盖会固定 Agent SDK 0.3.220、Claude Code 2.1.220、八个平台包、SDK 所选执行路径,以及载荷缺失时不回退宿主命令的失败。Codex 覆盖会固定 wrapper 0.147.0、六个平台 alias、包声明的执行路径、原生后代进程停稳,以及同样的载荷缺失行为。工作区验证会从 Bundle 声明派生每份发布 patch,而非维护包目录。包与 base 断言加上实际 pnpm 生产证据会证明默认与所选产品的依赖边界;真实 Bundle patch 与 Agent Preset 组装则覆盖未安装、任一单包、双包、工具授权交集、后续 Session 采纳以及零启动进程。
|
||||
包测试会固定两个 Bundle 的 manifest、发布 patch、准确的自身提供方行与产品运行时依赖。Claude 覆盖会固定 Agent SDK 0.3.241、Claude Code 2.1.241、八个平台包、SDK 所选执行路径,以及载荷缺失时不回退宿主命令的失败。Codex 覆盖会固定 wrapper 0.149.1、六个平台 alias、包声明的执行路径、原生后代进程停稳,以及同样的载荷缺失行为。工作区验证会从 Bundle 声明派生每份发布 patch,而非维护包目录。包与 base 断言加上实际 pnpm 生产证据会证明默认与所选产品的依赖边界;真实 Bundle patch 与 Agent Preset 组装则覆盖未安装、任一单包、双包、工具授权交集、后续 Session 采纳以及零启动进程。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-21-product-subagent-minimal-diagnostics.md
|
||||
2026-08-21-product-subagent-minimal-diagnostics.md: b99ed487911a7ac9bf7c0751ea02290ff92cad6a
|
||||
2026-08-21-product-subagent-minimal-diagnostics.zh.md: 46f948334df0268a095f8cb34ab1a7707eee811b
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
# Agent Note: Product subagents expose minimal actionable diagnostics
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-21-product-subagent-minimal-diagnostics.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The Claude Code and Codex providers receive structured failures from independently versioned product runtimes. Mirroring every member of those upstream error unions into model-visible diagnostics makes each runtime upgrade expand the Provider contract even when the parent agent would take the same next action for several categories.
|
||||
|
||||
Raw SDK errors, app-server payloads, stderr, commands, paths, task content, environment values, and credentials cannot replace the structured mapping because they cross the product process boundary without a stable safety guarantee. The parent still needs enough information to distinguish a product limit, access restriction, service or transport problem, invalid result, and managed process failure.
|
||||
|
||||
## Decision
|
||||
|
||||
Each product Provider derives a small action category from the current operation and safe structured product facts. The existing `SubagentResult.diagnostic` string remains the only public representation: consumers display it but do not parse it, and the shared subagent result boundary continues to enforce the complete 4096-byte UTF-8 limit.
|
||||
|
||||
Claude Code maps Agent SDK results into five categories:
|
||||
|
||||
| Category | Safe input |
|
||||
| --- | --- |
|
||||
| `limit` | Turn, budget, or structured-output retry limits |
|
||||
| `product-error` | A general SDK execution failure |
|
||||
| `invalid-result` | An error-marked or blank success, or no terminal result |
|
||||
| `process` | The managed CLI exits before a terminal result |
|
||||
| `unknown` | Startup, teardown, unrecognized SDK values, or failures without a more specific safe fact |
|
||||
|
||||
The diagnostic also retains the derived `query-start`, `query-run`, `process`, or `teardown` stage and independently observed exit code and signal. A contributing permission decision follows the failure line. Successful completion and local cancellation expose no failure diagnostic, and original SDK text remains only on the internal cause chain and in Host observation.
|
||||
|
||||
Codex maps app-server failures into eight categories:
|
||||
|
||||
| Category | Safe input |
|
||||
| --- | --- |
|
||||
| `limit` | Context, session-budget, or usage limits |
|
||||
| `access-policy` | Authentication, cyber-policy, product-policy, or sandbox failures |
|
||||
| `service` | Overload or internal service failures |
|
||||
| `transport` | HTTP and response-stream connection failures or exhausted attempts |
|
||||
| `product-error` | Invalid requests, rollback, active-turn, or other product failures |
|
||||
| `invalid-result` | A completed turn without a nonblank final answer |
|
||||
| `process` | The managed app-server exits before another terminal result |
|
||||
| `unknown` | Startup, teardown, malformed protocol values, or failures without a more specific safe fact |
|
||||
|
||||
The Codex diagnostic retains `initialize`, `thread-start`, `turn-start`, `turn`, `process`, or `teardown`, plus applicable numeric HTTP status and independently observed exit code and signal. `contextWindowExceeded` still maps the shared stop reason to `max-tokens`; every other category remains `error`. Only structured protocol facts contribute permission detail. Product stderr is Host-only observation and is neither classified nor copied into the result.
|
||||
|
||||
### Ownership and lifecycle
|
||||
|
||||
| Fact or operation | Owner | Result |
|
||||
| --- | --- | --- |
|
||||
| Product error interpretation | Official product runtime | The Provider consumes only structured facts exposed by its pinned integration |
|
||||
| Action category and stage | One product Provider run | Derived at the failure site and discarded after result settlement |
|
||||
| Exit code and signal | `dsh-subprocess` handle | Displayed independently when observed, without inferring missing values |
|
||||
| Permission decision | Product Provider permission callbacks or protocol | Appended only when it contributed to the failed run |
|
||||
| Diagnostic delivery and byte limit | `dsh-subagent` and its foreground or Job consumers | One bounded optional string remains separate from assistant output |
|
||||
|
||||
## Verification
|
||||
|
||||
Claude Code package tests cover every coarse category, all four stages, unknown structured values, permission ordering, raw-text exclusion, success and cancellation omission, concurrent-run isolation, and independent exit code and signal fields. The real Agent SDK 0.3.241 and Claude Code 2.1.241 fixture produces an actual max-turns limit, process failure, permission denial, strict final answer, cancellation, and whole-tree quiescence. Codex package tests cover every coarse category, all six stages, applicable HTTP status, structured permission ordering, stderr exclusion, success and cancellation omission, concurrency, and cleanup aggregation. The real 0.149.1 app-server fixture produces service, product-error, process, final-answer, model-isolation, cancellation, and quiescence evidence. Loader and keyless product compositions continue to expose static tools without a diagnostic parser or model-visible category input.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Preserve every upstream enum member.** Several members lead to the same parent action, while the display-only consumer gains no behavior from their exact names. Keeping them would make routine product upgrades redefine a larger Harness-facing promise.
|
||||
|
||||
**Return raw product text or classify general stderr.** Free-form text can contain sensitive task and environment data and has no stable versioned meaning. Only structured product facts, Provider stage, permission decisions, and managed process outcomes qualify as input.
|
||||
|
||||
**Add a shared structured error type.** The products version their failures independently, and current consumers only present a bounded string. A shared enum would move product release churn into the provider-neutral seam without a consumer that needs it.
|
||||
|
||||
**Retry or fall back based on the category.** The Provider owns one one-shot run and has no retry authority, fallback model, persistent product session, or recovery state. The category informs the parent; it does not start another product action.
|
||||
|
||||
## Consequences
|
||||
|
||||
Product runtime upgrades no longer require a model-visible promise for every SDK or app-server error member. Parents still distinguish limits, access and policy restrictions, service and transport failures, general product failures, invalid results, managed process exits, and unknown failures while retaining applicable stage, permission, HTTP, and process facts.
|
||||
|
||||
The diagnostic remains safe display text rather than a recovery protocol. This change adds no raw error forwarding, fallback model, automatic retry, product session persistence, public structured result field, or dynamic provider and model selection.
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
# Agent Note: 产品 subagent 公开最小可行动诊断
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-21-product-subagent-minimal-diagnostics.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
Claude Code 与 Codex 提供方会从彼此独立版本化的产品运行时接收结构化失败。如果把这些上游错误联合的每个成员都镜像进模型可见诊断,那么即使多个类别会让父 agent 采取同一种下一步动作,每次运行时升级仍会扩大提供方约定。
|
||||
|
||||
原始 SDK 错误、app-server payload、stderr、命令、路径、任务内容、环境值与凭证都不能取代结构化映射,因为这些内容跨越产品进程边界时没有稳定的安全保证。父 agent 仍需要足够信息来区分产品限制、访问限制、服务或传输问题、无效结果与受管进程失败。
|
||||
|
||||
## Decision
|
||||
|
||||
每个产品提供方都会根据当前操作与安全的结构化产品事实派生少量行动类别。现有 `SubagentResult.diagnostic` 字符串仍是唯一公共表示:消费方只展示而不解析,共享 subagent 结果边界继续对完整文本执行 4096 字节 UTF-8 上限。
|
||||
|
||||
Claude Code 会把 Agent SDK 结果映射为五种类别:
|
||||
|
||||
| 类别 | 安全输入 |
|
||||
| --- | --- |
|
||||
| `limit` | 轮次、预算或结构化输出重试限制 |
|
||||
| `product-error` | 一般 SDK 执行失败 |
|
||||
| `invalid-result` | 标记为错误或内容空白的成功结果,或缺失终态结果 |
|
||||
| `process` | 受管 CLI 在终态结果前退出 |
|
||||
| `unknown` | 启动、清理、无法识别的 SDK 值,或没有更具体安全事实的失败 |
|
||||
|
||||
诊断还会保留派生的 `query-start`、`query-run`、`process` 或 `teardown` 阶段,以及分别观测到的退出码与信号。参与失败的权限决定会跟在失败行之后。成功完成与本地取消不公开失败诊断,原始 SDK 文本只留在内部 cause 链和 Host 观测中。
|
||||
|
||||
Codex 会把 app-server 失败映射为八种类别:
|
||||
|
||||
| 类别 | 安全输入 |
|
||||
| --- | --- |
|
||||
| `limit` | 上下文、会话预算或用量限制 |
|
||||
| `access-policy` | 身份验证、网络安全策略、产品策略或 sandbox 失败 |
|
||||
| `service` | 过载或内部服务失败 |
|
||||
| `transport` | HTTP 与响应 stream 连接失败或尝试耗尽 |
|
||||
| `product-error` | 无效请求、回滚、活动轮次或其他产品失败 |
|
||||
| `invalid-result` | 已完成轮次没有非空白最终答案 |
|
||||
| `process` | 受管 app-server 在其他终态结果前退出 |
|
||||
| `unknown` | 启动、清理、格式错误的协议值,或没有更具体安全事实的失败 |
|
||||
|
||||
Codex 诊断会保留 `initialize`、`thread-start`、`turn-start`、`turn`、`process` 或 `teardown`,以及适用的数值 HTTP status 和分别观测到的退出码与信号。`contextWindowExceeded` 仍把共享终止原因映射为 `max-tokens`;其他所有类别仍使用 `error`。只有结构化协议事实会贡献权限说明。产品 stderr 只供 Host 观测,既不会被分类,也不会复制进结果。
|
||||
|
||||
### 所有权与生命周期
|
||||
|
||||
| 事实或操作 | 责任方 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 产品错误解释 | 官方产品运行时 | 提供方只消费其锁定集成公开的结构化事实 |
|
||||
| 行动类别与阶段 | 单次产品提供方运行 | 在失败位置派生,并在结果结算后丢弃 |
|
||||
| 退出码与信号 | `dsh-subprocess` 句柄 | 观测到时分别展示,不推断缺失值 |
|
||||
| 权限决定 | 产品提供方权限回调或协议 | 只在参与失败运行时追加 |
|
||||
| 诊断传递与字节上限 | `dsh-subagent` 及其前台或 Job 消费方 | 一个有界可选字符串继续与 assistant 输出分离 |
|
||||
|
||||
## Verification
|
||||
|
||||
Claude Code 包测试覆盖全部粗粒度类别、四个阶段、未知结构化值、权限顺序、原始文本排除、成功与取消时省略、并发运行隔离,以及彼此独立的退出码与信号字段。真实 Agent SDK 0.3.241 与 Claude Code 2.1.241 fixture 会产生实际 max-turns 限制、进程失败、权限拒绝、严格最终答案、取消与整棵进程树完全停稳。Codex 包测试覆盖全部粗粒度类别、六个阶段、适用 HTTP status、结构化权限顺序、stderr 排除、成功与取消时省略、并发及清理聚合。真实 0.149.1 app-server fixture 会产生服务、产品错误、进程、最终答案、模型隔离、取消与完全停稳证据。Loader 与无密钥产品组合继续公开静态工具,不增加诊断解析器或模型可见类别输入。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**保留每个上游枚举成员。** 多个成员会触发同一种父级动作,而只负责展示的消费方无法从准确名称获得额外行为。继续保留会让常规产品升级重新定义更大的 Harness 可见承诺。
|
||||
|
||||
**返回原始产品文本或分类一般 stderr。** 自由文本可能包含敏感任务与环境数据,也没有稳定的版本化含义。只有结构化产品事实、提供方阶段、权限决定与受管进程结果可以成为输入。
|
||||
|
||||
**增加共享结构化错误类型。** 两个产品独立版本化失败,而当前消费方只呈现有界字符串。共享枚举会在没有消费需求的情况下,把产品发布变动移入提供方无关的 seam。
|
||||
|
||||
**根据类别重试或 fallback。** 提供方只拥有一次 one-shot 运行,没有重试权限、fallback 模型、持久产品会话或恢复状态。类别只用于通知父级,不会启动另一项产品动作。
|
||||
|
||||
## Consequences
|
||||
|
||||
产品运行时升级不再要求为每个 SDK 或 app-server 错误成员提供模型可见承诺。父级仍可区分限制、访问与策略限制、服务与传输失败、一般产品失败、无效结果、受管进程退出与未知失败,并保留适用的阶段、权限、HTTP 与进程事实。
|
||||
|
||||
诊断仍是安全展示文本,而不是恢复协议。本改动不增加原始错误转发、fallback 模型、自动重试、产品会话持久化、公共结构化结果字段或动态提供方与模型选择。
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-25-fail-closed-session-event-vocabulary.md
|
||||
2026-08-25-fail-closed-session-event-vocabulary.md: 537e9a754f7034067d1da31ba2a1bed5bc70cb7e
|
||||
2026-08-25-fail-closed-session-event-vocabulary.zh.md: f37bcf34bef3d503aca712d99122e334ff29c258
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user