Bladeren bron

Merge remote-tracking branch 'origin/master' into worktree/deepseek-harness-proxy-config-2f5b4a

Yichen Jiang 1 week geleden
bovenliggende
commit
bdd294dc47
100 gewijzigde bestanden met toevoegingen van 880 en 764 verwijderingen
  1. 2 2
      .agents/notes/implemented/architecture/2026-06-20-branded-ids.i18n.yaml
  2. 1 1
      .agents/notes/implemented/architecture/2026-06-20-branded-ids.md
  3. 1 1
      .agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.i18n.yaml
  5. 1 1
      .agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.md
  6. 1 1
      .agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.zh.md
  7. 2 2
      .agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml
  8. 0 0
      .agents/notes/implemented/architecture/2026-07-30-web-config-plane.md
  9. 0 0
      .agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md
  10. 2 2
      .agents/notes/implemented/architecture/2026-08-04-draft-provider-endpoint-interrogation.i18n.yaml
  11. 5 5
      .agents/notes/implemented/architecture/2026-08-04-draft-provider-endpoint-interrogation.md
  12. 5 5
      .agents/notes/implemented/architecture/2026-08-04-draft-provider-endpoint-interrogation.zh.md
  13. 2 2
      .agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.i18n.yaml
  14. 24 25
      .agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.md
  15. 28 29
      .agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.zh.md
  16. 2 2
      .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml
  17. 16 24
      .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md
  18. 19 27
      .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md
  19. 2 2
      .agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.i18n.yaml
  20. 1 1
      .agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.md
  21. 1 1
      .agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.zh.md
  22. 3 3
      .agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.i18n.yaml
  23. 82 0
      .agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md
  24. 82 0
      .agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md
  25. 6 0
      .agents/notes/implemented/architecture/2026-08-31-session-sequence-and-log-offset-brands.i18n.yaml
  26. 45 0
      .agents/notes/implemented/architecture/2026-08-31-session-sequence-and-log-offset-brands.md
  27. 45 0
      .agents/notes/implemented/architecture/2026-08-31-session-sequence-and-log-offset-brands.zh.md
  28. 2 2
      .agents/notes/implemented/bug-fix/2026-08-10-child-agents-join-their-parent-preset.i18n.yaml
  29. 3 3
      .agents/notes/implemented/bug-fix/2026-08-10-child-agents-join-their-parent-preset.md
  30. 3 3
      .agents/notes/implemented/bug-fix/2026-08-10-child-agents-join-their-parent-preset.zh.md
  31. 2 2
      .agents/notes/implemented/bug-fix/2026-08-13-feedback-note-editor-popover.i18n.yaml
  32. 1 1
      .agents/notes/implemented/bug-fix/2026-08-13-feedback-note-editor-popover.md
  33. 1 1
      .agents/notes/implemented/bug-fix/2026-08-13-feedback-note-editor-popover.zh.md
  34. 6 0
      .agents/notes/implemented/bug-fix/2026-08-17-subagent-message-settlement-ordering.i18n.yaml
  35. 44 0
      .agents/notes/implemented/bug-fix/2026-08-17-subagent-message-settlement-ordering.md
  36. 44 0
      .agents/notes/implemented/bug-fix/2026-08-17-subagent-message-settlement-ordering.zh.md
  37. 0 44
      .agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.md
  38. 0 44
      .agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.zh.md
  39. 2 2
      .agents/notes/implemented/feature/2026-06-30-session-store-fork-api.i18n.yaml
  40. 3 3
      .agents/notes/implemented/feature/2026-06-30-session-store-fork-api.md
  41. 3 3
      .agents/notes/implemented/feature/2026-06-30-session-store-fork-api.zh.md
  42. 2 2
      .agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.i18n.yaml
  43. 3 3
      .agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md
  44. 3 3
      .agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md
  45. 2 2
      .agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.i18n.yaml
  46. 0 0
      .agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md
  47. 0 0
      .agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md
  48. 2 2
      .agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.i18n.yaml
  49. 1 1
      .agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md
  50. 1 1
      .agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.zh.md
  51. 2 2
      .agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.i18n.yaml
  52. 5 7
      .agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md
  53. 5 7
      .agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md
  54. 0 118
      .agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md
  55. 0 118
      .agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.zh.md
  56. 2 2
      .agents/notes/implemented/feature/2026-07-31-web-default-search.i18n.yaml
  57. 5 3
      .agents/notes/implemented/feature/2026-07-31-web-default-search.md
  58. 5 3
      .agents/notes/implemented/feature/2026-07-31-web-default-search.zh.md
  59. 2 2
      .agents/notes/implemented/feature/2026-08-05-durable-web-schedule.i18n.yaml
  60. 2 2
      .agents/notes/implemented/feature/2026-08-05-durable-web-schedule.md
  61. 2 2
      .agents/notes/implemented/feature/2026-08-05-durable-web-schedule.zh.md
  62. 0 60
      .agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.md
  63. 0 60
      .agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.zh.md
  64. 2 2
      .agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.i18n.yaml
  65. 9 9
      .agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.md
  66. 9 9
      .agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.zh.md
  67. 2 2
      .agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.i18n.yaml
  68. 2 2
      .agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.md
  69. 2 2
      .agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.zh.md
  70. 2 2
      .agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.i18n.yaml
  71. 6 6
      .agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.md
  72. 6 6
      .agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.zh.md
  73. 3 3
      .agents/notes/implemented/feature/2026-09-01-shared-base-web-fetch-default.i18n.yaml
  74. 27 0
      .agents/notes/implemented/feature/2026-09-01-shared-base-web-fetch-default.md
  75. 27 0
      .agents/notes/implemented/feature/2026-09-01-shared-base-web-fetch-default.zh.md
  76. 3 3
      .agents/notes/implemented/feature/2026-09-01-web-elevation-stroke-shadows.i18n.yaml
  77. 42 0
      .agents/notes/implemented/feature/2026-09-01-web-elevation-stroke-shadows.md
  78. 42 0
      .agents/notes/implemented/feature/2026-09-01-web-elevation-stroke-shadows.zh.md
  79. 6 0
      .agents/notes/implemented/feature/2026-09-01-web-superellipse-corner-smoothing.i18n.yaml
  80. 34 0
      .agents/notes/implemented/feature/2026-09-01-web-superellipse-corner-smoothing.md
  81. 34 0
      .agents/notes/implemented/feature/2026-09-01-web-superellipse-corner-smoothing.zh.md
  82. 2 2
      .agents/notes/implemented/simplification/2026-07-27-intent-named-subagent-continuation-operations.i18n.yaml
  83. 6 6
      .agents/notes/implemented/simplification/2026-07-27-intent-named-subagent-continuation-operations.md
  84. 6 6
      .agents/notes/implemented/simplification/2026-07-27-intent-named-subagent-continuation-operations.zh.md
  85. 2 2
      .agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.i18n.yaml
  86. 7 7
      .agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md
  87. 7 7
      .agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.zh.md
  88. 0 3
      apps/cli/composition.md
  89. 0 1
      apps/cli/package.json
  90. 2 2
      apps/cli/reference/README.i18n.yaml
  91. 1 1
      apps/cli/reference/README.md
  92. 1 1
      apps/cli/reference/README.zh.md
  93. 2 2
      apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl
  94. 2 2
      apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/parent.override.json
  95. 4 4
      apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl
  96. 15 4
      apps/cli/tests/profiles/headless/tests/keyless-smoke.e2e.ts
  97. 7 6
      apps/cli/tests/profiles/headless/tests/semantic-checkpoint.expected.e2e.ts
  98. 5 4
      apps/cli/tests/profiles/headless/tests/session-format-guard.expected.e2e.ts
  99. 8 6
      apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts
  100. 7 6
      apps/cli/tests/profiles/headless/tests/subagent-inheritance.expected.e2e.ts

+ 2 - 2
.agents/notes/implemented/architecture/2026-06-20-branded-ids.i18n.yaml

@@ -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-06-20-branded-ids.md
-2026-06-20-branded-ids.md: 954fd89aa229ba587cd1293973b4038cfeb20473
-2026-06-20-branded-ids.zh.md: 0dd761da2e5b5fc3e864fe03c250b9781be9ee59
+2026-06-20-branded-ids.md: 1f579a7afb7ac5f7facd6c5e8040d5df719c4bed
+2026-06-20-branded-ids.zh.md: eaf032027f2f61bec9e4ab624622a5012c97e9b9

+ 1 - 1
.agents/notes/implemented/architecture/2026-06-20-branded-ids.md

@@ -51,7 +51,7 @@ Kept deliberately narrow per the "not every string needs a brand" policy. Each o
 - **`ModelId`** (`GenerateOptions.model`, the `LlmRuntime` adapter-registry key) — a real cross-package lookup key (config → agent → llm → adapter); a reasonable next brand, left out only to keep this decision's blast radius focused.
 - **`ToolName`** (the `ToolRuntime` key) — author-defined, human-readable, and rarely confused with another id; the weakest candidate, likely not worth a brand.
 - **`ErrorCode`** (`HarnessError.code`) — a closed vocabulary (`ABORTED`, `NO_ADAPTER`, …), not a per-instance id; better served by a string-literal union than a brand, if anything.
-- **Numeric ordinals** — turn number, step number, and the event `seq` are `number`, not `string`, so `Branded<string>` does not apply; a parallel `number & { readonly [BRAND]: B }` variant could brand them, but they are positional ordinals rarely passed across boundaries, so the payoff is low.
+- **Other numeric ordinals** — the [Session sequence and log-offset decision](2026-08-31-session-sequence-and-log-offset-brands.md) brands event identities and log gaps because they cross persistence and reference seams. Turn and step numbers remain plain numbers: they are payload-local ordinals and are not interchangeable with Session event positions.
 - **Validated construction** — `brandString<T>()` performs no runtime check, and every boundary (ACP `sessionId`, provider-issued `call.id`, the empty-string fallback in `dsh-llm-deepseek`) trusts the raw string. A `SessionId.parse()` / `isValid()` companion that throws on malformed input at boundaries is a genuine gap, but it is a runtime-behavior change with its own design (what is "malformed"? what happens on failure?) and belongs in its own decision.
 
 ## Verification

+ 1 - 1
.agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md

@@ -51,7 +51,7 @@ const owner = brandString<OwnerToken>('session-1')
 - **`ModelId`**(`GenerateOptions.model`,`LlmRuntime` 适配器注册表的键):一个真正的跨包查找键(config → agent → llm → 适配器);合理的下一个 brand,仅为控制本决策的影响范围而暂不纳入。
 - **`ToolName`**(`ToolRuntime` 的键):由作者定义、人类可读,且很少与其他 id 混淆;最弱的候选,可能不值得加 brand。
 - **`ErrorCode`**(`HarnessError.code`):一个封闭词汇(`ABORTED`、`NO_ADAPTER`……),不是逐实例的 id;如果要做,string 字面量联合类型比 brand 更合适。
-- **数值序号**:轮次号、步骤号和事件 `seq` 是 `number` 而非 `string`,`Branded<string>` 不适用;可以用并行的 `number & { readonly [BRAND]: B }` 变体来 brand 它们,但它们是位置序号、很少跨边界传递,收益较低
+- **其他数值序号**:[Session 序列号与日志偏移决策](2026-08-31-session-sequence-and-log-offset-brands.zh.md)会为事件身份与日志间隙加 brand,因为它们跨越 persistence 与引用 seam。turn 与 step number 保持普通 number:它们是 payload-local ordinal,不会与 Session 事件位置互换
 - **带校验的构造**:`brandString<T>()` 不执行运行时检查,且每个边界(ACP `sessionId`、提供方签发的 `call.id`、`dsh-llm-deepseek` 中的空字符串回退)都信任裸 string。一个在边界处对格式错误的输入抛异常的 `SessionId.parse()` / `isValid()` 配套工具确实是缺口,但它属于运行时行为变更,有自己的设计问题(什么算「格式错误」?失败时会怎样?),应在独立决策中处理。
 
 ## 验证

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.i18n.yaml

@@ -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-session-end-seed-log-boundary.md
-2026-07-30-session-end-seed-log-boundary.md: 1c5a8097a6b901f133205dcd52d674a8e3594b28
-2026-07-30-session-end-seed-log-boundary.zh.md: ea3549543229a15d0fba7ad0316a8683c674a558
+2026-07-30-session-end-seed-log-boundary.md: c6ed3911a797480804d064273922d85412664c79
+2026-07-30-session-end-seed-log-boundary.zh.md: 1e9517f9a5aed819fdaff6194ab952c322c85b82

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.md

@@ -40,7 +40,7 @@ The predicate holds for a bracket *this* session inherited, not as a liveness si
 
 **A boundary appended at loop start.** The loop calls `resumeWith`, so it covers the resume paths, but it misses `fork()` and `adopt()` entirely, and the event would have to fire on `'startup'` — the source a fork child publishes — so `SessionStartSource` would stop discriminating. It also publishes the session before the marker is appended, so a `session/created` listener could observe a seeded log with no boundary.
 
-**Reusing `header.seedLength`.** It is the durable *fork-lineage* boundary and deliberately keeps the original fork value across a resume, where the constructor seed is the whole stored log. The two facts differ and conflating them would lose both.
+**Reusing `Session.inheritedEventCount`.** It is the durable *fork-lineage* cut and deliberately keeps the original fork value across a resume, where the constructor seed is the whole stored log. The two facts differ and conflating them would lose both.
 
 **Crash repair closing `compaction/*` alongside turn boundaries.** Rejected: it moves every plugin's bracket semantics into core's repair pass, and core cannot know what closing another package's bracket should record.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.zh.md

@@ -40,7 +40,7 @@ Status: implemented
 
 **在 loop 启动时追加边界。** loop 调用 `resumeWith`,因此覆盖恢复路径,但完全漏掉 `fork()` 与 `adopt()`,而且事件不得不在 `'startup'` 上触发——那是 fork 子会话发布的来源——于是 `SessionStartSource` 将不再具有区分力。它还会在追加标记之前就发布会话,因此 `session/created` 监听方可能观察到一份没有边界的带种子日志。
 
-**复用 `header.seedLength`。** 它是持久的 *fork 血缘*边界,并且刻意在恢复时保留原始 fork 取值——而恢复时构造种子是整份存储日志。这两个事实并不相同,混同会同时失去两者。
+**复用 `Session.inheritedEventCount`。** 它是持久的 *fork 血缘* cut,并且刻意在恢复时保留原始 fork 取值——而恢复时构造种子是整份存储日志。这两个事实并不相同,混同会同时失去两者。
 
 **让崩溃修复连同轮次边界一起关闭 `compaction/*`。** 否决:这会把每个插件的括号语义搬进核心的修复流程,而核心无法知道关闭另一个包的括号应该记录什么。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml

@@ -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-web-config-plane.md
-2026-07-30-web-config-plane.md: 81b501db529bf1b2974fd4541045991c5a8bf087
-2026-07-30-web-config-plane.zh.md: 3f02a17e4826bb35ecfd45da25c4b0170be270cb
+2026-07-30-web-config-plane.md: a919487ba48cd7735a9f7fbc65a548bc5bfb7114
+2026-07-30-web-config-plane.zh.md: ca9e9f4427bba80a63865e891e42656aeb1b5c64

File diff suppressed because it is too large
+ 0 - 0
.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md


File diff suppressed because it is too large
+ 0 - 0
.agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md


+ 2 - 2
.agents/notes/implemented/architecture/2026-08-04-draft-provider-endpoint-interrogation.i18n.yaml

@@ -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-04-draft-provider-endpoint-interrogation.md
-2026-08-04-draft-provider-endpoint-interrogation.md: 502d9bab15dcb91a59deb26443d869a36b028b48
-2026-08-04-draft-provider-endpoint-interrogation.zh.md: e0605369c7f4707eb682cc1c32d11123140b449a
+2026-08-04-draft-provider-endpoint-interrogation.md: d4112d813ad4f5781b74639209d13952e459f7dd
+2026-08-04-draft-provider-endpoint-interrogation.zh.md: 1626a34cb3163949d70688cefeec77d328c62caa

+ 5 - 5
.agents/notes/implemented/architecture/2026-08-04-draft-provider-endpoint-interrogation.md

@@ -17,11 +17,11 @@ The awkward part is that the question is about something that does not exist yet
 Interrogation is keyed by **settings namespace**, not by provider route:
 
 - `ctx.llm.registerModelDiscovery(settingsNs, discover)` lets an adapter plugin offer to interrogate endpoints for the namespace it owns, and `ctx.llm.discoverModels(settingsNs, request)` asks. There is no way to enumerate which namespaces registered: a surface that cannot interrogate learns it from the refusal, and a list nothing consumed would be a required wire field doing nothing. The namespace is the right key because a configuration surface already holds it from the configurable-provider directory, and because a provider being added has no route to name.
-- `LlmModelDiscoveryRequest` carries the draft — an optional `provider`, an optional `baseURL`, an optional `api`, an optional `apiKey`, and a signal — and needs at least one of `provider` or `baseURL` to have anything to answer about. `provider` exists because a route the adapter already describes is answered from its own registry with no network call at all; only a route it does not describe reaches an endpoint. Nothing in this path writes settings or credentials. The one read is the credential of a route the request names: a configuration surface holds a redacted descriptor rather than the stored secret, so the draft's `apiKey` is present only while the user is typing one, and without that read an already-configured route would be interrogated unauthenticated and answer 401. The typed key wins, being the one under test.
+- `LlmModelDiscoveryRequest` carries the draft — an optional `provider`, an optional `baseURL`, an optional `api`, an optional `apiKey`, and a signal — and needs at least one of `provider` or `baseURL` to have anything to answer about. `provider` exists because a route the adapter already describes is answered from its own registry with no network call at all; only a route it does not describe reaches an endpoint. Nothing in this path writes settings or credentials. A named configured route reads its stored credential and deployment-owned profile `headers` inside the Host: the credential is write-only and the curated Models page does not edit headers, so neither can be reconstructed from that page's draft. The typed key wins over the stored credential, while the profile headers still accompany the request.
 - `LlmDiscoveredModel` makes every field but `id` optional, because most listings disclose an id and nothing else. The reply is candidates, not a catalog: a surface adopting one still owes the capacities the adapter requires.
 - `llm.discoverModels` carries the same draft over the wire. Its `apiKey` is the third and last payload on which a secret may ride, alongside `settings.update`/`mutate` and `credentials.set`, and it is never stored or echoed back. It does ride the client's outgoing envelope like every other secret-bearing payload, where a `subscribeEnvelopes()` observer can see it; redacting that tap is a configuration-plane-wide change, not this method's to make alone. Connection authenticates the method with the complete Host API: it makes the host issue a GET to a caller-chosen URL and reports the outcome, which an anonymous caller must not receive. Every refusal folds into `model-discovery-failed`, whose message is the adapter's own text and whose details name the endpoint asked but never the credential offered.
 
-`dsh-llm-pi-ai` implements the wire path as a plain `GET {baseURL}/models`, reading `openai-completions` and `openai-responses`: their `GET /models` shape with bearer auth is the one a gateway, a self-hosted server, and the official endpoints all agree on. Azure is excluded despite its OpenAI lineage — it authenticates with an `api-key` header and requires an `api-version` query — and Codex uses OAuth; both would have reported an authentication failure as a provider with no models. Every other protocol answers `DISCOVERY_UNSUPPORTED`, so the surface falls back to hand-entry rather than reporting a guessed response shape as an empty provider. `baseURL` is treated as a prefix rather than a URL to resolve against, so a deployment path such as `https://gateway.example/openai/v1` keeps its segments. The reply is read under a four-megabyte ceiling enforced on the bytes actually received — the endpoint is a URL the user typed, so a declared `content-length` is checked first as a courtesy but never trusted as the bound, matching `dsh-web-fetch`'s two-stage shape for its own caller-supplied URLs.
+`dsh-llm-pi-ai` implements the wire path as a plain `GET {baseURL}/models`, reading `openai-completions` and `openai-responses`: their `GET /models` shape with bearer auth is the one a gateway, a self-hosted server, and the official endpoints all agree on. Profile resolution rejects names and values Fetch cannot represent, so a malformed deployment header is reported as a configuration error before interrogation. Configured profile headers are installed first; the fixed JSON accept header, a typed-or-stored bearer credential, and Harness attribution then win case-insensitive collisions in that order. Azure is excluded despite its OpenAI lineage — it authenticates with an `api-key` header and requires an `api-version` query — and Codex uses OAuth; both would have reported an authentication failure as a provider with no models. Every other protocol answers `DISCOVERY_UNSUPPORTED`, so the surface falls back to hand-entry rather than reporting a guessed response shape as an empty provider. `baseURL` is treated as a prefix rather than a URL to resolve against, so a deployment path such as `https://gateway.example/openai/v1` keeps its segments. The reply is read under a four-megabyte ceiling enforced on the bytes actually received — the endpoint is a URL the user typed, so a declared `content-length` is checked first as a courtesy but never trusted as the bound, matching `dsh-web-fetch`'s two-stage shape for its own caller-supplied URLs.
 
 ### Why not pi-ai's own refresh machinery
 
@@ -33,7 +33,7 @@ pi-ai supplies `createProvider({ fetchModels })` plus `Models.refresh()` and a `
 
 **Put the capability on `LlmAdapter`.** Adapters are reached through a route registration, so this has the same problem, plus it would make an adapter instance answer questions about endpoints it does not serve.
 
-**Have the host read the stored profile instead of accepting a draft.** No secret would cross the wire for an already-configured provider. But adding a provider would then require saving an unusable configuration first, and a form whose endpoint was edited but not yet saved would silently interrogate the old one. Accepting the draft keeps what the user sees and what is asked identical — with the credential as the one exception, because it is the one field a surface is never shown and so can never put in the draft.
+**Have the host read the entire stored profile instead of accepting a draft.** No secret would cross the wire for an already-configured provider. But adding a provider would then require saving an unusable configuration first, and a form whose endpoint was edited but not yet saved would silently interrogate the old one. The draft remains authoritative for the endpoint and protocol. The narrow Host-side exceptions are the stored credential, which is write-only, and profile headers, which remain deployment configuration rather than Models-page fields.
 
 **Interrogate every pi-ai protocol.** Anthropic's listing happens to share OpenAI's envelope, and Google's does not. Supporting the ones that are easy would make coverage arbitrary and, worse, make a wrong guess at a response shape indistinguishable from a provider with no models. A protocol that says it cannot be interrogated sends the user to hand-entry, which is the documented fallback.
 
@@ -41,10 +41,10 @@ pi-ai supplies `createProvider({ fetchModels })` plus `Models.refresh()` and a `
 
 ## Consequences
 
-A person adding a gateway can ask it what it serves instead of hunting through its documentation, and the answer arrives as candidates they choose from rather than as configuration written behind their back. The seam gained a registry that is deliberately small: one offer per namespace, no storage, no lifecycle beyond the fiber.
+A person adding a gateway can ask it what it serves instead of hunting through its documentation, and the answer arrives as candidates they choose from rather than as configuration written behind their back. An already-configured enterprise gateway uses the same deployment headers for interrogation and model requests without adding a header injection field to the browser protocol. The seam gained a registry that is deliberately small: one offer per namespace, no storage, no lifecycle beyond the fiber.
 
 What it costs: the wire gained a third secret-carrying payload, so the configuration plane's write-only surface is now three methods rather than two. Discovery coverage is protocol-shaped rather than provider-shaped — an Anthropic-compatible gateway must be filled in by hand even though its listing would parse. And because nothing re-runs the question, a model list is still only as current as its last edit; that is the same trade the layer below made deliberately.
 
 ## Testing
 
-`packages/llm/llm/tests/topology.spec.ts` covers the registry: one offer per namespace, disposal with the fiber, normalization that drops duplicate and unusable ids without inventing capacities, the `NO_DISCOVERY`/`INVALID_DISCOVERY` refusals, and the `model-discovery-failed` Remote mapping. `packages/llm/llm-pi-ai/tests/discovery.spec.ts` drives the probe against local HTTP servers — a listing with and without disclosed capacities, a preserved deployment path, an absent credential, a configured route supplying its own where the draft has none and a typed key winning over it, a catalog route answering without resolving one at all, dropped rows, 401/403 versus a server fault, a non-listing and a non-JSON body, an unreachable endpoint, caller cancellation, an unsupported protocol, and the size ceiling in both its declared-length and streamed forms. `packages/client/connection/tests/node-half.host.spec.ts` pins the `llm/discoverModels` `/api` carrier registration, while `packages/client/ui-settings-models/tests/provider-form.client.spec.tsx` verifies that the draft reaches the Remote whole, absent fields stay absent, and no settings namespace or credential is written before selection.
+`packages/llm/llm/tests/topology.spec.ts` covers the registry: one offer per namespace, disposal with the fiber, normalization that drops duplicate and unusable ids without inventing capacities, the `NO_DISCOVERY`/`INVALID_DISCOVERY` refusals, and the `model-discovery-failed` Remote mapping. `packages/llm/llm-pi-ai/tests/discovery.spec.ts` drives the probe against local HTTP servers — a listing with and without disclosed capacities, a preserved deployment path, an absent credential, a configured route supplying its stored credential and headers while a typed key wins without resolving the stored one, a catalog route answering without resolving one at all, dropped rows, 401/403 versus a server fault, a non-listing and a non-JSON body, an unreachable endpoint, caller cancellation, an unsupported protocol, and the size ceiling in both its declared-length and streamed forms. `packages/llm/llm-pi-ai/tests/loader-composition.spec.ts` boots settings and credentials through the Loader and proves settings-only headers reach `GET /models` with request-owned headers winning collisions. `packages/llm/llm-pi-ai/tests/adapter.spec.ts` rejects profile headers Fetch cannot represent, and `packages/llm/llm-pi-ai/tests/dynamic-config.spec.ts` proves a settings write reports that configuration error while its last good routes keep serving. `packages/client/connection/tests/node-half.host.spec.ts` pins the `llm/discoverModels` `/api` carrier registration, while `packages/client/ui-settings-models/tests/provider-form.client.spec.tsx` verifies that the draft reaches the Remote whole, absent fields stay absent, and no settings namespace or credential is written before selection.

+ 5 - 5
.agents/notes/implemented/architecture/2026-08-04-draft-provider-endpoint-interrogation.zh.md

@@ -17,11 +17,11 @@ Status: implemented
 询问以 **settings namespace** 为键,而不是提供方路由:
 
 - `ctx.llm.registerModelDiscovery(settingsNs, discover)` 让适配器插件为自己拥有的 namespace 提供「询问端点」的能力,`ctx.llm.discoverModels(settingsNs, request)` 发起询问。没有任何办法枚举哪些 namespace 注册过:询问不了的界面会从那句拒绝里知道,而一份无人消费的列表只会变成一个什么都不做的必填协议字段。以 namespace 为键是对的,因为配置界面已经从可配置提供方目录里拿到了它,也因为正在新增的提供方没有路由可点名。
-- `LlmModelDiscoveryRequest` 携带草稿——可选的 `provider`、可选的 `baseURL`、可选的 `api`、可选的 `apiKey`,以及一个 signal——且 `provider` 与 `baseURL` 至少要有一个,才有东西可答。`provider` 之所以存在,是因为适配器已经描述过的路由直接由它自己的注册表作答、完全不联网;只有它未描述的路由才会抵达某个端点。这条路径不写 settings 与 credentials。唯一的读取是请求所点名路由的凭据:配置界面拿到的是脱敏描述符而非已存的机密,因此草稿里的 `apiKey` 只在用户正键入时才存在;没有这次读取,已配置好的路由就会被不带认证地询问,只换回一个 401。键入的密钥优先,因为那正是被测试的那一把
+- `LlmModelDiscoveryRequest` 携带草稿——可选的 `provider`、可选的 `baseURL`、可选的 `api`、可选的 `apiKey`,以及一个 signal——且 `provider` 与 `baseURL` 至少要有一个,才有东西可答。`provider` 之所以存在,是因为适配器已经描述过的路由直接由它自己的注册表作答、完全不联网;只有它未描述的路由才会抵达某个端点。这条路径不写 settings 与 credentials。已配置且具名的路由会在 Host 内读取已存凭据和部署方持有的 profile `headers`:凭据只写,而精选的 Models 页面不编辑 headers,因此页面草稿无法重建两者。键入的密钥优先于已存凭据,profile headers 则仍随请求发送
 - `LlmDiscoveredModel` 除 `id` 外每个字段都可选,因为大多数列表只公布 id。回复是候选而非 catalog:采纳其中一条的界面仍要补上适配器所需的容量。
 - `llm.discoverModels` 把同一份草稿送过协议层。它的 `apiKey` 是可承载机密的第三个、也是最后一个载荷(另两个是 `settings.update`/`mutate` 与 `credentials.set`),且绝不被存储或回显。它确实会像其他承载机密的载荷一样随客户端外发信封同行,`subscribeEnvelopes()` 观察者看得到;把那个抽头脱敏是整个配置面的改动,不该由这一个方法独自决定。Connection 用与完整 Host API 相同的会话认证该方法:它让宿主向调用方选定的 URL 发起 GET 并回报结果,匿名调用者绝不能获得这类探测能力。每一种拒绝都折叠为 `model-discovery-failed`,其消息是适配器自己的文本,details 点名被询问的端点,绝不点名所提供的凭据。
 
-`dsh-llm-pi-ai` 的实现只是一次朴素的 `GET {baseURL}/models`,且仅限 OpenAI 兼容协议。它们的列表形状是网关、自建服务与官方端点三方一致认可的那一种,而这正是该动作存在的场景。其余协议一律以 `DISCOVERY_UNSUPPORTED` 回答,让界面回退到手工填写,而不是把猜错的响应形状报成一个空提供方。`baseURL` 按前缀而非待解析 URL 处理,因此 `https://gateway.example/openai/v1` 这类部署路径会保留其路径段。回复在四兆字节上限下读取,且上限落在实际收到的字节上——端点是用户自己填的 URL,因此会先看声明的 `content-length` 作为善意提示,但绝不把它当作边界;这与 `dsh-web-fetch` 面对自己的调用方提供 URL 时所用的两段式形状一致。
+`dsh-llm-pi-ai` 的实现只是一次朴素的 `GET {baseURL}/models`,且仅限 OpenAI 兼容协议。它们的列表形状是网关、自建服务与官方端点三方一致认可的那一种,而这正是该动作存在的场景。Profile 解析会拒绝 Fetch 无法表示的名称与值,因此格式错误的部署 header 会在询问前以配置错误报告。已配置的 profile headers 最先装入;固定的 JSON accept header、键入或已存的 bearer 凭据以及 Harness attribution 随后依次以大小写不敏感方式赢得冲突。其余协议一律以 `DISCOVERY_UNSUPPORTED` 回答,让界面回退到手工填写,而不是把猜错的响应形状报成一个空提供方。`baseURL` 按前缀而非待解析 URL 处理,因此 `https://gateway.example/openai/v1` 这类部署路径会保留其路径段。回复在四兆字节上限下读取,且上限落在实际收到的字节上——端点是用户自己填的 URL,因此会先看声明的 `content-length` 作为善意提示,但绝不把它当作边界;这与 `dsh-web-fetch` 面对自己的调用方提供 URL 时所用的两段式形状一致。
 
 ### 为什么不用 pi-ai 自己的 refresh 机制
 
@@ -33,7 +33,7 @@ pi-ai 提供了 `createProvider({ fetchModels })` 加上 `Models.refresh()` 与
 
 **把能力挂在 `LlmAdapter` 上。** 适配器要经由路由注册才能抵达,因此问题相同;而且这会让一个适配器实例去回答它并不服务的端点的问题。
 
-**让 host 读已存 profile,而不是接受草稿。** 对已配置好的提供方来说,不会有机密跨越协议层。但这样一来新增提供方就必须先保存一份不可用的配置,而端点已改却尚未保存的表单会静默地去询问旧地址。接受草稿让用户看见的与被询问的保持一致——凭据是唯一的例外,因为它是从不向界面展示、因而永远无法放进草稿的那个字段
+**让 Host 读取整个已存 profile,而不是接受草稿。** 对已配置好的提供方来说,不会有机密跨越协议层。但这样一来新增提供方就必须先保存一份不可用的配置,而端点已改却尚未保存的表单会静默地去询问旧地址。草稿仍是端点和协议的权威来源。Host 侧的狭窄例外是只写的已存凭据,以及仍属部署配置、而非 Models 页面字段的 profile headers
 
 **询问 pi-ai 的每一种协议。** Anthropic 的列表恰好与 OpenAI 共用同一层信封,而 Google 的不是。只支持容易的那几种会让覆盖范围变得任意;更糟的是,猜错的响应形状会与「该提供方没有模型」无法区分。一个明说自己无法被询问的协议,会把用户送去手工填写——那正是既定的回退路径。
 
@@ -41,10 +41,10 @@ pi-ai 提供了 `createProvider({ fetchModels })` 加上 `Models.refresh()` 与
 
 ## Consequences
 
-接入网关的人可以直接问它服务什么,而不必去翻它的文档;答案以候选形式抵达,由用户自己挑选,而不是被背着写进配置。seam 因此多了一个刻意保持很小的注册表:每个 namespace 一份、不存储、生命周期不超出 fiber。
+接入网关的人可以直接问它服务什么,而不必去翻它的文档;答案以候选形式抵达,由用户自己挑选,而不是被背着写进配置。已配置的企业网关会为询问与模型请求使用同一组部署 headers,而无需给浏览器协议增加 header 注入字段。seam 因此多了一个刻意保持很小的注册表:每个 namespace 一份、不存储、生命周期不超出 fiber。
 
 代价是:协议层多了第三个承载机密的载荷,配置面的只写接口从两个方法变成三个。发现覆盖范围按协议而非按提供方划分——一个 Anthropic 兼容网关即便其列表能被解析,也仍须手工填写。而且由于没有任何环节会重跑该询问,模型列表的新鲜度依旧只到最近一次编辑为止;这与下层刻意做出的取舍是同一个。
 
 ## Testing
 
-`packages/llm/llm/tests/topology.spec.ts` 覆盖注册表:每个 namespace 一份、随 fiber dispose(资源释放)、丢弃重复与不可用 id 且不凭空补容量的归一化、`NO_DISCOVERY`/`INVALID_DISCOVERY` 两种拒绝,以及 `model-discovery-failed` Remote 映射。`packages/llm/llm-pi-ai/tests/discovery.spec.ts` 针对本地 HTTP 服务器驱动探测——含与不含公布容量的列表、被保留的部署路径、无凭据、草稿没带密钥时已配置路由自行取用凭据且键入的密钥压过它、catalog 路由完全不解析凭据即作答、被丢弃的行、401/403 与服务器故障之别、非列表与非 JSON 响应、不可达端点、调用方取消、不支持的协议,以及尺寸上限的「声明长度」与「流式」两种形态。`packages/client/connection/tests/node-half.host.spec.ts` 固定 `llm/discoverModels` 的 `/api` 承载注册,`packages/client/ui-settings-models/tests/provider-form.client.spec.tsx` 则验证草稿完整抵达 Remote、缺席字段保持缺席,以及选择前没有 settings namespace 或凭据被写入。
+`packages/llm/llm/tests/topology.spec.ts` 覆盖注册表:每个 namespace 一份、随 fiber dispose(资源释放)、丢弃重复与不可用 id 且不凭空补容量的归一化、`NO_DISCOVERY`/`INVALID_DISCOVERY` 两种拒绝,以及 `model-discovery-failed` Remote 映射。`packages/llm/llm-pi-ai/tests/discovery.spec.ts` 针对本地 HTTP 服务器驱动探测——含与不含公布容量的列表、被保留的部署路径、无凭据、已配置路由提供自己的已存凭据与 headers 且键入的密钥无需解析已存凭据便可压过它、catalog 路由完全不解析凭据即作答、被丢弃的行、401/403 与服务器故障之别、非列表与非 JSON 响应、不可达端点、调用方取消、不支持的协议,以及尺寸上限的「声明长度」与「流式」两种形态。`packages/llm/llm-pi-ai/tests/loader-composition.spec.ts` 通过 Loader 启动 settings 与 credentials,并证明仅配置在 settings 中的 headers 会抵达 `GET /models`,且请求所持有的 headers 赢得冲突。`packages/llm/llm-pi-ai/tests/adapter.spec.ts` 拒绝 Fetch 无法表示的 profile headers,`packages/llm/llm-pi-ai/tests/dynamic-config.spec.ts` 证明 settings 写入会报告该配置错误,同时上一组可用路由仍继续服务。`packages/client/connection/tests/node-half.host.spec.ts` 固定 `llm/discoverModels` 的 `/api` 承载注册,`packages/client/ui-settings-models/tests/provider-form.client.spec.tsx` 则验证草稿完整抵达 Remote、缺席字段保持缺席,以及选择前没有 settings namespace 或凭据被写入。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.i18n.yaml

@@ -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-06-subagent-list-identity-projection.md
-2026-08-06-subagent-list-identity-projection.md: aeed828530f615b1bb4958a360b5ba4db543f714
-2026-08-06-subagent-list-identity-projection.zh.md: b2b64eaa7c06b738734a7b975adb5948704465bd
+2026-08-06-subagent-list-identity-projection.md: cbb15696314930acfaf20ba8651699c53c5dbde2
+2026-08-06-subagent-list-identity-projection.zh.md: dbb62dbfc6bb6ca3ab63504de1ba8dd35327bbbf

File diff suppressed because it is too large
+ 24 - 25
.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.md


+ 28 - 29
.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.zh.md

@@ -14,16 +14,16 @@ Status: implemented
 
 ## 决策
 
-mode 与 label 由新的 `subagent` projection unit(纯身份两臂)折叠,unit 是折叠规则的唯一权威;`listChildren` 不再依赖 session-query——枚举是 subagent 自管的 live-preferred 合并,取值走三级「算完即止」阶梯:live child 同步读注册表的既有水位缓存(零日志读);cold child 先问可选的 `sessionProjectionCache` checkpoint,取到过 seq 门的身份即定值;否则一次 `persistence.inspect` 整读加经注册的 `subagent` unit 折叠。无索引、不自建缓存、无回写。
+mode 与 label 由 `subagent` projection unit(纯身份两臂)折叠,unit 是折叠规则的唯一权威。枚举使用共享 Session query corpus,取值则走三级「算完即止」阶梯:live child 同步读注册表的既有水位缓存(零日志读);unseeded cold child 可以使用可选 `sessionProjectionCache` checkpoint,因为其精确 inherited cut 已知为零;每个 seeded child 与每次 cache miss 都执行一次含正文的 Session observation,再经注册的 `subagent` unit 折叠。无索引、不自建缓存、列表侧无回写。
 
 消除逐 child 扫描的出路有三类:把 mode/label 提升进 header(写路承担);为投影建持久派生(checkpoint 阶梯,或随查询索引重建落值、读端对账);读时现算(live 走水位缓存,cold 一次整读)。本记录取第三条。「值随查询索引落库」已整体退役:查询基础设施被迫认识领域词汇,而唯一消费方读时现算即可满足——live child 的零读由 session-projection 既有水位缓存白拿,cold child 的一次整读被「算完即止」显式接受。前两条与退役理由详见考虑过的替代方案一节。
 
 要点:
 
-- **subagent 列表不依赖 session-query**:枚举由 subagent 自管的 live-preferred 合并完成,mode/label 经 `ctx.sessionProjections` 取值;没有 query backend 的部署照常列表
-- **取值三级「算完即止」阶梯**:live child 读 `sessionProjections.snapshot(session, ['subagent'])`(注册表既有水位缓存,零日志读);cold child 先读可选 `sessionProjectionCache.cachedSnapshot(header, ['subagent'])`,非 null 身份通过 seq 门(`seq >= seedLength ?? 0`)即直接使用;否则执行一次完整 Session 观察,再经注册的 `subagent` unit 折叠;再没有就没有——不自建缓存、无回写、无索引。
+- **subagent 列表使用 Session query corpus 完成枚举与含正文 observation**:mode/label 仍经 `ctx.sessionProjections` 获取,列表不拥有 descriptor parser 或领域索引
+- **取值三级「算完即止」阶梯**:live child 读 `sessionProjections.snapshot(session, ['subagent'])`(注册表既有水位缓存,零日志读);unseeded cold child 可读 `sessionProjectionCache.cachedSnapshot(header, SessionLogOffset(0), ['subagent'])`;seeded child 或 cache miss 执行一次携带 `inheritedEventCount` 的 Session observation,再经注册的 `subagent` unit 折叠。再没有就没有——不自建缓存、列表侧无回写、无索引。
 - **`subagent` projection unit 是折叠规则唯一权威**:live 与 cold 快照都运行同一份已注册 unit,不存在第二份描述符解释逻辑。
-- **header、描述符(v2)、session-persistence、session-projection(-cache)、session-query(-sqlite) 全部零改动**;存量数据第一次被列表时一次 `inspect` 现算获得精确值,无 unknown 降级态、无迁移。
+- **描述符(v2)保持不变**。Session、persistence、projection cache 与 query 在 logical header 之外单独携带精确 inherited cut;listing 无法证明 cut 为零时,存量数据经一次含正文 observation 获得精确值——无 unknown 降级态,也无持久格式迁移。
 
 与既有记录的关系:
 
@@ -36,8 +36,8 @@ mode 与 label 由新的 `subagent` projection unit(纯身份两臂)折叠
 
 ```ts ignore-check
 export type SubagentIdentityProjection =
-  | { mode: 'one-shot'; label?: string; seq: number }
-  | { mode: 'continuable'; label: string; seq: number }
+  | { mode: 'one-shot'; label?: string; seq: SessionSeq }
+  | { mode: 'continuable'; label: string; seq: SessionSeq }
 
 declare module '@deepseek-ai/dsh-session-projection/types' {
   interface SessionProjectionStateMap {
@@ -52,19 +52,19 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
 
 - 投影是纯身份,**projection 体系不做失败通道**:unit 永不抛错;载荷损坏、版本不认识与整日志没有描述符一样。host checkpoint 状态使用可序列化的包装 `{ identity?: SubagentIdentityProjection }`,缺席为 `{}`;客户端 view 则是非可选的 `SubagentIdentityProjection | null` 条目。`null` 完好通过 JSON,因此推送 reset 会替换旧身份,而不会被 stringify 丢掉。判定纪律:消费面把 null 与客户端 key 缺席一律视为无值。「算出来没有」如何呈现是消费方自己的事(见下文 `listChildren` 四态映射)。
 - label 强度由描述符 schema 决定:continuable 的 label 解析强制必有,one-shot 的本就可选;mode/label 判别与下文 child 行的强约定完全一致(行不携带 `seq`——它是投影内部的 own-suffix 证明)。
-- 身份携带 `seq`:折出该身份的 `subagent/descriptor` 事件 seq,两臂必有、null 哨兵无——`seq >= header.seedLength ?? 0` 证明身份折叠自 child 自身后缀,而非 fork 种子回放的祖先描述符。unit 把包装状态中校验后的身份映射为客户端 wire view,并与其他 unit 一律检查点化(`persist` 选项已删除);`stateVersion` 为 2,在增加 `seq` 时升版。更早的 checkpoint 行按 registry 约定版本失配失效、落权威重折。
+- 身份携带品牌化 `seq`:折出该身份的 `subagent/descriptor` 事件 seq,两臂必有、null 哨兵无。live Session 通过 `isOwnSeq()` 检查它;cold 含正文 observation 则与 `inheritedEventCount` 比较。仅 header 的 seeded candidate 会跳过 cache,因为 header 有意不暴露整数 cut;unseeded candidate 知道 cut 为零。unit 把包装状态中校验后的身份映射为客户端 wire view,并与其他 unit 一律检查点化(`persist` 选项已删除);`stateVersion` 为 2,在增加 `seq` 时升版。更早的 checkpoint 行按 registry 约定版本失配失效、落权威重折。
 - 折叠规则:`subagent/descriptor` last-wins,与 `subagentTiming` 同一条 descriptor-reset 纪律——fork 前缀里的祖先描述符被自身描述符覆盖。损坏或版本不认识的载荷同样 last-wins:重置为 null 哨兵而非保留先前身份,健康祖先的 fork 不会继承自身描述符立不住的身份。
 
-### 枚举:subagent 自管 live-preferred 合并
+### 枚举:query corpus 与 live preference
 
-`listChildren`([list-children.ts](../../../../packages/subagent/subagent/src/list-children.ts))的枚举不经任何查询服务:`ctx.sessions.list()` 与 `ctx.get('sessionPersistence')?.list()` 两个来源按 id 合并,live 记录整条覆盖同 id 持久化记录、不做 header 一致性校验。枚举所需全部是 header 事实:
+`listChildren`([list-children.ts](../../../../packages/subagent/subagent/src/list-children.ts))通过 `sessionQuery.listSessions()` 取得 canonical live-preferred corpus,再把每个 listed id 与可能存在的 `ctx.sessions.get(id)` 配对;同 id 存在 live Session 时使用 live header。枚举所需全部是 header 事实:
 
 - 过滤:`header.origin === 'subagent' && header.parentSession === parentSessionId`。
 - `hasChildren`:同一份合并材料向下看一层——存在 `origin === 'subagent'` 且 `parentSession` 为该 child 的直接后代。
 - `activity`:live 记录为 `running`,仅存在于持久化的为 `inactive`。
 - 排序:`createdAt` 升序、再按 child id 升序(与旧约定一致)。
-- **persistence 缺席退为 live-only 枚举,不报错**:没有 persistence 的部署,cold child 本就无法 resume,列出 live child 仍然有意义。(对照:旧实现在 sessionQuery 缺失时整体拒绝。)
-- persistence 列表失败使整次枚举失败;per-child 隔离只作用于逐 child 的冷读
+- `sessionQuery` 服务缺席时以 `SUBAGENT_CONTROL_QUERY_UNAVAILABLE` 失败;共享 query corpus 负责决定部署能枚举 live-only 还是持久化 Session。
+- query corpus 失败使整次枚举失败;per-child 隔离只适用于逐 child cold observation
 
 ### 取值:三级「算完即止」阶梯
 
@@ -73,20 +73,20 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
 | 级 | 读法 | 成本 |
 | --- | --- | --- |
 | 1:live child | `ctx.sessionProjections.snapshot(session, ['subagent'])` | 零日志读——注册表既有水位缓存,同步取值 |
-| 2:cold child,cache 命中 | 可选 `sessionProjectionCache.cachedSnapshot(header, ['subagent'])`,非 null 身份满足 `identity.seq >= header.seedLength ?? 0` 才直接使用——own descriptor 一经追加不可变,seq 门证明该值折叠自 child 自身后缀,无视行水位 | 零日志读 |
-| 3:cold child,兜底 | `persistence.inspect(id)` 整读 + 经注册的 `subagent` unit 折叠 | 每次列表一次整读现算 |
+| 2:unseeded cold child,cache 命中 | 可选 `sessionProjectionCache.cachedSnapshot(header, SessionLogOffset(0), ['subagent'])`;精确 cut 为零时,每个合法 seq 都归 child 自有 | 零日志读 |
+| 3:seeded child 或 cold 兜底 | 一次含正文 `sessionQuery.observeSession(id)` 加已注册的 `subagent` projection,使用 `inheritedEventCount` 做 own-suffix 检查 | 每次列表一次整读现算 |
 
-- 错误约定:`sessionProjections` 是必需注入——`SubagentRuntime` 在 inject 集里声明它,没有 registry 的部署根本无法激活服务(与 loop),`listChildren` 不可达,而不是供出降级行([mandatory-seam 记录](2026-08-19-session-projection-mandatory-seam.zh.md));响亮运行时检查与 `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` 随之删除。会话存储保留显式姿态:`ctx.get('sessions')`(严格全局读取,不走调用方作用域的属性代理)缺席以 `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE` 失败。apiproxy 为 `PROJECTIONS_UNAVAILABLE` 设的专门 wire 脸随码删除;`SESSION_STORE_UNAVAILABLE` 走通用 internal 兜底——apiproxy 组合自身就 inject `sessions`,该错误在其部署不可达,专门映射违反 need 原则。`SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` 已随 session-query 依赖删除
-- cache 是纯可选加速层:服务缺席判空跳过——无错误码、不进配置校验(与 `sessionProjections` 的必需注入相对)。第二级任何抛错(包括缓存内任一 unit 行中毒使 `viewCheckpoint` 引爆)静默落第三级——缓存是派生数据,其故障不产生 `corrupt` 判决,终审归权威重折;checkpoint 切面早于描述符的行,`subagent` key 天然缺席,自动落底,无特判;行里的 null 哨兵同样不作数——一律落第三级,由权威重折裁决。创建窗口内的 count/interval checkpoint 可能把 fork 种子回放的祖先身份落进行——祖先 seq 落在 seed 区间,被 seq 门拒绝,同样落第三级裁决
+- 错误约定:`sessionProjections`、Session store 与 `sessionQuery` 都是 listing 所需的 runtime service。三者分别以 `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE`、`SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE` 与 `SUBAGENT_CONTROL_QUERY_UNAVAILABLE` 显式失败;缺失分类或 corpus 能力不会伪装成空结果
+- cache 是纯可选加速层:服务缺席判空跳过——无错误码、不进配置校验(与 `sessionProjections` 的必需注入相对)。seeded header 会跳过该级,因为不读取正文就无法提供 cache identity 所需的精确 cut。对 unseeded child,第二级任何抛错(包括中毒 unit 行引爆 `viewCheckpoint`)都会静默落第三级——缓存是派生数据,其故障不产生 `corrupt` 判决,终审归权威重折;checkpoint 早于 descriptor、key 缺席或 null 哨兵也都会落底
 - per-child 隔离:单 child 的 cold 整读失败只使该行成为 `unavailable` diagnostic,下次列表自然重试,不影响 sibling(见四态映射)。
-- 冷路径的生命周期见证:preparation 的结果必须仍指向枚举时的那个生命周期——见证字段集与旧 SOURCE_CONFLICT 检查同款七字段(version、id、createdAt、cwd、parentSession、seedLength、delegationDepth);同 id 删除后重新发布的会话对旧 parent 的目录降级为 `corrupt` 行,不外漏新 owner 的 child。
+- 冷路径的生命周期见证:observation 必须仍指向枚举时的那个生命周期。见证字段为 version、id、createdAt、cwd、parentSession、isSeeded、delegationDepth、origin 与 agentPreset;同 id 删除后重新发布的 Session 对旧 parent 的目录降级为 `corrupt` 行,不外漏新 owner 的 child。
 - 冷读并发以常数 4 有界——它约束的是本地介质的一次只读扫描而非部署行为;出现联网 persistence backend 时提升为验证过的 `Config` 字段。
-- 冷读成本如实记录:cache 未挂载或未命中时,cold child 每次列表才付一次整读,成本与其 transcript 大小成正比;定案「算完即止」,不自建缓存。整读经 `inspect()` 走 [Session 准备阶段](2026-08-05-session-preparation.zh.md)的冷读,同 id 短期重复读取可命中其 LRU 复用,但列表不依赖此。live child 全程零日志读。
+- 冷读成本如实记录:每个 seeded child 与每次 unseeded cache miss 都会在每次列表时支付一次完整 query observation,成本与其 transcript 大小成正比;定案「算完即止」,不自建缓存。observation 可以复用 query/persistence preparation 层,但列表不依赖该优化。live child 全程零日志读。
 - 取消:每次 persistence 读前后检查调用方 signal,abort 之后才结算的读拒绝归一化为稳定错误码 `CANCELLED`。
 
 ### 权威模型
 
-- session log 是唯一权威;本方案不新增任何派生持久化——没有索引值、没有自己的 checkpoint、没有进程 memo;第二级读取的 `sessionProjectionCache` checkpoint 是既有组合项的派生数据,本方案只读不写。取值现算现弃,值的新鲜度就是读取时点的 live 状态或持久化 revision(own descriptor 一经追加不可变——缓存身份过 seq 门后无陈旧性问题,门防的是种子回放的祖先身份)
+- session log 是唯一权威;本方案不新增领域索引、自有 checkpoint 或进程 memo。第二级读取的 `sessionProjectionCache` checkpoint 是既有组合项的派生数据,列表只读。取值现算现弃;seeded candidate 用含正文 observation 按精确 cut 分类,unseeded cached identity 无需 seq 门,因为每个合法 seq 都归自身所有
 - Session 与 persistence 写路完全不感知列表与投影消费:没有事件监听回写,没有写时折叠。
 - 枚举与取值不构成第二个鉴权来源,也不让尚未发布的 child 可见——两个来源只见已发布的 live 记录与已落盘的持久化记录,与 durable-subagent-catalog 记录对派生读面立下的规则一致。
 
@@ -127,25 +127,24 @@ export type SubagentListEntry =
 
 已知边界偏差(有意接受,随本记录留档):
 
-- 死于发布窗口的 fork child,seed 里若有祖先描述符,last-wins 会给出祖先身份,误现为 child 行;恢复仍按 own-suffix 折叠权威失败(`NOT_RESUMABLE`)。旧实现靠 `seedLength` 过滤将其 omit;projection unit 看不到 header,接受此残骸级偏差(`subagentTiming` 有同类既有暴露)。
 - own suffix 出现多个描述符,旧实现判 corrupt,现 last-wins 取末者(提供方约定本就保证恰一)。
 - live/persisted header 冲突,旧实现是 per-child corrupt;现枚举 live 优先、不做一致性校验,冲突不再被察觉,以 live 记录成行。
 - 损坏存储的源读失败(如坏 surface 被冷读整读拒收),旧实现映射 per-child `corrupt`,现统一成 `unavailable` 行(读侧无从区分成因)。
 - 未知 parent,旧实现经 session-query 抛 not-found(「parent session … was not found」);现自管合并对不存在的 parent 得到空子集,枚举返回空列表,wire 上后续操作落到 child 级 subagent-not-found——语义与文案的静默变化,显式接受。
-- rung 2 的更晚事件窗口:cache 行恰在首个自有描述符之后落盘,日志随后追加第二个自有描述符(或 malformed 载荷置 null 哨兵),且进程在下一次 checkpoint 前崩溃——此后冷列表的 rung 2 凭 seq≥seedLength 门持续供出行内旧身份(第一个自有描述符的值),与权威重折(last-wins 第二个)分歧,且 rung 2 命中期间不触发重折、无从察觉。边界三条:①前提是同一 child 出现第二个自有描述符,违反建档提供方「恰追加一次」约定,属损坏类数据,与多描述符偏差同族同源;②需「损坏 + 崩溃错过 checkpoint(turn/end 与 disposal 两个 mandatory 点及 count/interval 节流点全部未及)」双条件同时成立;③健康 child(恰一自有描述符)不受影响——seq 门放行的正是唯一真身份。自愈条件:该 child 任一次 live 运行(turn/end mandatory checkpoint)或任何触发 cache.write 的时点,都会以新 fold 整行覆写(whole-record replace),rung 2 随即供正;权威路径(rung 3 重折、live snapshot、resume 折叠)自始正确,分歧只存在于持续冷、行未再更新期间的列表读。机制修法不采:gate 对账需知日志末端 seq,冷路径零读不可得;cache 行携 revision 是 opaque token,无法比较且跨域改 schema——按「cache 永不为权威」总纲归档为接受项
+- rung 2 的更晚事件窗口只适用于 unseeded child:cache 行恰在首个 descriptor 后落盘,日志随后追加第二个 descriptor(或 malformed 载荷置 null 哨兵),且进程在下一次 checkpoint 前崩溃。cold listing 可能持续供出旧身份,直到一次 live 运行或 cache write 替换该行。其前提违反 provider 的「恰追加一次」约定,并且还需错过所有 mandatory checkpoint;健康 child 不受影响。seeded child 没有 body-owned cut 时绝不进入 rung 2
 
-消费面:wire、tool、GUI 的 diagnostic 处理**全部保持原状零改动**(`list_agents` 的 description 与 output schema 未动;该插件的加载要求变化——inject 去掉 `sessionQuery`、新增必需注入 `sessionProjections`)。行为上动的只有 apiproxy:路由段的 `hasSubagentDescriptor()` 扫描已删除,`hasSubagentOwner` 只看 `header.origin`——pre-#1569 的无 `origin` 存量不再被认作 subagent 属主,其本就不进目录,pre-release 立场接受;`subagents.history` 与 `session.history` 同源对齐——live child 用内存事件与注册表水位快照,cold child 用 `inspectServable` 直读持久化并 detached 折叠,不经查询服务,SESSION_QUERY_* 错误臂随之退役,wire 形状不变(`history` 的 JSDoc 措辞改为 live 内存快照/cold 持久日志双臂)
+消费面保持相同的 row 与 diagnostic wire 形状。`list_agents` 使用必需的 query corpus 与 projection registry;live identity 来自 registry snapshot,cold identity 来自 cache 或 query observation。Host ownership 仍使用 `header.origin`,history 使用共享的 live/cold Session query source;没有消费方独立解析 descriptor event
 
 ### 改动落点
 
 | 区域 | 文件 | 改动 |
 | --- | --- | --- |
 | subagent | projection.ts、projection-types.ts、index.ts | 新客户端可见 `subagent` unit 与注册 |
-| subagent | list-children.ts 及类型 | 重写为自管枚举 + 投影阶梯四态映射;删 session-query 依赖、逐 child 事件读取与就地分类机器;错误码 `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` 删除,`sessionProjections` 转为必需注入(不再存在投影错误码);新增可选依赖 dsh-session-projection-cache(纯加速读取,缺席跳过) |
-| host/apiproxy | api-proxy.ts | 删 `hasSubagentDescriptor`,属主判定只看 `header.origin`;`subagents.history` 与 `session.history` 同源——live 用内存事件与注册表水位快照,cold 用 `inspectServable` 直读持久化并 detached 折叠,不经查询服务,SESSION_QUERY_* 错误臂与 `PROJECTIONS_UNAVAILABLE` 专门 wire 脸随之退役 |
-| tool | tool-subagent-control/list-agents.ts | 加载要求收窄(inject 去 `sessionQuery`);model-visible schema、描述与渲染零改动 |
+| subagent | list-children.ts 及类型 | query-corpus 枚举加 projection 阶梯四态映射;必需 projections/query service 与可选 projection-cache 加速 |
+| host/apiproxy | Session controller/query integration | owner 检查使用 `header.origin`;live/cold history 与 listing 消费共享 query 和 projection source |
+| tool | tool-subagent-control/list-agents.ts | model-visible schema、描述与渲染保持不变 |
 | wire/client | api/subagents.ts、runtime sessions/service.ts、GUI | 类型、行形状与 diagnostic 处理**零改动**;api/subagents.ts 仅 `history` 的 JSDoc 措辞改为双臂 |
-| core/session、session-persistence、session-projection(-cache)、session-query(-sqlite) | — | **零改动** |
+| core/session、session-persistence、session-projection(-cache)、session-query(-sqlite) | 含正文 cut 与品牌化 seq 传递 | Logical header 暴露 `isSeeded`;Session、persistence observation、cache identity 与 query record 单独携带精确 `inheritedEventCount` |
 
 ## 考虑过的替代方案
 
@@ -169,20 +168,20 @@ export type SubagentListEntry =
 
 ## 验证
 
-`packages/subagent/subagent/tests/list-children.spec.ts` 重写为本约定:无 persistence、query 服务与继续运行时的 live-only 列表;registry 缺席时服务根本不激活(mandatory seam——`setup` 变体断言 `ctx.get('subagents')` 保持 undefined);live child 全程零 `inspect`、cold child 每次列表恰一次;多描述符 last-wins 取末者;损坏载荷与未知版本折为 `corrupt`;冷读失败映射 `unavailable` 且下次列表重试;fork seed 里的祖先描述符按该身份成行(偏差一钉住);普通 fork 与无 subagent origin 的后代不入列也不计入 `hasChildren`;`createdAt`→id 排序;提供方未挂载不影响列表;压缩与未压缩孪生一致;预中止、持久化列表与冷读取消三例归一 `CANCELLED`;空列表与稳定错误码(存储缺席时 `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE`)。第二级例:own-seq 身份直用零 `inspect`、fork 种子祖先身份(seq 落在 seed 区间)被门拒绝落底、行内无身份(null 哨兵或 key 缺席)落底、cache 服务缺席落底、缓存行中毒静默落底重折;冷路径 lifecycle 篡改按见证七字段逐一(`it.each`)降级为 `corrupt`。`tool-subagent-control` 的 list-agents 测试随加载要求收窄更新;`optional-session-query.spec.ts` 随依赖消失删除;既有无密钥快照(`subagent-list-agents` 等)零变化,钉住健康路径的 wire 与 model-visible 面不变;新增无密钥快照 `subagent-diagnostic`(examples/headless-agent)钉住四态映射的诊断分类——descriptor-less 定局残骸成 `corrupt` 行等模型可见变化
+`packages/subagent/subagent/tests/list-children.spec.ts` 固定本约定:live identity 通过 `Session.isOwnSeq()` 检查;unseeded cold identity 可在 cut 零时使用 cache;seeded candidate 跳过该 cache rung,转而使用携带 `inheritedEventCount` 的 observation;祖先 identity 无法通过 own-suffix 检查;缺席、null、中毒与不可用的 cache/observation 会按约定落底或产生 diagnostic;lifecycle 篡改按完整见证字段集降级为 `corrupt`。既有无密钥快照保持健康 wire 与 model-visible 面不变,`subagent-diagnostic` 则固定诊断分类
 
 ## 后果
 
 - live child 的列表全程零日志读;cold child 在 cache 未挂载或未命中时每次列表一次 `inspect` 整读,成本与其 transcript 大小成正比、随列表频率重复——定案「算完即止」,不自建缓存、不回写,同 id 短期重复整读可命中准备阶段 LRU 但列表不依赖它。
-- subagent 列表不再要求 query backend:纯 live 与无 persistence 的部署都能列表;`SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` 消失,`list_agents` 插件加载不再要求 `sessionQuery`,而 `sessionProjections` 转为 `SubagentRuntime` 的必需注入——没有投影 registry 的部署根本不会激活服务(mandatory seam)
+- subagent 列表要求 Session query corpus 与 projection registry;服务缺失会显式失败,而不是供出不完整 row。可选 projection cache 只改变正文读取次数
 - 身份解释只存在于 registry 注册的一份 unit:列表三级阶梯与 GUI history 冷读使用其 live、cached 或 observed wire 快照,不存在手写旁路折叠;若未来某消费面绕开该 unit 手写折叠,各读面的值将漂移——这是本设计要求维持的纪律,不是机制保证。
 - per-child 隔离回归:单 child 冷读失败只损失该行,healthy sibling 不受影响;persistence 列表失败仍使整次枚举失败。
-- 诊断与枚举语义留下六处边界偏差(stillborn fork 祖先身份误现、多描述符取末者、header 冲突不再被察觉、损坏源读失败由 `corrupt` 转 `unavailable`、未知 parent 由 not-found 改为空列表、rung 2 更晚事件窗口),完整语义见已知边界偏差清单;前四处为残骸级数据的展示或分类偏差,未知 parent 一处是查询语义的静默变化,rung 2 窗口一处是损坏加崩溃双条件下可自愈的缓存供值分歧;恢复鉴权均不受影响,显式接受
+- 诊断与枚举语义留下五处边界偏差(多描述符取末者、header 冲突不再被察觉、损坏源读失败改变分类、未知 parent 由 not-found 改为空列表、unseeded rung 2 更晚事件窗口)。seeded 祖先 identity 已不再构成偏差,因为含正文读取会把它与 `inheritedEventCount` 比较;恢复鉴权始终不受影响
 - pre-#1569 的无 `origin` 存量不再被认作 subagent 属主;其本就不进目录,pre-release 无兼容承诺。
 
 ## 相关
 
-- [durable-subagent-catalog 与 list_agents](../feature/2026-07-22-durable-subagent-catalog-and-list-agents.zh.md)——被本记录部分取代:描述符仍是 mode/label 的持久权威与折叠输入,列表的枚举与取值改为自管合并加投影阶梯。
+- [durable-subagent-catalog 与 list_agents](../feature/2026-07-22-durable-subagent-catalog-and-list-agents.zh.md)——被本记录部分取代:描述符仍是 mode/label 的持久权威与折叠输入,取值改为共享 query corpus 上的 projection 阶梯。
 - [session projections 与命令生命周期日志](../../proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md)——registry 约定的权威;本记录为其新增 `subagent` 身份 unit,并消费其 live 与 cold wire 快照。
 - [session projection 状态与客户端视图](2026-08-19-session-projection-state-and-client-views.zh.md)——state/client 拆分;`subagent` 与 `subagentTiming` 都提供客户端 wire view。
 - [session projections 作为必需接缝](2026-08-19-session-projection-mandatory-seam.zh.md)——`sessionProjections` 转为必需注入;列表的错误约定随其变化(registry 缺席是激活期失败,投影错误码删除)。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml

@@ -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-fork-children-stay-one-shot.md
-2026-08-10-fork-children-stay-one-shot.md: ba1e99c4d78d14230a2199cd7fb3c3eb9d3ad754
-2026-08-10-fork-children-stay-one-shot.zh.md: 0d4e214be952f5a4d96c296f38a5b0dbfbd85c86
+2026-08-10-fork-children-stay-one-shot.md: 0edf6ca9347dcf57313365e570daed22589f69fd
+2026-08-10-fork-children-stay-one-shot.zh.md: e832846d4018cf328e02c693bf21fe47a401d320

+ 16 - 24
.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md

@@ -1,4 +1,4 @@
-# Agent Note: Forked children stay one-shot
+# Agent Note: Forked children preserve the parent request prefix
 
 Status: implemented
 
@@ -6,44 +6,36 @@ English | [中文](2026-08-10-fork-children-stay-one-shot.zh.md)
 
 ## Problem
 
-Fork's only difference from spawn is that the child Session is seeded with the parent's completed-turn prefix ([subagent-fork-in-process](../../../../packages/subagent/subagent-fork-in-process/README.md)). That seed costs real tokens — the inherited history is re-sent in every child request — and its one concrete payoff is provider-side prefix reuse: under the same provider and model, a child request whose leading bytes are identical to the parent's re-prefills none of the shared span. Anything a child scope adds *ahead* of the inherited history spends that payoff, because reuse stops at the first differing byte.
+Fork differs from spawn by seeding the child Session with the parent's completed-turn prefix. That seed costs tokens, and its intended payoff is provider-side prefix reuse: under the same provider and model, a child request whose leading bytes match the parent's does not prefill the shared span again. A child-only system-prompt section or tool schema ahead of the inherited history defeats that payoff.
 
-The child-scoped `report` return channel is now the largest such addition, and since [the report obligation](../feature/2026-08-06-continuable-child-report-obligation.md) it is two deltas rather than one: the `report` tool schema and the `tool:report` system-prompt section. Both live in the request head — the system block and the tool block precede every message — so a continuable forked child invalidates reuse before the first inherited turn and re-prefills the whole transcript it was forked to reuse. That composition pays fork's duplication cost and collects none of its benefit, while the parent still holds a reusable prefix the child could have shared.
+The earlier shipped composition avoided this mismatch by keeping forked children one-shot. That restriction was a consequence of the former child-only return tool, not an intrinsic property of continuable fork.
 
 ## Decision
 
-Every shipped composition inherits the fork delegation tool's `backgroundMode: one-shot` from the [base bundle](../../../../packages/bundle/base/cordis.patch.yml). The base bundle leaves `run_in_background` available because it also mounts the task service needed to settle background work.
+The model-facing `send_message` tool is registered globally for every Agent in a composition. A continuable forked child therefore receives the same tool name, description, schema, and ordering as its parent. Its initial task is appended after the inherited Session seed, and the task includes the direct parent id plus guidance to return results with `send_message({ agent_id, message })` when that tool is visible to the child.
 
-One-shot children — foreground and background alike — are created through `SubagentRuntime.start()`, which never enters the continuable activation-setup registry, so neither `report` nor its prompt section is installed. A forked one-shot child's system prompt and tool schemas therefore equal its parent's, apart from the `persona` and `toolFilter` deltas a deployment opts into per delegation tool.
+The base and headless compositions retain one-shot fork as their conservative lifecycle policy. The `cordis`, `standard`, and `ptc` CLI presets may bind fork to the continuable lifecycle because that binding no longer inserts child-only request-head fields. `ForkInProcessProvider.prepareContinuable()` and `ctx.subagents.startContinuable()` remain the implementation seam for those presets.
 
-`spawn` keeps `backgroundMode: continuable`. Continuable children and the report obligation ship unchanged for the provider whose child starts with no inherited prefix to protect, so this decision costs the report channel nothing.
-
-### The restriction is composition, not code
-
-`ForkInProcessProvider.prepareContinuable` stays implemented and `ctx.subagents.startContinuable()` still accepts `fork`; only the shipped `cordis.yml` rows changed. `tool-subagent` knows both the provider's `inheritsParentContext` and its own `backgroundMode` at mount, so a load-time rejection of the pair was available and is deliberately not added: the pair is not wrong in general. It is wrong only while a child-scope delta precedes inherited history, and the package that creates that delta — [`dsh-tool-subagent-report`](../../../../packages/subagent/tool-subagent-report/README.md) — is separately installable and, by its own design, invisible to `tool-subagent`. A deployment that omits the report package can run continuable forked children with the prefix intact. Encoding one roster's consequence as a delegation-tool invariant would make the tool assert something it cannot observe.
-
-The reintroduction condition is recorded as a `TODO(fork-continuable-prefix-reuse)` marker on `prepareContinuable` itself, the one method the shipped compositions do not call, and tracked as issue #2124: continuable fork reopens when a child's system prompt and tool schemas can match its parent's byte for byte.
+Byte-identical prefix reuse is qualified by explicit deployment choices. A fork delegation that applies a child persona or `toolFilter` may still change the request head. In particular, filtering out `send_message` removes both the schema and the return guidance from the child; the runtime does not bypass an explicit allow-list.
 
 ## Alternatives considered
 
-**Reject `inheritsParentContext` + `continuable` at mount.** A loud load-time failure would prevent silent reintroduction, which is what the configuration change cannot do. Rejected because the delegation tool cannot see the report package and the combination is legitimate without it; the invariant would be false for a deployment that never installs a child-scope delta, and `tool-subagent` would be asserting a fact owned by the roster.
-
-**Stop mounting the fork provider at all.** This was the broader form of the restriction. Rejected because foreground fork *is* the prefix-reusing case and is untouched by the report channel, so a full ban gives up the capability without buying anything the one-shot binding does not already buy — and would leave no shipped composition exercising session seeding.
+**Keep every fork one-shot.** This preserves the prefix but unnecessarily gives up durable, multi-turn forked children after the child-only schema difference is gone.
 
-**Ship continuable forked children and accept the loss.** Rejected because the loss is total rather than marginal: reuse breaks ahead of the inherited history, so the child pays full prefill on a transcript it duplicated for the sole purpose of not paying it. A deployment that wants a long-lived child with no inherited context already has `spawn`.
+**Install a child-only return alias.** A recipient-free alias would make child calls shorter, but it would recreate a tool-schema and prompt delta before inherited history and duplicate the adjacent-Agent operation.
 
-**Make `report` visible to every Agent.** A global registration would restore byte-identical prefixes by giving parent and child the same schema and section. Rejected because roots, one-shot children, remote children, and agentless callers would advertise a tool with no derivable recipient, and execution-time rejection would make schema visibility disagree with authority — the scope-local decision the [report tool Agent Note](../feature/2026-07-30-continuable-subagent-report-tool.md) already settled.
+**Add the return instruction to the system prompt.** This would place child-only bytes ahead of inherited messages. Appending it to the initial user task preserves the inherited prefix and keeps the parent id next to the task that needs it.
 
-**Install the child-scope deltas after the inherited history.** Rejected as unrepresentable: the system prompt and the tool schemas are request-head structures in every provider's wire format, so no ordering within them can place a child-only addition behind the message list.
+**Ignore an explicit child `toolFilter`.** Structural return tools previously bypassed the child allow-list. Rejected because a declared tool restriction must determine both schema visibility and guidance; hidden authority would make the model-facing roster inaccurate.
 
 ## Consequences
 
-- No shipped composition creates a continuable forked child; `subagent_fork` returns a result to its caller's turn, and `send_message` addresses only spawned children.
-- A forked child's request prefix stays byte-identical to its parent's unless the deployment configures `persona` or `toolFilter` on the fork delegation tool, so the token cost of seeding buys provider-side reuse again.
-- The fork provider's continuable path has no production caller and no assembled-composition coverage. It keeps its package-level tests, and the seam still accepts it, so a bundle or `--patch` overlay can reintroduce it with no code change and no warning.
-- `subagent_fork`'s model-visible schema changes: the continuable background wording is replaced by the one-shot task wording in the base bundle, and disappears entirely from the two examples. The affected keyless snapshot tool-schema sidecars are re-recorded in the same change.
-- The report obligation's reach narrows to spawned children in shipped deployments. Its default `next-step` scheduling, authority model, and coverage remain independent of fork composition.
+- Parent and continuable-fork child expose byte-identical ordered tool schemas when the delegation does not request a persona or tool filter.
+- The inherited Session seed precedes the child's initial task and return guidance.
+- The base and headless profiles keep one-shot fork, while selected CLI presets exercise continuable fork without a child-only request-head addition.
+- A child sends zero or more messages to its direct parent explicitly; its final answer is not implicitly copied. The manager-owned settlement notice remains unconditional and separate.
+- Keyless snapshots and package tests pin schema equality, inherited-history ordering, parent-id guidance, and child-to-parent delivery through the same `send_message` operation used in the other direction.
 
 ### Accepted risks
 
-The constraint lives in three configuration files and a code comment, not in a gate. A future bundle row or profile patch can set `backgroundMode: continuable` on a fork tool and silently reintroduce the prefix loss; nothing fails loud. That is the accepted cost of not encoding one roster's consequence into `tool-subagent`.
+Provider-side prefix reuse still depends on the selected provider and model and on the absence of explicit persona or tool-filter differences. The harness proves equality of its assembled request-head inputs, not a provider's cache behavior.

+ 19 - 27
.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md

@@ -1,49 +1,41 @@
-# Agent Note: fork 出的 child 保持 one-shot
+# Agent Note:Fork child 保留 parent 请求前缀
 
-Status: implemented
+状态:已实现
 
 [English](2026-08-10-fork-children-stay-one-shot.md) | 中文
 
 ## 问题
 
-fork 与 spawn 的唯一区别是 child 的 Session 会以 parent 已完成轮次的前缀作为初始内容(见 [subagent-fork-in-process](../../../../packages/subagent/subagent-fork-in-process/README.zh.md))。这份初始内容有实打实的 token 成本——继承的历史会在 child 的每次请求中重新发送——而它唯一确定的回报是提供方侧的前缀复用:在提供方与模型相同的前提下,起始字节与 parent 逐字节相同的 child 请求,无需为这段共享区间重新预填充。任何由 child 作用域添加在继承历史*之前*的内容都会消耗掉这份回报,因为复用在第一个不同字节处即告停止
+fork 与 spawn 的差异在于:fork 会用 parent 已完成轮次的前缀作为 child Session 的种子。该种子会消耗 token,其预期收益是提供方侧的前缀复用:使用相同提供方和模型时,如果 child 请求的开头字节与 parent 相同,共享区段就无需再次预填充。任何位于继承历史之前、仅属于 child 的系统提示词 section 或工具 schema 都会破坏这项收益
 
-作用域局部的 `report` 返回通道现在是此类添加中最大的一项,而自[report 义务](../feature/2026-08-06-continuable-child-report-obligation.zh.md)起它是两项而非一项增量:`report` 工具 schema,以及 `tool:report` 系统提示词 section。两者都位于请求头部——系统块与工具块先于所有消息——因此一个可继续的 fork child 会在第一条继承轮次之前就使复用失效,并重新预填充它当初 fork 就是为了复用的整份 transcript(文本记录)。这种组合付出了 fork 的复制成本却收不到它的收益,而 parent 手上仍握着一份 child 本可共享的可复用前缀
+先前的随附组合通过把 fork child 保持为 one-shot 来避开这种不匹配。该限制源于原来的 child-only 返回工具,并非可继续 fork 的固有属性
 
 ## 决策
 
-所有交付组合都从 [base bundle](../../../../packages/bundle/base/cordis.patch.yml)继承 fork 委派工具的 `backgroundMode: one-shot`。base bundle 保留 `run_in_background`,因为它也挂载了结算后台工作所需的 task 服务
+面向模型的 `send_message` 工具在组合中的每个 Agent 上全局注册。因此,可继续 fork child 获得与 parent 相同的工具名称、描述、schema 和顺序。其初始任务追加在继承的 Session 种子之后;当 child 可以看到该工具时,任务还会包含直接 parent id,以及使用 `send_message({ agent_id, message })` 返回结果的指引
 
-one-shot child——前台与后台皆然——经由 `SubagentRuntime.start()` 创建,该路径从不进入可继续的 activation setup 注册表,因此 `report` 与它的提示词 section 都不会被安装。于是一个 fork 出的 one-shot child 的系统提示词与工具 schema 与其 parent 相同,只差部署逐个委派工具主动选择的 `persona` 与 `toolFilter` 增量
+base 与 headless 组合保留 one-shot fork 作为其保守生命周期策略。`cordis`、`standard` 和 `ptc` CLI preset 可以把 fork 绑定为可继续生命周期,因为该绑定不再插入 child-only 请求头字段。`ForkInProcessProvider.prepareContinuable()` 与 `ctx.subagents.startContinuable()` 仍是这些 preset 使用的实现 seam
 
-`spawn` 保持 `backgroundMode: continuable`。对于 child 起步时本就没有继承前缀需要保护的那个提供方,可继续 child 与 report 义务随附行为不变,因此本决策没有让 report 通道付出任何代价
+逐字节相同的前缀复用受显式部署选择约束。配置 child persona 或 `toolFilter` 的 fork 委派仍可能改变请求头。尤其是过滤掉 `send_message` 时,child 会同时失去该 schema 与返回指引;运行时不会绕过显式 allow-list
 
-### 该限制在于组合,不在于代码
+## 考虑过的替代方案
 
-`ForkInProcessProvider.prepareContinuable` 仍然实现完好,`ctx.subagents.startContinuable()` 也仍接受 `fork`;改动的只有随附的 `cordis.yml` 行。`tool-subagent` 在挂载时同时知道提供方的 `inheritsParentContext` 与自身的 `backgroundMode`,因此一个加载期拒绝该组合的检查是可行的,而这里刻意不加:该组合并非普遍错误。它只在某个 child 作用域增量位于继承历史之前时才是错的,而产生该增量的包——[`dsh-tool-subagent-report`](../../../../packages/subagent/tool-subagent-report/README.zh.md)——是独立安装的,并且按其自身设计对 `tool-subagent` 不可见。一个不安装 report 包的部署可以在前缀完好的前提下运行可继续的 fork child。把某一份插件清单的后果写成委派工具的不变量,会让该工具断言它无法观察到的事实
+**让所有 fork 保持 one-shot。** 这能保留前缀,但 child-only schema 差异消失后,继续放弃持久且多轮的 fork child 已无必要
 
-重新开放的条件记录为 `prepareContinuable` 方法上的 `TODO(fork-continuable-prefix-reuse)` 标记——随附组合不调用这个方法——并由 issue #2124 跟踪:当 child 的系统提示词与工具 schema 能与其 parent 逐字节一致时,可继续 fork 即可重新开放
+**安装 child-only 返回别名。** 无需填写接收方的别名可以缩短 child 调用,但会在继承历史之前重新产生工具 schema 与提示词差异,并重复相邻 Agent 操作
 
-## 备选方案
+**把返回指令放进系统提示词。** 这会在继承消息之前加入 child-only 字节。将其追加到初始用户任务,可以保留继承前缀,并让 parent id 紧邻需要它的任务。
 
-**在挂载时拒绝 `inheritsParentContext` 与 `continuable` 的组合。** 一次响亮的加载期失败可以阻止悄然的重新引入,而配置改动做不到这一点。否决的原因是委派工具看不到 report 包,且在没有它时该组合是合法的;对于从不安装任何 child 作用域增量的部署,这个不变量是假的,而 `tool-subagent` 会去断言一件由插件清单拥有的事实。
-
-**干脆不挂载 fork 提供方。** 这是该限制更彻底的形式。否决的原因是前台 fork *正是*复用前缀的那种情形,且不受 report 通道影响,因此全面禁用会在不换来任何 one-shot 绑定尚未换来的东西的同时放弃该能力——并且随附组合将没有任何一个演练 session 初始内容。
-
-**照常随附可继续的 fork child 并接受这份损失。** 否决的原因是这份损失是全额而非边际的:复用在继承历史之前就已中断,于是 child 为一份自己复制过来、目的恰恰是不必付费的 transcript 付了全额预填充。想要一个没有继承上下文的长期 child 的部署,本来就有 `spawn`。
-
-**让 `report` 对每个 Agent 可见。** 全局注册会通过让 parent 与 child 拥有相同的 schema 与 section 来恢复逐字节相同的前缀。否决的原因是根 agent、one-shot child、远端 child 与无 agent 调用方都会宣告一件推导不出收件方的工具,而执行期拒绝会让 schema 可见性与权限彼此矛盾——这正是[report 工具 Agent Note](../feature/2026-07-30-continuable-subagent-report-tool.zh.md)已经定下的作用域局部决策。
-
-**把 child 作用域增量安装到继承历史之后。** 否决的原因是它无法表达:在每个提供方的协议格式中,系统提示词与工具 schema 都是请求头部结构,因此它们内部的任何排序都无法把仅属于 child 的添加放到消息列表之后。
+**忽略显式 child `toolFilter`。** 结构性返回工具过去会绕过 child allow-list。否决该方案,因为声明的工具限制必须同时决定 schema 可见性与指引;隐藏权限会让面向模型的工具清单失真。
 
 ## 后果
 
-- 没有任何随附组合会创建可继续的 fork child;`subagent_fork` 把结果返回给调用方的轮次,而 `send_message` 只寻址 spawn 出的 child
-- 除非部署在 fork 委派工具上配置了 `persona` 或 `toolFilter`,fork child 的请求前缀与其 parent 逐字节相同,因此初始内容的 token 成本重新换来了提供方侧的复用
-- fork 提供方的可继续路径没有生产调用方,也没有整体组装层面的覆盖。它保留自己的包内测试,seam 也仍然接受它,因此某个组合包或 `--patch` 覆盖层可以无需改动代码、也不会有任何警告地把它重新引入
-- `subagent_fork` 面向模型的 schema 发生变化:base 组合包中可继续的后台措辞被 one-shot 的 task 措辞取代,在两个示例中则完全消失。受影响的无密钥快照工具 schema 伴随文件在同一次改动中重新记录
-- 在随附部署中,report 义务的覆盖范围收窄到 spawn 出的 child。它的 `next-step` 默认调度、权限模型与覆盖仍独立于 fork 组合
+- 未请求 persona 或工具过滤时,parent 与可继续 fork child 暴露逐字节相同且顺序一致的工具 schema。
+- 继承的 Session 种子位于 child 初始任务及其返回指引之前。
+- base 与 headless profile 保持 one-shot fork;选定的 CLI preset 会在没有 child-only 请求头增量的前提下使用可继续 fork。
+- child 显式向直接 parent 发送零条或多条消息;最终回答不会被隐式复制。管理器负责的结算通知仍然无条件执行,并且与 Agent 消息分离。
+- keyless snapshot 与包测试固定 schema 相等性、继承历史顺序、parent-id 指引,以及通过同一个 `send_message` 操作完成的 child-to-parent 投递。
 
-### 已接受风险
+### 已接受风险
 
-该限制存在于三个配置文件与一处代码注释中,而不在门禁里。未来某个组合包行或 profile 补丁可以在 fork 工具上设置 `backgroundMode: continuable`,从而悄然重新引入前缀损失;没有任何东西会失败得很响亮。这就是不把某一份插件清单的后果写入 `tool-subagent` 所接受的代价
+提供方侧前缀复用仍取决于选定的提供方和模型,以及是否不存在显式 persona 或工具过滤差异。harness 证明的是自己组装出的请求头输入相等,而不是提供方的缓存行为

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.i18n.yaml

@@ -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-25-sparse-first-party-prompt-section-orders.md
-2026-08-25-sparse-first-party-prompt-section-orders.md: ffa2e6a4f602178007a6702938dfd71a2f85cbaa
-2026-08-25-sparse-first-party-prompt-section-orders.zh.md: d26ac18b075a2f0ebccccdbd072c7c066c2fe1b0
+2026-08-25-sparse-first-party-prompt-section-orders.md: 3cfdb58af3c576b3e46449a763db1c811073517e
+2026-08-25-sparse-first-party-prompt-section-orders.zh.md: 31cbb98eab9eb786ac9d854b76df4c18efb88563

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.md

@@ -24,7 +24,7 @@ The allocation preserves the established first-party sequence except for two del
 | Work modes | `plan:policy` 500, `team:policy` 600 |
 | Invocation prelude | `tools:ptc-only` 800, `context:file-reference` 900 |
 | Local tools | `tool:bash` 1000, `tool:pwsh` 1010, `tool:read` 1100, `tool:write` 1200, `tool:edit` 1300, `tool:glob` 1400, `tool:grep` 1500, `tool:jobs` 1600, `tool:pty` 1700 |
-| Higher-level tools | `tool:web_search` 2000, `tool:web_fetch` 2100, `tool:lsp` 2200, `tool:session-query` 2300, `tool:goal` 2400, `tool:cordis` 2500, `tool:workflow` 2600, `tool:ralph` 2700, continuable-subagent guidance 2800, `tool:report` 2900 |
+| Higher-level tools | `tool:web_search` 2000, `tool:web_fetch` 2100, `tool:lsp` 2200, `tool:session-query` 2300, `tool:goal` 2400, `tool:cordis` 2500, `tool:workflow` 2600, `tool:ralph` 2700, continuable-subagent guidance 2800 |
 | Generated protocol | `tools:sdk` 5000 |
 | Final-output obligations | deliverable file references 9000, `tool:structured_output` 9900 |
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.zh.md

@@ -24,7 +24,7 @@ Status: implemented
 | 工作模式 | `plan:policy` 500、`team:policy` 600 |
 | 调用前置说明 | `tools:ptc-only` 800、`context:file-reference` 900 |
 | 本地工具 | `tool:bash` 1000、`tool:pwsh` 1010、`tool:read` 1100、`tool:write` 1200、`tool:edit` 1300、`tool:glob` 1400、`tool:grep` 1500、`tool:jobs` 1600、`tool:pty` 1700 |
-| 高层工具 | `tool:web_search` 2000、`tool:web_fetch` 2100、`tool:lsp` 2200、`tool:session-query` 2300、`tool:goal` 2400、`tool:cordis` 2500、`tool:workflow` 2600、`tool:ralph` 2700、可继续运行的 subagent 指导 2800、`tool:report` 2900 |
+| 高层工具 | `tool:web_search` 2000、`tool:web_fetch` 2100、`tool:lsp` 2200、`tool:session-query` 2300、`tool:goal` 2400、`tool:cordis` 2500、`tool:workflow` 2600、`tool:ralph` 2700、可继续运行的 subagent 指导 2800 |
 | 生成协议 | `tools:sdk` 5000 |
 | 最终输出义务 | 可交付文件引用 9000、`tool:structured_output` 9900 |
 

+ 3 - 3
.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.i18n.yaml → .agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.i18n.yaml

@@ -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-07-30-continuable-subagent-report-tool.md
-2026-07-30-continuable-subagent-report-tool.md: 07d17f18f318a86070d9b8612512fa3c2a3815e2
-2026-07-30-continuable-subagent-report-tool.zh.md: 9cbf76a9f3f50d9b1a84b573621cd6fc2a1611c4
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md
+2026-08-27-adjacent-agent-steer-messaging.md: 31c5a4fc8c0e807ff8ab869561d0acd32eccc060
+2026-08-27-adjacent-agent-steer-messaging.zh.md: c49da882a5beb7a7e8379d15a0628fb5c617e040

+ 82 - 0
.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md

@@ -0,0 +1,82 @@
+# Agent Note: Adjacent Agents share one Steer send_message operation
+
+Status: implemented
+
+English | [中文](2026-08-27-adjacent-agent-steer-messaging.zh.md)
+
+## Problem
+
+Continuable Agents originally used direction-specific model controls. A parent called `send_message({ subagent_id, message })`, which delegated to a FIFO `followup` service operation. A child instead received a child-scoped `report({ output })` tool, a `tool:report` system-prompt section, and deployment-selected quiet or waking delivery. The tools described one adjacent-Agent operation through different schemas, service paths, provenance, and scheduling.
+
+A continuable child owns its own Session, so its parent does not automatically receive the child's transcript, tool output, or reasoning. The return path must therefore remain explicit and repeatable: a child may send progress before it finishes, remain available after sending, or fail before it can cooperate. Turning every final assistant message into an implicit result would conflate turn completion with model-selected communication and would not cover abnormal endings.
+
+The child-only tool and system-prompt section also preceded every inherited fork turn. They made a continuable fork child's request head differ from its parent's before the history that fork exists to reuse, forcing the provider to prefill the entire copied transcript again.
+
+## Decision
+
+`SubagentRuntime.sendMessage(sender, targetId, content, { signal })` is the only public model-authored message operation. The continuation manager accepts only the exact live sender and a target on one adjacent edge:
+
+- parent to direct continuable child, authorized by the child's durable `SessionHeader.parentSession`;
+- resident continuable child to its exact live direct parent, authorized by the child's Activation.
+
+Siblings, self-targets, ancestors beyond one edge, stale Agent objects, unknown targets, and one-shot children are not alternate routes. The operation has no caller-supplied source, delivery mode, offline parent mailbox, or provider dispatch.
+
+Every accepted message uses `Agent.steer()`. A running target receives it at the nearest step boundary; an idle target starts a turn. An absent direct child is cold-resumed through the existing continuation lifecycle before the same Steer delivery. The manager retains waking-send accounting so a continuation-managed target cannot settle between synchronous inbox insertion and driver admission.
+
+Every direction uses one durable source. The service derives `senderSessionId` from the authorized Agent and frames the model-visible content as `Agent <sender-id> sent a message:`, so attribution cannot diverge from authority.
+
+```ts
+import type { SessionId } from '@deepseek-ai/dsh-session'
+
+interface AgentMessageSource {
+  readonly kind: 'agent-message'
+  readonly form: 'relay'
+  readonly senderSessionId: SessionId
+}
+```
+
+### One model tool and one return instruction
+
+The globally registered model tool is direction-neutral and has one fixed schema:
+
+```ts
+interface SendMessageInput {
+  readonly agent_id: string
+  readonly message: string
+}
+```
+
+Parents and children inherit the same definition in the same registry order. The standard definition carries a process-stable internal identity that a scoped same-name tool does not satisfy. A child `toolFilter` may explicitly remove the inherited tool, and a scoped replacement may provide different semantics; neither case receives the standard call instruction. When the standard tool remains visible, the continuation manager appends the JSON-encoded direct parent id and the instruction to send one self-contained result before finishing, plus earlier actionable findings, to the child's initial user task. For a fork child this task follows the inherited completed-turn prefix; no child-only system-prompt section or tool schema precedes that prefix.
+
+The instruction is guidance, not settlement enforcement. Sending does not end the child's turn, zero or several calls remain mechanically valid, and the runtime never rejects a child for staying silent. The manager-owned `subagent-settled` notice remains unconditional and separately attributed because it records how an Activation ended and preserves terminal output when the child cannot cooperate.
+
+Human browser prompts are not model-authored Agent messages. The remote prompt path keeps a private Queue delivery so each human prompt remains a distinct turn. Interrupt behavior and settlement delivery remain independent.
+
+### Complete removal and reintroduction condition
+
+The standalone `@deepseek-ai/dsh-tool-subagent-report` package, `report` schema, `tool:report` prompt section, `reportDelivery` configuration, report-specific message source, catalog entries, composition rows, and supported-behavior snapshots are absent. The unified tool gives up the recipient-free child shortcut and the old ability for a structural return tool to survive an explicit child allow-list. Those capabilities return only if a concrete use case requires semantics that an adjacent `agent_id` and fixed Steer cannot express; reintroducing them requires a distinct model operation and prefix-cost evidence, not an alias over `sendMessage()`.
+
+## Alternatives considered
+
+**Keep `followup` and add child-to-parent routing.** The name promises a later turn and inherits `Agent.followup()` semantics. It would obscure the chosen nearest-step behavior and preserve a parent-centric name for a direction-neutral capability.
+
+**Keep a recipient-free `report` wrapper over `sendMessage()`.** This preserves a convenient child shortcut and lets a scope-local registration survive global tool filtering. It loses because the separate schema and prompt duplicate one operation, make parent and child request heads differ, and let equivalent directions drift again.
+
+**Make `report` global.** Roots, one-shot children, remote children, and agentless callers cannot derive a report recipient. Advertising it globally would make schema visibility disagree with authority, while `send_message` already makes the recipient explicit.
+
+**Turn every child final message into an implicit send.** A long-lived child may have nothing useful to send in one turn and several findings in another. Automatic delivery would merge model-authored communication with the runtime's settlement account and could not replace the unconditional notice on errors, cancellation, or token exhaustion.
+
+**Rely only on the tool description.** A tool description helps after the model considers that tool; the failure mode is a child that believes it is finished without considering any return call. Initial-task guidance reaches that decision without changing the inherited system or tool prefix.
+
+**Keep quiet delivery as deployment policy.** A quiet model-authored message can be accepted while an idle target never reads it. Fixed Steer gives both directions one delivery meaning and preserves accepted order with later settlement notices.
+
+## Consequences
+
+- Model consumers expose one `send_message({ agent_id, message })` definition to parents and children, with no model-selected Queue versus Steer parameter.
+- The continuation manager remains the sole owner of adjacency authorization, residency, cold resume, waking admission, and teardown races.
+- Accepted messages may extend a running target's current turn; messages waiting together share next-step FIFO ordering.
+- Caller cancellation owns work only until inbox acceptance and does not retract an accepted message or dispose the target.
+- The initial task carries JSON-encoded dynamic parent addressing after a fork prefix, while the request-head system prompt and tool ordering remain reusable.
+- Human prompts, settlement notices, QueueDock, and the base bundle's one-shot fork policy remain separate decisions.
+
+This decision consolidates and removes the fully superseded report-tool and child-report-obligation records. It supersedes the `followup` naming choice in [Intent-named subagent continuation operations](../simplification/2026-07-27-intent-named-subagent-continuation-operations.md) and retains the accepted-order guarantee in [Child Agent messages precede their settlement notices](../bug-fix/2026-08-17-subagent-message-settlement-ordering.md).

+ 82 - 0
.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md

@@ -0,0 +1,82 @@
+# Agent Note: 相邻 Agent 共享一个 Steer send_message 操作
+
+Status: implemented
+
+[English](2026-08-27-adjacent-agent-steer-messaging.md) | 中文
+
+## 问题
+
+可继续 Agent 最初使用方向专属的模型控制。parent 调用 `send_message({ subagent_id, message })`,委托给 FIFO `followup` 服务操作。child 则获得 child 作用域的 `report({ output })` 工具、`tool:report` 系统提示词 section,以及由部署选择的静默或唤醒投递。两个工具用不同 schema、服务路径、来源与调度描述同一个相邻 Agent 操作。
+
+可继续 child 拥有自己的 Session,因此 parent 不会自动收到 child 的 transcript(文本记录)、工具输出或推理。返回路径必须保持显式且可重复:child 可以在结束前发送进度、发送后仍保持可用,也可能在来得及配合前失败。把每条最终 assistant 消息变成隐式结果会混淆轮次完成与模型选择的通信,而且无法覆盖异常结束。
+
+child 专属工具与系统提示词 section 还位于每个继承 fork 轮次之前。它们让可继续 fork child 的请求头在 fork 旨在复用的历史之前就与 parent 不同,迫使提供方重新预填充整份复制 transcript。
+
+## 决策
+
+`SubagentRuntime.sendMessage(sender, targetId, content, { signal })` 是唯一公开的模型编写消息操作。继续执行管理器只接受确切在线 sender 与一条相邻边上的目标:
+
+- parent 到直接可继续 child,由 child 的持久化 `SessionHeader.parentSession` 授权;
+- 驻留的可继续 child 到其确切在线直接 parent,由 child 的 Activation 授权。
+
+sibling、自身目标、超过一条边的 ancestor、陈旧 Agent 对象、未知目标与一次性 child 都不是替代路由。该操作没有调用方提供的 source、投递模式、离线 parent mailbox 或提供方分发。
+
+每条被接受的消息都使用 `Agent.steer()`。运行中目标在最近 step 边界接收消息;空闲目标启动轮次。缺失的直接 child 会先通过现有继续执行生命周期冷恢复,再接受同一 Steer 投递。管理器保留唤醒发送记账,因此受继续执行管理的目标不会在同步 inbox 插入与 driver 准入之间结算。
+
+两个方向使用同一种持久来源。服务从已授权 Agent 推导 `senderSessionId`,并把模型可见内容组装为 `Agent <sender-id> sent a message:`,因此来源信息不会偏离权限。
+
+```ts
+import type { SessionId } from '@deepseek-ai/dsh-session'
+
+interface AgentMessageSource {
+  readonly kind: 'agent-message'
+  readonly form: 'relay'
+  readonly senderSessionId: SessionId
+}
+```
+
+### 一个模型工具与一条返回指令
+
+全局注册的模型工具与方向无关,并使用一个固定 schema:
+
+```ts
+interface SendMessageInput {
+  readonly agent_id: string
+  readonly message: string
+}
+```
+
+parent 与 child 以相同注册表顺序继承相同定义。标准定义携带进程稳定的内部身份,同名的作用域工具不满足该身份。child `toolFilter` 可以显式移除继承的工具,作用域替代工具也可以提供不同语义;两种情况都不会收到标准调用指令。当标准工具仍可见时,继续执行管理器会把经过 JSON 编码的直接 parent id、结束前发送一份自包含结果的指令,以及更早发送可操作发现的指令追加到 child 初始用户任务。对 fork child 而言,该任务位于继承的已完成轮次前缀之后;没有 child 专属系统提示词 section 或工具 schema 位于此前缀之前。
+
+该指令是指导,不是结算强制。发送不会结束 child 轮次,机制仍允许零次或多次调用,runtime 绝不会因 child 保持沉默而拒绝它。由管理器负责的 `subagent-settled` 通知仍无条件发送并采用独立来源,因为它记录 Activation 如何结束,并在 child 无法配合时保留终态输出。
+
+浏览器中的人类提示不是模型编写的 Agent 消息。远程提示路径保留私有 Queue 投递,使每条人类提示保持为独立轮次。中断行为与结算投递保持独立。
+
+### 完整移除与重新引入条件
+
+独立的 `@deepseek-ai/dsh-tool-subagent-report` 包、`report` schema、`tool:report` 提示词 section、`reportDelivery` 配置、report 专属消息来源、目录项、组合行和受支持行为快照均已不存在。统一工具放弃了无需接收方的 child 快捷方式,也放弃了让结构性返回工具绕过显式 child allow-list 的旧能力。只有具体用例需要相邻 `agent_id` 与固定 Steer 无法表达的语义时,这些能力才会重新出现;重新引入需要独立的模型操作与前缀成本证据,而非 `sendMessage()` 之上的别名。
+
+## 考虑过的替代方案
+
+**保留 `followup` 并添加 child 到 parent 路由。** 该名称承诺后续轮次并继承 `Agent.followup()` 语义。它会掩盖选定的最近 step 行为,并为方向无关能力保留以 parent 为中心的名称。
+
+**保留 `sendMessage()` 之上无需接收方的 `report` 包装层。** 这会保留便利的 child 快捷方式,并让作用域局部注册绕过全局工具过滤。它落选是因为独立 schema 与提示词重复一个操作、使 parent 与 child 请求头不同,并允许等价方向再次漂移。
+
+**让 `report` 全局可见。** 根 Agent、一次性 child、远程 child 与无 Agent 调用方无法推导 report 接收方。全局宣传它会让 schema 可见性与权限不一致,而 `send_message` 已显式给出接收方。
+
+**把每条 child 最终消息变成隐式发送。** 长期运行的 child 可能在某个轮次没有值得发送的内容,在另一个轮次却有多条发现。自动投递会混合模型编写通信与 runtime 结算说明,而且无法替代错误、取消或 token 耗尽时的无条件通知。
+
+**只依赖工具描述。** 工具描述会在模型考虑该工具后提供帮助;失败模式是 child 认为自己已经完成而根本没有考虑返回调用。初始任务指导能触及该决策,又不会改变继承的系统或工具前缀。
+
+**保留静默投递作为部署策略。** 静默的模型编写消息可能被接受,但空闲目标永远不会读取它。固定 Steer 为两个方向提供一种投递含义,并保持与后续结算通知的接受顺序。
+
+## 后果
+
+- 模型 Consumer 向 parent 与 child 公开一个 `send_message({ agent_id, message })` 定义,不提供模型选择的 Queue 与 Steer 参数。
+- 继续执行管理器仍是相邻关系授权、驻留、冷恢复、唤醒准入与拆卸竞态的唯一所有者。
+- 被接受的消息可以延长运行中目标的当前轮次;一起等待的消息共享 next-step FIFO 顺序。
+- 调用方取消只在 inbox 接受前掌管工作,不会撤回已接受消息或 dispose(资源释放)目标。
+- 初始任务在 fork 前缀之后携带经过 JSON 编码的动态 parent 地址,而请求头系统提示词与工具顺序保持可复用。
+- 人类提示、结算通知、QueueDock 与 base bundle 的一次性 fork 策略仍是独立决策。
+
+本决策合并并删除了已完全被取代的 report 工具与 child report 义务记录。它取代[按意图命名的 subagent 继续执行操作](../simplification/2026-07-27-intent-named-subagent-continuation-operations.zh.md)中的 `followup` 命名选择,并保留[Child Agent 消息先于其结算通知](../bug-fix/2026-08-17-subagent-message-settlement-ordering.zh.md)中的接受顺序保证。

+ 6 - 0
.agents/notes/implemented/architecture/2026-08-31-session-sequence-and-log-offset-brands.i18n.yaml

@@ -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-31-session-sequence-and-log-offset-brands.md
+2026-08-31-session-sequence-and-log-offset-brands.md: 0057b2b6c95391abbb4ec2464ee58b54e2fe24f2
+2026-08-31-session-sequence-and-log-offset-brands.zh.md: f51ad5953cab34294a53db6e3c91884292497faf

+ 45 - 0
.agents/notes/implemented/architecture/2026-08-31-session-sequence-and-log-offset-brands.md

@@ -0,0 +1,45 @@
+# Agent Note: Distinguish Session event identities from log offsets
+
+Status: implemented
+
+English | [中文](2026-08-31-session-sequence-and-log-offset-brands.zh.md)
+
+## Problem
+
+Session positions used one structural `number` type for two incompatible meanings. An event reference names an existing row, while a prefix length, next append position, or read cut names a gap and may equal the event count. The compiler therefore accepted an offset where an event identity was required and could not expose a missed sequence-field migration.
+
+`SessionHeader.seedLength` also mixed a v0 storage coordinate into metadata used by body-free readers. Listing needs to know whether a Session has fork lineage, but only a reader that holds the event body can interpret the exact inherited prefix length.
+
+## Decision
+
+`@deepseek-ai/dsh-brand` exports the erased numeric primitive `BrandedNumber<B>` and the runtime-identity helper `brandNumber()`. `@deepseek-ai/dsh-session` owns two validated brands: `SessionSeq` names one existing event and `SessionLogOffset` names a log gap, prefix length, or read offset. `SessionSeqCursor = SessionSeq | -1` represents an inclusive watermark before or after the first event, and `OptionalSessionSeq = SessionSeq | null` represents an event identity whose absence is data.
+
+`SessionEvent.seq`, surface replacement endpoints, provenance, and owner payload fields that identify Session events use `SessionSeq`. `Session.seq`, `Session.firstLiveSeq`, `Session.inheritedEventCount`, body-read offsets, and inherited prefix cuts use `SessionLogOffset`. Arithmetic returns an ordinary number and re-enters either domain through its validating constructor.
+
+The logical `SessionHeader` carries `isSeeded: boolean` and no numeric seed cut. Body-bearing storage values and observations carry `inheritedEventCount` beside the header; `Session.ownEvents()` and `Session.isOwnSeq()` hide the comparison from ordinary consumers. A seeded constructor requires an explicit seed and exact cut, including an empty seed with cut zero, because constructor input may contain child-owned setup events after the inherited prefix.
+
+The v0 JSONL header remains byte-compatible: absent `seedLength` decodes to `isSeeded: false` with cut zero, while present zero or nonzero values decode to `isSeeded: true` with the exact cut. Header-only listing translates only the presence bit. API, SDK, DeepSeek, telemetry, query-row, and JSON representations continue to carry ordinary numbers; their owning adapters validate and brand values when they enter same-process domain code.
+
+## Admission and ownership
+
+Domain constructors reject negative, fractional, non-finite, and unsafe integer values. Parsers validate a raw number once and retain the parsed object where the brand does not require a runtime wrapper. A compile-time brand does not discover unknown numeric fields in an external event; a format migration still needs an exhaustive owner disposition and must refuse schemas it cannot safely rewrite.
+
+`session/end-seed` remains a lifecycle marker, not the source of the inherited cut. Every constructor restore appends or retains that marker, including unseeded replay, so projections and cold readers receive `inheritedEventCount` explicitly instead of scanning the log.
+
+## Alternatives considered
+
+**Keep every position as `number`.** Rejected because event identities, counts, and cursors cross package and persistence seams frequently enough that accidental interchange is a migration risk, not a local arithmetic convenience.
+
+**Use one branded Session position for identities and offsets.** Rejected because it would again permit `eventCount` or `fromSeq` where an existing event is required and would force the `-1` and `null` sentinels into unrelated operations.
+
+**Derive the inherited cut from `session/end-seed`.** Rejected because the marker records constructor lifecycle, not only fork lineage, and a constructor seed may contain child-owned events after the inherited prefix.
+
+## Consequences
+
+Sequence-bearing code now states whether a number identifies an event or a gap. Header-only readers receive stable lineage metadata without opening the body, while persistence, projection, query, and authorization paths retain the exact cut they need. The on-disk v0 format and public numeric wires do not change.
+
+The cost is explicit conversion at durable and wire parsers and a separate exact-cut field on body-bearing observations. Projection-cache identity includes the lineage bit and exact cut, so its disposable storage domain advances and older rows rebuild on demand; body-free readers skip seeded cache hints when they do not hold the cut. Turn numbers, step numbers, message-list indexes, workflow member ordinals, token counts, and unrelated numeric domains remain plain numbers because they do not identify Session events.
+
+## Testing
+
+Type assertions pin that `SessionSeq` and `SessionLogOffset` are not interchangeable. Runtime suites cover constructor validation, mixed inherited and child-owned seeds, empty seeds, `ownEvents()` and `isOwnSeq()`, v0 JSONL absent/zero/nonzero headers in plain and Zstandard encodings, header-only listing, cold prepare and reopen, query and projection cuts, and unchanged numeric wire values.

+ 45 - 0
.agents/notes/implemented/architecture/2026-08-31-session-sequence-and-log-offset-brands.zh.md

@@ -0,0 +1,45 @@
+# Agent Note: 区分 Session 事件身份与日志偏移
+
+Status: implemented
+
+[English](2026-08-31-session-sequence-and-log-offset-brands.md) | 中文
+
+## Problem
+
+Session 位置曾用同一个结构化 `number` 类型表达两种不兼容的含义。事件引用指向一条已存在的记录,而前缀长度、下一追加位置或读取切点指向记录间隙,并且可以等于事件总数。因此,编译器会在需要事件身份的位置接受偏移,也无法暴露迁移时漏改的序号字段。
+
+`SessionHeader.seedLength` 还把 v0 存储坐标混入了无须读取正文的 metadata consumer。列表只需要知道 Session 是否有 fork lineage,只有同时持有事件正文的读取方才能解释精确的继承前缀长度。
+
+## Decision
+
+`@deepseek-ai/dsh-brand` 导出编译后消失的数值原语 `BrandedNumber<B>` 与运行时保持原值的 helper `brandNumber()`。`@deepseek-ai/dsh-session` 拥有两个经验证的 brand:`SessionSeq` 指明一条已存在事件,`SessionLogOffset` 指明日志间隙、前缀长度或读取偏移。`SessionSeqCursor = SessionSeq | -1` 表达首条事件之前或之后的闭区间 watermark,`OptionalSessionSeq = SessionSeq | null` 表达允许以缺失为数据的事件身份。
+
+`SessionEvent.seq`、surface 替换端点、provenance 以及 owner payload 中指向 Session 事件的字段使用 `SessionSeq`。`Session.seq`、`Session.firstLiveSeq`、`Session.inheritedEventCount`、带正文读取的偏移与继承前缀切点使用 `SessionLogOffset`。算术结果恢复为普通 number,并通过对应的验证构造函数重新进入任一领域。
+
+逻辑 `SessionHeader` 携带 `isSeeded: boolean`,不携带数值 seed cut。包含正文的存储值和 observation 在 header 旁携带 `inheritedEventCount`;`Session.ownEvents()` 与 `Session.isOwnSeq()` 向普通 consumer 隐藏比较。seeded constructor 必须显式提供 seed 与精确 cut,包括 cut 为零的空 seed,因为 constructor 输入可能在继承前缀之后还包含 child-owned setup event。
+
+v0 JSONL header 保持字节兼容:缺少 `seedLength` 时解码为 `isSeeded: false` 和零 cut,存在零或非零值时解码为 `isSeeded: true` 和对应精确 cut。仅 header 的 listing 只转换字段是否存在。API、SDK、DeepSeek、telemetry、query row 与 JSON 表示继续携带普通 number;由它们各自的 adapter 在值进入同进程 domain code 时完成验证与 brand。
+
+## Admission and ownership
+
+Domain constructor 拒绝负数、小数、非有限值与非安全整数。parser 验证原始 number 一次;brand 不需要运行时 wrapper 时,保留原解析对象。编译期 brand 无法发现外部事件里的未知数值字段;格式迁移仍须获得穷尽的 owner disposition,并拒绝无法安全改写的 schema。
+
+`session/end-seed` 仍是 lifecycle marker,不是继承 cut 的来源。每次 constructor restore 都会追加或保留该 marker,unseeded replay 也一样,因此 projection 与 cold reader 会显式接收 `inheritedEventCount`,而不是扫描日志。
+
+## Alternatives considered
+
+**继续让所有位置都使用 `number`。** 拒绝,因为事件身份、计数与 cursor 已频繁跨越 package 与 persistence seam,意外混用是迁移风险,而不是局部算术便利。
+
+**用同一个 branded Session position 表达身份和偏移。** 拒绝,因为这样仍会在需要已存在事件的位置接受 `eventCount` 或 `fromSeq`,还会迫使 `-1` 与 `null` sentinel 进入互不相关的操作。
+
+**从 `session/end-seed` 推导继承 cut。** 拒绝,因为该 marker 记录 constructor lifecycle,并不只记录 fork lineage,而且 constructor seed 可以在继承前缀之后包含 child-owned event。
+
+## Consequences
+
+携带序号的代码会明确说明一个 number 指向事件还是间隙。仅 header 的 reader 无须打开正文即可取得稳定 lineage metadata,persistence、projection、query 与 authorization path 则保留所需的精确 cut。磁盘 v0 格式与公共数值 wire 不变。
+
+代价是在 durable 与 wire parser 处显式转换,并让含正文 observation 携带独立的精确 cut 字段。Projection cache identity 包含 lineage bit 与精确 cut,因此其可丢弃的 storage domain 会推进,旧 row 按需重建;不持有 cut 的仅 header reader 会跳过 seeded cache hint。turn number、step number、message-list index、workflow member ordinal、token count 与无关数值领域保持普通 number,因为它们不指向 Session 事件。
+
+## Testing
+
+类型断言钉住 `SessionSeq` 与 `SessionLogOffset` 不可互换。运行时 suite 覆盖 constructor 验证、混合继承与 child-owned seed、空 seed、`ownEvents()` 与 `isOwnSeq()`、plain 与 Zstandard 编码中的 v0 JSONL 缺失/零/非零 header、仅 header 的 listing、cold prepare 与 reopen、query 和 projection cut,以及不变的数值 wire 值。

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-10-child-agents-join-their-parent-preset.i18n.yaml

@@ -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-08-10-child-agents-join-their-parent-preset.md
-2026-08-10-child-agents-join-their-parent-preset.md: 5a3d0d1c5e2b492b10a0274204c10e244c37196f
-2026-08-10-child-agents-join-their-parent-preset.zh.md: ce5d69404d4f8b6a9879668d2ed0cb1c9d133a40
+2026-08-10-child-agents-join-their-parent-preset.md: 5321d99bad35cf26ebc0e7842dc68c2775bc384b
+2026-08-10-child-agents-join-their-parent-preset.zh.md: f0d5b59c3787ef69c2c983da0c8ec0ff4a43e6e7

+ 3 - 3
.agents/notes/implemented/bug-fix/2026-08-10-child-agents-join-their-parent-preset.md

@@ -22,7 +22,7 @@ This is a bind, not a mount, and both differences are load-bearing. The child ge
 
 `dsh-subagent` reaches the roster through `ctx.get('agentPresets')` with a type-only import and an optional peer dependency — the documented opportunistic-consumption pattern it already uses for `sandboxPolicy` and `approval`.
 
-Giving the child its parent's tools exposed a second defect the same agent-plane move introduced: `ToolRuntime` exempted SCOPED registrations from a restriction and filtered only the global layer, so once every model-facing row became an ancestor contribution, a child's `toolFilter` stopped constraining anything — and, with the global layer empty, `restrict()` rejected every name it was given as unknown, failing the child outright. The exempt set is the tools a scope registers ITSELF, not the tools that happen to live in the global layer; reading it the second way held only while those two sets coincided. `view()` now filters everything a scope inherits — the global layer and every ancestor layer — and exempts only its own. The own-layer exemption is load-bearing rather than incidental: the delegation runtime registers a child's `report` and structured-output tools into the child's own layer, and a filter naming the capabilities the child may use must not strip the machinery it answers through.
+Giving the child its parent's tools exposed a second defect the same agent-plane move introduced: `ToolRuntime` exempted SCOPED registrations from a restriction and filtered only the global layer, so once every model-facing row became an ancestor contribution, a child's `toolFilter` stopped constraining anything — and, with the global layer empty, `restrict()` rejected every name it was given as unknown, failing the child outright. The exempt set is the tools a scope registers ITSELF, not the tools that happen to live in the global layer; reading it the second way held only while those two sets coincided. `view()` now filters everything a scope inherits — the global layer and every ancestor layer — and exempts only its own. The own-layer exemption is load-bearing rather than incidental: the delegation runtime registers a child's structured-output tool into the child's own layer, and a filter naming the capabilities the child may use must not strip the machinery it answers through.
 
 ## Alternatives considered
 
@@ -30,11 +30,11 @@ Giving the child its parent's tools exposed a second defect the same agent-plane
 
 **Bind the child's key to the PARENT's key rather than to the standing mount.** Rejected because it changes what a child inherits: the parent's own scope layer carries its per-agent restrictions, which would then intersect into every descendant, and a child outliving its parent would hang off a disposed agent's key. Joining the standing mount gives the child its parent's composition and nothing else.
 
-**Extend the continuable activation setup registry to cover one-shot children.** Rejected because that registry's contribution type is synchronous `(childCtx) => () => void` with per-installation revocation, modelling deployment capabilities that come and go, while a preset join is a one-time bind with no revocation of its own. Widening it would have made the omission possible again for any driver that skipped the registry.
+**Introduce one shared child-setup registry for both drivers.** Rejected because a synchronous, revocable contribution models deployment capabilities that come and go, while a preset join is a one-time bind with no revocation of its own. Routing composition through an optional registry would make the omission possible again for any driver that skipped it.
 
 **Let `dsh-subagent` import `resolveSessionPreset` and mount by the resolved id.** Rejected because it makes the preset roster a hard module edge for a package that must work without one, and it lands back on the remount semantics above.
 
-**Filter every layer on the chain, including the scope's own.** Rejected because it makes a per-child capability filter delete that child's reporting and structured-output tools, which the delegation runtime registers into the child's own layer — an `allow` naming the capabilities a child may use would leave it unable to answer at all.
+**Filter every layer on the chain, including the scope's own.** Rejected because it makes a per-child capability filter delete that child's structured-output tool, which the delegation runtime registers into the child's own layer — an `allow` naming the capabilities a child may use would leave it unable to produce the requested result.
 
 **Leave the durable header alone and fix only the live join.** Rejected because the live child and the same child read cold would then disagree about which composition produced its history — the same class of defect, moved rather than fixed.
 

+ 3 - 3
.agents/notes/implemented/bug-fix/2026-08-10-child-agents-join-their-parent-preset.zh.md

@@ -22,7 +22,7 @@ Status: implemented
 
 `dsh-subagent` 以类型级导入加可选 peer 依赖的方式,通过 `ctx.get('agentPresets')` 触达 roster——这正是它对 `sandboxPolicy` 与 `approval` 已在使用的、有明确文档的机会性消费模式。
 
-把父方的工具交给子 agent 之后,暴露出同一次 agent 平面搬迁引入的第二个缺陷:`ToolRuntime` 把**作用域级**注册排除在限制之外、只过滤全局层,因此当所有面向模型的行都变成祖先贡献之后,子 agent 的 `toolFilter` 就不再约束任何东西——而且全局层为空时,`restrict()` 会把收到的每个名字都判为未知并直接让子 agent 创建失败。豁免集合应当是作用域**自己注册**的工具,而不是恰好位于全局层的工具;后一种读法只在这两个集合重合时才成立。`view()` 现在过滤作用域继承来的一切——全局层与每个祖先层——只豁免它自己那层。这条自身层豁免是承重的而非顺带的:委派运行时把子 agent 的 `report` 与结构化输出工具注册进子 agent 自己那层,而一个只点名子 agent 可用能力的过滤器绝不能把它回报所依赖的机制一并剥掉。
+把父方的工具交给子 agent 之后,暴露出同一次 agent 平面搬迁引入的第二个缺陷:`ToolRuntime` 把**作用域级**注册排除在限制之外、只过滤全局层,因此当所有面向模型的行都变成祖先贡献之后,子 agent 的 `toolFilter` 就不再约束任何东西——而且全局层为空时,`restrict()` 会把收到的每个名字都判为未知并直接让子 agent 创建失败。豁免集合应当是作用域**自己注册**的工具,而不是恰好位于全局层的工具;后一种读法只在这两个集合重合时才成立。`view()` 现在过滤作用域继承来的一切——全局层与每个祖先层——只豁免它自己那层。这条自身层豁免是承重的而非顺带的:委派运行时把子 agent 的结构化输出工具注册进子 agent 自己那层,而一个只点名子 agent 可用能力的过滤器绝不能把它产出请求结果所依赖的机制一并剥掉。
 
 ## 考虑过的替代方案
 
@@ -30,11 +30,11 @@ Status: implemented
 
 **把子 agent 的 key 绑到**父方的** key 而不是常驻挂载上。** 否决,因为这改变了子 agent 继承的内容:父方自己的 scope 层携带其逐 agent 限制,那些限制会就此与每个后代求交,而活得比父方久的子 agent 会挂在一个已 dispose 的 agent key 上。加入常驻挂载给到子 agent 的是父方的组装,仅此而已。
 
-**扩展可继续 activation setup 注册表以覆盖一次性子 agent。** 否决,因为该注册表的贡献类型是同步的 `(childCtx) => () => void` 并带有逐次安装的撤销,建模的是会来会走的部署能力,而 preset 加入是一次性认父、自身没有撤销可言。扩展它反而会让任何绕过该注册表的驱动重新具备遗漏的可能。
+**为两个驱动引入一份共享 child setup 注册表。** 否决,因为同步且可撤销的贡献建模的是会来会走的部署能力,而 preset 加入是一次性认父、自身没有撤销可言。让组合经由可选注册表完成,反而会让任何绕过它的驱动重新具备遗漏的可能。
 
 **让 `dsh-subagent` 导入 `resolveSessionPreset` 并按解析出的 id 挂载。** 否决,因为这会给一个必须在没有 roster 时也能工作的包引入硬模块边,而且最终仍落回上述的重新挂载语义。
 
-**过滤链上的每一层,包括作用域自身那层。** 否决,因为那会让逐子 agent 的能力过滤器把该子 agent 的回报与结构化输出工具一并删掉——它们由委派运行时注册进子 agent 自己那层——于是一个点名"子 agent 可用哪些能力"的 `allow` 会让它彻底无法回报
+**过滤链上的每一层,包括作用域自身那层。** 否决,因为那会让逐子 agent 的能力过滤器删掉该子 agent 的结构化输出工具——它由委派运行时注册进子 agent 自己那层——于是一个点名“子 agent 可用哪些能力”的 `allow` 会让它无法产出所请求的结果
 
 **只修活着的加入,不动持久化 header。** 否决,因为那样活着的子 agent 与冷读同一个子 agent 会对"哪份组装产出了这段历史"给出不同答案——同一类缺陷,只是被搬了个地方而不是被修掉。
 

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-13-feedback-note-editor-popover.i18n.yaml

@@ -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-08-13-feedback-note-editor-popover.md
-2026-08-13-feedback-note-editor-popover.md: 42c08e39cfb054db689503e23306c5049a97b6cb
-2026-08-13-feedback-note-editor-popover.zh.md: 553029f8a42dae42a38e909d716b41e2c6dd252e
+2026-08-13-feedback-note-editor-popover.md: 8f51f090cc24292ad02d96fefb1f45bc05df08c9
+2026-08-13-feedback-note-editor-popover.zh.md: e64e95ac6599be2d7c343b4432415f0e99c6ef9b

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-08-13-feedback-note-editor-popover.md

@@ -18,7 +18,7 @@ The note editor does not enter the row's flex layout at all. It is a popover: a
 
 **The action strip.** The like/dislike buttons and the note trigger stay in the row, unchanged. The trigger is a plain button (`aria-haspopup="dialog"`, `aria-expanded` while open) that shows "Add a note" before a note exists and the note text afterward.
 
-**The popover.** While open, the panel contains the textarea plus Save and Cancel, and any note-save failure, as `role="dialog"` with a title distinct from the textarea's own label so both are addressable by name. It opens beneath the trigger (4px gap), clamps to 12px from the viewport edges, auto-focuses the textarea, and closes on Escape or an outside pointer-down. Closing returns focus to the trigger only when the panel was really open, never on the initial mount (a freshly rendered rated message must not pull focus into its action row). A rating action during an open editor closes the panel. The four undefined tokens are replaced with the ones the theme actually defines, matching the primitives' precedent: `border-l2` and `bg-layer-1` for the input, `button-primary-fill` with `label-primary-foreground` plus a `button-primary-hover` state for Save; the panel surface reuses the Menu card recipe (`--dsw-specific-menu`, `--dsw-shadow-lv3`, inverted hairline `--dsw-alias-border-inverted`, `border-radius: 12px`).
+**The popover.** While open, the panel contains the textarea plus Save and Cancel, and any note-save failure, as `role="dialog"` with a title distinct from the textarea's own label so both are addressable by name. It opens beneath the trigger (4px gap), clamps to 12px from the viewport edges, auto-focuses the textarea, and closes on Escape or an outside pointer-down. Closing returns focus to the trigger only when the panel was really open, never on the initial mount (a freshly rendered rated message must not pull focus into its action row). A rating action during an open editor closes the panel. The four undefined tokens are replaced with the ones the theme actually defines, matching the primitives' precedent: `border-l2` and `bg-layer-1` for the input, `button-primary-fill` with `label-primary-foreground` plus a `button-primary-hover` state for Save; the panel surface reuses the Menu card surface recipe (`--dsw-specific-menu`, the `--dsw-elevation-prominent` shadow with the `--dsw-alias-border-l1` stroke rebind and `border: 0`) at `border-radius: 12px`.
 
 **Failure surfaces split by where the human is looking.** A rating or list-load failure shows beside the buttons in the row, legible whether or not the popover is open. A note-save failure shows inside the popover, next to Save/Cancel, and the panel stays open so the draft survives to be corrected.
 

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-08-13-feedback-note-editor-popover.zh.md

@@ -18,7 +18,7 @@ Status: implemented
 
 **操作条。** 点赞/点踩按钮与备注触发按钮保持原样留在行内。触发按钮是普通 `button`(`aria-haspopup="dialog"`,打开时 `aria-expanded`),在没有备注时显示「补充说明」,已有备注时显示备注文本。
 
-**浮层。** 打开时,面板内含 textarea、Save 与 Cancel,以及任何备注保存失败提示,作为 `role="dialog"`,其标题与 textarea 自身的标签不同,以便两者都能按名称寻址。它在触发按钮下方打开(4px 间距),钳制到距视口边缘 12px,自动聚焦 textarea,并在 Escape 或外部 pointer-down 时关闭。关闭时仅当面板确实曾经打开才把焦点还给触发按钮,绝不会在初始挂载时(新渲染出的一条已评分消息不得把焦点拉进其操作条)。编辑器打开时进行评分操作会关闭面板。四个未定义 token 换成主题确实定义的那些,与 primitives 的既有做法一致:输入框用 `border-l2` 与 `bg-layer-1`,Save 用 `button-primary-fill` 配 `label-primary-foreground` 并加 `button-primary-hover` 状态;面板表面复用 Menu 卡片的配方(`--dsw-specific-menu`、`--dsw-shadow-lv3`、反色发丝线 `--dsw-alias-border-inverted`、`border-radius: 12px`)
+**浮层。** 打开时,面板内含 textarea、Save 与 Cancel,以及任何备注保存失败提示,作为 `role="dialog"`,其标题与 textarea 自身的标签不同,以便两者都能按名称寻址。它在触发按钮下方打开(4px 间距),钳制到距视口边缘 12px,自动聚焦 textarea,并在 Escape 或外部 pointer-down 时关闭。关闭时仅当面板确实曾经打开才把焦点还给触发按钮,绝不会在初始挂载时(新渲染出的一条已评分消息不得把焦点拉进其操作条)。编辑器打开时进行评分操作会关闭面板。四个未定义 token 换成主题确实定义的那些,与 primitives 的既有做法一致:输入框用 `border-l2` 与 `bg-layer-1`,Save 用 `button-primary-fill` 配 `label-primary-foreground` 并加 `button-primary-hover` 状态;面板表面复用 Menu 卡片的表面配方(`--dsw-specific-menu`、`--dsw-elevation-prominent` 投影配 `--dsw-alias-border-l1` 描边重绑与 `border: 0`),圆角取 `border-radius: 12px`
 
 **失败提示按人的视线所落之处拆分。** 评分或列表加载失败显示在按钮旁的图标行里,无论浮层是否打开都清晰可读。备注保存失败显示在浮层内、Save/Cancel 旁,且面板保持打开,以便草稿留存待修正。
 

+ 6 - 0
.agents/notes/implemented/bug-fix/2026-08-17-subagent-message-settlement-ordering.i18n.yaml

@@ -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/bug-fix/2026-08-17-subagent-message-settlement-ordering.md
+2026-08-17-subagent-message-settlement-ordering.md: cdc996643c84c5f50a3bd1836e82645660dc8c57
+2026-08-17-subagent-message-settlement-ordering.zh.md: 1143da1560e4969dcc4f6a0c6d5ca18060b56191

+ 44 - 0
.agents/notes/implemented/bug-fix/2026-08-17-subagent-message-settlement-ordering.md

@@ -0,0 +1,44 @@
+# Agent Note: Child Agent messages precede their settlement notices
+
+Status: implemented
+
+English | [中文](2026-08-17-subagent-message-settlement-ordering.zh.md)
+
+## Problem
+
+A continuable child can send selected content and later produce an unconditional manager-authored settlement notice. If those two messages enter queues with different claim priority, the later settlement notice can reach the parent model before the earlier child message. The first step of a turn claims the complete `next-step` batch before one `next-turn` message, so mixing a FIFO later-turn send with a next-step settlement reverses causal order. [Issue #2600](https://github.com/deepseek-harness/deepseek-harness/issues/2600) records the defect.
+
+The child instruction says to send a finding whenever it changes what the parent should do next. Deferring that message to a later turn contradicts its scheduling meaning and separates causally ordered messages across queues with different claim priority.
+
+## Decision
+
+Every model-authored adjacent-Agent message uses fixed Steer delivery through `SubagentRuntime.sendMessage()`. A running parent reads the child message at its nearest safe step boundary and an idle parent starts a turn. There is no quiet or next-turn model delivery option.
+
+The continuation manager retains `sendWaking()` and `admitWaking()` around messages delivered to resident continuable parents. Their purpose is waking-send admission accounting: the receiving Activation remains live between synchronous inbox insertion and the microtask that observes the wake.
+
+### Ordering across parent states
+
+A running parent receives an accepted child message and the child's later settlement notice in the same `next-step` FIFO. If the parent becomes idle before settlement arrives, it has already claimed the child message; settlement may then open a later turn without reversing observed order.
+
+During parent maintenance, the child message occupies `next-step` and latches a wake, while settlement may occupy `next-turn` because maintenance reports idle status. The initial claim still takes next-step input before the queued turn. Waking input submitted after cancellation follows the core Agent's cancellation convergence rather than bypassing it.
+
+### Verification
+
+The control-tool suite holds a parent inside an active model request, submits child messages, settles the child, and verifies sender identity, Steer admission, FIFO batching, and preservation after settlement. Continuation coverage pins waking admission accounting for a resident continuable parent and keeps the runtime-owned settlement source distinct from `agent-message`.
+
+The keyless continuable-subagent snapshot uses the shipped fixed delivery. Its child-visible tool schema is the same as the parent's, and the accepted child message precedes the later settlement notice without a scheduling overlay.
+
+## Alternatives considered
+
+**Offer quiet delivery.** A quiet message can remain unread after an idle parent parks. It also gives equivalent model-authored messages different liveness semantics and reopens deployment-dependent ordering.
+
+**Offer next-turn delivery.** A later next-step settlement notice can still overtake it. Preserving message-before-settlement would require a cross-queue ordering barrier, and no current model operation requires later-turn isolation strongly enough to own that mechanism.
+
+**Move settlement notices to `next-turn`.** Settlement batching uses the next-step queue so several children finishing together cost one parent step instead of one turn each. Moving settlement would increase latency and model work to retain an unnecessary message scheduling mode.
+
+## Consequences
+
+- A child message may extend an open parent turn. It never interrupts the active model request or tool execution; the agent loop admits it only at a step boundary.
+- Messages accepted together share one next-step batch, preserving FIFO order and limiting turn amplification.
+- Model callers cannot choose a delivery mode, so ordering and wake behavior do not vary by deployment or call.
+- A child-to-parent send still requires the direct parent to remain live; the service provides no durable parent mailbox.

+ 44 - 0
.agents/notes/implemented/bug-fix/2026-08-17-subagent-message-settlement-ordering.zh.md

@@ -0,0 +1,44 @@
+# Agent Note: Child Agent 消息先于其结算通知
+
+Status: implemented
+
+[English](2026-08-17-subagent-message-settlement-ordering.md) | 中文
+
+## 问题
+
+可继续 child 可以发送选中内容,之后还会产生一条由管理器编写且无条件投递的结算通知。如果这两条消息进入领取优先级不同的队列,较晚的结算通知可能先于较早的 child 消息到达 parent 模型。一个轮次的第一个 step 会先领取完整 `next-step` 批次,再领取一条 `next-turn` 消息,因此混用 FIFO 后续轮次发送与 next-step 结算会颠倒因果顺序。[Issue #2600](https://github.com/deepseek-harness/deepseek-harness/issues/2600)记录了该缺陷。
+
+child 指令要求在发现会改变 parent 下一步动作时发送该发现。把这条消息推迟到后续轮次既违背其调度含义,也会把具有因果顺序的消息拆到领取优先级不同的队列。
+
+## 决策
+
+每条模型编写的相邻 Agent 消息都通过 `SubagentRuntime.sendMessage()` 使用固定 Steer 投递。运行中的 parent 在最近安全 step 边界读取 child 消息,空闲 parent 则启动一个轮次。模型没有静默或 next-turn 投递选项。
+
+继续执行管理器在投递到驻留可继续 parent 的消息周围保留 `sendWaking()` 与 `admitWaking()`。它们负责唤醒发送准入记账:接收方 Activation 会在同步 inbox 插入与观察到唤醒的微任务之间保持在线。
+
+### 不同 parent 状态下的顺序
+
+运行中的 parent 在同一条 `next-step` FIFO 中接收已接受的 child 消息与该 child 随后的结算通知。如果 parent 在结算到达前变为空闲,它已经领取 child 消息;结算随后可以开启后续轮次,而不会颠倒观察顺序。
+
+parent 处于 maintenance 时,child 消息占用 `next-step` 并锁存一次唤醒,而结算可能因 maintenance 报告空闲状态而占用 `next-turn`。初始领取仍会先取 next-step 输入,再取排队轮次。取消后提交的唤醒输入遵循核心 Agent 的取消收敛,而不会绕过它。
+
+### 验证
+
+控制工具测试套件让 parent 保持在活跃模型请求中,提交 child 消息、结算 child,并验证 sender 身份、Steer 准入、FIFO 批处理与结算后保留。继续执行覆盖固定驻留可继续 parent 的唤醒准入记账,并让 runtime 所有的结算来源与 `agent-message` 保持不同。
+
+无密钥可继续 subagent 快照使用随附的固定投递。其 child 可见工具 schema 与 parent 相同,且已接受的 child 消息先于之后的结算通知,无需调度 overlay。
+
+## 考虑过的替代方案
+
+**提供静默投递。** 空闲 parent 停驻后可能永远不读取静默消息。它还会让等价的模型编写消息具有不同存活语义,并重新引入依赖部署的顺序。
+
+**提供 next-turn 投递。** 较晚的 next-step 结算通知仍可能越过它。保持消息先于结算需要跨队列顺序屏障,而当前没有模型操作对后续轮次隔离的需求强到足以承担该机制。
+
+**把结算通知移到 `next-turn`。** 结算批处理使用 next-step 队列,使多个 child 同时结束只消耗 parent 的一个 step,而不是每个 child 一个轮次。移动结算会为了保留不必要的消息调度模式而增加延迟与模型工作。
+
+## 后果
+
+- child 消息可以延长开放的 parent 轮次。它绝不会中断活跃模型请求或工具执行;agent loop 只在 step 边界接纳它。
+- 一起被接受的消息共享一个 next-step 批次,保持 FIFO 顺序并限制轮次放大。
+- 模型调用方不能选择投递模式,因此顺序与唤醒行为不会随部署或调用而变化。
+- child 到 parent 的发送仍要求直接 parent 保持在线;服务不提供持久 parent mailbox。

+ 0 - 44
.agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.md

@@ -1,44 +0,0 @@
-# Agent Note: Subagent reports precede their settlement notices
-
-Status: implemented
-
-English | [中文](2026-08-17-subagent-report-settlement-ordering.zh.md)
-
-## Problem
-
-A continuable child can explicitly report selected content and later produce an unconditional manager-authored settlement notice. Report delivery used `Agent.followup()` and entered the parent's `next-turn` queue, while settlement delivery to a running parent used `Agent.steer()` and entered `next-step`. The first step of a turn claims the complete `next-step` batch before one `next-turn` message, so the later settlement notice could reach the model before the earlier report. The assembled report scenario required `reportDelivery: quiet` to avoid that nondeterministic interleaving. [Issue #2600](https://github.com/deepseek-harness/deepseek-harness/issues/2600) records the defect.
-
-The report tool tells a child to report whenever a finding changes what its parent should do next. Deferring that message to a later turn contradicted the tool's scheduling meaning and separated causally ordered messages across queues with different claim priority.
-
-## Decision
-
-`SubagentReportDelivery` is `'quiet' | 'next-step'`, and `next-step` is the default. Next-step delivery calls `parent.steer()`, so a running parent reads the report at its nearest safe step boundary and an idle parent starts a turn. Quiet delivery continues to call `parent.inject()` and enters the same queue without waking an idle parent.
-
-The continuation manager retains `sendWaking()` and `admitWaking()` around next-step reports delivered to resident continuable parents. Their purpose is waking-send admission accounting, independent of whether the message targets a step or a turn: the receiving Activation remains live between synchronous inbox insertion and the microtask that observes the wake.
-
-### Ordering across parent states
-
-A running parent receives an accepted report and the child's later settlement notice in the same `next-step` FIFO. If the parent becomes idle before settlement arrives, it has already claimed the report; settlement may then open a later turn without reversing the observed order.
-
-During parent maintenance, the report occupies `next-step` and latches a wake, while settlement may occupy `next-turn` because maintenance reports idle status. The initial claim still takes next-step input before the queued turn. Waking input submitted after cancellation is redirected by `Agent.send()` to `next-turn`, so report and settlement follow the core agent's cancellation convergence rather than bypassing it.
-
-### Verification
-
-The report package holds a parent inside an active model request, submits a child report, settles that child, and asserts the pending parent batch is ordered `subagent-report`, then `subagent-settled`, with no queued later turn. Separate coverage pins repeated reports as one FIFO next-step batch, idle-parent wakeup, and waking admission accounting for a continuable parent.
-
-The assembled ACP report scenario uses the shipped default. Its scheduling fence keeps the child behind the parent's delegation turn and holds the parent in maintenance until settlement follows the report. The report latches the wake while the settlement notice queues a turn; when maintenance ends, the parent claims next-step input before next-turn input and observes both notices in causal order without a quiet-delivery overlay.
-
-## Alternatives considered
-
-**Keep the `wakeup` name but change its implementation to `steer()`.** The existing public description defined `wakeup` as one later parent turn. Reusing the value for a different inbox target would leave configuration unable to state the behavior it selects. The pre-release configuration instead names `next-step` directly.
-
-**Expose `quiet | next-step | next-turn`.** A next-turn report still permits a later next-step settlement notice to overtake it. Preserving report-before-settlement would require a cross-queue ordering barrier, and no current deployment requires next-turn isolation strongly enough to own that mechanism.
-
-**Move settlement notices to `next-turn`.** Settlement batching deliberately uses the next-step queue so several children finishing together cost one parent step instead of one turn each. Moving settlement would increase latency and model work to retain a report scheduling mode with no current consumer.
-
-## Consequences
-
-- A report may extend an open parent turn. It never interrupts the active model request or tool execution; the agent loop admits it only at a step boundary.
-- Reports accepted together share one next-step batch, preserving FIFO order and reducing the turn amplification of the former one-turn-per-report behavior.
-- The `wakeup` configuration value is rejected rather than retained as an alias. This repository has no external pre-release compatibility promise for Cordis configuration.
-- `quiet` remains the deployment escape for reports that must not wake a parked parent, with the existing risk that no model reads them until another waking input arrives.

+ 0 - 44
.agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.zh.md

@@ -1,44 +0,0 @@
-# Agent Note: Subagent report 先于其结算通知
-
-Status: implemented
-
-[English](2026-08-17-subagent-report-settlement-ordering.md) | 中文
-
-## 问题
-
-可继续 child 可以显式上报选中内容,之后还会产生一条由管理器撰写且无条件投递的结算通知。报告投递曾使用 `Agent.followup()` 并进入 parent 的 `next-turn` 队列,而面向运行中 parent 的结算投递使用 `Agent.steer()` 并进入 `next-step`。一个轮次的第一个 step 会先领取完整 `next-step` 批次,再领取一条 `next-turn` 消息,因此较晚的结算通知可能先于较早的报告到达模型。整体组装的报告场景必须使用 `reportDelivery: quiet`,才能避开这种不确定交错。[Issue #2600](https://github.com/deepseek-harness/deepseek-harness/issues/2600)记录了该缺陷。
-
-report 工具要求 child 在发现会改变 parent 下一步动作的信息时上报。把这条消息推迟到后续轮次,既违背了工具的调度含义,也让具有因果顺序的消息分散到领取优先级不同的队列中。
-
-## 决策
-
-`SubagentReportDelivery` 为 `'quiet' | 'next-step'`,默认值为 `next-step`。Next-step 投递调用 `parent.steer()`,因此运行中的 parent 会在最近的安全 step 边界读取报告,空闲 parent 则会启动一个轮次。静默投递继续调用 `parent.inject()`,进入同一队列但不唤醒空闲 parent。
-
-对于投递到驻留可继续 parent 的 next-step 报告,继续执行管理器会保留外围的 `sendWaking()` 与 `admitWaking()`。它们负责唤醒发送的准入记账,与消息面向 step 还是 turn 无关:接收方 Activation 在同步插入 inbox 与观察该唤醒的微任务之间保持在线。
-
-### 不同 parent 状态下的顺序
-
-运行中的 parent 会在同一个 `next-step` FIFO 中接收已接受的报告和该 child 稍后的结算通知。若 parent 在结算到达前变为空闲,它已经领取了报告;结算随后可以开启一个更晚的轮次,而不会反转观察顺序。
-
-parent 处于 maintenance 时,报告占据 `next-step` 并锁存一次唤醒,而结算可能因为 maintenance 呈现空闲状态而占据 `next-turn`。首次领取仍会先取 next-step 输入,再取排队轮次。取消后提交的唤醒输入会由 `Agent.send()` 重定向到 `next-turn`,因此报告和结算会遵循核心 agent 的取消收敛,而不会绕过它。
-
-### 验证
-
-report 包把 parent 保持在一个活动模型请求中,提交 child 报告,再让该 child 结算,并断言等待中的 parent 批次按 `subagent-report`、`subagent-settled` 排序,且没有排队的后续轮次。独立覆盖还会固定重复报告形成一个 FIFO next-step 批次、空闲 parent 唤醒,以及可继续 parent 的唤醒准入记账。
-
-整体组装的 ACP 报告场景使用随附默认值。调度围栏让 child 等到 parent 的委派轮次之后,并让 parent 保持 maintenance,直至结算跟在报告之后到达。报告会锁存唤醒,结算通知则排入后续轮次;maintenance 结束时,parent 先领取 next-step 输入、再领取 next-turn 输入,因此无需静默投递 overlay 也能按因果顺序观察两条通知。
-
-## 备选方案
-
-**保留 `wakeup` 名称,但把其实现改为 `steer()`。** 既有公开描述把 `wakeup` 定义为一个后续 parent 轮次。让该值复用于不同的 inbox 目标,会使配置无法准确说明自己选择的行为。预发布配置因此直接使用 `next-step` 名称。
-
-**暴露 `quiet | next-step | next-turn`。** Next-turn 报告仍可能被稍后的 next-step 结算通知超越。要保住报告先于结算,需要跨队列顺序屏障;当前没有任何部署对 next-turn 隔离的需求强到足以承担该机制。
-
-**把结算通知移到 `next-turn`。** 结算批处理刻意使用 next-step 队列,使多个一起结束的 child 只花费 parent 的一个 step,而不是各自一个轮次。移动结算会增加延迟和模型工作量,只为保留一个没有当前消费方的报告调度模式。
-
-## 后果
-
-- 报告可能延长已打开的 parent 轮次。它绝不会打断活动模型请求或工具执行;agent loop 只会在 step 边界准入它。
-- 一起接受的报告会共享一个 next-step 批次,保持 FIFO 顺序,并减少原先每份报告各占一个轮次所造成的轮次放大。
-- `wakeup` 配置值会被拒绝,而不是保留为别名。本仓库对预发布 Cordis 配置不作外部兼容承诺。
-- 对于不得唤醒停驻 parent 的报告,`quiet` 仍是部署退路,同时保留既有风险:在另一条唤醒输入到达之前,没有模型会读取这些报告。

+ 2 - 2
.agents/notes/implemented/feature/2026-06-30-session-store-fork-api.i18n.yaml

@@ -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-30-session-store-fork-api.md
-2026-06-30-session-store-fork-api.md: ff7617f4bb306926a7b0782751ae82f6ef0c6371
-2026-06-30-session-store-fork-api.zh.md: ecef7ba2985321677b29d3fc7692a8dcb7afb2b6
+2026-06-30-session-store-fork-api.md: a2d169a36a8a0b624abef98377d644d713e5d64c
+2026-06-30-session-store-fork-api.zh.md: 5ebf05e677f2d3676f860220b1cf9ff960565402

+ 3 - 3
.agents/notes/implemented/feature/2026-06-30-session-store-fork-api.md

@@ -20,11 +20,11 @@ The store exposes one operation:
 type SessionForkSource = Session | SessionId
 
 class SessionStore extends Service {
-  fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
+  fork(source: SessionForkSource, boundary?: SessionSeq, childSessionId?: SessionId): Session
 }
 ```
 
-`boundary` is the inclusive source event `seq` to copy through. When omitted, it defaults to the source session's current last event; on an empty source, omitted `boundary` creates an empty child. Fork-specific validation checks that the requested boundary exists and that the selected prefix's latest turn boundary is not an unmatched `turn/start`. The selected prefix may therefore end at `turn/end` or at a later standalone event, then is deep-cloned into the child seed. The child inherits the source session's `cwd`, stamps `parentSession` to the source id, and sets `seedLength` to the copied prefix length. When `childSessionId` is omitted, `SessionStore` generates one using its existing id policy.
+`boundary` is the branded inclusive source event `seq` to copy through. When omitted, it defaults to the source session's current last event; on an empty source, omitted `boundary` creates an empty child. Fork-specific validation checks that the requested boundary exists and that the selected prefix's latest turn boundary is not an unmatched `turn/start`. The selected prefix may therefore end at `turn/end` or at a later standalone event, then is deep-cloned into the child seed. The child inherits the source session's `cwd`, stamps `parentSession` to the source id, sets logical `isSeeded: true`, and supplies the copied prefix length separately as `inheritedEventCount`. When `childSessionId` is omitted, `SessionStore` generates one using its existing id policy.
 
 An empty prefix is forkable; any non-empty boundary must be a safe existing sequence outside an open turn. Typed errors distinguish missing sources, stale objects, duplicate child ids, invalid boundaries, and prefixes ending during execution. Broader log validation and crash repair remain with their existing owners.
 
@@ -44,6 +44,6 @@ The Host creates the child through the agent registry with the selected seed and
 
 ## Consequences
 
-The public API stays small and discoverable: live session branching is part of `ctx.sessions`, next to `create({ seed })`, rather than a standalone service or a two-step helper pair. Persistence continues to work through existing `session/created` and `session/flush` behavior: a forked child starts life with seeded events, so existing backends persist that seed once and preserve `parentSession` / `seedLength` in the header.
+The public API stays small and discoverable: live session branching is part of `ctx.sessions`, next to `create({ seed })`, rather than a standalone service or a two-step helper pair. Persistence continues to work through existing `session/created` and `session/flush` behavior: a forked child starts life with seeded events, so the JSONL backend persists that seed once and preserves logical `parentSession` / `isSeeded` plus the separate exact cut (encoded as v0 physical `seedLength`).
 
 This decision excludes ACP `session/fork`, unloaded persisted-session forking, model-facing tools, and subagent refactors. If a future ACP method is added, it should advertise the capability only after it has protocol and snapshot coverage; this Agent Note adds no ACP wire behavior, so no ACP snapshot is required. Fork-child replay remains covered by the existing [seed-boundary testing Agent Note](../testing/2026-06-22-fork-child-replay-seed-boundary.md); focused store, Host, carrier, and client tests pin the boundary and reconciliation contracts, while the real Chromium scenario pins the assembled message action and lineage tree.

+ 3 - 3
.agents/notes/implemented/feature/2026-06-30-session-store-fork-api.zh.md

@@ -20,11 +20,11 @@ store 暴露一个操作:
 type SessionForkSource = Session | SessionId
 
 class SessionStore extends Service {
-  fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
+  fork(source: SessionForkSource, boundary?: SessionSeq, childSessionId?: SessionId): Session
 }
 ```
 
-`boundary` 是要复制到的源事件 `seq`(含该序号)。省略时默认为源会话当前的最后一个事件;对空源会话省略 `boundary` 则创建一个空的子会话。fork 特有的校验会检查请求的边界存在,并确认所选前缀最近的轮次边界不是未匹配的 `turn/start`。因此,所选前缀可以结束于 `turn/end` 或更晚的独立事件,随后被深拷贝到子会话的种子中。子会话继承源会话的 `cwd`,将 `parentSession` 设为源会话 id,并将 `seedLength` 设为已复制前缀的长度。省略 `childSessionId` 时,`SessionStore` 使用其现有的 id 策略生成一个。
+`boundary` 是要复制到的品牌化源事件 `seq`(含该序号)。省略时默认为源会话当前的最后一个事件;对空源会话省略 `boundary` 则创建一个空的子会话。fork 特有的校验会检查请求的边界存在,并确认所选前缀最近的轮次边界不是未匹配的 `turn/start`。因此,所选前缀可以结束于 `turn/end` 或更晚的独立事件,随后被深拷贝到子会话的种子中。子会话继承源会话的 `cwd`,将 `parentSession` 设为源会话 id,设置 logical `isSeeded: true`,并把复制前缀的长度单独作为 `inheritedEventCount` 传入。省略 `childSessionId` 时,`SessionStore` 使用其现有的 id 策略生成一个。
 
 空前缀可以被 fork;任何非空边界都必须是位于开放轮次之外且安全、已存在的序号。类型化的错误区分源缺失、对象陈旧、子 id 重复、边界无效和前缀结束于执行过程中等情况。更广泛的日志校验与崩溃恢复仍由其现有的负责方处理。
 
@@ -44,6 +44,6 @@ Host 通过 agent(智能体)注册表,以选定的种子和谱系创建子
 
 ## 后果
 
-公开 API 保持精简且易于发现:活跃会话分支是 `ctx.sessions` 的一部分,紧邻 `create({ seed })`,而非一个独立服务或一对两步辅助函数。持久化继续通过现有的 `session/created` 和 `session/flush` 行为运作:fork 出的子会话创建时便带有种子事件,因此现有后端只需持久化该种子一次,并在 header 中保存 `parentSession`/`seedLength`
+公开 API 保持精简且易于发现:活跃会话分支是 `ctx.sessions` 的一部分,紧邻 `create({ seed })`,而非一个独立服务或一对两步辅助函数。持久化继续通过现有的 `session/created` 和 `session/flush` 行为运作:fork 出的子会话创建时便带有种子事件,因此 JSONL 后端只需持久化该种子一次,并保留 logical `parentSession`/`isSeeded` 与单独的精确 cut(编码为 v0 物理 `seedLength`)
 
 本决策排除 ACP(Agent Client Protocol)`session/fork`、对未加载的已持久化会话执行 fork、面向模型的工具,以及 subagent 重构。如果未来添加 ACP 方法,应在具备协议与快照覆盖后才声明支持该能力;本 Agent Note 不添加任何 ACP 协议行为,因此不需要 ACP 快照。fork 子会话的回放仍由现有的[种子边界测试 Agent Note](../testing/2026-06-22-fork-child-replay-seed-boundary.zh.md)覆盖;store、Host、载体与客户端的专项测试固定边界和对账约定,真实 Chromium 场景则固定组装后的消息操作与谱系树。

+ 2 - 2
.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.i18n.yaml

@@ -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-21-continuable-background-subagents.md
-2026-07-21-continuable-background-subagents.md: b2a3a8c53db5ae2860ed5cc6edccadfcd417e7fa
-2026-07-21-continuable-background-subagents.zh.md: 24cc09e621731cbb54f4d081d232f0598220d418
+2026-07-21-continuable-background-subagents.md: 24ab64879fa18f5de9952af2dba321e2cae17477
+2026-07-21-continuable-background-subagents.zh.md: 0cf06f7fe30d0953bb6e7f1a4c96527fa181805a

+ 3 - 3
.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md

@@ -57,7 +57,7 @@ The continuation manager does not serialize two callers that race a stopped chil
 
 ### Model-facing `send_message`
 
-The model receives one `send_message(subagent_id, message)` tool backed by `SubagentRuntime.followup()`, matching the intent verb on `Agent`. The service operation owns steer-or-resume orchestration and is distinct from the run's `SubagentRun.steer?()`, which only delivers to an already active run. The tool performs no lifecycle routing of its own. It attributes the follow-up as `{ kind: 'coordinator', senderSessionId: parent.id }` and forwards `{ source, signal }`; the service requires both facts in one options object. The source crosses both live steering and cold resume, while cancellation owns only a pending live-delivery wait because a cold-resume Task returns immediately and owns its later cancellation. The child model still receives ordinary user-role content, while the durable source prevents model-generated follow-ups from being classified as direct human input. A human adapter instead supplies `{ kind: 'user' }` and its interaction signal. The tool lives in the separately loaded `@deepseek-ai/dsh-tool-subagent-control` package so provider-bound `@deepseek-ai/dsh-tool-subagent` instances can continue registering distinct delegation tools for spawn, fork, or ACP without registering duplicate global control tools.
+The model receives one global `send_message(agent_id, message)` tool backed by `SubagentRuntime.sendMessage()`. The exact live sender may name only its direct parent or direct continuable child; the service owns adjacency checks, cold resume, fixed Steer scheduling, and durable `{ kind: 'agent-message', senderSessionId }` attribution. Cancellation owns work only until inbox acceptance. A human adapter remains separate because browser-authored input carries user provenance and request identity rather than Agent authority. The tool lives in the separately loaded `@deepseek-ai/dsh-tool-subagent-control` package so provider-bound `@deepseek-ai/dsh-tool-subagent` instances can continue registering distinct delegation tools for spawn, fork, or ACP without registering duplicate global control tools.
 
 - If the child has a running Task and live-steering capability, the service calls `run.steer(message, source)` and returns the existing Job id; it creates no Task of its own.
 - If the child has no running Task, `send_message` creates a fresh Task, cold-resumes the durable session with the message, and returns the new Job id.
@@ -71,9 +71,9 @@ Human input uses the same `followup` operation. The UI may display the child tra
 
 ### Durable child handle and cold resume
 
-The continuation manager snapshots every descriptor input with the seam's `snapshotSubagentDescriptor()` (built on [`snapshotJsonValue`](../../../../packages/util/values/src/index.ts)) before Task creation, matching the detached lossless-JSON boundary already used by Agent messages. A child-scoped setup contribution — a prepended one-shot `agent/prompt-submit` listener installed by the in-process driver — appends one model-hidden `subagent/descriptor` event before downstream prompt admission can block or throw. Allowed admission opens the initial child turn afterward; rejected admission leaves the descriptor as a pre-turn log-only fact, and the activation's final required checkpoint persists it. The event carries no `surfaceOp`, remains outside model history, and survives when compaction replaces surface history. A known child id is resumable only when loading that child session yields a supported descriptor in the child's own suffix (after `seedLength`, so a fork seed cannot leak an ancestor's descriptor) and its header identifies the caller as the direct parent.
+The continuation manager snapshots every descriptor input with the seam's `snapshotSubagentDescriptor()` (built on [`snapshotJsonValue`](../../../../packages/util/values/src/index.ts)) before Task creation, matching the detached lossless-JSON boundary already used by Agent messages. A child-scoped setup contribution — a prepended one-shot `agent/prompt-submit` listener installed by the in-process driver — appends one model-hidden `subagent/descriptor` event before downstream prompt admission can block or throw. Allowed admission opens the initial child turn afterward; rejected admission leaves the descriptor as a pre-turn log-only fact, and the activation's final required checkpoint persists it. The event carries no `surfaceOp`, remains outside model history, and survives when compaction replaces surface history. A known child id is resumable only when loading that child session yields a supported descriptor at or after its exact `inheritedEventCount`, so a fork seed cannot leak an ancestor's descriptor, and its header identifies the caller as the direct parent.
 
-The continuable arm of the versioned descriptor (`SUBAGENT_DESCRIPTOR_VERSION` in [descriptor.ts](../../../../packages/subagent/subagent/src/descriptor.ts)) carries `mode: 'continuable'`, the subagent provider name, resolved child `agentOptions.provider` and `agentOptions.model`, and optional `persona` and `toolFilter`. It does not snapshot the merge-extensible `AgentOptions` object: unrelated extension values cannot make continuation fail merely because they are not JSON. It deliberately omits `subagentDepth`; cold resume relies on the persisted header's `delegationDepth` rather than reconstructing depth from the descriptor. `outputSchema` belongs to one activation's result contract rather than durable child composition. The child header remains authoritative for the child id, `cwd`, `parentSession`, `seedLength`, and `delegationDepth`, while the persisted child transcript owns the fork seed and subsequent history. [`delegationDepthOf()`](../../../../packages/subagent/subagent/src/index.ts) takes the maximum of header and runtime values, so reconstructed runtime options may deepen the persisted value but never lower it and a resumed child cannot regain a top-level delegation budget.
+The continuable arm of the versioned descriptor (`SUBAGENT_DESCRIPTOR_VERSION` in [descriptor.ts](../../../../packages/subagent/subagent/src/descriptor.ts)) carries `mode: 'continuable'`, the subagent provider name, resolved child `agentOptions.provider` and `agentOptions.model`, and optional `persona` and `toolFilter`. It does not snapshot the merge-extensible `AgentOptions` object: unrelated extension values cannot make continuation fail merely because they are not JSON. It deliberately omits `subagentDepth`; cold resume relies on the persisted header's `delegationDepth` rather than reconstructing depth from the descriptor. `outputSchema` belongs to one activation's result contract rather than durable child composition. The child header remains authoritative for the child id, `cwd`, `parentSession`, `isSeeded`, and `delegationDepth`; body-bearing persistence metadata owns the exact `inheritedEventCount`, while the child transcript owns the fork seed and subsequent history. [`delegationDepthOf()`](../../../../packages/subagent/subagent/src/index.ts) takes the maximum of header and runtime values, so reconstructed runtime options may deepen the persisted value but never lower it and a resumed child cannot regain a top-level delegation budget.
 
 Cold resume cannot depend on an optional method of `SubagentRun`, because that run has been disposed and is not retained across process restart. A run represents one disposable activation and exposes only activation-scoped operations. `SubagentRun.steer?()` names the confirmed live-only capability so it cannot be confused with service orchestration or the model-facing tool.
 

+ 3 - 3
.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md

@@ -57,7 +57,7 @@ durable child Session
 
 ### 面向模型的 `send_message`
 
-模型获得一个由 `SubagentRuntime.followup()` 支撑的 `send_message(subagent_id, message)` 工具,与 `Agent` 上的意图动词一致。该服务操作负责在 steering 与恢复之间编排;它不同于 run 的 `SubagentRun.steer?()`,后者只能向已活跃的 run 发送消息。工具本身不执行生命周期路由。该工具将后续消息的来源标记为 `{ kind: 'coordinator', senderSessionId: parent.id }`,并转发 `{ source, signal }`;服务要求在一个选项对象中同时提供这两项信息。来源会贯穿在线 steering 和 cold resume 两条路径,而取消只控制尚未完成的在线投递等待,因为 cold resume Task 会立即返回,并自行负责后续取消。child 模型收到的仍是普通的 user role 内容,而持久化的来源信息可防止模型生成的后续消息被归类为直接用户输入。用户适配器则提供 `{ kind: 'user' }` 及其交互信号。该工具位于单独加载的 `@deepseek-ai/dsh-tool-subagent-control` 包中,因此按提供方绑定的 `@deepseek-ai/dsh-tool-subagent` 实例可以继续为 spawn、fork 或 ACP 注册不同的委派工具,而不会重复注册全局控制工具。
+模型获得一个由 `SubagentRuntime.sendMessage()` 支撑的全局 `send_message(agent_id, message)` 工具。确切在线 sender 只能指定其直接 parent 或直接可继续 child;服务负责相邻关系检查、冷恢复、固定 Steer 调度,以及持久化 `{ kind: 'agent-message', senderSessionId }` 来源信息。取消只负责 inbox 接受前的工作。用户适配器保持分离,因为浏览器编写的输入携带用户来源信息与请求身份,而不是 Agent 权限。该工具位于单独加载的 `@deepseek-ai/dsh-tool-subagent-control` 包中,因此按提供方绑定的 `@deepseek-ai/dsh-tool-subagent` 实例可以继续为 spawn、fork 或 ACP 注册不同的委派工具,而不会重复注册全局控制工具。
 
 - 如果 child 存在运行中的 Task 并支持在线消息,服务会调用 `run.steer(message, source)` 并返回现有 job id;它不会创建新 Task。
 - 如果 child 没有运行中的 Task,`send_message` 会创建新 Task,使用该消息从持久化存储恢复会话,并返回新的 job id。
@@ -71,9 +71,9 @@ durable child Session
 
 ### 持久化 child handle 与从持久化存储恢复
 
-继续执行管理器在创建 Task 前,通过 seam 的 `snapshotSubagentDescriptor()`(基于 [`snapshotJsonValue`](../../../../packages/util/values/src/index.ts) 构建)对每项描述符输入建立快照;这一边界与 Agent 消息现有的分离式无损 JSON 边界一致。作用于 child 作用域的 setup contribution——由进程内驱动前置安装的一次性 `agent/prompt-submit` 监听器——会在下游 prompt admission 能够阻止请求或抛出异常之前追加一个对模型隐藏的 `subagent/descriptor` 事件。admission 获准后才会开启 child 的初始轮次;admission 被拒绝时,描述符会作为轮次前的仅日志事实保留,并由该 activation 最终的必需检查点持久化。该事件不携带 `surfaceOp`,不进入模型历史,并在压缩替换 surface 历史时继续保留。只有在加载已知 child id 对应的 child 会话后,能在该 child 自身的后缀中(`seedLength` 之后,因此 fork seed 不会泄露祖先的描述符)得到受支持的描述符,且会话 header 将调用方标识为直接 parent 时,该 id 才可恢复。
+继续执行管理器在创建 Task 前,通过 seam 的 `snapshotSubagentDescriptor()`(基于 [`snapshotJsonValue`](../../../../packages/util/values/src/index.ts) 构建)对每项描述符输入建立快照;这一边界与 Agent 消息现有的分离式无损 JSON 边界一致。作用于 child 作用域的 setup contribution——由进程内驱动前置安装的一次性 `agent/prompt-submit` 监听器——会在下游 prompt admission 能够阻止请求或抛出异常之前追加一个对模型隐藏的 `subagent/descriptor` 事件。admission 获准后才会开启 child 的初始轮次;admission 被拒绝时,描述符会作为轮次前的仅日志事实保留,并由该 activation 最终的必需检查点持久化。该事件不携带 `surfaceOp`,不进入模型历史,并在压缩替换 surface 历史时继续保留。只有在加载已知 child id 对应的 child 会话后,能在其精确 `inheritedEventCount` 位置或之后得到受支持的描述符,从而阻止 fork seed 泄露祖先描述符,且会话 header 将调用方标识为直接 parent 时,该 id 才可恢复。
 
-版本化描述符的可继续分支([descriptor.ts](../../../../packages/subagent/subagent/src/descriptor.ts) 中的 `SUBAGENT_DESCRIPTOR_VERSION`)携带 `mode: 'continuable'`、subagent 提供方名称、已解析的 child `agentOptions.provider` 和 `agentOptions.model`,以及可选的 `persona` 与 `toolFilter`。它不会对可通过声明合并扩展的 `AgentOptions` 对象建立快照:与此无关的扩展值不会仅因无法表示为 JSON 而导致继续执行失败。描述符会特意省略 `subagentDepth`;从持久化存储恢复时,系统依赖持久化 header 中的 `delegationDepth`,而不根据描述符重建深度。`outputSchema` 属于单次激活的结果约定,不属于持久化 child 组合配置。child header 仍是 child id、`cwd`、`parentSession`、`seedLength` 和 `delegationDepth` 的权威信息,持久化 child transcript 则负责保存 fork seed 和后续历史。[`delegationDepthOf()`](../../../../packages/subagent/subagent/src/index.ts) 会在 header 值和运行时值中取最大值,因此重建后的运行时选项可以加深持久化值,但绝不能降低它,恢复后的 child 无法重新获得顶层委派预算。
+版本化描述符的可继续分支([descriptor.ts](../../../../packages/subagent/subagent/src/descriptor.ts) 中的 `SUBAGENT_DESCRIPTOR_VERSION`)携带 `mode: 'continuable'`、subagent 提供方名称、已解析的 child `agentOptions.provider` 和 `agentOptions.model`,以及可选的 `persona` 与 `toolFilter`。它不会对可通过声明合并扩展的 `AgentOptions` 对象建立快照:与此无关的扩展值不会仅因无法表示为 JSON 而导致继续执行失败。描述符会特意省略 `subagentDepth`;从持久化存储恢复时,系统依赖持久化 header 中的 `delegationDepth`,而不根据描述符重建深度。`outputSchema` 属于单次激活的结果约定,不属于持久化 child 组合配置。child header 仍是 child id、`cwd`、`parentSession`、`isSeeded` 和 `delegationDepth` 的权威信息;含正文的持久化 metadata 拥有精确 `inheritedEventCount`,child transcript 则负责保存 fork seed 和后续历史。[`delegationDepthOf()`](../../../../packages/subagent/subagent/src/index.ts) 会在 header 值和运行时值中取最大值,因此重建后的运行时选项可以加深持久化值,但绝不能降低它,恢复后的 child 无法重新获得顶层委派预算。
 
 从持久化存储恢复不能依赖 `SubagentRun` 的可选方法,因为该 run 已被 dispose,并且进程重启后不会保留。run 表示一次可 dispose 的激活,只暴露作用于当前激活的操作。`SubagentRun.steer?()` 这一名称明确指代提供确认语义且仅适用于在线消息的功能,以免该功能与服务编排或面向模型的工具混淆。
 

+ 2 - 2
.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.i18n.yaml

@@ -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-23-session-telemetry-otel-revival.md
-2026-07-23-session-telemetry-otel-revival.md: 836531f605eee5c0dcdf108e9ee2b2744d6f72aa
-2026-07-23-session-telemetry-otel-revival.zh.md: 9874598a58ff91bd2cc5ef58c771744d3f3d9610
+2026-07-23-session-telemetry-otel-revival.md: 1110db4b0dfcfd68bde97f9964383ddd041f328b
+2026-07-23-session-telemetry-otel-revival.zh.md: 6c046c5f963760638deacacda51f8d94a13e0efd

File diff suppressed because it is too large
+ 0 - 0
.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md


File diff suppressed because it is too large
+ 0 - 0
.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md


+ 2 - 2
.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.i18n.yaml

@@ -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-25-subagent-policy-inheritance.md
-2026-07-25-subagent-policy-inheritance.md: 34751a4e29e48c84d37425857b8b1b56c8d866eb
-2026-07-25-subagent-policy-inheritance.zh.md: 3d5a6c7a88579583a0ab027c52dd3063eed0a8b2
+2026-07-25-subagent-policy-inheritance.md: 6df9ad53f8588018fca53ebae3a9dfba0d4382de
+2026-07-25-subagent-policy-inheritance.zh.md: f3f7328d558ea55379cb95e53bb010f9f2d2c792

+ 1 - 1
.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md

@@ -12,7 +12,7 @@ Sandbox and approval overrides are per-session log folds. An in-process subagent
 
 The delegation boundary snapshots `sandboxPolicy.overrideOf(parent.session)` before its first await, through the shared child-agent helpers (`captureDelegatedPolicyOverrides`/`appendDelegatedPolicyOverrides` in `dsh-subagent`), which the one-shot driver and the [continuable start](2026-08-10-continuable-subagent-policy-inheritance.md) both call. A later parent switch belongs to the parent's future; cancel-and-redelegate takes a new snapshot. The sandbox-policy service is optional, and only the explicit session override is copied, never deployment defaults or one-shot grants. The approval policy is not inherited: the same capture pins every child to `'never'` — the [approvals-pinned decision](2026-08-10-subagent-approval-pinned-never.md) supersedes this note's original approval-override inheritance.
 
-Each captured value becomes a source-tagged `sandbox/mode` or `approval/policy` event appended during the child factory's unpublished setup. The session constructor has already fixed `Session.firstLiveSeq` at the fork-prefix length, so the inherited facts follow fork history, reach telemetry when the child is announced, and leave `SessionHeader.seedLength` at the prefix length. Existing last-event-wins folds therefore make the delegation snapshot beat stale fork history and let a later child switch beat the snapshot. A grandchild folds its parent's logged state, so the rule composes without another inheritance mechanism.
+Each captured value becomes a source-tagged `sandbox/mode` or `approval/policy` event appended during the child factory's unpublished setup. The session constructor has already fixed `Session.firstLiveSeq` after the constructor seed, while `Session.inheritedEventCount` keeps the exact fork-prefix length, so the inherited facts follow fork history and reach telemetry when the child is announced without changing its lineage cut. Existing last-event-wins folds therefore make the delegation snapshot beat stale fork history and let a later child switch beat the snapshot. A grandchild folds its parent's logged state, so the rule composes without another inheritance mechanism.
 
 Ordinary session appends validate the inherited events before publication, and persistence captures the complete unpublished log when the session is announced. Any materialized child log therefore stores the inherited events with its first batch; there is no second policy store, schema field, or query index. The `source: 'delegation'` marker lets approval narration distinguish inheritance from a child-side user switch.
 

+ 1 - 1
.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.zh.md

@@ -12,7 +12,7 @@ Status: implemented
 
 委派边界在第一次 await 之前,经由共享的子 agent 辅助函数(`dsh-subagent` 中的 `captureDelegatedPolicyOverrides`/`appendDelegatedPolicyOverrides`)对 `sandboxPolicy.overrideOf(parent.session)` 获取快照;一次性驱动器与[可继续启动](2026-08-10-continuable-subagent-policy-inheritance.zh.md)都会调用这些辅助函数。父级后续的切换属于父级的未来;取消后重新委派会取得新快照。沙箱策略服务为可选,仅复制显式会话覆盖项,绝不复制部署默认值或一次性授权。审批策略不继承:同一次捕获会把每个子 agent 钉定为 `'never'`——[审批钉定决策](2026-08-10-subagent-approval-pinned-never.zh.md)取代了本 note 原先的审批覆盖项继承。
 
-每个捕获值都会成为子 agent 工厂在未发布设置阶段追加的一条带来源标记的 `sandbox/mode` 或 `approval/policy` 事件。会话构造函数已将 `Session.firstLiveSeq` 固定为 fork 前缀的长度,因此继承事实会排在 fork 历史之后,在子 agent 公布时进入遥测,同时让 `SessionHeader.seedLength` 保持为此前缀的长度。因此,既有的末事件胜出折叠会让委派快照压过陈旧的 fork 历史,并让子 agent 后续的切换压过该快照。孙代 agent 会折叠其父级已记录的状态,因此无需另一套继承机制即可组合此规则。
+每个捕获值都会成为子 agent 工厂在未发布设置阶段追加的一条带来源标记的 `sandbox/mode` 或 `approval/policy` 事件。会话构造函数已把 `Session.firstLiveSeq` 固定在 constructor seed 之后,而 `Session.inheritedEventCount` 保留精确的 fork 前缀长度,因此继承事实会排在 fork 历史之后,并在子 agent 公布时进入遥测,却不改变其谱系 cut。因此,既有的末事件胜出折叠会让委派快照压过陈旧的 fork 历史,并让子 agent 后续的切换压过该快照。孙代 agent 会折叠其父级已记录的状态,因此无需另一套继承机制即可组合此规则。
 
 普通的会话追加会在发布前校验继承事件,持久化层则在会话公布时捕获完整的未发布日志。因此,任何已物化的子 agent 日志都会在首批数据中存下继承事件;不存在第二套策略存储、schema 字段或查询索引。`source: 'delegation'` 标记让审批叙述能够区分继承与子 agent 侧的用户切换。
 

+ 2 - 2
.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.i18n.yaml

@@ -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-continuable-subagent-conversations.md
-2026-07-28-continuable-subagent-conversations.md: f456bacbf775bf914b47051e19639811e2385f65
-2026-07-28-continuable-subagent-conversations.zh.md: ea6df53026b6a76d8e100fa4fef0aebcd8fcd7cf
+2026-07-28-continuable-subagent-conversations.md: 1efb18ab69cfe779a7de85f288a6e3aa7caaca83
+2026-07-28-continuable-subagent-conversations.zh.md: 7cb1b4a4cff1fc7f6fc41662d05693917084b63a

+ 5 - 7
.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md

@@ -44,7 +44,7 @@ Cold resume does not dispatch through a subagent provider. The continuation mana
 
 `SubagentProvider.start()` and `SubagentRun` remain exclusively on the unchanged one-shot path. A continuable Activation directly owns its `AgentHandle` and never creates, wraps, or retains a `SubagentRun`; `SubagentRun.steer?()` is therefore absent.
 
-`ctx.subagents.followup(parent, childId, content, { source, signal })` remains the sole parent-to-child continuation-message operation. The exact live parent Agent authorizes delivery; cold resume checks that authority before reconstruction and every path checks it again in the final no-await inbox-admission span, so a parent unregistered or replaced during materialization cannot authorize delivery. `source` records who supplied the admitted message and grants no authority. The model-facing `send_message` tool keeps only its stable `subagent_id` and `message` fields and always submits a follow-up turn. Both start and follow-up return the accepted `MessageId`, and neither reports how the manager materialized the Activation.
+`ctx.subagents.sendMessage(sender, targetId, content, { signal })` is the sole model-authored continuation-message operation. The exact live sender authorizes delivery to its direct parent or direct continuable child; cold resume checks direct-child authority before reconstruction and every path checks again in the final no-await inbox-admission span, so an Agent unregistered or replaced during materialization cannot authorize delivery. The service derives durable `agent-message` provenance from that sender. The model-facing `send_message` tool keeps only `agent_id` and `message` and uses fixed Steer scheduling. Both start and send return the accepted `MessageId`, and neither reports how the manager materialized the Activation.
 
 For start and follow-up, the caller signal owns lookup, materialization, and admission only until inbox acceptance. After the operation returns its `MessageId`, the manager owns the Activation independently; later caller cancellation does not cancel the accepted turn or dispose the child.
 
@@ -111,15 +111,13 @@ Top-level teardown is host-owned rather than represented as another Activation.
 
 The activation-owner scope exists because ordinary Cordis owner effects unwind in reverse registration order, which cannot express the dynamic child graph. Manager initialization registers the private scope's structural disposer first and its drain disposer afterward, so reverse unwind invokes the drain before releasing that scope; merely registering a cleanup effect on the same scope as later Agent handles would allow structural handle disposal to bypass child-first ordering. Each materialization registers its barrier participant and snapshots its exact live ancestry before starting the inner transaction, then remains tracked until it installs an Activation or fully rolls back. The Activation retains weak membership of that ancestry, so an intermediate Agent may leave the registry without hiding a still-live descendant from its host root. Each Activation installs one memoized disposal promise before cancellation or recursive callbacks, allowing scoped host shutdown, global manager unload, child release, and normal settlement to converge without double release. Cancellation propagates top-down before slow descendant cleanup; handle release remains child-first. Sibling branches drain independently; one disposal failure is recorded but does not prevent the manager from attempting the remaining selected handles, and the aggregate drain reports failure after all selected branches settle. Durable child Sessions survive this process-local teardown.
 
-### Report delivery extension
+### Adjacent-Agent messaging
 
-The optional child-scoped `report(output)` tool was added later without changing Activation residency or adding another queue. It can be called zero or multiple times per turn, derives the live direct parent rather than accepting a recipient, and selects quiet injection or a waking parent follow-up through deployment config. The [report-tool Agent Note](2026-07-30-continuable-subagent-report-tool.md) owns its authority, acknowledgement, setup-contribution, and delivery contracts.
+The shared `sendMessage(sender, targetId, content, options)` service operation adds no second queue. It accepts an exact live sender, permits only its direct parent or direct continuable child, and uses fixed Steer scheduling through the Agent inbox. The global `send_message({ agent_id, message })` tool exposes that same operation in both directions; the child's initial task identifies its direct parent when the tool is visible. The [adjacent-Agent messaging Agent Note](../architecture/2026-08-27-adjacent-agent-steer-messaging.md) owns its schema, authority, attribution, and prompt placement.
 
-### Deferred steering
+### Fixed Steer scheduling
 
-This version exposes no subagent steering operation. Parent continuation messages always open later FIFO turns, so the continuation layer stores no current-turn controller and adds no controller-aware Agent admission contract.
-
-A later host UI may expose separate **Steer** and **Follow up** actions. Host steering would be strict and live-only: it may call the existing Agent steering path only while the Activation accepts a next step, must reject otherwise, and must never fall back to queueing or cold resume. Exposing parent steering to a model-facing tool remains a separate design.
+Every accepted Agent message uses `Agent.steer()`. A running target claims it at the nearest step boundary; an idle or cold-resumed target starts a turn. The continuation layer does not expose a caller-selectable quiet, next-turn, or follow-up mode.
 
 ### Authority and recorded sender identity
 

+ 5 - 7
.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md

@@ -44,7 +44,7 @@ inbox 接受消息前发生任何失败,操作都会在不返回任何 id 的
 
 `SubagentProvider.start()` 和 `SubagentRun` 只保留在不变的 one-shot 路径上。可继续激活直接持有自身的 `AgentHandle`,绝不创建、包装或保留 `SubagentRun`;因此,`SubagentRun.steer?()` 不存在。
 
-`ctx.subagents.followup(parent, childId, content, { source, signal })` 仍是唯一的从 parent 到 child 的继续执行消息操作。确切的在线 parent Agent 授权投递;冷恢复会在重建前检查该权限,每条路径还会在最终无 await 的 inbox 准入区间再次检查,因此在物化期间被注销或替换的 parent 无法授权投递。`source` 记录谁提供了获准消息,不赋予任何权限。面向模型的 `send_message` 工具只保留稳定的 `subagent_id` 和 `message` 字段,并始终提交一个 follow-up 轮次。start 和 follow-up 都返回已接受的 `MessageId`,两者都不报告管理器如何物化激活
+`ctx.subagents.sendMessage(sender, targetId, content, { signal })` 是唯一由模型编写的继续执行消息操作。确切在线 sender 授权向其直接 parent 或直接可继续 child 投递;冷恢复会在重建前检查直接 child 权限,每条路径还会在最终无 await 的 inbox 准入区间再次检查,因此在物化期间被注销或替换的 Agent 无法授权投递。服务从该 sender 推导持久化 `agent-message` 来源信息。面向模型的 `send_message` 工具只保留 `agent_id` 和 `message`,并使用固定 Steer 调度。start 与 send 都返回已接受的 `MessageId`,两者都不报告管理器如何物化 Activation
 
 对于 start 和 follow-up,调用方 signal 只在 inbox 接受消息前持有查找、物化和准入。操作返回 `MessageId` 后,管理器会独立持有该激活;调用方之后的取消不会取消已接受的轮次,也不会 dispose child。
 
@@ -111,15 +111,13 @@ Agent inbox 是唯一队列。每条继续执行消息都使用 `Agent.followup(
 
 activation-owner 作用域之所以存在,是因为普通 Cordis owner effect 按注册逆序撤销,无法表达动态 child 图。管理器初始化时先注册私有作用域的结构化 disposer,再注册自身的 drain disposer,使逆序撤销先执行 drain、再释放该作用域;如果只在与后续 Agent handle 相同的作用域上注册 cleanup effect,结构化 handle dispose 就可能绕过 child-first 顺序。每个物化过程都会在启动内部事务前注册其屏障参与项,并对其确切的在线祖先建立快照,然后保持跟踪,直到安装 Activation 或完全回滚。Activation 会保留其在这组祖先中的弱成员关系,因此中间 Agent 即使离开注册表,也不会让仍在线的后代脱离宿主根节点的可见范围。每个 Activation 都会在取消或递归回调前安装一个记忆化的 dispose promise,使限定作用域的宿主关闭、全局管理器卸载、child 释放和正常结算能够汇合,而不会重复释放。取消会在等待缓慢的后代清理之前自顶向下传播;handle 释放仍是 child-first。同级分支独立 drain;系统会记录单次 dispose 失败,但仍会尝试其余选中 handle,聚合 drain 则在所有选中分支结算后报告失败。这次进程内拆卸不会销毁持久化 child 会话。
 
-### 报告投递扩展
+### 相邻 Agent 消息
 
-后来添加的可选 child 作用域 `report(output)` 工具不会改变 Activation 驻留状态,也不会增加另一条队列。它每轮可调用零次或多次,不允许指定接收方,而是推导在线的直接 parent;投递采用静默注入还是唤醒 parent follow-up,由部署配置选择。[report 工具 Agent Note](2026-07-30-continuable-subagent-report-tool.zh.md)规定其权限、确认、设置贡献和投递约定
+共享的 `sendMessage(sender, targetId, content, options)` 服务操作不会增加第二条队列。它接收确切在线 sender,只允许其直接 parent 或直接可继续 child,并通过 Agent inbox 使用固定 Steer 调度。全局 `send_message({ agent_id, message })` 工具在两个方向暴露同一个操作;当 child 可以看到该工具时,其初始任务会标明直接 parent。[相邻 Agent 消息 Agent Note](../architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md)规定其 schema、权限、来源信息与提示词位置
 
-### 延后的 steering(中途引导)
+### 固定 Steer 调度
 
-本版本不暴露 subagent steering 操作。parent 的继续执行消息始终开启后续 FIFO 轮次,因此继续执行层不存储当前轮次控制方,也不新增能够感知控制方的 Agent 准入约定。
-
-后续宿主 UI 可以分别暴露 **Steer** 和 **Follow up** 操作。宿主 steering 必须严格且仅限在线使用:只有当激活接受下一步骤时,它才能调用现有的 Agent steering 路径;其他情况必须拒绝,而且绝不能转为排队或冷恢复。是否通过面向模型的工具暴露 parent steering 仍需单独设计。
+每条已接受的 Agent 消息都使用 `Agent.steer()`。运行中的目标会在最近的 step 边界领取消息;空闲或冷恢复的目标会启动一个轮次。继续执行层不暴露由调用方选择的 quiet、next-turn 或 follow-up 模式。
 
 ### 权限与已记录的发送方身份
 

+ 0 - 118
.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md

@@ -1,118 +0,0 @@
-# Agent Note: Continuable subagent report tool
-
-Status: implemented
-
-English | [中文](2026-07-30-continuable-subagent-report-tool.zh.md)
-
-## Problem
-
-Continuable in-process subagents can receive later parent messages, retain descendants, settle, and cold-resume, but the base lifecycle gives them no way to send selected content back to their direct parent. Their complete output already remains reconstructable from the durable child Session, so the missing capability is explicit delivery rather than result storage.
-
-Treating every final assistant message as an implicit result would conflate turn completion with reporting. A long-lived child may have nothing useful to report in one turn, may report progress several times in another, and must remain available after reporting. Recipient authority, quiet versus next-step delivery, acknowledgement, durability, and retry behavior therefore need one explicit contract.
-
-## Decision
-
-Add the independently installed `@deepseek-ai/dsh-tool-subagent-report` package. It contributes an ordinary model-facing `report` tool to each continuable in-process child Activation. The mechanism accepts zero or multiple calls in a turn; the child is separately instructed to call it once before finishing ([the report obligation](2026-08-06-continuable-child-report-obligation.md)). Success neither concludes the turn, settles the Activation, nor prevents later parent follow-ups, and finishing a turn never reports automatically.
-
-The feature is a collaboration control, not a result-bearing execution wrapper. It adds no Task, `SubagentRun`, result promise, Activation state, delivery queue, or replay path.
-
-### Model-facing contract
-
-`report` accepts exactly `{ output: string }` and returns exactly `{ messageId: string }`. It accepts no child id, recipient id, or delivery mode. `exec.agent` binds the tool call to the reporting child, the service derives the sole recipient from durable `parentSession`, and deployment config owns scheduling.
-
-`messageId` is the stable `MessageId` of the user-role message accepted into the parent's inbox. It is not a read receipt, parent-log acknowledgement, turn-completion receipt, or persistence flush.
-
-The description states that reporting is required before finishing, repeatable, direct-parent-only, and non-terminal. It warns that a failed tool result may still follow an accepted send because a later `tools/post-execute` failure can replace the result. Without an idempotency key, stronger wording would encourage duplicate retries after ambiguous failure.
-
-The tool uses generic rendering with no locations. Its acknowledgement includes `messageId`. Scope-local registration keeps presentation and execution aligned: roots, one-shot children, remote providers, sibling scopes, and agentless execution neither see nor execute `report`. It installs after the child's global `toolFilter`, so a delegation allow-list cannot accidentally remove the structural return channel; deployments that require no return channel omit the package.
-
-### Service authority
-
-The subagent seam exposes `ctx.subagents.reportFrom(child, content, { delivery, signal }): Promise<MessageId>`. The exact live child Agent is the sender credential. The continuation manager accepts only an Activation whose `handle.agent === child`, derives its direct parent from the child's durable header, and requires that id to resolve to a live parent Agent in the final synchronous authorization-and-send span. The API accepts no caller-selected recipient, ancestor, or sender fields.
-
-Roots, one-shot children, forged objects, stale Agents, and same-id replacements fail with `UNAUTHORIZED`. A closing child Activation fails with `ACTIVATION_CLOSING`; manager drain and pre-acceptance cancellation retain their existing lifecycle errors. A missing or send-rejecting direct parent fails with `PARENT_UNAVAILABLE` and `direct parent is not live; report was not delivered`. Failure returns no id, cold-resumes no parent, writes no offline mailbox, and mutates no absent-parent Session.
-
-Nested reporting crosses exactly one edge. A grandchild reports to its direct child parent, never to the top-level coordinator. That intermediate child may explicitly report a derived update later.
-
-### Delivery policy
-
-The package validates `reportDelivery: 'quiet' | 'next-step'`; the default is `next-step` ([ordering decision](../bug-fix/2026-08-17-subagent-report-settlement-ordering.md)).
-
-Quiet delivery calls `parent.inject()`. It adds model-visible next-step context without waking an idle parent; a running parent stages the report for the next safe log position.
-
-Next-step delivery calls `parent.steer()`. It wakes a parked parent and joins a running parent's nearest step boundary. When that parent is itself a continuable Activation, the send uses the manager's existing admission accounting so the parent cannot settle between synchronous inbox insertion and the admission microtask. Reports share the next-step FIFO with a later settlement notice, preserving their accepted causal order.
-
-Both modes frame one user-role message as `Background subagent <child-id> reported:` followed by the exact `output`. The durable message source is `{ kind: 'subagent-report', senderSessionId: child.id }`. Normal Agent ordering governs concurrent sends; the subagent layer creates no second queue.
-
-### Acknowledgement and recovery
-
-Success means the exact live parent synchronously accepted the message. The context becomes reconstructable only when it reaches its normal log boundary; a next-step delivery has woken the parent, while quiet delivery may remain pending. The inbox message id remains separate from the returned stable message id.
-
-The first version provides no durable mailbox, idempotency key, delivery receipt, retry protocol, or exactly-once claim. A process failure can leave the caller uncertain, and retry after an unknown outcome may duplicate a report. The durable child transcript remains the recovery source when the parent is unavailable.
-
-### Composition and lifecycle
-
-The subagent seam adds `registerContinuableSetup(contribution): () => void`, backed by `SubagentActivationSetupRegistry`. Each synchronous contribution receives the unpublished child context and returns the disposer for its installation. The continuation manager first applies base child composition, then current contributions in registration order through the same setup closure used for fresh creation and cold resume.
-
-The registry owns registration, per-child installation records, setup rollback, child-scope cleanup, and immediate revocation. Applying a batch returns the Agent setup commit that revalidates provisioning after every setup await and immediately before Agent publication. A throwing or concurrently revoked contribution therefore rejects before either Agent or Session publication and rolls back the batch. New registrations affect a resident child only on its next Activation; removing a registration first closes it to new setup and then revokes every provisioning or resident installation immediately. Registration disposal and child-context disposal are idempotent and attempt every release before aggregating failures.
-
-This seam keeps the continuation manager unaware of tool names. The report package installs only `report` and its child-scoped guidance section; `@deepseek-ai/dsh-tool-subagent-control` independently installs parent-side `send_message` and `list_agents`. A deployment can install either direction, both, or neither. Providers remain data-only, durable descriptors do not snapshot report availability or delivery mode, and cold resume uses the deployment's current contributions and policy.
-
-### Snapshot coverage
-
-The ACP snapshot harness adds `waitForSubagentTurnEnd`, selecting the Nth harvested child by the same order as `session.N.jsonl`. It waits for a closed child turn containing a request header so a continuable child's earlier descriptor-seed turn cannot satisfy the boundary. This lets the assembled scenario wait for the child-side report without inventing a parent-visible signal.
-
-The authored snapshot starts a continuable child, executes the real scope-local `report` tool, and observes default next-step delivery before the manager's later settlement notice. A snapshot-only maintenance fence holds the parent until both messages are pending, proving next-step input is claimed before queued next-turn input when the parent resumes. It declares child pins `1`, so the otherwise non-global `report` schema and the child's own prompt are checked against `tool-schemas.1.expected.json` and `system-prompt.1.expected.md` while the root keeps the class pins. The generated tool catalog separately mints a child scope to include the same scope-local schema.
-
-## Alternatives considered
-
-### Automatically deliver every final answer
-
-Automatic delivery cannot represent zero reports, progress reports, or several selected updates. It also couples reporting to settlement and can duplicate content already reported explicitly.
-
-### Always wake the parent
-
-Waking on every report creates unsolicited turns and can cascade through nested subagents. Quiet delivery was chosen as the default on the assumption that the parent had another reason to read its context. [The report obligation](2026-08-06-continuable-child-report-obligation.md) supersedes that choice: a parked background coordinator has no such reason, so waking is the default and this paragraph now records why `quiet` still exists.
-
-### Let the child choose the delivery mode
-
-Giving the model a mode argument grants it control over scheduler pressure and makes behavior deployment-dependent. The child chooses content and timing; deployment config chooses whether that content wakes the parent.
-
-### Register a global tool
-
-A global `report` would advertise an unusable capability to roots, one-shot children, remote children, and agentless callers. Execution-time rejection would make schema visibility disagree with authority.
-
-### Combine both directions in the control package
-
-`send_message` and `report` have different audiences, scopes, configuration, and lifecycle. Independent packages let deployments grant either direction without implying the other.
-
-### Persist an offline parent mailbox
-
-Mutating or cold-resuming an absent parent requires a new durable addressing, authorization, conflict, acknowledgement, and replay protocol. Requiring a live direct parent keeps the first version on the existing Agent send path.
-
-### Reintroduce a Task or result promise
-
-A result-bearing wrapper makes one report or one turn appear terminal and recreates the lifetime mismatch that continuable Activations removed. Explicit repeatable sends need no intermediate execution object.
-
-### Validate setup after Agent creation
-
-A post-creation revocation check can reject the Activation only after the Agent and Session have been published. Disposing the returned handle removes the live objects but cannot delete persistence through the current seam, leaving a resumable child that the continuation manager said was never established. Returning an `AgentSetupCommit` instead lets the Agent factory perform the same mutable-state check synchronously at its publication boundary.
-
-## Consequences
-
-- A continuable in-process child exposes exactly one scope-local `report` schema only while the report package's contribution is installed; unrelated Agents never expose it.
-- The tool returns the parent message's stable `MessageId`; its inbox occurrence is not a separate public identity.
-- Only the exact resident child may report, and only to the exact live direct parent derived from durable lineage. The service has no recipient parameter or offline fallback.
-- Next-step delivery is the validated default: it wakes an idle parent or extends a running parent's turn at the nearest step boundary. Quiet delivery never wakes an idle parent.
-- Child cancellation or disposal after parent acceptance does not retract the report. Before acceptance, child disposal, drain, parent loss, or caller cancellation rejects the operation.
-- Fresh and resumed Activations compose current setup contributions before publication. Grants wait for the next Activation; revocation is immediate for resident children.
-- Unit coverage pins visibility, allow-list behavior, both delivery modes, stable message and sender identities, nested routing, invalid senders, absent parents, cancellation, drain, revocation races, and the absence of Jobs or implicit final reporting.
-- The keyless assembled snapshot proves the real child tool, default next-step ordering before settlement, and durable parent framing.
-
-### Accepted risks
-
-The acceptance boundary is weaker than durable end-to-end delivery. A crash can leave the result ambiguous, and retries may duplicate reports.
-
-Next-step delivery can amplify model work when nested children report frequently. Reports waiting together share one step, and deployment ownership through `reportDelivery` bounds but does not remove that risk.
-
-Registry presence is the parent liveness signal. A host-owned parent whose `AgentHandle.dispose()` has started but has not yet unwound its scope can still accept and append a report that it will not act on in this process. Closing that gap requires an Agent-level disposal-start signal rather than subagent-layer inference.

+ 0 - 118
.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.zh.md

@@ -1,118 +0,0 @@
-# Agent Note: 可继续 subagent 报告工具
-
-Status: implemented
-
-[English](2026-07-30-continuable-subagent-report-tool.md) | 中文
-
-## 问题
-
-可继续的进程内 subagent 能够接收 parent 后续发来的消息、保留后代、结算并冷恢复,但基础生命周期无法让它们将选中内容发送给直接 parent。child 的完整输出已可从持久化会话中重建,因此缺失的能力是显式投递,而非结果存储。
-
-如果将每条 assistant 最终消息都视为隐式结果,就会混淆轮次完成与报告。长期运行的 child 可能在某个轮次中无内容可报告,也可能在另一个轮次多次报告进展,而且报告后必须仍可继续工作。因此,接收方权限、静默投递与 next-step 投递、确认、持久性和重试行为都需要一份显式约定。
-
-## 决策
-
-新增可独立安装的 `@deepseek-ai/dsh-tool-subagent-report` 包。它会向每个可继续进程内 child Activation 贡献一个普通的面向模型 `report` 工具。机制本身接受一个轮次中调用零次或多次;child 会另行被要求在结束前调用一次(见[报告义务](2026-08-06-continuable-child-report-obligation.zh.md))。调用成功既不会结束该轮次或结算 Activation,也不会阻止 parent 之后继续 follow-up;完成轮次也绝不会自动报告。
-
-该功能是协作控制,不是承载结果的执行包装层。它不新增 Task、`SubagentRun`、结果 promise、Activation 状态、投递队列或回放路径。
-
-### 面向模型的约定
-
-`report` 只接受 `{ output: string }`,也只返回 `{ messageId: string }`。它不接受 child id、接收方 id 或投递模式。`exec.agent` 将工具调用绑定到发送报告的 child;服务从持久化 `parentSession` 中推导唯一接收方,调度则由部署配置决定。
-
-`messageId` 是已接受进入 parent inbox 的用户角色消息所对应的稳定 `MessageId`。它不是已读回执、parent 日志确认、轮次完成回执或持久化 flush。
-
-工具描述会明确报告操作在结束前必须执行、可重复、仅限直接 parent 且不会结束轮次。它还会警告:发送被接受后,后续 `tools/post-execute` 失败可能替换工具结果,因此工具结果失败时内容仍可能已经送达。没有幂等键时,更强的表述会诱导调用方在结果不明确的失败后重复重试。
-
-该工具使用不带 location 的通用渲染,其确认中包含 `messageId`。作用域局部注册使呈现与执行保持一致:root、one-shot child、远程提供方、同级作用域和无 agent(智能体)执行既不能看到,也不能执行 `report`。它会在 child 的全局 `toolFilter` 之后安装,因此委派 allow-list 不会意外移除这条结构性返回通道;不需要返回通道的部署不安装该包。
-
-### 服务权限
-
-subagent seam 暴露 `ctx.subagents.reportFrom(child, content, { delivery, signal }): Promise<MessageId>`。确切的在线 child Agent 是发送方凭据。继续执行管理器只接受 `handle.agent === child` 的 Activation,从 child 的持久化 header 中推导其直接 parent,并要求该 id 在最终的同步授权与发送区间解析为一个在线 parent Agent。该 API 不接受由调用方选择的接收方、祖先或发送方字段。
-
-root、one-shot child、伪造对象、陈旧 Agent 和同 id 替换对象都以 `UNAUTHORIZED` 失败。正在关闭的 child Activation 以 `ACTIVATION_CLOSING` 失败;管理器 drain 和接受前取消保留既有的生命周期错误。直接 parent 不存在或拒绝接受时,以 `PARENT_UNAVAILABLE` 和 `direct parent is not live; report was not delivered` 失败。失败不返回 id,不冷恢复 parent,不写入离线邮箱,也不会修改缺失 parent 的会话。
-
-嵌套报告恰好跨越一条边。grandchild 会向其直接 child parent 报告,绝不会直接向顶层 coordinator 报告。中间 child 可以稍后显式报告自己归纳的更新。
-
-### 投递策略
-
-该包会校验 `reportDelivery: 'quiet' | 'next-step'`,默认值为 `next-step`(见[顺序决策](../bug-fix/2026-08-17-subagent-report-settlement-ordering.zh.md))。
-
-静默投递调用 `parent.inject()`。它会添加模型可见的 next-step 上下文,但不唤醒空闲 parent;运行中的 parent 会把报告暂存到下一个安全日志位置。
-
-Next-step 投递调用 `parent.steer()`。它会唤醒停驻的 parent,并加入运行中 parent 最近的 step 边界。当该 parent 本身也是可继续 Activation 时,发送会使用管理器现有的准入记账,防止 parent 在同步插入 inbox 与准入微任务之间结算。报告与稍后的结算通知共享 next-step FIFO,从而保持其被接受时的因果顺序。
-
-两种模式都会将一条用户角色消息封装为 `Background subagent <child-id> reported:`,后面跟随完全原样的 `output`。持久化消息来源为 `{ kind: 'subagent-report', senderSessionId: child.id }`。并发发送的顺序由 Agent 的常规规则决定;subagent 层不会创建第二条队列。
-
-### 确认与恢复
-
-成功表示确切的在线 parent 已同步接受该消息。上下文只有到达正常日志边界后才可重建;next-step 投递已经唤醒 parent,而静默投递可能继续等待。inbox 消息 id 不会成为另一个公开身份。
-
-首个版本不提供持久化邮箱、幂等键、投递回执、重试协议或恰好一次保证。进程故障可能让调用方无法确定结果,在结果未知时重试则可能重复报告。parent 不可用时,持久化 child transcript(文本记录)仍是恢复来源。
-
-### 组合与生命周期
-
-subagent seam 新增 `registerContinuableSetup(contribution): () => void`,由 `SubagentActivationSetupRegistry` 支撑。每个同步贡献都会接收尚未发布的 child 上下文,并返回其安装的 disposer。继续执行管理器首先应用基础 child 组合,然后通过同一个用于首次创建与冷恢复的设置闭包,按注册顺序应用当前贡献。
-
-注册表负责注册、每个 child 的安装记录、设置回滚、child 作用域清理和立即撤销。应用一个批次会返回 Agent setup 提交对象,用于在每次 setup 的 await 结算后以及紧邻 Agent 发布前重新校验配置状态。因此,某项贡献抛出异常或被并发撤销时,会在 Agent 与会话发布前拒绝操作并回滚该批次。新注册项只会在驻留 child 的下一个 Activation 生效;移除注册项时,会先将它对新设置关闭,再立即撤销为正在预配置或驻留的每个 child 安装的实例。注册 dispose(资源释放)与 child 上下文 dispose 都是幂等的,两者都会先尝试每项释放,再聚合失败。
-
-该 seam 使继续执行管理器无需知道工具名。report 包只安装 `report` 及其 child 作用域指引 section;`@deepseek-ai/dsh-tool-subagent-control` 则独立安装 parent 侧的 `send_message` 和 `list_agents`。部署时可安装任一方向、同时安装两者或两者均不安装。提供方仍只负责数据,持久化描述符不会对 report 可用性或投递模式建立快照,冷恢复则使用部署当前的贡献与策略。
-
-### 快照覆盖
-
-ACP(Agent Client Protocol)快照 harness 新增 `waitForSubagentTurnEnd`,按与 `session.N.jsonl` 相同的顺序选择第 N 个已收集 child。它会等待一个包含请求 header 的已闭合 child 轮次,以防可继续 child 早期播种描述符的轮次错误满足该边界。这样,整体组装的场景无需伪造 parent 可见信号,就能等待 child 侧报告。
-
-手写快照会启动一个可继续 child,执行真实的作用域局部 `report` 工具,并观察默认 next-step 投递先于管理器稍后的结算通知。一个仅用于快照的 maintenance 围栏会保持 parent,直至两条消息都处于待领取状态,从而证明 parent 恢复时先领取 next-step 输入、再领取排队的 next-turn 输入。它声明 child pin `1`,因此本不属于全局的 `report` schema 与该 child 自身的提示词会分别与 `tool-schemas.1.expected.json` 和 `system-prompt.1.expected.md` 比对,root 则继续使用类别 pin。生成的工具目录会另外铸造一个 child 作用域,以收录同一个作用域局部 schema。
-
-## 曾考虑的替代方案
-
-### 自动投递每个最终回答
-
-自动投递无法表示零次报告、进展报告或多次精选更新。它还会将报告与结算耦合,并可能重复投递已显式报告的内容。
-
-### 始终唤醒 parent
-
-每次报告都唤醒 parent 会产生未经请求的轮次,还可能沿嵌套 subagent 级联扩散。当初选择静默投递作为默认值,前提是 parent 还有别的理由去读自己的上下文。[报告义务](2026-08-06-continuable-child-report-obligation.zh.md)取代了该选择:已经停驻的后台协调者并没有这样的理由,因此唤醒成为默认值,而本段现在记录的是 `quiet` 为何仍然保留。
-
-### 允许 child 选择投递模式
-
-向模型提供 mode 参数会赋予其控制调度器压力的能力,并使行为依赖部署。child 只决定内容和时机;该内容是否唤醒 parent,由部署配置决定。
-
-### 注册全局工具
-
-全局 `report` 会向 root、one-shot child、远程 child 和无 agent 调用方公布一项无法使用的能力。到执行时才拒绝,会使 schema 可见性与权限不一致。
-
-### 将两个方向合并到 control 包
-
-`send_message` 与 `report` 的受众、作用域、配置和生命周期各不相同。独立的包可让部署授予任意一个方向,而不暗示也授予另一个方向。
-
-### 持久化离线 parent 邮箱
-
-修改或冷恢复不在线的 parent,需要一套新的持久化寻址、权限、冲突、确认和回放协议。要求直接 parent 在线,可以让首个版本继续使用现有 Agent 发送路径。
-
-### 重新引入 Task 或结果 promise
-
-承载结果的包装层会让一次报告或一个轮次看似具有终止性,并重新引入可继续 Activation 已经移除的生命周期不匹配。显式、可重复的发送无需中间执行对象。
-
-### 在 Agent 创建后校验 setup
-
-创建完成后的撤销检查只能在 Agent 与会话均已发布后拒绝 Activation。对返回的 handle 执行 dispose 会移除实时对象,但当前 seam 无法删除持久化内容,因此会留下一个仍可恢复的 child,而继续执行管理器却判定它从未建立。改为返回 `AgentSetupCommit`,Agent 工厂便可在自身的发布边界同步执行同一项可变状态检查。
-
-## 影响
-
-- 只有安装 report 包贡献时,可继续进程内 child 才会恰好暴露一个作用域局部 `report` schema;无关 Agent 永远不会暴露该 schema。
-- 工具返回 parent 消息的稳定 `MessageId`;其 inbox 中的出现不会成为另一个公开身份。
-- 只有确切的驻留 child 才能报告,且只能报告给根据持久化谱系推导的确切在线直接 parent。服务不接受接收方参数,也不提供离线 fallback。
-- Next-step 投递是校验后的默认模式:它会唤醒空闲 parent,或在最近的 step 边界延长运行中 parent 的轮次。静默投递绝不会唤醒空闲 parent。
-- parent 接受后取消或 dispose child 不会撤回报告。接受前,child dispose、drain、parent 丢失或调用方取消都会拒绝操作。
-- 新建和恢复的 Activation 都会在发布前组合当前设置贡献。新授权等待下一个 Activation 才生效,而已驻留 child 的授权撤销立即生效。
-- 单元覆盖固定可见性、allow-list 行为、两种投递模式、稳定的消息与发送方身份、嵌套路由、无效发送方、缺失的 parent、取消、drain、撤销竞争,以及不存在 Task 或隐式最终报告。
-- 无密钥整体组装快照证明真实 child 工具、默认 next-step 顺序先于结算,以及持久化 parent 封装。
-
-### 已接受的风险
-
-该接受边界弱于持久化端到端投递。崩溃可能导致结果不明,重试则可能重复报告。
-
-嵌套 child 频繁报告时,next-step 投递可能放大模型工作量。一起等待的报告会共享一个 step,通过 `reportDelivery` 交由部署所有者控制也会限制该风险,但无法完全消除。
-
-注册表中的存在性就是 parent 在线信号。宿主拥有的 parent 如果已开始 `AgentHandle.dispose()` 但尚未完成其作用域清理,仍可能接受并追加一条本进程不会再处理的报告。要弥合这个缺口,需要 Agent 层面的 dispose 开始信号,不能由 subagent 层推断。

+ 2 - 2
.agents/notes/implemented/feature/2026-07-31-web-default-search.i18n.yaml

@@ -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-web-default-search.md
-2026-07-31-web-default-search.md: eb0de16b5bf6133bbdb5275106f42eba75ddf607
-2026-07-31-web-default-search.zh.md: e1cc622e70d8af8e71a8c60aa7e7ceb9a2b91a5e
+2026-07-31-web-default-search.md: f196bfcd0c42bcfd6aacaac46971b4b9948732d7
+2026-07-31-web-default-search.zh.md: cd313c714acc22ca470ece681624bae19c1e8b4f

+ 5 - 3
.agents/notes/implemented/feature/2026-07-31-web-default-search.md

@@ -4,13 +4,15 @@ Status: implemented
 
 English | [中文](2026-07-31-web-default-search.zh.md)
 
+The [shared-base Web fetch default](2026-09-01-shared-base-web-fetch-default.md) supersedes this record's fetch opt-in decision. This record remains authoritative for the default search provider, credential resolution, endpoint, timeout, and the separation between provider availability and model-tool registration.
+
 ## Problem
 
 The harness had a complete Web capability family—provider registry, DeepSeek/Exa/Perplexity search providers, local fetch, stable model tools, and structured result presentation—but the shipped `dsh web` composition mounted none of it. The model could not discover current information unless a deployment supplied a custom overlay. Merely mounting the existing DeepSeek provider would not complete the WebUI path: the Models page stores `DEEPSEEK_API_KEY` through `ctx.credentials`, while the search provider froze only the process environment at plugin load, so a key entered or rotated in the running UI would not reach search.
 
 ## Decision
 
-`apps/cli/config/base.cordis.yml` explicitly mounts `dsh-web` with `searchProvider: deepseek-official` and `fetchProvider: http`, `dsh-web-search-deepseek`, `dsh-web-fetch-http`, and `dsh-tool-web` with `fetch: false` and `searchTimeoutMs: 60000`. The shared base therefore keeps only `web_search` visible unless a product preset enables fetch; the shipped Web `cordis`, `code`, and `standard` presets do so. Explicit provider ids keep selection independent of registration order and leave personal or `--config` overlays able to replace or disable the rows. The one-minute shipped budget covers an auxiliary DeepSeek Messages request plus server-side retrieval while leaving `dsh-tool-web`'s provider-neutral 30-second default unchanged for custom compositions. The [Web capability seam decision](../architecture/2026-06-24-web-capability-seam.md) owns the public-fetch security policy and Web preset default.
+`packages/bundle/base/cordis.patch.yml` explicitly mounts `dsh-web` with `searchProvider: deepseek-official` and `fetchProvider: http`, `dsh-web-search-deepseek`, `dsh-web-fetch-http`, and `dsh-tool-web` with `searchTimeoutMs: 60000`. The [shared-base Web fetch default](2026-09-01-shared-base-web-fetch-default.md) owns the current `fetch: true`; this record continues to own provider selection, search credentials, and timeout. Explicit provider ids keep selection independent of registration order and leave personal or `--patch` overlays able to replace or disable the rows. The one-minute shipped budget covers an auxiliary DeepSeek Messages request plus server-side retrieval while leaving `dsh-tool-web`'s provider-neutral 30-second default unchanged for custom compositions. The [Web capability seam decision](../architecture/2026-06-24-web-capability-seam.md) owns the public-fetch security policy.
 
 DeepSeek search uses the same `DEEPSEEK_API_KEY` credential reference as the official conversation adapter. The provider resolves that reference inside every search through the optional `ctx.credentials` service; only a composition without the seam falls back to the launching process environment, and a non-empty literal `apiKey` remains the programmatic last resort. A stored or rotated Web Models key therefore reaches the next search without restarting or retaining the value on the provider. Because `WebSearchProvider.available()` is synchronous, it treats an installed resolver as locally usable and missing dynamic credentials fail the operation with the provider-specific `WEB_PROVIDER_CREDENTIAL_MISSING` code while the stable tool schema stays registered.
 
@@ -30,8 +32,8 @@ The default mount does not create a Web-specific permission policy. `web_search`
 
 **Raise `dsh-tool-web`'s provider-neutral timeout.** Rejected because custom providers and deployments own different latency expectations; the shipped DeepSeek composition owns this deployment budget.
 
-**Enable fetch on every shared-base surface.** Rejected because the shared base serves products with different network postures. It mounts the public-only provider but keeps the tool opt-in; the shipped Web presets deliberately enable it, while another product can leave it hidden or add stricter network policy.
+**Enable fetch on every shared-base surface.** This record rejected the alternative because shared-base products could require different network policies. The [shared-base Web fetch default](2026-09-01-shared-base-web-fetch-default.md) supersedes that rejection after the shipped products converged on one full tool roster; its public-destination and no-approval constraints remain current.
 
 ## Consequences
 
-Native model requests on every shared-base surface carry the `web_search` schema and search guidance; Web/headless PTC mode exposes the same search capability beneath `run_code`. Search adds a complete auxiliary model call and may use the native server tool multiple times; its exact secret-free request remains reconstructable from the initiating session log. The shipped Web `cordis`, `ptc`, and `standard` presets additionally expose `web_fetch` with public-address enforcement and no per-call approval. The Web snapshot lane boots the shipped tree, drives a replayed `web_search` call through the real DeepSeek provider against a local Messages fixture, asserts the durable auxiliary request and structured result, and pins the settled browser presentation. Composition smokes pin the shared search roster and per-preset fetch choices; the built composition dump pins the one-minute shipped search budget; provider tests pin missing, stored, and rotated credential behavior plus literal and ambient compatibility.
+Native model requests on headless, full SDK, ACP, and custom base-only profiles carry the `web_search` and `web_fetch` schemas and guidance; Web presets expose the same pair, including beneath `run_code` in PTC mode. Search adds a complete auxiliary model call and may use the native server tool multiple times; its exact secret-free request remains reconstructable from the initiating session log. Fetch enforces public addresses and requires no per-call approval. The Web snapshot lane boots the shipped tree, drives a replayed `web_search` call through the real DeepSeek provider against a local Messages fixture, asserts the durable auxiliary request and structured result, and pins the settled browser presentation. Shared snapshot headers pin the common fetch schema and prompt guidance. Composition smokes pin the tool roster; the built composition dump pins the one-minute shipped search budget; provider tests pin missing, stored, and rotated credential behavior plus literal and ambient compatibility.

+ 5 - 3
.agents/notes/implemented/feature/2026-07-31-web-default-search.zh.md

@@ -4,13 +4,15 @@ Status: implemented
 
 [English](2026-07-31-web-default-search.md) | 中文
 
+[共享 base 的 Web 抓取默认值](2026-09-01-shared-base-web-fetch-default.zh.md)取代本文关于抓取按需启用的决策。本文继续负责默认搜索提供方、凭据解析、端点、超时,以及提供方可用性与模型工具注册之间的区分。
+
 ## 问题
 
 该 harness 已具备完整的 Web 能力体系:提供方注册表、DeepSeek、Exa 和 Perplexity 搜索提供方、本地抓取、稳定的面向模型工具,以及结构化结果呈现,但已交付的 `dsh web` 组合没有挂载其中任何一项。除非部署提供自定义覆盖层,否则模型无法发现最新信息。仅挂载现有 DeepSeek 提供方仍无法打通 WebUI 链路:Models 页面通过 `ctx.credentials` 存储 `DEEPSEEK_API_KEY`,而搜索提供方只会在插件加载时固定读取进程环境,因此在运行中的 UI 输入或轮换的密钥无法用于搜索。
 
 ## 决策
 
-`apps/cli/config/base.cordis.yml` 明确挂载 `dsh-web`,配置 `searchProvider: deepseek-official` 与 `fetchProvider: http`,同时挂载 `dsh-web-search-deepseek`、`dsh-web-fetch-http`,并以 `fetch: false` 和 `searchTimeoutMs: 60000` 挂载 `dsh-tool-web`。因此,共享 base 只会暴露 `web_search`,除非产品 preset 启用抓取;已交付的 Web `cordis`、`ptc` 与 `standard` preset 会启用抓取。显式提供方 id 使选择不受注册顺序影响,同时个人覆盖层或 `--config` 覆盖层仍可替换或禁用这些配置项。已交付的一分钟预算用于覆盖一次辅助 DeepSeek Messages 请求及服务端检索,同时保持 `dsh-tool-web` 提供方无关的 30 秒默认值不变,以供自定义组合使用。[Web 能力 seam 决策](../architecture/2026-06-24-web-capability-seam.zh.md)负责公开抓取安全策略与 Web preset 默认值
+`packages/bundle/base/cordis.patch.yml` 明确挂载 `dsh-web`,配置 `searchProvider: deepseek-official` 与 `fetchProvider: http`,同时挂载 `dsh-web-search-deepseek`、`dsh-web-fetch-http`,并以 `searchTimeoutMs: 60000` 挂载 `dsh-tool-web`。[共享 base 的 Web 抓取默认值](2026-09-01-shared-base-web-fetch-default.zh.md)负责当前的 `fetch: true`;本文继续负责提供方选择、搜索凭据与超时。显式提供方 id 使选择不受注册顺序影响,同时个人覆盖层或 `--patch` 覆盖层仍可替换或禁用这些配置项。已交付的一分钟预算用于覆盖一次辅助 DeepSeek Messages 请求及服务端检索,同时保持 `dsh-tool-web` 提供方无关的 30 秒默认值不变,以供自定义组合使用。[Web 能力 seam 决策](../architecture/2026-06-24-web-capability-seam.zh.md)负责公开抓取安全策略。
 
 DeepSeek 搜索使用与官方会话适配器相同的 `DEEPSEEK_API_KEY` 凭据引用。提供方在每次搜索内部通过可选的 `ctx.credentials` 服务解析该引用;只有未挂载该 seam 的组合才会回退到启动进程的环境变量,非空的 `apiKey` 字面值仍作为程序化配置的最后兜底。因此,由 Web 的 Models 页存储或轮换的密钥无需重启即可用于下一次搜索,提供方也无需保留该值。由于 `WebSearchProvider.available()` 是同步方法,它会将已安装解析器视为本地可用;若动态凭据缺失,操作会以提供方专属错误码 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败,而稳定的工具 schema 仍保持注册。
 
@@ -30,8 +32,8 @@ DeepSeek 搜索使用与官方会话适配器相同的 `DEEPSEEK_API_KEY` 凭据
 
 **提高 `dsh-tool-web` 的提供方无关超时。** 不予采纳:自定义提供方和部署有各自不同的延迟预期;这一部署预算应归已交付的 DeepSeek 组合所有。
 
-**在每个共享 base surface 上启用抓取。** 不予采纳:共享 base 服务于网络策略不同的产品。它会挂载仅限公网的提供方,但保持工具按需启用;已交付的 Web preset 会有意启用该工具,其他产品则可以继续隐藏它或添加更严格的网络策略
+**在每个共享 base surface 上启用抓取。** 本文曾因各产品可能需要不同网络策略而否决该方案。已交付产品采用同一个完整工具集合后,[共享 base 的 Web 抓取默认值](2026-09-01-shared-base-web-fetch-default.zh.md)取代了该否决;仅限公开目的地址与无需逐次审批的约束仍然有效
 
 ## 后果
 
-每个共享 base surface 的原生模型请求都会携带 `web_search` schema 与搜索指引;Web/无头 PTC 模式 通过 `run_code` 公开相同的搜索能力。搜索会增加一次完整的辅助模型调用,并可能多次使用原生服务器工具;发起会话的日志仍可精确重建其不含密钥的请求。已交付的 Web `cordis`、`ptc` 与 `standard` preset 还会暴露 `web_fetch`,实施公开地址强制校验且无需逐次审批。Web 快照通道会启动已交付配置树,使用本地 Messages fixture(测试前置数据),经由真实 DeepSeek 提供方驱动一次回放的 `web_search` 调用,断言持久化的辅助请求与结构化结果,并固定最终浏览器呈现。组合冒烟测试会固定共享搜索清单与各 preset 的抓取选择;构建后组合配置的转储固定已交付的一分钟搜索预算;提供方测试固定缺失、已存储及已轮换凭据的行为,以及字面值与环境变量的兼容性。
+headless、完整 SDK、ACP 与仅使用 base 的自定义 profile 的原生模型请求都会携带 `web_search` 和 `web_fetch` schema 与指引;Web preset 会暴露同一对工具,PTC mode 还会通过 `run_code` 暴露它们。搜索会增加一次完整的辅助模型调用,并可能多次使用原生服务器工具;发起会话的日志仍可精确重建其不含密钥的请求。抓取会强制使用公开地址,并且无需逐次审批。Web 快照通道会启动已交付配置树,使用本地 Messages fixture(测试前置数据),经由真实 DeepSeek 提供方驱动一次回放的 `web_search` 调用,断言持久化的辅助请求与结构化结果,并固定最终浏览器呈现。共享 snapshot header 会固定通用的抓取 schema 与提示指引。组合冒烟测试会固定工具集合;构建后组合配置的转储固定已交付的一分钟搜索预算;提供方测试固定缺失、已存储及已轮换凭据的行为,以及字面值与环境变量的兼容性。

+ 2 - 2
.agents/notes/implemented/feature/2026-08-05-durable-web-schedule.i18n.yaml

@@ -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-durable-web-schedule.md
-2026-08-05-durable-web-schedule.md: bac4a5cfd8965032dad2cf689ca42b1f8e5da1e3
-2026-08-05-durable-web-schedule.zh.md: 266a90d3a56caccda56be9fe648c059939b8155f
+2026-08-05-durable-web-schedule.md: 1e7a343472cce638a7d1476871cec221fa9fd889
+2026-08-05-durable-web-schedule.zh.md: 50f3347a2c312e3c1a2ea28c582a277cc93a614e

+ 2 - 2
.agents/notes/implemented/feature/2026-08-05-durable-web-schedule.md

@@ -22,11 +22,11 @@ The user-visible boundary is `session-local`: the original Session runs an on-ti
 | Due while busy | Active create remains in the fold | Owner waits for idle maintenance, queues one follow-up, then appends dispatch | A later ordinary conversation turn |
 | Several Every records are overdue | Each active record retains its earliest unaccepted anchor-aligned target | One decision selects each record's latest occurrence and advances it past now | One ordinary follow-up containing one occurrence per record |
 | Process stopped or Session cold | Active create remains persisted | No timer or background scan; resume rebuilds the owner | Future target waits; overdue target is attempted |
-| Fork | Parent events remain in the inherited prefix | Child fold starts at `seedLength` | Parent work does not become active in the child |
+| Fork | Parent events remain in the inherited prefix | Child fold starts at the exact `inheritedEventCount` | Parent work does not become active in the child |
 
 ### Session-log authority and tools
 
-The version-1 `schedule/change` stream is the only durable Schedule authority. A create record owns a Session-local, non-reused branded id, the trimmed prompt, its rule discriminator, and UTC target. Delete and one-shot dispatch are terminal transitions. Every dispatch stores its id and decision time so the fold advances that record directly past missed occurrences. The strict decoder and pure fold reject unknown versions, extra fields, reused ids, mismatched dispatch shapes, and transitions against inactive records. A normal Session folds its complete stream; a fork folds only events at or after `SessionHeader.seedLength`.
+The version-1 `schedule/change` stream is the only durable Schedule authority. A create record owns a Session-local, non-reused branded id, the trimmed prompt, its rule discriminator, and UTC target. Delete and one-shot dispatch are terminal transitions. Every dispatch stores its id and decision time so the fold advances that record directly past missed occurrences. The strict decoder and pure fold reject unknown versions, extra fields, reused ids, mismatched dispatch shapes, and transitions against inactive records. A normal Session folds its complete stream; a fork folds only events at or after the `inheritedEventCount` passed into projection initialization.
 
 When `ctx.sessionProjections` exists, Schedule registers a strict unit that uses the same transition and publishes the complete active `ScheduleRecord[]`; the shared [projection state decision](../architecture/2026-08-19-session-projection-state-and-client-views.md) owns its initialization and restore contract. Corrupt durable input fails the existing read path rather than yielding a partial array. The browser-safe record vocabulary is exposed through the type-only `@deepseek-ai/dsh-schedule/client` subpath.
 

+ 2 - 2
.agents/notes/implemented/feature/2026-08-05-durable-web-schedule.zh.md

@@ -22,11 +22,11 @@ Status: implemented
 | 到期时繁忙 | 活动 create 仍在 fold 中 | owner 等待 idle maintenance,排入一个 follow-up,再追加 dispatch | 后续一个普通对话轮次 |
 | 多条 Every 记录逾期 | 每条活动记录都保留最早一个尚未接受且与锚点对齐的目标 | 一次决策选择每条记录的最新发生时点,并将其推进到当前时刻之后 | 一个普通 follow-up,其中每条记录各有一个发生时点 |
 | 进程停止或 Session cold | 活动 create 仍在 persistence 中 | 不存在 timer 或后台扫描;resume 重建 owner | 未来目标继续等待;overdue 目标会被尝试 |
-| fork | 父 event 留在继承前缀 | child fold 从 `seedLength` 开始 | 父工作不会在 child 中变为活动状态 |
+| fork | 父 event 留在继承前缀 | child fold 从精确 `inheritedEventCount` 开始 | 父工作不会在 child 中变为活动状态 |
 
 ### Session 日志权威与工具
 
-版本 1 `schedule/change` stream 是唯一持久的 Schedule 权威。create 记录拥有一个 Session 内不复用的品牌 id、trim 后的提示词、规则判别字段和 UTC 目标。delete 与一次性 dispatch 是终结转换。Every dispatch 会存储 id 与决策时点,使 fold 将该记录直接推进到错过的发生时点之后。严格 decoder 与纯 fold 会拒绝未知版本、额外字段、重复使用的 id、形状不匹配的 dispatch,以及针对非活动记录的转换。普通 Session 折叠完整 stream;fork 只折叠 `SessionHeader.seedLength` 位置及其后的 event。
+版本 1 `schedule/change` stream 是唯一持久的 Schedule 权威。create 记录拥有一个 Session 内不复用的品牌 id、trim 后的提示词、规则判别字段和 UTC 目标。delete 与一次性 dispatch 是终结转换。Every dispatch 会存储 id 与决策时点,使 fold 将该记录直接推进到错过的发生时点之后。严格 decoder 与纯 fold 会拒绝未知版本、额外字段、重复使用的 id、形状不匹配的 dispatch,以及针对非活动记录的转换。普通 Session 折叠完整 stream;fork 只折叠传入 projection 初始化的 `inheritedEventCount` 位置及其后的 event。
 
 `ctx.sessionProjections` 存在时,Schedule 会注册一个复用同一 transition 的严格单元,并发布完整的活动 `ScheduleRecord[]`;共享的 [projection state 决策](../architecture/2026-08-19-session-projection-state-and-client-views.zh.md)拥有其初始化与 restore 约定。损坏的持久输入会使既有读取路径失败,而不会产生部分数组。浏览器安全的记录词汇通过纯类型子路径 `@deepseek-ai/dsh-schedule/client` 暴露。
 

+ 0 - 60
.agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.md

@@ -1,60 +0,0 @@
-# Agent Note: The continuable child return channel is an obligation
-
-Status: implemented
-
-English | [中文](2026-08-06-continuable-child-report-obligation.zh.md)
-
-## Problem
-
-A continuable background child owns its own Session, so nothing it writes there reaches the agent that started it. [The report tool](2026-07-30-continuable-subagent-report-tool.md) gave that child a return channel and then presented it as one option among several: the schema said "call this zero or more times", nothing in the child's prompt asked it to call the tool at all, and the accepted default scheduling (`quiet`) added the report to a parked parent's next request without waking it.
-
-Each of those choices is defensible alone. Together they made the return channel unusable as a delegation contract. A child that finished its work, wrote its answer into its own transcript, and stopped left the parent with nothing; a child that did report reached a parent that had already parked and would not read the report until something unrelated woke it. External reports of parents busy-polling `list_agents`, re-sending messages to settled children, and abandoning `subagent` for `workflow` all reduce to the same missing guarantee.
-
-## Decision
-
-The return channel is an instruction the child receives, not a capability it may discover. The report package installs two scope-local registrations into every continuable in-process child, and one disposer revokes both:
-
-- the `report` tool, whose description now states that the child calls it once before finishing with a self-contained final result, and earlier for progress that changes what the parent should do next;
-- a `tool:report` system-prompt section at first-party order 2900 carrying the same obligation in the child's own voice, so a child that never reads tool descriptions closely still receives it.
-
-`reportDelivery` defaults to `next-step`. An accepted report wakes a parked parent driver or joins a running parent's nearest step boundary, matching the instruction to report findings that change the parent's next action. `quiet` remains available for deployments that prefer unread reports over model-work amplification. The [report/settlement ordering decision](../bug-fix/2026-08-17-subagent-report-settlement-ordering.md) owns the scheduling rationale.
-
-### Why the section and the description both exist
-
-They address different failure modes. The tool description is read when the model is already considering `report`; the prompt section is read when it is deciding whether it is finished. The obligation belongs at both points because the failure this fixes — a child that simply stops — happens at the second one.
-
-The section is registered on the child's own scope, the same mechanism [child composition](../../../../packages/subagent/subagent/src/child-agent.ts) already uses for a shadowing persona, so the parent and every sibling see neither the tool nor the guidance. `installReportTool` rolls the section back if tool registration fails, and its returned disposer attempts both revocations before surfacing cleanup failures.
-
-### Instruction, not enforcement
-
-Nothing rejects a child that never reports. No runtime path inspects whether a report was sent, and `report` still accepts zero or many calls per turn. The change is model-facing wording plus a scheduling default; the service authority, acknowledgement, and recovery contracts are unchanged.
-
-That boundary is deliberate: prompt text can only reach a child that is still running its own loop. A child stopped by an error, a token ceiling, cancellation, or teardown never gets the chance to comply, which is why the runtime keeps its own account of settlement rather than trusting this instruction ([manager-owned settlement delivery](2026-08-06-manager-owned-subagent-settlement-delivery.md)).
-
-### Snapshot coverage
-
-The assembled ACP `subagent-report` scenario exercises the shipped default: the child reports while the parent is in maintenance, the later settlement notice queues behind it, and the resumed parent claims the next-step report before next-turn settlement. Because the child's scope composes a prompt the class pin cannot describe, the snapshot harness has `pinsChildSystemPrompts`, the exact counterpart of `pinsChildToolSchemas`: it moves one child fixture's prompt into `system-prompt.<n>.expected.md`, leaves every other request-header field to the class pin, requires the sidecar exactly when declared, and rejects a sidecar identical to that class pin so a redundant copy cannot drift.
-
-## Alternatives considered
-
-**Keep `quiet` as the default and rely on the prompt alone.** This was the shipped position, and it supersedes nothing on its own: a report the parent never reads is indistinguishable from a report never sent. The [report-tool note's](2026-07-30-continuable-subagent-report-tool.md) rejection of always-waking assumed the parent had another reason to look at its context; a parked background coordinator does not. Turn amplification is the real cost, and it is now the reason `quiet` still exists rather than the reason it is the default.
-
-**Let the child choose the delivery mode per call.** Unchanged from the original rejection: the model would own scheduler pressure, and behavior would vary per call rather than per deployment.
-
-**Put the obligation only in the tool description.** A description is read while choosing among tools. The child this change targets is not choosing a tool; it believes it is done. Prompt guidance is the surface that reaches that decision.
-
-**Enforce the obligation at settlement by rejecting a silent child.** There is nothing to reject: by the time settlement is observable the child's loop is over, and failing its teardown would destroy work rather than deliver it. Delivering the terminal facts unconditionally from the runtime is the answer to that case, and it belongs to the continuation manager, not to this package.
-
-## Consequences
-
-- Every continuable in-process child with this package loaded carries one extra prompt section and a longer `report` description in every request; no other Agent's request changes.
-- The default deployment wakes the parent once per accepted report. A nested tree that reports frequently consumes extra parent requests, while reports waiting together share one step; `quiet` is the documented escape.
-- `installReportTool` requires `ctx.systemPrompt` in the child scope, so the package declares `systemPrompt` in `inject` and fails at load rather than at the next child materialization.
-- Unit coverage pins the new default, two load-bearing instruction phrases, the section's child-only scope against both the parent and a sibling, and rollback or revocation of both registrations.
-- Three assembled ACP scenarios with continuable children pin the complete instruction text through the new sidecar; a future change to any child-scoped section fails those scenarios instead of passing silently.
-
-### Accepted risks
-
-Next-step delivery by default amplifies model work in deep trees. The deployment owns that through `reportDelivery`; reports waiting together share one step, and one accepted report causes at most one wake.
-
-A child can still finish without reporting, and this change cannot detect it. Only the runtime's own [settlement account](2026-08-06-manager-owned-subagent-settlement-delivery.md) closes that case.

+ 0 - 60
.agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.zh.md

@@ -1,60 +0,0 @@
-# Agent Note: 可继续 child 的返回通道是一项义务
-
-Status: implemented
-
-[English](2026-08-06-continuable-child-report-obligation.md) | 中文
-
-## 问题
-
-可继续后台 child 拥有自己的 Session,因此它写在那里的任何内容都不会到达启动它的 agent。[report 工具](2026-07-30-continuable-subagent-report-tool.zh.md)为该 child 提供了一条返回通道,却把它呈现为若干选项之一:schema 里写着「可调用零次或多次」,child 的提示词中没有任何地方要求它调用该工具,而已采纳的默认调度(`quiet`)会把报告加入已停驻 parent 的下一次请求,却不唤醒它。
-
-这些选择单独看都站得住脚。合在一起,它们让这条返回通道无法作为委派契约使用。一个完成工作、把答案写进自己 transcript(文本记录)随后停止的 child,会让 parent 一无所获;而确实上报了的 child,面对的是一个已经停驻、要等到别的事件把它唤醒才会读到报告的 parent。外部反馈中的 parent 忙轮询 `list_agents`、反复向已结算 child 发送消息、以及放弃 `subagent` 改用 `workflow`,都可归结为同一处缺失的保证。
-
-## 决策
-
-返回通道是 child 收到的一条指令,而不是它需要自行发现的能力。report 包会向每个可继续进程内 child 安装两项作用域局部注册,并由同一个 disposer 撤销两者:
-
-- `report` 工具,其描述现在说明 child 要在结束前调用一次并给出自足的最终结果,并在部分进展会改变 parent 下一步动作时提前调用;
-- 一个 first-party order 为 2900 的 `tool:report` 系统提示词 section,用 child 自己的语气承载同一条义务,使从不细读工具描述的 child 仍能收到它。
-
-`reportDelivery` 的默认值为 `next-step`。一条被接受的报告会唤醒停驻的 parent driver,或加入运行中 parent 最近的 step 边界,与发现会改变 parent 下一步动作时上报的指令一致。对于宁可让报告无人阅读也要避免模型工作量放大的部署,`quiet` 依旧可用。[报告与结算顺序决策](../bug-fix/2026-08-17-subagent-report-settlement-ordering.zh.md)负责调度理由。
-
-### 为什么 section 与描述同时存在
-
-两者针对不同的失效模式。工具描述是在模型已经在考虑 `report` 时被读到的;提示词 section 是在它判断自己是否已经完成时被读到的。这条义务必须同时出现在两处,因为本次修复的失效——child 直接停下——发生在第二处。
-
-该 section 注册在 child 自己的作用域上,与[child 组合](../../../../packages/subagent/subagent/src/child-agent.ts)为遮蔽式 persona 已经使用的机制相同,因此 parent 与所有同级都看不到该工具与该指引。工具注册失败时,`installReportTool` 会回滚该 section;它返回的 disposer 会先尝试撤销两项注册,再抛出清理失败。
-
-### 是指令,不是强制
-
-没有任何东西会拒绝一个从不上报的 child。没有任何运行时路径会检查是否发送过报告,`report` 仍接受一个轮次中调用零次或多次。本次改动是面向模型的措辞加上一个调度默认值;服务权限、确认与恢复契约都保持不变。
-
-这条边界是刻意划定的:提示词文本只能到达仍在运行自身循环的 child。被错误、token 上限、取消或拆卸终止的 child 根本没有机会遵守,因此运行时会自己记录结算这件事,而不是信任这条指令(见[由管理器负责的结算投递](2026-08-06-manager-owned-subagent-settlement-delivery.zh.md))。
-
-### 快照覆盖
-
-整体组装的 ACP `subagent-report` 场景演练随附的默认行为:child 在 parent 处于 maintenance 时上报,稍后的结算通知排在其后,而恢复的 parent 会先领取 next-step 报告、再领取 next-turn 结算。由于该 child 的作用域组合出类别 pin 无法描述的提示词,快照 harness 提供 `pinsChildSystemPrompts`,它与 `pinsChildToolSchemas` 完全对称:把一个 child fixture 的提示词移入 `system-prompt.<n>.expected.md`,其余请求 header 字段仍归类别 pin 所有,要求 sidecar 恰好在声明时存在,并拒绝与该类别 pin 完全相同的 sidecar,使冗余副本无法悄悄漂移。
-
-## 备选方案
-
-**保留 `quiet` 作为默认值,只依赖提示词。** 这曾是随附的立场,而它本身什么也没有解决:一条 parent 从不阅读的报告,与一条从未发送的报告无法区分。[report 工具 Agent Note](2026-07-30-continuable-subagent-report-tool.zh.md)对「始终唤醒」的否决,前提是 parent 还有别的理由去查看自己的上下文;已停驻的后台协调者并没有。轮次放大才是真正的代价,而它现在是 `quiet` 仍然保留的理由,而不是它作为默认值的理由。
-
-**让 child 按调用选择投递模式。** 与最初的否决相同:模型将掌握调度压力,行为也会随调用而非随部署变化。
-
-**只把义务写在工具描述里。** 描述是在从多个工具中选择时被读到的。本次改动针对的 child 并不在选择工具,它认为自己已经做完了。提示词指引才是能触及该判断的界面。
-
-**在结算时拒绝沉默的 child,以此强制该义务。** 没有什么可以拒绝:当结算可被观察时 child 的循环已经结束,让它的拆卸失败只会毁掉工作而不会送达结果。由运行时无条件投递终止事实才是这一情形的答案,而它属于继续执行管理器,不属于本包。
-
-## 后果
-
-- 加载本包后,每个可继续进程内 child 的每次请求都会多出一个提示词 section 和一段更长的 `report` 描述;其他任何 Agent 的请求都不变。
-- 默认部署会为每条被接受的报告唤醒 parent 一次。频繁上报的嵌套树会消耗额外的 parent 请求,而一起等待的报告会共享一个 step;`quiet` 是有文档记载的退路。
-- `installReportTool` 需要 child 作用域中的 `ctx.systemPrompt`,因此本包在 `inject` 中声明 `systemPrompt`,从而在加载时失败,而不是等到下一次 child 物化时。
-- 单元覆盖固定了新默认值、两处关键指令措辞、该 section 相对 parent 与同级均仅限 child 的作用域,以及两项注册在安装回滚或撤销时的清理。
-- 三个带可继续 child 的整体组装 ACP 场景通过新的 sidecar 逐字固定完整的 child 提示词;今后任何对 child 作用域 section 的改动都会让这些场景失败,而不是悄悄通过。
-
-### 已接受的风险
-
-默认 next-step 投递会在深层树中放大模型工作量。部署通过 `reportDelivery` 掌握该取舍;一起等待的报告会共享一个 step,且每条被接受的报告至多产生一次唤醒。
-
-child 仍可能不上报就结束,本次改动无法检测这一点。只有运行时自己的[结算记账](2026-08-06-manager-owned-subagent-settlement-delivery.zh.md)才能补上这一情形。

+ 2 - 2
.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.i18n.yaml

@@ -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-06-manager-owned-subagent-settlement-delivery.md
-2026-08-06-manager-owned-subagent-settlement-delivery.md: 27daa6d5150950efb50bf23dea945498651d2c09
-2026-08-06-manager-owned-subagent-settlement-delivery.zh.md: 1e3b866204d54e26b87061153fe97ffaed369141
+2026-08-06-manager-owned-subagent-settlement-delivery.md: 7dfa05c247ee0efb71963e057731c7ff6a5a1989
+2026-08-06-manager-owned-subagent-settlement-delivery.zh.md: 6bd8836d966c85ee0ef08ec19bbf96a4f72b0dd4

+ 9 - 9
.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.md

@@ -8,7 +8,7 @@ English | [中文](2026-08-06-manager-owned-subagent-settlement-delivery.zh.md)
 
 Continuable background delegation was the one asynchronous operation a model could start but could not reach the end of. Every other shape has a retrieval primitive or a return value: a background bash command and a one-shot background subagent both settle through a Task that `job_output(wait: true)` can block on, a workflow and a foreground subagent return their result to the caller. A continuable background child returned only its durable id, and nothing existed that a parent could wait on or would be handed.
 
-[The report obligation](2026-08-06-continuable-child-report-obligation.md) closed the cooperative half of that gap by instructing the child to report before it finishes. Instruction cannot close the rest. A child stopped by a token ceiling, a model failure, cancellation, or teardown never reaches the point where it could comply — not rarely, but never — and those are precisely the endings a waiting parent most needs to hear about. The observable downstream symptoms were parents busy-polling `list_agents`, re-sending messages to children that had already settled, and deployments abandoning `subagent` for `workflow` because a workflow at least returns something.
+Child-authored messages close the cooperative half of that gap: a child can send progress and a final handoff to its direct parent. Model choice cannot close the rest. A child stopped by a token ceiling, a model failure, cancellation, or teardown may never send such a message, and those are precisely the endings a waiting parent most needs to hear about. The observable downstream symptoms were parents busy-polling `list_agents`, re-sending messages to children that had already settled, and deployments abandoning `subagent` for `workflow` because a workflow at least returns something.
 
 The signal already existed. `subagent/end` has carried `stopReason` and `lastAssistantMessage` since continuable Activations shipped. What was missing was any consumer that turned it into context the parent's model could see.
 
@@ -20,7 +20,7 @@ When a resident Activation settles, `notifySettlement()` resolves the child's du
 
 ### Provenance
 
-The notice carries `{ kind: 'subagent-settled', form: 'notice', summary, senderSessionId }`. It is deliberately not the existing `subagent-report` kind. A report is content the child chose; this is the runtime stating what became of the child. Merging them would credit the child with words it never wrote, and would make a durable log unable to distinguish "the child said it was done" from "the harness observed that it stopped". The `notice` form also gives a UI the collapsed one-line presentation this message wants, where `relay` would present it as correspondence.
+The notice carries `{ kind: 'subagent-settled', form: 'notice', summary, senderSessionId }`. It is deliberately not the `agent-message` kind used by `send_message`. An Agent message is content the child chose; this is the runtime stating what became of the child. Merging them would credit the child with words it never wrote, and would make a durable log unable to distinguish "the child said it was done" from "the harness observed that it stopped". The `notice` form also gives a UI the collapsed one-line presentation this message wants, where `relay` presents Agent correspondence.
 
 ### Two ordering rules, and why the manager owns them
 
@@ -56,13 +56,13 @@ Both matter past the notice: `subagent/end` carries `stopReason` to the jsonrpc
 
 ### Snapshot coverage
 
-Three assembled ACP scenarios cover the notice: a child that never reports, a child that reports first, and a child driven through several follow-up turns. All three needed an explicit fence. The notice arrives once the child's teardown finishes, which races whatever the parent is already doing, so each scenario holds the child behind the parent's spawn turn and then waits for the parent turn the notice opens (`waitForTurnStart` at that turn, then `waitForTurnEnd`) before the script continues. Waiting for a turn the run is not fenced to produce is not coverage: it is a timeout when the notice lands in the turn already running instead.
+Three assembled ACP scenarios cover the notice: a child that sends no message, a child that sends a message first, and a child driven through several Agent-message turns. All three need an explicit fence. The notice arrives once the child's teardown finishes, which races whatever the parent is already doing, so each scenario holds the child behind the parent's spawn turn and then waits for the parent turn the notice opens (`waitForTurnStart` at that turn, then `waitForTurnEnd`) before the script continues. Waiting for a turn the run is not fenced to produce is not coverage: it is a timeout when the notice lands in the turn already running instead.
 
 `subagent-continuable` is the one that pins a failure. Its child's last turn dies on the forced durability checkpoint without entering a step, so that transcript is where the stop-reason rule above is visible end to end: the notice says the child *failed*, carries the earlier `SECOND_OK` as its last content rather than as a result, and the parent's own acknowledgement turn reaches the ACP client.
 
-A keyless headless Loader snapshot covers the user-visible path end to end. Its replay parent omits `run_in_background` to exercise the continuable background default, never calls `list_agents`, `send_message`, or Task tools, consumes the manager-authored `subagent-settled` notice, and produces its final answer. The child never calls `report`, so the transcript cannot pass through the cooperative report path. A test-only Loader fence holds the parent's post-spawn request until the real manager notice enters its inbox, removing platform scheduling from the transcript without synthesizing the notice.
+A keyless headless Loader snapshot covers the user-visible path end to end. Its replay parent omits `run_in_background` to exercise the continuable background default, never calls `list_agents`, `send_message`, or Task tools, consumes the manager-authored `subagent-settled` notice, and produces its final answer. The child sends no Agent message, so the transcript depends only on the runtime notice. A test-only Loader fence holds the parent's post-spawn request until the real manager notice enters its inbox, removing platform scheduling from the transcript without synthesizing the notice.
 
-The `subagent-report` scenario uses the default next-step report delivery. A snapshot-only fence holds the child until the parent's spawn turn ends, then holds the parent in maintenance until settlement follows the report. The resumed parent claims the next-step report before the queued next-turn settlement. The [report/settlement ordering decision](../bug-fix/2026-08-17-subagent-report-settlement-ordering.md) owns this cross-state ordering.
+The `subagent-send-message` scenario holds the child until the parent's spawn turn ends, then holds the parent in maintenance until settlement follows the child-authored message. The resumed parent claims the next-step Agent message before the queued next-turn settlement. The [message/settlement ordering decision](../bug-fix/2026-08-17-subagent-message-settlement-ordering.md) owns this cross-state ordering.
 
 The refusal and interruption wordings are pinned verbatim in unit tests rather than in a replayed transcript: producing them needs a rejecting policy plugin or a cancellation fenced at a step boundary, which the keyless assemblies do not otherwise carry, and the assembled scenarios already pin the notice pathway itself end to end.
 
@@ -72,7 +72,7 @@ The refusal and interruption wordings are pinned verbatim in unit tests rather t
 
 **Attach an external `subagent/end` listener.** Rejected on three counts above — no parent in the payload, a disposed child handle, and an ordering the listener cannot influence. A listener would also have to be strictly synchronous to beat the release, and nothing at that seam enforces it, so the correct version would be correct only by accident.
 
-**Deliver only when the child did not report.** This was the first design. It needs per-Activation bookkeeping, still misses the child that reported progress and then died before its result, and — decisively — makes the parent-facing promise conditional. "Usually you are told" is not a contract a tool description can state, and a model that cannot rely on the notice will poll anyway.
+**Deliver only when the child sent no message.** This was the first design. It needs per-Activation bookkeeping, still misses the child that sent progress and then died before its result, and — decisively — makes the parent-facing promise conditional. "Usually you are told" is not a contract a tool description can state, and a model that cannot rely on the notice will poll anyway.
 
 **Make delivery configurable.** A deployment switch would return the model-facing text to "usually", which is the failure this change exists to remove. Protocol constants and safety invariants stay fixed; this is one of them.
 
@@ -87,8 +87,8 @@ The refusal and interruption wordings are pinned verbatim in unit tests rather t
 - `Activation` carries `parentSession` and `announced`. The first exists because the child handle is disposed before delivery; the second is what keeps a rolled-back materialization silent.
 - `foldConsumedWork()` replaces `dsh-session`'s `findLastMessageTurnEnd()` and moves to `dsh-agent`, which owns the inbox marker it reads; the one-shot in-process path folds the same answer and does not classify a cut-short one-shot child as `completed`.
 - Unit coverage pins the unconditional contract, each terminal reason, idle and busy scheduling, the batch, the maintenance regression, the pre-release ordering, a parent that is gone, and a rejected send that must not fail teardown.
-- Three ACP scenarios use an explicit settlement fence, and `subagent-report` pins the default report-before-settlement next-step order.
-- A keyless headless Loader snapshot pins background start → manager-authored settlement notice → final parent answer with no polling or child `report` call.
+- Three ACP scenarios use an explicit settlement fence, and `subagent-send-message` pins the Agent-message-before-settlement next-step order.
+- A keyless headless Loader snapshot pins background start → manager-authored settlement notice → final parent answer with no polling or child-authored message.
 
 ### Accepted risks
 
@@ -100,4 +100,4 @@ Stop-reason attribution is a best effort over the log's existing splice vocabula
 
 Turn amplification is real for deep or wide trees, and it is not configurable by design. The step-boundary batch bounds it for simultaneous settlement but not for children that settle apart.
 
-Reports and their later settlement notices are ordered through the parent's next-step FIFO. Independent settlements from sibling children retain their actual delivery order rather than a synthetic sibling ordering.
+Agent messages and their later settlement notices are ordered through the parent's next-step FIFO. Independent settlements from sibling children retain their actual delivery order rather than a synthetic sibling ordering.

+ 9 - 9
.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.zh.md

@@ -8,7 +8,7 @@ Status: implemented
 
 可继续后台委派是模型唯一一种能够发起、却无法抵达终点的异步操作。其他每一种形态都有取回原语或返回值:后台 bash 命令与一次性后台 subagent 都通过 Task 结算,`job_output(wait: true)` 可以阻塞等待;workflow 与前台 subagent 会把结果返回给调用方。可继续后台 child 只返回它持久化的 id,而父级既没有可等待的对象,也不会被交付任何东西。
 
-[报告义务](2026-08-06-continuable-child-report-obligation.zh.md)通过要求 child 在结束前上报,补上了这一缺口中协作的那一半。指令无法补上其余部分。被 token 上限、模型失败、取消或拆卸终止的 child 永远走不到能够遵守的那一步——不是很少,而是从不——而这些恰恰是等待中的父级最需要被告知的结束方式。可观察到的下游症状包括:父级忙轮询 `list_agents`、向已经结算的 child 反复发送消息,以及部署放弃 `subagent` 转用 `workflow`,因为 workflow 至少会返回点什么。
+由 child 编写的消息补上了这一缺口中协作的那一半:child 可以向直接 parent 发送进度与最终交接。模型选择无法补上其余部分。被 token 上限、模型失败、取消或拆卸终止的 child 可能永远不会发送这种消息,而这些恰恰是等待中的父级最需要被告知的结束方式。可观察到的下游症状包括:父级忙轮询 `list_agents`、向已经结算的 child 反复发送消息,以及部署放弃 `subagent` 转用 `workflow`,因为 workflow 至少会返回点什么。
 
 信号本身早就存在。自可继续 Activation 发布以来,`subagent/end` 就一直携带 `stopReason` 与 `lastAssistantMessage`。缺的是把它变成父级模型能看到的上下文的那个消费者。
 
@@ -20,7 +20,7 @@ Status: implemented
 
 ### 来源信息
 
-该通知携带 `{ kind: 'subagent-settled', form: 'notice', summary, senderSessionId }`,刻意不复用既有的 `subagent-report` kind。上报是 child 选择的内容;这条消息则是运行时在陈述这个 child 后来怎样了。把两者合并会把 child 从未写过的话算到它头上,也会让持久化日志无法区分「child 说它做完了」和「harness 观察到它停下了」。`notice` 形态还为 UI 提供了这条消息想要的折叠单行呈现,而 `relay` 会把它呈现为往来信件
+该通知携带 `{ kind: 'subagent-settled', form: 'notice', summary, senderSessionId }`,刻意不复用 `send_message` 使用的 `agent-message` kind。Agent 消息是 child 选择的内容;这条消息则是运行时在陈述这个 child 后来怎样了。把两者合并会把 child 从未写过的话算到它头上,也会让持久化日志无法区分「child 说它做完了」和「harness 观察到它停下了」。`notice` 形态还为 UI 提供了这条消息想要的折叠单行呈现,而 `relay` 会把 Agent 往来消息呈现为通信
 
 ### 两条顺序规则,以及为什么归管理器所有
 
@@ -56,13 +56,13 @@ Status: implemented
 
 ### 快照覆盖
 
-三个整体组装的 ACP 场景覆盖该通知:一个从不上报的 child、一个先上报的 child,以及一个被多轮 follow-up 驱动的 child。三者都需要显式栅栏。通知在 child 拆卸完成后才到达,会与父级当时正在做的事竞争,因此每个场景都会把 child 保持到父级启动轮次结束,再等待该通知开启的那个父级轮次(先 `waitForTurnStart` 到该轮次,再 `waitForTurnEnd`),然后脚本才继续。等待一个运行并未被栅栏保证会产生的轮次不算覆盖:一旦通知落进已经在跑的那个轮次,它就是一次超时。
+三个整体组装的 ACP 场景覆盖该通知:一个不发送消息的 child、一个先发送消息的 child,以及一个被多轮 Agent 消息驱动的 child。三者都需要显式栅栏。通知在 child 拆卸完成后才到达,会与父级当时正在做的事竞争,因此每个场景都会把 child 保持到父级启动轮次结束,再等待该通知开启的那个父级轮次(先 `waitForTurnStart` 到该轮次,再 `waitForTurnEnd`),然后脚本才继续。等待一个运行并未被栅栏保证会产生的轮次不算覆盖:一旦通知落进已经在跑的那个轮次,它就是一次超时。
 
 `subagent-continuable` 是其中固定失败结局的那个。它的 child 最后一个轮次在被强制的持久化检查点上死亡,且未进入任何 step,因此该 transcript 正是上面那条终止原因规则的端到端可见之处:通知说该 child **失败**,把此前的 `SECOND_OK` 作为它最后产出的内容而非结果携带,而父级自己的确认轮次会到达 ACP 客户端。
 
-另有一个无密钥的 headless Loader 快照端到端覆盖用户可见路径。其重放父级省略 `run_in_background` 以覆盖可继续后台默认路径,从不调用 `list_agents`、`send_message` 或 Task 工具,消费管理器写入的 `subagent-settled` 通知,并给出最终答案。child 从不调用 `report`,因此该 transcript 不可能经由协作式上报路径通过。一个仅用于测试的 Loader 栅栏会把父级启动后的请求保持到真实管理器通知进入其 inbox 为止,从 transcript 中排除平台调度差异,但不会伪造该通知。
+另有一个无密钥的 headless Loader 快照端到端覆盖用户可见路径。其重放父级省略 `run_in_background` 以覆盖可继续后台默认路径,从不调用 `list_agents`、`send_message` 或 Task 工具,消费管理器写入的 `subagent-settled` 通知,并给出最终答案。child 不发送 Agent 消息,因此该 transcript 只依赖运行时通知。一个仅用于测试的 Loader 栅栏会把父级启动后的请求保持到真实管理器通知进入其 inbox 为止,从 transcript 中排除平台调度差异,但不会伪造该通知。
 
-`subagent-report` 场景使用默认 next-step 报告投递。一个仅用于快照的围栏会让 child 等到 parent 的派生轮次结束,随后让 parent 保持 maintenance,直至结算跟在报告之后到达。恢复的 parent 会先领取 next-step 报告、再领取排队的 next-turn 结算。[报告与结算顺序决策](../bug-fix/2026-08-17-subagent-report-settlement-ordering.zh.md)负责说明这种跨状态顺序。
+`subagent-send-message` 场景会让 child 等到 parent 的派生轮次结束,随后让 parent 保持 maintenance,直至结算跟在 child 编写的消息之后到达。恢复的 parent 会先领取 next-step Agent 消息、再领取排队的 next-turn 结算。[消息与结算顺序决策](../bug-fix/2026-08-17-subagent-message-settlement-ordering.zh.md)负责说明这种跨状态顺序。
 
 拒绝与中断两种措辞在单元测试中逐字钉死,而不进入重放 transcript:触发它们需要一个会拒绝的策略插件、或一次在 step 边界被栅栏卡住的取消,而无密钥组装本身并不携带这些;通知通路本身已由整体组装场景端到端钉住。
 
@@ -72,7 +72,7 @@ Status: implemented
 
 **挂一个外部 `subagent/end` listener。** 因上文三点被否决——payload 里没有父级、child handle 已被 dispose,以及 listener 无法影响的顺序。listener 还必须严格同步才能抢在释放之前,而该 seam 上没有任何东西强制这一点,因此正确的版本只能靠碰巧正确。
 
-**仅在 child 没有上报时投递。** 这是最初的设计。它需要按 Activation 记账,仍会漏掉「报了进度、随后在给出结果前死掉」的 child,而且最关键的是:它让面向父级的承诺变成有条件的。「通常你会被告知」不是工具描述能陈述的契约,而无法依赖该通知的模型无论如何都会去轮询。
+**仅在 child 没有发送消息时投递。** 这是最初的设计。它需要按 Activation 记账,仍会漏掉「发送了进度、随后在给出结果前死掉」的 child,而且最关键的是:它让面向父级的承诺变成有条件的。「通常你会被告知」不是工具描述能陈述的契约,而无法依赖该通知的模型无论如何都会去轮询。
 
 **把投递做成可配置。** 部署开关会把面向模型的文本重新变回「通常」,而这正是本次改动要消除的失效。协议常量与安全不变量保持固定;这就是其中之一。
 
@@ -87,8 +87,8 @@ Status: implemented
 - `Activation` 携带 `parentSession` 与 `announced`。前者存在是因为 child handle 在投递前已被 dispose;后者让被回滚的物化保持静默。
 - `foldConsumedWork()` 取代 `dsh-session` 的 `findLastMessageTurnEnd()`,并迁移到 `dsh-agent`——它拥有该 fold 所读取的 inbox 标记;一次性 in-process 路径折叠同一个答案,不会把被中途切断的一次性 child 归类为 `completed`。
 - 单元覆盖固定了无条件约定、每种终止原因、空闲与繁忙两种调度、批量语义、维护期回归、释放前顺序、父级已消失,以及一次不得让拆卸失败的发送被拒。
-- 三个 ACP 场景使用显式的结算围栏,`subagent-report` 固定默认的报告先于结算的 next-step 顺序。
-- 一个无密钥的 headless Loader 快照固定了「后台启动 → 管理器写入的结算通知 → 父级最终答案」路径,其中没有轮询,也没有 child `report` 调用
+- 三个 ACP 场景使用显式的结算围栏,`subagent-send-message` 固定 Agent 消息先于结算的 next-step 顺序。
+- 一个无密钥的 headless Loader 快照固定了「后台启动 → 管理器写入的结算通知 → 父级最终答案」路径,其中没有轮询,也没有 child 编写的消息
 
 ### 已接受的风险
 
@@ -100,4 +100,4 @@ Status: implemented
 
 对于深或宽的树,轮次放大是真实存在的,而且按设计不可配置。step 边界的批量语义只能约束同时结算的情形,无法约束分散结算的 child。
 
-报告与其稍后的结算通知通过 parent 的 next-step FIFO 排序。来自同级 child 的独立结算保留其实际投递顺序,不会虚构同级间的顺序。
+Agent 消息与其稍后的结算通知通过 parent 的 next-step FIFO 排序。来自同级 child 的独立结算保留其实际投递顺序,不会虚构同级间的顺序。

+ 2 - 2
.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.i18n.yaml

@@ -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-10-continuable-subagent-policy-inheritance.md
-2026-08-10-continuable-subagent-policy-inheritance.md: a33a211747cf19cd465c56b8bda6ebfe1d7b6e06
-2026-08-10-continuable-subagent-policy-inheritance.zh.md: af43bbdc10c926b5255e8e5af98f7ea950b60091
+2026-08-10-continuable-subagent-policy-inheritance.md: e567c0557be45ba9e0e0515d742bd6f99a5f10e7
+2026-08-10-continuable-subagent-policy-inheritance.zh.md: 5c9cc677e9154d5b14214979283c00fd28a4c2df

+ 2 - 2
.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.md

@@ -6,7 +6,7 @@ English | [中文](2026-08-10-continuable-subagent-policy-inheritance.zh.md)
 
 ## Problem
 
-The one-shot in-process driver has seeded parent sandbox/approval overrides into its children since the [in-process policy-inheritance decision](2026-07-25-subagent-policy-inheritance.md), but the continuable path never did: `SubagentContinuationManager` materialization applied only child composition and the activation setup registry. The default bundle wires both delegation tools as `backgroundMode: continuable`, so in a default deployment every background child silently fell back to deployment defaults — a parent switched to `danger-full-access` produced children stuck at `workspace-write` whose every out-of-workspace operation raised an approval prompt, and a parent's unattended `'never'` approval stance reverted to prompting ([dsh-external/issues#334](https://github.com/dsh-external/issues/issues/334)).
+The one-shot in-process driver has seeded parent sandbox/approval overrides into its children since the [in-process policy-inheritance decision](2026-07-25-subagent-policy-inheritance.md), but the continuable path never did: `SubagentContinuationManager` materialization applied only child composition. The default bundle wires both delegation tools as `backgroundMode: continuable`, so in a default deployment every background child silently fell back to deployment defaults — a parent switched to `danger-full-access` produced children stuck at `workspace-write` whose every out-of-workspace operation raised an approval prompt, and a parent's unattended `'never'` approval stance reverted to prompting ([dsh-external/issues#334](https://github.com/dsh-external/issues/issues/334)).
 
 ## Decision
 
@@ -16,7 +16,7 @@ The capture/append pair moved from the one-shot driver into the seam's shared ch
 
 ## Alternatives considered
 
-- **An activation-setup-registry contribution** (`registerContinuableSetup`) — rejected: a contribution receives only the child context, so it cannot capture the parent's overrides at the delegation boundary; the registry applies on cold resume as well as fresh creation, which would re-append or re-capture; and nothing ties a contribution's capture to the start call's synchronous prefix, so the pre-await capture guarantee would be lost.
+- **A generic child-setup contribution** — rejected: a contribution receives only the child context, so it cannot capture the parent's overrides at the delegation boundary; applying it on cold resume as well as fresh creation would re-append or re-capture; and nothing ties its capture to the start call's synchronous prefix, so the pre-await capture guarantee would be lost.
 - **Re-capturing the parent's overrides at cold resume** — rejected: a resumed child would silently change policy with the parent's later switches, breaking the snapshot-at-delegation semantic and making effective policy depend on resume timing instead of the child's own log. A parent that wants a resumed child under new policy re-delegates.
 - **Importing the one-shot driver's inline logic from the continuation manager** — rejected: the Service Definition package cannot depend on its own provider package, and duplicating the capture/append pair in `continuation.ts` invites drift; `child-agent.ts` already holds every other shared composition step.
 - **Seeding the events into the descriptor seed turn** — rejected: the capture value is not known when the seed is assembled for every caller, and the one-shot precedent already establishes unpublished-setup appends as the ordering that places inherited facts after fork history with `firstLiveSeq` intact.

+ 2 - 2
.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.zh.md

@@ -6,7 +6,7 @@ Status: implemented
 
 ## 问题
 
-自[进程内策略继承决策](2026-07-25-subagent-policy-inheritance.zh.md)以来,一次性进程内驱动器一直会把父级的沙箱/审批覆盖项注入其子级,但可继续路径从未这样做:`SubagentContinuationManager` 的物化只应用子级组合与 Activation(激活)设置注册表。默认组合包把两个委派工具都配置为 `backgroundMode: continuable`,因此在默认部署中,每个后台子 agent(智能体)都静默回退到部署默认值:切换到 `danger-full-access` 的父级产出的子 agent 卡在 `workspace-write`,每次工作区外操作都会触发审批提示;父级无人值守的 `'never'` 审批立场也退回为发起提示的行为([dsh-external/issues#334](https://github.com/dsh-external/issues/issues/334))。
+自[进程内策略继承决策](2026-07-25-subagent-policy-inheritance.zh.md)以来,一次性进程内驱动器一直会把父级的沙箱/审批覆盖项注入其子级,但可继续路径从未这样做:`SubagentContinuationManager` 的物化只应用子级组合。默认组合包把两个委派工具都配置为 `backgroundMode: continuable`,因此在默认部署中,每个后台子 agent(智能体)都静默回退到部署默认值:切换到 `danger-full-access` 的父级产出的子 agent 卡在 `workspace-write`,每次工作区外操作都会触发审批提示;父级无人值守的 `'never'` 审批立场也退回为发起提示的行为([dsh-external/issues#334](https://github.com/dsh-external/issues/issues/334))。
 
 ## 决策
 
@@ -16,7 +16,7 @@ Status: implemented
 
 ## 考虑过的替代方案
 
-- **一项 Activation 设置注册表贡献**(`registerContinuableSetup`):不予采纳。贡献只接收子级上下文,因此无法在委派边界捕获父级的覆盖项;该注册表在冷恢复与全新创建时都会应用,会导致重复追加或重复捕获;而且没有任何机制把贡献的捕获绑定到 start 调用的同步前缀,await 前捕获的保证会因此丢失。
+- **一项通用 child 设置贡献**:不予采纳。贡献只接收子级上下文,因此无法在委派边界捕获父级的覆盖项;在冷恢复与全新创建时都应用它会导致重复追加或重复捕获;而且没有任何机制把它的捕获绑定到 start 调用的同步前缀,await 前捕获的保证会因此丢失。
 - **在冷恢复时重新捕获父级覆盖项**:不予采纳。恢复的子 agent 会随父级后续切换静默改变策略,这会破坏委派时快照的语义,并让生效策略取决于恢复时机而非子级自身的日志。希望恢复的子 agent 采用新策略的父级应重新委派。
 - **让继续执行管理器导入一次性驱动器的内联逻辑**:不予采纳。Service Definition 包不能依赖自己的提供方包,而在 `continuation.ts` 中复制捕获/追加这对函数会招致偏差;`child-agent.ts` 已经承载其余每个共享组合步骤。
 - **把这些事件写入描述符种子轮次**:不予采纳。种子为每个调用方组装时,捕获值尚不可知;而且一次性路径的先例已经确立:在未发布的设置阶段追加,才是把继承事实排在 fork 历史之后、同时保持 `firstLiveSeq` 不变的顺序。

+ 2 - 2
.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.i18n.yaml

@@ -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-11-background-first-continuable-delegation.md
-2026-08-11-background-first-continuable-delegation.md: 59232ae8821ef4a093fd610ecbbb39690316ce6c
-2026-08-11-background-first-continuable-delegation.zh.md: d39f129e0ceb4aa75ba91c6860fb017c69f0fbe6
+2026-08-11-background-first-continuable-delegation.md: a3dcd75c820742eda622b04ce6077b444f66846b
+2026-08-11-background-first-continuable-delegation.zh.md: f0cbdca1c1cba7400e6f9926453fb640bde575bb

+ 6 - 6
.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.md

@@ -8,7 +8,7 @@ English | [中文](2026-08-11-background-first-continuable-delegation.zh.md)
 
 A continuable child already has a durable id, independent turns, follow-up messaging, and a manager-owned settlement notice. Treating an omitted `run_in_background` as foreground makes that lifecycle depend on the model restating `true` on every call. It also obscures the useful scheduling test: the parent should wait only when its next action requires the child's result.
 
-The child-scoped `report` prompt requires a self-contained final report, while [manager-owned settlement delivery](2026-08-06-manager-owned-subagent-settlement-delivery.md) independently sends the run outcome and closing message. A completed child can therefore wake its parent with a final report and again with settlement. Background-first scheduling must preserve both deliveries: the child-authored handoff remains mandatory guidance, while the manager-authored notice covers every terminal path regardless of model compliance.
+The child's initial task tells it how to address its direct parent with the shared `send_message` tool, while [manager-owned settlement delivery](2026-08-06-manager-owned-subagent-settlement-delivery.md) independently sends the run outcome and closing message. A child may send progress or a final handoff before settlement. Background-first scheduling preserves both: Agent-authored messages remain explicit model choices, while the manager-authored notice covers every terminal path regardless of model compliance.
 
 ## Decision
 
@@ -20,9 +20,9 @@ The model-facing text divides responsibility by location:
 - the `run_in_background` parameter states the lifecycle-specific default and when to override it;
 - a `tool:<toolName>` system-prompt section tells the model to start independent delegations together, continue useful work while they run, and choose foreground only when the next action depends on the result. The section renders only when that tool remains visible in the assembly scope, so a child tool restriction removes the schema and its guidance together.
 
-The [continuable child report obligation](2026-08-06-continuable-child-report-obligation.md) remains unchanged: the child prompt requires one self-contained final report and earlier reports for findings that change the parent's next action. Manager-owned settlement remains unconditional and does not inspect whether a report arrived. The two messages may repeat final content, but they retain distinct authors and purposes: `report` is the child's explicit handoff, while settlement records how the run ended and preserves terminal output when the child cannot cooperate. `reportDelivery` remains deployment scheduling policy with `next-step` as its default, preserving report-before-settlement order through the parent inbox.
+The child receives its direct parent id and return guidance in the initial task after any inherited fork seed. It may call `send_message` zero or more times, including for findings that change the parent's next action and for a self-contained final handoff. Manager-owned settlement remains unconditional and does not inspect whether an Agent message arrived. The two messages may repeat final content, but they retain distinct authors and purposes: `send_message` carries content the child chose, while settlement records how the run ended and preserves terminal output when the child cannot cooperate. Both use the Agent inbox and fixed Steer scheduling; the accepted child message precedes the later settlement notice.
 
-The keyless headless `subagent-settlement` scenario omits `run_in_background`, receives the immediate child id, and reaches the final parent answer through the manager-authored settlement notice even though its fixture deliberately does not call `report`. Package tests separately pin explicit `false` as foreground, the parent scheduling text, and the child's mandatory-report prompt.
+The keyless headless `subagent-settlement` scenario omits `run_in_background`, receives the immediate child id, and reaches the final parent answer through the manager-authored settlement notice even though its fixture deliberately sends no child-authored message. Package tests separately pin explicit `false` as foreground, the parent scheduling text, and the child's parent-id return guidance.
 
 ## Alternatives considered
 
@@ -32,14 +32,14 @@ The keyless headless `subagent-settlement` scenario omits `run_in_background`, r
 
 **Change only the prompt.** Prompt preference without runtime resolution still turns an omitted argument into foreground. The model must be able to rely on the advertised default rather than reproduce it perfectly on every tool call.
 
-**Suppress settlement after a final report arrives.** Conditional settlement reintroduces per-Activation bookkeeping and loses the unconditional runtime guarantee when a child reports progress and then fails. Settlement remains unconditional even when the resulting message overlaps a final report.
+**Suppress settlement after a final Agent message arrives.** Conditional settlement reintroduces per-Activation bookkeeping and loses the unconditional runtime guarantee when a child sends progress and then fails. Settlement remains unconditional even when the resulting message overlaps a final handoff.
 
-**Use `report` only for progress before settlement.** This removes duplicate final content but also removes the explicit child-authored handoff from the child prompt. The final-report obligation remains, and runtime settlement remains its independent fallback and terminal record.
+**Reserve `send_message` for progress before settlement.** This removes duplicate final content but makes the shared adjacent-Agent operation depend on message purpose. The child may explicitly hand off a final result, while runtime settlement remains its independent fallback and terminal record.
 
 ## Consequences
 
 - An ordinary continuable call is non-blocking without spelling `run_in_background: true`; serialized delegation is an explicit `false` choice.
 - Independent subagent calls in one assistant message overlap under the tool loop's concurrency-safe dispatch, while dependent foreground calls can still be issued one at a time.
 - Parent guidance, tool schema, runtime resolution, and settlement delivery state the same default.
-- A compliant child reports one self-contained final result and may report important findings earlier. Every Activation also produces an unconditional settlement notice, so a completed run may deliver overlapping final content twice.
+- A child may send one self-contained final result and important findings earlier. Every Activation also produces an unconditional settlement notice, so a completed run may deliver overlapping final content twice.
 - One-shot background Jobs and disabled-background tool instances retain their existing behavior.

+ 6 - 6
.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.zh.md

@@ -8,7 +8,7 @@ Status: implemented
 
 可继续 child 已经具备持久化 id、独立轮次、后续消息以及由管理器负责的结算通知。如果把省略的 `run_in_background` 视为前台,模型就必须在每次调用时重复写出 `true`,才能得到这套生命周期。这样也会掩盖真正有用的调度判断:只有当 parent 的下一步动作需要 child 结果时,parent 才应等待。
 
-child 作用域的 `report` 提示词要求发送自包含的最终报告,而[由管理器负责的结算投递](2026-08-06-manager-owned-subagent-settlement-delivery.zh.md)会独立发送本次运行的结束结果与收尾消息。已完成的 child 因而可能先用最终报告唤醒 parent,再用结算通知唤醒一次。后台优先调度会保留两次投递:由 child 编写的交接仍是强制提示词指引,由管理器生成的通知则不依赖模型是否遵循指令,覆盖每种终止路径。
+child 的初始任务会告诉它如何使用共享的 `send_message` 工具向直接 parent 发送消息,而[由管理器负责的结算投递](2026-08-06-manager-owned-subagent-settlement-delivery.zh.md)会独立发送本次运行的结束结果与收尾消息。child 可以在结算前发送进度或最终交接。后台优先调度会保留两者:由 Agent 编写的消息仍是模型的显式选择,由管理器生成的通知则不依赖模型是否遵循指令,覆盖每种终止路径。
 
 ## 决策
 
@@ -20,9 +20,9 @@ child 作用域的 `report` 提示词要求发送自包含的最终报告,而[
 - `run_in_background` 参数说明具体生命周期的默认值以及何时覆盖;
 - `tool:<toolName>` 系统提示词 section 会告诉模型同时启动相互独立的委派、在它们运行时继续有用工作,并且仅当下一步动作依赖结果时选择前台。只有当该工具在组装作用域中仍可见时才会渲染这个 section,因此子级工具限制会同时移除 schema 与对应指引。
 
-[可继续 child 上报义务](2026-08-06-continuable-child-report-obligation.zh.md)保持不变:child 提示词要求发送一份自包含的最终报告,并在发现会改变 parent 下一步动作的信息时提前报告。由管理器负责的结算仍然无条件执行,不检查报告是否已经到达。这两条消息可能重复最终内容,但作者和用途不同:`report` 是 child 的显式交接,结算则记录本次运行如何结束,并在 child 无法配合时保留终止输出。`reportDelivery` 仍是部署调度策略,默认值为 `next-step`,通过 parent inbox 保持报告先于结算的顺序
+child 会在继承的 fork 种子之后,从初始任务获得直接 parent id 与返回指引。它可以调用零次或多次 `send_message`,包括发送会改变 parent 下一步动作的发现,以及自包含的最终交接。由管理器负责的结算仍然无条件执行,不检查 Agent 消息是否已经到达。这两条消息可能重复最终内容,但作者和用途不同:`send_message` 携带 child 自己选择的内容,结算则记录本次运行如何结束,并在 child 无法配合时保留终止输出。两者都使用 Agent inbox 与固定 Steer 调度;已接受的 child 消息先于后续结算通知
 
-无密钥 headless `subagent-settlement` 场景省略 `run_in_background`,收到立即返回的 child id;尽管 fixture(测试前置数据)有意不调用 `report`,它仍通过管理器生成的结算通知到达 parent 最终答案。包测试另行固定了显式 `false` 的前台语义、parent 调度文本以及 child 的强制报告提示词
+无密钥 headless `subagent-settlement` 场景省略 `run_in_background`,收到立即返回的 child id;尽管 fixture(测试前置数据)有意不发送 child 编写的消息,它仍通过管理器生成的结算通知到达 parent 最终答案。包测试另行固定了显式 `false` 的前台语义、parent 调度文本以及 child 的 parent-id 返回指引
 
 ## 考虑过的替代方案
 
@@ -32,14 +32,14 @@ child 作用域的 `report` 提示词要求发送自包含的最终报告,而[
 
 **只修改提示词。** 如果运行时解析不变,提示词偏好仍会让省略参数的调用进入前台。模型必须能够依赖公布的默认值,而不是在每次工具调用中完美复述它。
 
-**最终报告到达后抑制结算通知。** 条件结算会重新引入每次 Activation 的记账,并且当 child 先报告进度、随后失败时丢掉无条件运行时保证。即使生成的消息与最终报告重叠,结算仍然无条件执行。
+**最终 Agent 消息到达后抑制结算通知。** 条件结算会重新引入每次 Activation 的记账,并且当 child 先发送进度、随后失败时丢掉无条件运行时保证。即使生成的消息与最终交接重叠,结算仍然无条件执行。
 
-**只用 `report` 发送结算前的进度。** 这样可以消除重复的最终内容,但也会从 child 提示词中移除由 child 编写的显式交接。最终报告义务保持不变,运行时结算则继续作为它的独立后备和终止记录。
+**只用 `send_message` 发送结算前的进度。** 这样可以消除重复的最终内容,但会让共享的相邻 Agent 操作依赖消息用途。child 可以显式交接最终结果,运行时结算则继续作为它的独立后备和终止记录。
 
 ## 后果
 
 - 普通可继续调用无需写出 `run_in_background: true` 即为非阻塞;串行委派需要显式选择 `false`。
 - 同一条 assistant 消息中的独立 subagent 调用会在工具循环的并发安全分发下重叠执行;有依赖的前台调用仍可逐个发出。
 - parent 指引、工具 schema、运行时解析和结算投递陈述同一个默认值。
-- 遵循指令的 child 会发送一份自包含的最终结果,也可以更早报告重要发现。每次 Activation 还会产生无条件结算通知,因此已完成的运行可能两次投递相互重叠的最终内容。
+- child 可以发送一份自包含的最终结果,也可以更早发送重要发现。每次 Activation 还会产生无条件结算通知,因此已完成的运行可能两次投递相互重叠的最终内容。
 - 一次性后台 Task 与禁用后台的工具实例保留现有行为。

+ 3 - 3
.agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.i18n.yaml → .agents/notes/implemented/feature/2026-09-01-shared-base-web-fetch-default.i18n.yaml

@@ -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/bug-fix/2026-08-17-subagent-report-settlement-ordering.md
-2026-08-17-subagent-report-settlement-ordering.md: 30dfab5e96a7cea2ef6d4f03f480d17a86c5e775
-2026-08-17-subagent-report-settlement-ordering.zh.md: 658eb18e3a8cb40734136af32c6c62faef066a6e
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-01-shared-base-web-fetch-default.md
+2026-09-01-shared-base-web-fetch-default.md: eceb2009d8a845b4a82f62b99eae13d86e89d050
+2026-09-01-shared-base-web-fetch-default.zh.md: 9be9c6350b6abce2e21bafe1f8c09efcb4265386

+ 27 - 0
.agents/notes/implemented/feature/2026-09-01-shared-base-web-fetch-default.md

@@ -0,0 +1,27 @@
+# Agent Note: Shared-base Web fetch default
+
+Status: implemented
+
+English | [中文](2026-09-01-shared-base-web-fetch-default.zh.md)
+
+This decision partially supersedes the fetch opt-in choice in [Default Web search in shipped compositions](2026-07-31-web-default-search.md). That record continues to own search provider selection, credentials, endpoint, timeout, and the separation between provider availability and model-tool registration; no active Agent Note is fully superseded or eligible for archival.
+
+## Problem
+
+Every shipped full agent product accepts anonymous public Web fetch, but `dsh-base` disabled `web_fetch` and required each application bundle to repeat the same override. The repeated configuration omitted ACP, made new base-backed profiles search-only unless their authors noticed the exception, and forced otherwise identical snapshot headers to split by product.
+
+## Decision
+
+`packages/bundle/base/cordis.patch.yml` mounts `dsh-tool-web` with `fetch: true` and the shipped 60-second search timeout. Headless, full SDK, ACP, and custom base-only profiles inherit both `web_search` and `web_fetch` without application-level overrides. The Web app disables the base tool row and composes the same pair per agent preset. The standalone `sdk-minimal` profile remains independent of base.
+
+The base HTTP provider permits anonymous `http:` and `https:` requests only to validated public destinations. Fetch executes outside shell and filesystem sandbox or approval presets and requires no per-call approval; public-destination validation does not prevent public data egress. A product that requires a different network policy overrides the complete `tool-web` config in a later bundle or profile patch.
+
+## Alternatives considered
+
+**Keep fetch disabled in base and enable it in each product.** Rejected because every shipped full product selects the same capability, so the repeated rows encode no product difference and can omit future base-backed profiles.
+
+**Add only an ACP override.** Rejected because it repairs the current omission while retaining three redundant application-level settings and the same failure mode for future profiles.
+
+## Consequences
+
+Base-backed model requests expose the fetch schema and prompt guidance by default, including ACP automation and custom profiles that name only `dsh-base`. Restricted deployments must opt out explicitly. Headless, SDK, and ACP can share the same model-header snapshot sources, while focused real-profile tests pin the shipped tool roster.

+ 27 - 0
.agents/notes/implemented/feature/2026-09-01-shared-base-web-fetch-default.zh.md

@@ -0,0 +1,27 @@
+# Agent Note: 共享 base 的 Web 抓取默认值
+
+Status: implemented
+
+[English](2026-09-01-shared-base-web-fetch-default.md) | 中文
+
+本决策部分取代[已交付组合中的默认 Web 搜索](2026-07-31-web-default-search.zh.md)里关于抓取按需启用的选择。该记录继续负责搜索提供方选择、凭据、端点、超时,以及提供方可用性与模型工具注册之间的区分;没有任何 active Agent Note 被完全取代或符合归档条件。
+
+## 问题
+
+所有随附的完整 agent 产品都接受匿名公开 Web 抓取,但 `dsh-base` 会禁用 `web_fetch`,要求每个应用组合包重复相同的覆盖。重复配置遗漏了 ACP,使新的 base-backed profile 默认只有搜索能力,除非作者注意到这个例外,还迫使产品之间原本相同的 snapshot header 分开维护。
+
+## 决策
+
+`packages/bundle/base/cordis.patch.yml` 以 `fetch: true` 和随附的 60 秒搜索超时挂载 `dsh-tool-web`。Headless、完整 SDK、ACP 与仅使用 base 的自定义 profile 会继承 `web_search` 和 `web_fetch`,无需应用级覆盖。Web app 会禁用 base 工具配置项,并按 agent preset 组合相同的一对工具。独立的 `sdk-minimal` profile 不使用 base,因此保持不变。
+
+base HTTP 提供方只允许匿名请求经过验证的公开 `http:` 与 `https:` 目的地址。抓取在 shell 和文件系统 sandbox 或审批 preset 之外执行,无需逐次审批;公开目的地址校验不会阻止向公网发送数据。需要不同网络策略的产品应在后续组合包或 profile patch 中覆盖完整的 `tool-web` 配置。
+
+## 考虑过的替代方案
+
+**在 base 中禁用抓取,再由每个产品分别启用。** 不予采纳:所有随附的完整产品都选择相同能力,重复配置没有表达产品差异,还可能遗漏未来的 base-backed profile。
+
+**只增加 ACP 覆盖。** 不予采纳:这种方式能修复当前遗漏,但会保留三处重复的应用级设置,也会让未来 profile 面临相同问题。
+
+## 后果
+
+基于 base 的模型请求默认暴露抓取 schema 与 prompt 指引,包括 ACP 自动化和只列出 `dsh-base` 的自定义 profile。受限部署必须显式关闭。Headless、SDK 与 ACP 可以共享相同的模型 header snapshot 来源,聚焦的真实 profile 测试会固定随附工具集合。

+ 3 - 3
.agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.i18n.yaml → .agents/notes/implemented/feature/2026-09-01-web-elevation-stroke-shadows.i18n.yaml

@@ -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-06-continuable-child-report-obligation.md
-2026-08-06-continuable-child-report-obligation.md: 422d3ab74389e084becb75a145a82139dda38ac7
-2026-08-06-continuable-child-report-obligation.zh.md: ec7e19885204ddc99b2640407c605288f1e9c045
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-01-web-elevation-stroke-shadows.md
+2026-09-01-web-elevation-stroke-shadows.md: 2bc3105203b63c87aaf39e3d68780650b163dcca
+2026-09-01-web-elevation-stroke-shadows.zh.md: bda3ee27af03d20937253fa23968afd0cd7d39c7

+ 42 - 0
.agents/notes/implemented/feature/2026-09-01-web-elevation-stroke-shadows.md

@@ -0,0 +1,42 @@
+# Agent Note: Web elevation — hairline stroke drawn in shadow
+
+Status: implemented
+
+English | [中文](2026-09-01-web-elevation-stroke-shadows.zh.md)
+
+## Problem
+
+Elevated web-client surfaces — menus, popovers, modals, panels, floating buttons, the composer — each paired a real `border: 1px solid <neutral token>` with a `--dsw-shadow-lv2`/`lv3` shadow. The border consumes layout (1px per side, and it is the UA-default replacement on `<button>` elements), the light theme drew most floats with no stroke at all (`--dsw-alias-border-inverted` is transparent in light) while `lv3` faked one with a blurred 1px ring, and the composer wore a broad soft `lv2` patch that read as a smudge rather than a lifted surface. Current desktop chat UIs instead draw elevation as one `box-shadow` list: a 0.5px hairline stroke plus two faint soft layers, with `border: 0` on the surface.
+
+## Decision
+
+`gradient-shadow-text.css` (the ui-theme shadow owner) defines the elevation tokens beside the `--dsw-shadow-lv*` scale:
+
+- `--dsw-elevation-stroke-color` — the hairline color, defaulting to `--dsw-alias-border-l4` (black 16% light, white 20% dark); components rebind it per surface or state: every menu-fill surface (`--dsw-specific-menu` background) rebinds the lightest `--dsw-alias-border-l1` and the composer rebinds `--dsw-alias-border-l2`, both quieter than panels and buttons. The default is declared on `body` alone while the derived tokens below are re-declared on `body, body *`: a custom property computes with `var()` already substituted, so body-only derived tokens would bake in body's color and make every rebind a no-op (the same per-element re-substitution scrollbar.css states for `--dsh-scrollbar-thumb`).
+- `--dsw-elevation-stroke: 0 0 0 0.5px var(--dsw-elevation-stroke-color)` — the stroke alone, used standalone by inline cards that want only an outline (the plugin-inventory card).
+- `--dsw-elevation-panel` / `--dsw-elevation-prominent` — the stroke plus two faint soft layers (3px directional + 16/20px glow at 2–5% black), panel for small floating widgets and cards, prominent for floats, and soft — larger blur at lower alpha — for the composer.
+
+Converted surfaces set `border: 0` and one elevation shadow: every `--dsw-shadow-lv3` float (Menu, Modal, popup selects, model select, usage/context popovers, feedback actions, schedule/job popovers, subagent lineage, settings panel, cordis panel, experimental team panel) takes prominent, and the lv2 surfaces (scroll-to-bottom button, turn-preview card, attachment-rail arrows, question composer, trajectory tooltip) take panel. The composer card takes the soft tier with the l2 stroke rebind, and its workspace-trigger state sets the stroke color `transparent` instead of the former `border-color: transparent`. Dark theme needs no shadow overrides: the soft layers are near-invisible there and the stroke carries the separation.
+
+`packages/client/ui-theme/tests/elevation-styles.client.spec.ts` pins the token composition and scans every stylesheet under `packages/`: a rule pairing an lv/elevation `box-shadow` with a `--dsw-alias-border-*` border fails, and every `solid` border on a neutral `--dsw-alias-border-*` token must be `0.5px` wide. Deliberate keeps: Toast and HoverCard (inverted fills where a theme-following stroke is meaningless), ImageLightbox (bare image), and the warn-bordered approval/plan panels, whose state-colored borders stay real borders and pass the scan.
+
+Flat widgets keep real borders at hairline width: every neutral-token `1px solid` border — buttons (the shared outline variant, add/retry/inspect buttons), inputs, inline cards, code blocks, and the settings row separators — is `0.5px solid`, with full-box strokes deepened to `--dsw-alias-border-l4` (buttons one step lighter at `--dsw-alias-border-l3`) while row separators keep `--dsw-alias-border-l2`; state logic (`border-color` swaps on focus/hover) is unchanged. Separators drawn as filled boxes take the same weight: the 1px-tall or 1px-wide lines with a border-token background (menu separators, the conversation header seam, markdown `hr`, the tool IO dividers, the trajectory rail, the directory-browser divider) are 0.5px, and the context-injection divider that read the never-defined `--dsw-alias-line-secondary` (so it never rendered) now draws `0.5px solid var(--dsw-alias-border-l2)`. Chromium paints sub-device-pixel borders as one device pixel, so 1x displays render exactly the former line and 2x displays get the hairline. Dashed affordances stay 1px (a 0.5px dash pattern degrades), and the two border-drawn spinner rings keep their track width through the spec's explicit allowlist.
+
+## Alternatives considered
+
+**Keeping 1px real borders and only softening the shadows.** Leaves the layout-consuming border, the light-theme stroke gap on `border-inverted` floats, and the double outline wherever both existed; the stroke-in-shadow form is what produces the crisp hairline edge.
+
+**A 1px stroke instead of 0.5px.** At 2x displays 0.5px renders one physical pixel, and on 1x it blends lighter, which is the intended hairline. 1px reads as the old border.
+
+**Drawing flat-widget hairlines as box-shadow strokes too.** Buttons and inputs swap `border-color` on hover and focus and several pair a box-shadow focus ring; moving their stroke into `box-shadow` would collide with those rings (one property) and rewrite every state rule, while `0.5px solid` keeps the whole state logic and changes only the weight.
+
+**Suppressing the composer trigger stroke with `box-shadow: none`.** Also drops the soft layers the trigger state keeps today; rebinding `--dsw-elevation-stroke-color: transparent` removes exactly the stroke.
+
+**Converting Toast/HoverCard too.** Their fills are inverted relative to the theme, so the theme-following stroke color is invisible-or-wrong on them; they keep `lv3` until an inverted-surface stroke token exists.
+
+## Consequences
+
+- Every converted surface gains a hairline outline in the light theme (most floats previously had none) and loses 1px of border from its box; the visual size change is at most 2px on small buttons and imperceptible on panels.
+- The neutral-border-plus-shadow pairing is now rejected by the ui-theme elevation spec, so a new elevated surface must choose the elevation tokens; the rule lives in [docs/web-styling.md](../../../../docs/web-styling.md).
+- The composer's broad `lv2` patch becomes stroke + tight glow; its dark stroke keeps the figma one-notch-weaker value through the rebind.
+- `--dsw-shadow-lv1`/`lv1-blur` currently have no consumer and `lv2`/`lv3` remain only on the deliberate keeps; the scale stays for inverted and bespoke surfaces.

+ 42 - 0
.agents/notes/implemented/feature/2026-09-01-web-elevation-stroke-shadows.zh.md

@@ -0,0 +1,42 @@
+# Agent Note: Web elevation — hairline stroke drawn in shadow
+
+Status: implemented
+
+[English](2026-09-01-web-elevation-stroke-shadows.md) | 中文
+
+## Problem
+
+Web 客户端的高层级表面——菜单、浮层、对话框、面板、悬浮按钮、输入框——原先都把真 `border: 1px solid <中性 token>` 与 `--dsw-shadow-lv2`/`lv3` 投影配对。border 占布局(每侧 1px,且在 `<button>` 上是对 UA 默认边框的替换);浅色主题下多数浮层实际没有描边(`--dsw-alias-border-inverted` 在浅色下是透明的),靠 `lv3` 里模糊的 1px 环冒充;输入框则披着一大片柔和的 `lv2` 投影,读起来更像污渍而非悬浮表面。当前桌面聊天 UI 改用单个 `box-shadow` 列表绘制 elevation:0.5px 发丝描边加两层极淡柔光,表面本身 `border: 0`。
+
+## Decision
+
+`gradient-shadow-text.css`(ui-theme 的阴影归属地)在 `--dsw-shadow-lv*` 阶旁定义 elevation token:
+
+- `--dsw-elevation-stroke-color`——发丝描边颜色,默认 `--dsw-alias-border-l4`(浅色黑 16%、深色白 20%);组件可按表面或状态重绑:所有菜单面(`--dsw-specific-menu` 背景)重绑最浅的 `--dsw-alias-border-l1`、输入框重绑 `--dsw-alias-border-l2`,两者都比面板与按钮安静。默认色只声明在 `body` 上,而下述派生 token 在 `body, body *` 上逐元素重声明:自定义属性的计算值已替换完 `var()`,派生 token 若只在 body 声明会把 body 的颜色固化进去,让所有重绑失效(与 scrollbar.css 对 `--dsh-scrollbar-thumb` 声明的逐元素重替换是同一契约)。
+- `--dsw-elevation-stroke: 0 0 0 0.5px var(--dsw-elevation-stroke-color)`——单独的描边,供只要轮廓的行内卡片独立使用(插件清单卡片)。
+- `--dsw-elevation-panel` / `--dsw-elevation-prominent`——描边加两层极淡柔光(3px 方向光 + 16/20px 辉光,黑 2–5%),panel 用于小型悬浮部件与卡片,prominent 用于浮层,soft——更大模糊、更低透明度——用于输入框。
+
+被转换的表面设 `border: 0` 加一个 elevation 投影:所有 `--dsw-shadow-lv3` 浮层(Menu、Modal、弹出选择、模型选择、用量/上下文浮层、反馈操作条、日程/任务浮层、子代理谱系、设置面板、cordis 面板、实验性 team 面板)取 prominent;lv2 表面(回到底部按钮、回合预览卡、附件栏箭头、问题 composer、轨迹 tooltip)取 panel。输入框卡片取 soft 档并重绑 l2 描边,其 workspace-trigger 态把描边色设为 `transparent`,替代原先的 `border-color: transparent`。深色主题无需投影覆盖:柔光在深色下几乎不可见,分离由描边承担。
+
+`packages/client/ui-theme/tests/elevation-styles.client.spec.ts` 钉住 token 组成并扫描 `packages/` 下全部样式表:lv/elevation `box-shadow` 与 `--dsw-alias-border-*` border 配对的规则即失败;中性 `--dsw-alias-border-*` token 上的每个 `solid` border 必须为 `0.5px` 宽。有意保留:Toast 与 HoverCard(反色填充,跟随主题的描边色在其上无意义)、ImageLightbox(裸图片)、warn 描边的审批/计划面板——状态色 border 保持真 border,扫描放行。
+
+平面部件保留真 border 但取发丝线宽度:所有中性 token 的 `1px solid` border——按钮(共享 outline 变体、添加/重试/inspect 按钮)、输入框、行内卡片、代码块与设置行分割线——改为 `0.5px solid`,整框描边加深为 `--dsw-alias-border-l4`(按钮浅一档取 `--dsw-alias-border-l3`),行分割线保持 `--dsw-alias-border-l2`;状态逻辑(focus/hover 换 `border-color`)不变。以填充盒绘制的分隔线取同一粗细:高或宽为 1px、背景用 border token 的线(菜单分隔、对话标题栏接缝、markdown `hr`、工具 IO 分隔、轨迹竖轨、目录浏览器分隔)改为 0.5px;上下文注入分隔线原先读取从未定义的 `--dsw-alias-line-secondary`(因此从未渲染),现在绘制 `0.5px solid var(--dsw-alias-border-l2)`。Chromium 把亚设备像素 border 绘制为一个设备像素,因此 1x 屏与原先渲染完全一致,2x 屏得到发丝线。dashed 记号保持 1px(0.5px 虚线图案会退化),两个用 border 画的 spinner 圆环经 spec 显式豁免保留轨道宽度。
+
+## Alternatives considered
+
+**保留 1px 真 border、只调柔投影。** 留下占布局的 border、浅色主题 `border-inverted` 浮层的描边缺口,以及两者并存处的双轮廓;描边入投影正是产生锐利发丝边缘的形式。
+
+**用 1px 而非 0.5px 描边。** 0.5px 在 2x 屏渲染为一物理像素,1x 屏混合得更浅,正是发丝线意图。1px 读起来就是原来的 border。
+
+**平面部件的发丝线也用 box-shadow 描边画。** 按钮与输入框在 hover/focus 时切换 `border-color`,多处还配 box-shadow 焦点环;把描边挪进 `box-shadow`(单一属性)会与焦点环冲突并重写全部状态规则,而 `0.5px solid` 保留整套状态逻辑,只改粗细。
+
+**composer trigger 态用 `box-shadow: none` 抑制描边。** 会连带丢掉该状态今天保留的柔光;重绑 `--dsw-elevation-stroke-color: transparent` 恰好只去掉描边。
+
+**Toast/HoverCard 一并转换。** 其填充相对主题反色,跟随主题的描边色在其上不可见或错误;在出现反色表面描边 token 之前保留 `lv3`。
+
+## Consequences
+
+- 每个被转换表面在浅色主题下获得发丝轮廓(多数浮层此前没有),盒子少了 1px border;视觉尺寸变化在小按钮上至多 2px,面板上不可察觉。
+- 中性 border 加投影的配对现被 ui-theme elevation spec 拒绝,新的高层级表面必须选用 elevation token;规则记录于 [docs/web-styling.md](../../../../docs/web-styling.zh.md)。
+- 输入框的大片 `lv2` 变为描边加收紧的辉光;深色描边经重绑保留 figma 低一档取值。
+- `--dsw-shadow-lv1`/`lv1-blur` 目前无消费方,`lv2`/`lv3` 只剩有意保留者;该阶为反色与定制表面继续存在。

+ 6 - 0
.agents/notes/implemented/feature/2026-09-01-web-superellipse-corner-smoothing.i18n.yaml

@@ -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-09-01-web-superellipse-corner-smoothing.md
+2026-09-01-web-superellipse-corner-smoothing.md: 7088438a8068343434a27495b771515d4a5158ee
+2026-09-01-web-superellipse-corner-smoothing.zh.md: 26aa64fda9f557069cfc3040422c8e1b7e34676e

+ 34 - 0
.agents/notes/implemented/feature/2026-09-01-web-superellipse-corner-smoothing.md

@@ -0,0 +1,34 @@
+# Agent Note: Web superellipse corner smoothing
+
+Status: implemented
+
+English | [中文](2026-09-01-web-superellipse-corner-smoothing.zh.md)
+
+## Problem
+
+Every rounded surface in the web client — cards, composer, buttons, popovers — draws its corners as plain circular arcs, which read as visibly harder than the smooth (squircle-like) corners current desktop chat UIs ship. That smoothness comes from CSS `corner-shape: superellipse(1.5)` applied behind an `@supports` guard, not from larger radii or masking tricks; utility-class implementations attach it to every rounded-corner class except full-round. This client has no utility classes: `border-radius` values are px literals spread across CSS Modules in every client package, so there is no single class list to attach the property to, and full-round shapes (`border-radius: 50%` circles, 999px pills) must keep circular arcs — a superellipse deforms a circle into a squircle, so a border-drawn spinner would visibly wobble, and it squares off capsule ends.
+
+## Decision
+
+`packages/client/ui-theme/src/styles/corner-shape.css` is a global sheet mounted by ui-theme's client entry (after `base.css`). Inside `@supports (corner-shape: superellipse(1.5))` it defines `--dsw-corner-shape: superellipse(1.5)` on `:root` and applies `corner-shape: var(--dsw-corner-shape)` through `*, *::before, *::after` — `corner-shape` does not inherit, so the universal selector is the mechanism that reaches every rounded surface without a utility-class system. Engines without `corner-shape` keep circular corners because both declarations live inside the guard. `superellipse(1.5)` sits between `round` (`superellipse(1)`) and `squircle` (`superellipse(2)`), matching the smoothing current desktop chat UIs ship.
+
+Full-round shapes opt back out at their declaration: every `border-radius` of `50%`, `100%`, or a pill radius (≥ 99px) pairs `corner-shape: round` in the same rule of its owning component sheet. The pairing is enforced by `packages/client/ui-theme/tests/corner-shape-styles.client.spec.ts`, which scans every stylesheet under `packages/` (the shared scan helpers live in `tests/stylesheet-scan.ts`, extracted from the scrollbar spec); the same spec pins the guard and the universal application in `corner-shape.css`. Component-local radius indirections (`--dsl-*-radius`) all hold values far below the pill threshold, so the lexical scan covers current usage.
+
+Token-based implementations pair the shape change with a 1.25× radius scale; this client has no radius tokens (px literals per component), so radii are unchanged and only the corner curvature moves.
+
+## Alternatives considered
+
+**A radius token system first, then per-token application.** Faithful, but converting ~130 px-literal radii across every client package into tokens is a large refactor serving no other current need; the universal selector reaches the same surfaces with one rule.
+
+**Applying superellipse to full-round shapes too (no opt-outs).** Fewer declarations, but spinners built from `border-radius: 50%` borders wobble when the rotating shape is not a circle, and capsule ends square off.
+
+**Subtree opt-out via `--dsw-corner-shape: round` instead of per-declaration `corner-shape: round`.** The custom property inherits, so a pill's rounded descendants would silently lose smoothing; the explicit per-rule declaration keeps the opt-out exactly as wide as the full-round shape and is what the pairing spec can check.
+
+**Scaling radii by 1.25 alongside the curvature change.** Requires the token system above; the curvature change alone already delivers the smoothness, and radii stay as designed.
+
+## Consequences
+
+- On engines with `corner-shape` (Chromium ≥ 139 behind no flag), every rounded corner in the client curves along `superellipse(1.5)`; other engines render exactly as before, with no fallback code.
+- New full-round shapes must pair `corner-shape: round` or the ui-theme corner-shape spec fails; the rule is stated in [docs/web-styling.md](../../../../docs/web-styling.md) and costs one extra declaration per circle or pill.
+- The universal selector adds one non-inherited property to every element; the declaration is a constant and profiling concerns are theoretical at current tree sizes.
+- A rounded box whose radius equals half its height (an implicit pill under 99px) still receives the superellipse; the scan cannot see computed geometry, and such ends read as intentional smoothing rather than distortion.

+ 34 - 0
.agents/notes/implemented/feature/2026-09-01-web-superellipse-corner-smoothing.zh.md

@@ -0,0 +1,34 @@
+# Agent Note: Web superellipse corner smoothing
+
+Status: implemented
+
+[English](2026-09-01-web-superellipse-corner-smoothing.md) | 中文
+
+## Problem
+
+Web 客户端里每个圆角表面——卡片、输入框、按钮、浮层——都以普通圆弧绘制圆角,观感明显硬于当前桌面聊天 UI 普遍采用的平滑(类 squircle)圆角。这种平滑感来自在 `@supports` 守卫内应用 CSS `corner-shape: superellipse(1.5)`,而非更大的半径或遮罩技巧;工具类体系的实现把它挂到除正圆之外的所有圆角工具类上。本客户端没有工具类:`border-radius` 以 px 字面量散布在各客户端包的 CSS Modules 中,没有可以统一挂载该属性的类列表;而正圆形状(`border-radius: 50%` 的圆、999px 胶囊)必须保持圆弧——超级椭圆会把圆变形为 squircle,用 border 绘制的加载圈旋转时会明显晃动,胶囊两端也会变方。
+
+## Decision
+
+`packages/client/ui-theme/src/styles/corner-shape.css` 是由 ui-theme 客户端 entry 挂载的全局样式表(位于 `base.css` 之后)。它在 `@supports (corner-shape: superellipse(1.5))` 内于 `:root` 定义 `--dsw-corner-shape: superellipse(1.5)`,并通过 `*, *::before, *::after` 应用 `corner-shape: var(--dsw-corner-shape)`——`corner-shape` 不继承,通配选择器正是在没有工具类系统的前提下触达每个圆角表面的机制。两条声明都在守卫内,因此不支持 `corner-shape` 的引擎保持普通圆弧。`superellipse(1.5)` 介于 `round`(`superellipse(1)`)与 `squircle`(`superellipse(2)`)之间,与当前桌面聊天 UI 的平滑度一致。
+
+正圆形状在其声明处退出:每个取值为 `50%`、`100%` 或胶囊半径(≥ 99px)的 `border-radius`,都在所属组件样式表的同一规则内配对 `corner-shape: round`。该配对由 `packages/client/ui-theme/tests/corner-shape-styles.client.spec.ts` 强制,它扫描 `packages/` 下的全部样式表(共享扫描辅助函数位于 `tests/stylesheet-scan.ts`,自 scrollbar spec 抽出);同一 spec 也钉住 `corner-shape.css` 的守卫与通配应用。组件局部半径变量(`--dsl-*-radius`)取值都远低于胶囊阈值,因此词法扫描覆盖当前用法。
+
+基于 token 的实现会在改变曲线的同时把半径 token 乘以 1.25;本客户端没有半径 token(各组件 px 字面量),故半径不变,只改变圆角曲率。
+
+## Alternatives considered
+
+**先建半径 token 系统,再按 token 应用。** 更忠实,但把所有客户端包约 130 处 px 字面量半径改造成 token 是没有其他现实需求的大重构;通配选择器用一条规则触达同样的表面。
+
+**对正圆形状也应用超级椭圆(不设豁免)。** 声明更少,但用 `border-radius: 50%` border 绘制的加载圈在形状不是圆时旋转会晃动,胶囊两端会变方。
+
+**用子树级 `--dsw-corner-shape: round` 替代逐声明 `corner-shape: round` 豁免。** 自定义属性会继承,胶囊的圆角后代会静默失去平滑;逐规则显式声明让豁免范围恰好等于正圆形状本身,也是配对 spec 能检查的形式。
+
+**在改变曲率的同时把半径乘 1.25。** 需要上述 token 系统;仅曲率变化已带来平滑感,半径维持设计值。
+
+## Consequences
+
+- 在支持 `corner-shape` 的引擎(Chromium ≥ 139,无需 flag)上,客户端每个圆角都沿 `superellipse(1.5)` 弯曲;其他引擎渲染与之前完全一致,没有回退代码。
+- 新增正圆形状必须配对 `corner-shape: round`,否则 ui-theme 的 corner-shape spec 失败;该规则记录于 [docs/web-styling.md](../../../../docs/web-styling.zh.md),每个圆或胶囊多付一条声明。
+- 通配选择器给每个元素增加一个不继承的属性;声明是常量,在当前树规模下性能顾虑属于理论层面。
+- 半径等于自身高度一半的圆角盒(低于 99px 的隐式胶囊)仍会得到超级椭圆;扫描看不到计算后几何,此类端部读作有意的平滑而非变形。

+ 2 - 2
.agents/notes/implemented/simplification/2026-07-27-intent-named-subagent-continuation-operations.i18n.yaml

@@ -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-27-intent-named-subagent-continuation-operations.md
-2026-07-27-intent-named-subagent-continuation-operations.md: 72fb042978cdb8faa238d7483c17eb7c7fce80bd
-2026-07-27-intent-named-subagent-continuation-operations.zh.md: 8734c439fd4f3baf384d6fa1fc2ae197e2ed5b64
+2026-07-27-intent-named-subagent-continuation-operations.md: cb0827e63ed6bf88f7f661cf071d15aaa2278bde
+2026-07-27-intent-named-subagent-continuation-operations.zh.md: fba07eac66af8e27d1f548a68f2ac060d39d10e7

+ 6 - 6
.agents/notes/implemented/simplification/2026-07-27-intent-named-subagent-continuation-operations.md

@@ -4,17 +4,17 @@ Status: implemented
 
 English | [中文](2026-07-27-intent-named-subagent-continuation-operations.zh.md)
 
-The current activation-based realization is owned by [Continuable subagents](../feature/2026-07-28-continuable-subagent-conversations.md). It retains the `followup` operation this record names, returns the accepted `MessageId`, uses the bare `Agent` parameter as exact live-direct-parent authority, and limits provider participation in continuable children to `prepareContinuable`.
+The provider-request and session-flush decisions remain current. [Adjacent Agents share one Steer messaging operation](../architecture/2026-08-27-adjacent-agent-steer-messaging.md) supersedes this record's `followup` naming and options: the public operation is now `sendMessage(sender, targetId, content, { signal })` for either adjacent direction.
 
 ## Problem
 
-Merging continuable-child orchestration into `ctx.subagents` left provider dispatch and caller intent on the same public service. `resume(name, request)` accepted a descriptor, authorized parent, durable child id, and activation signal that only the internal continuation manager could resolve correctly. `sendMessage(...)` exposed transport wording rather than the `followup` intent already used by `Agent`, and its separate source and signal parameters widened an operation every caller had to use atomically.
+Merging continuable-child orchestration into `ctx.subagents` left provider dispatch and caller intent on the same public service. `resume(name, request)` accepted a descriptor, authorized parent, durable child id, and activation signal that only the internal continuation manager could resolve correctly. Direction-specific `followup` and `reportFrom` operations would also encode routing and scheduling differences for one adjacent-Agent capability.
 
 The durability boundary also exposed both `SessionStore.flush()` and `flushRequired()`. They performed the same scoped parallel dispatch and differed only in whether an empty listener snapshot was accepted, so the session interface encoded one consumer's policy as a second operation.
 
 ## Decision
 
-`SubagentRuntime` separates four execution intents: `start(name, request)` returns an ordinary holder-owned one-shot run; `startContinuable(spec)` establishes a durable child and returns its id plus the accepted initial `MessageId`; `followup(parent, childId, content, { source, signal })` sends later parent content; and `reportFrom(child, content, { delivery, signal })` sends selected child content to its direct parent. `followup` matches `Agent.followup()`, while `SubagentRun.steer()` remains the narrower confirmed live-run capability. The model-facing tools keep their stable `send_message` and `report` names and delegate routing to the corresponding intent methods.
+`SubagentRuntime` separates three caller intents: `start(name, request)` returns an ordinary holder-owned one-shot run; `startContinuable(spec)` establishes a durable child and returns its id plus the accepted initial `MessageId`; and `sendMessage(sender, targetId, content, { signal })` sends model-authored content across one direct parent-child edge. The message operation derives attribution from the exact live sender and owns adjacency checks, cold resume, and fixed Steer scheduling. The single model-facing `send_message({ agent_id, message })` tool delegates to that operation in either direction.
 
 Caller and provider requests are distinct. `SubagentStartRequest` contains caller-supplied one-shot data; `ResolvedSubagentStartRequest` adds the service-resolved descriptor before `SubagentProvider.start()`. For continuable creation, the manager passes a `ContinuableCreateRequest` to optional `SubagentProvider.prepareContinuable()` and receives detached creation data only. `SubagentRuntime.resume()` and provider resume dispatch are absent: the continuation manager loads the descriptor, authorizes the parent, and owns Agent materialization, prompt delivery, cold resume, and teardown.
 
@@ -24,7 +24,7 @@ Caller and provider requests are distinct. `SubagentStartRequest` contains calle
 
 **Keep public provider resume dispatch.** No production caller outside the continuation manager owns descriptor lookup, direct-parent authorization, Agent materialization, Activation ownership, and child-first teardown. A public method would expose resolved implementation data without a valid independent intent; providers instead contribute detached first-creation data through `prepareContinuable` and never participate in cold resume.
 
-**Keep `sendMessage` on the service.** The model tool sends a message, but the service operation represents a follow-up that may steer or cold-resume. `followup` aligns with the structural `Agent` interface and does not promise a particular route.
+**Keep direction-specific `followup` and `reportFrom` operations.** They would preserve shorter recipient-free child calls, but duplicate authority, attribution, and delivery behavior while making the service vocabulary depend on direction. One `sendMessage` operation names the shared intent and keeps the target explicit.
 
 **Keep `flushRequired()`.** A second method hides only an empty-listener check. Returning participation from the existing barrier keeps dispatch in one implementation and lets each caller state whether absence is acceptable.
 
@@ -33,6 +33,6 @@ Caller and provider requests are distinct. `SubagentStartRequest` contains calle
 ## Consequences
 
 - The Cordis service catalog contains only caller operations; a provider can opt into continuable first creation through `SubagentProvider.prepareContinuable?()` without receiving Agent lifecycle authority or a public resume operation.
-- Follow-up source and cancellation travel in one options object, matching the intent-helper shape on `Agent` while retaining the existing live-delivery and cold-resume semantics.
+- Sender authority is an exact live `Agent`; cancellation travels in one options object and owns work only until inbox acceptance.
 - Session durability has one barrier operation. Its participation result remains observable, but no continuable-child path treats arbitrary listener participation as proof that a persistence backend stored the state.
-- The `send_message` and `report` schemas, accepted message identities, `AgentHandle` ownership, durable event vocabulary, and model-visible transcript follow the activation-based realization linked above.
+- The single `send_message` schema, accepted message identities, `AgentHandle` ownership, durable event vocabulary, and model-visible transcript follow the activation-based realization linked above.

+ 6 - 6
.agents/notes/implemented/simplification/2026-07-27-intent-named-subagent-continuation-operations.zh.md

@@ -4,17 +4,17 @@ Status: implemented
 
 [English](2026-07-27-intent-named-subagent-continuation-operations.md) | 中文
 
-当前基于 Activation 的实现由[可继续的 subagent](../feature/2026-07-28-continuable-subagent-conversations.zh.md)负责。它保留本记录命名的 `followup` 操作,返回已接受的 `MessageId`,使用裸 `Agent` 参数作为确切的在线直属父级权限,并将提供方对可继续 child 的参与限制为 `prepareContinuable`。
+提供方请求与会话 flush 决策仍然有效。[相邻 Agent 共享一个 Steer 消息操作](../architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md)取代了本记录的 `followup` 命名与 options:公开操作现在是可用于任一相邻方向的 `sendMessage(sender, targetId, content, { signal })`。
 
 ## 问题
 
-将可继续 child 的编排合并到 `ctx.subagents` 后,提供方分发与调用方意图共存于同一个公开服务中。`resume(name, request)` 接受描述符、已鉴权的 parent、持久化 child id 与激活信号,而只有内部继续执行管理器才能正确解析这些数据。`sendMessage(...)` 暴露的是传输层措辞,而不是 `Agent` 已采用的 `followup` 意图;它还将来源与信号拆成独立参数,扩大了操作接口,而每个调用方都必须以原子方式同时使用二者
+将可继续 child 的编排合并到 `ctx.subagents` 后,提供方分发与调用方意图共存于同一个公开服务中。`resume(name, request)` 接受描述符、已鉴权的 parent、持久化 child id 与激活信号,而只有内部继续执行管理器才能正确解析这些数据。方向专属的 `followup` 与 `reportFrom` 操作还会为一项相邻 Agent 能力编码不同的路由与调度
 
 持久性边界还同时公开了 `SessionStore.flush()` 与 `flushRequired()`。二者执行相同的作用域内并行分发,唯一差别是是否接受空的监听器快照,因此会话接口将一个消费方的策略编码为第二项操作。
 
 ## 决策
 
-`SubagentRuntime` 分离四种执行意图:`start(name, request)` 返回普通的、由持有方负责的 one-shot run;`startContinuable(spec)` 建立持久化 child,并返回其 id 与已接受的初始 `MessageId`;`followup(parent, childId, content, { source, signal })` 发送后续 parent 内容;`reportFrom(child, content, { delivery, signal })` 将选定的 child 内容发送给其直接 parent。`followup` 与 `Agent.followup()` 一致,而 `SubagentRun.steer()` 仍是范围更窄的能力,仅向已确认仍在运行的 run 提供 steering(中途引导)。面向模型的工具保留稳定的 `send_message` 与 `report` 名称,并将路由委托给对应的意图方法
+`SubagentRuntime` 分离三种调用方意图:`start(name, request)` 返回普通的、由持有方负责的 one-shot run;`startContinuable(spec)` 建立持久化 child,并返回其 id 与已接受的初始 `MessageId`;`sendMessage(sender, targetId, content, { signal })` 则跨一条直接 parent-child 边发送由模型编写的内容。消息操作从确切在线 sender 推导来源信息,并负责相邻关系检查、冷恢复与固定 Steer 调度。唯一面向模型的 `send_message({ agent_id, message })` 工具会在两个方向委托给该操作
 
 调用方请求与提供方请求相互分离。`SubagentStartRequest` 包含调用方提供的 one-shot 数据;`ResolvedSubagentStartRequest` 会在调用 `SubagentProvider.start()` 前加入由服务解析的描述符。创建可继续 child 时,管理器将 `ContinuableCreateRequest` 传给可选的 `SubagentProvider.prepareContinuable()`,且只接收分离的创建数据。`SubagentRuntime.resume()` 与提供方恢复分发均不存在:继续执行管理器加载描述符、对 parent 进行鉴权,并负责 Agent 实体化、提示词投递、冷恢复与 teardown。
 
@@ -24,7 +24,7 @@ Status: implemented
 
 **保留公开的提供方恢复分发。** 继续执行管理器之外,没有任何生产调用方同时负责安全调用所需的描述符查找、直接 parent 鉴权、Agent 实体化、Activation 所有权与 child-first teardown。公开方法会暴露已解析的实现数据,却没有合理的独立调用意图;提供方改为通过 `prepareContinuable` 贡献分离的首次创建数据,且从不参与冷恢复。
 
-**在服务上保留 `sendMessage`。** 面向模型的工具发送消息,但服务操作表达的是后续操作,既可能对运行中的激活执行 steering,也可能从持久化存储恢复。`followup` 与结构化 `Agent` 接口保持一致,也不承诺特定路由
+**保留方向专属的 `followup` 与 `reportFrom` 操作。** 这样可以保留 child 无需填写接收方的短调用,但会重复权限、来源信息与投递行为,并让服务词汇取决于方向。一个 `sendMessage` 操作可以命名共享意图,并让目标保持显式
 
 **保留 `flushRequired()`。** 第二个方法只封装了空监听器检查。由现有屏障返回是否有监听器参与,可以让分发只保留一套实现,并让每个调用方自行判定缺少监听器是否可接受。
 
@@ -33,6 +33,6 @@ Status: implemented
 ## 影响
 
 - Cordis 服务目录只包含调用方操作;提供方可以通过 `SubagentProvider.prepareContinuable?()` 选择参与可继续 child 的首次创建,但不会获得 Agent 生命周期权限或公开恢复操作。
-- 后续操作的来源与取消信号通过同一个选项对象传递,与 `Agent` 上按意图命名的辅助方法形态一致,同时保留在线投递与从持久化存储恢复的语义
+- sender 权限来自确切在线 `Agent`;取消信号通过一个选项对象传递,并且只负责 inbox 接受前的工作
 - 会话持久性只有一个屏障操作。参与结果仍可观测,但任何可继续 child 路径都不会将任意监听器参与视为持久化后端已存储状态的证明。
-- `send_message` 与 `report` schema、已接受的消息标识、`AgentHandle` 所有权、持久化事件词汇与模型可见的 transcript(文本记录)遵循上文链接的基于 Activation 的实现。
+- 单一 `send_message` schema、已接受的消息标识、`AgentHandle` 所有权、持久化事件词汇与模型可见的 transcript(文本记录)遵循上文链接的基于 Activation 的实现。

+ 2 - 2
.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.i18n.yaml

@@ -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/testing/2026-06-22-fork-child-replay-seed-boundary.md
-2026-06-22-fork-child-replay-seed-boundary.md: cf4a974035b38ee61e4c2d1cab34776b2ad186c8
-2026-06-22-fork-child-replay-seed-boundary.zh.md: bcc60a583a5f9a4d1fd10ec16e90c49c879d295c
+2026-06-22-fork-child-replay-seed-boundary.md: 1fefc3546ebe029ba95318dad3ac2d463a3fe497
+2026-06-22-fork-child-replay-seed-boundary.zh.md: bb43eacf075318dc0101fca2cacb45a03d847764

+ 7 - 7
.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md

@@ -18,19 +18,19 @@ Deriving the child script from the whole fork-child log therefore replays the **
 
 Record where a session's **inherited** prefix ends, persist it, and have the replay harness derive a child's script from its **own** events only.
 
-### 1. `seedLength` on the session header
+### 1. Lineage metadata and an exact body-owned cut
 
-`SessionHeader` gains an optional `seedLength: number` — how many leading events were inherited via a seed rather than produced by this session. The fork backend stamps it (= the seeded-prefix length) when it creates the child; a fresh spawn leaves it absent (≡ 0). It is threaded through `CreateSessionOptions.meta` (and `CreateAgentOptions.meta`), set in `SessionStore.prepare`.
+`SessionHeader.isSeeded` records whether a Session has inherited lineage without exposing a body coordinate to header-only readers. The exact leading-event count is the separately branded `SessionLogOffset` `inheritedEventCount`; a fork supplies both `isSeeded: true` and the copied-prefix length, while a fresh spawn supplies an unseeded header and cut zero. The cut travels through `CreateSessionOptions`, `CreateAgentOptions`, persistence inspection, and restored Session state.
 
-`seedLength` is **explicit**, never inferred from `seed.length`. A reconstruction (resume/load) seeds the session with its WHOLE stored log, so `seed.length` there is the full length, not the original boundary — the resume path passes the persisted `seedLength` back from the loaded header instead. (Same shape as `createdAt`, which is also explicitly preserved on reconstruction rather than re-defaulted to now.)
+`inheritedEventCount` is **explicit**, never inferred from `seed.length`. A reconstruction (resume/load) seeds the session with its WHOLE stored log, so `seed.length` there is the full length, not the original boundary — the resume path passes the decoded cut beside the logical header instead.
 
 ### 2. JSONL round-trips it
 
-JSONL stores `seedLength` on the header line (`toHeaderLine`/`fromHeaderLine`) and returns it through the shared persistence contract.
+The v0 JSONL header keeps its optional numeric `seedLength` for byte compatibility. `toHeaderLine` / `fromHeaderLine` translate it to and from logical `isSeeded` plus the exact `inheritedEventCount`, which the shared body-bearing persistence values return separately.
 
 ### 3. Replay derives a child script after the boundary
 
-`dsh-llm-replay`'s `parseSessionHeader` now also reads `seedLength` (absent ⇒ 0), and `loadSessionScripts` derives a child's entries from `parseSessionLog(text).slice(seedLength)` — the events at or after the boundary, i.e. the child's own model calls. For a spawn child `seedLength` is 0 and this is a no-op, so spawn scenarios are byte-for-byte unchanged.
+`dsh-llm-replay`'s private v0 parser reads physical `seedLength` into `inheritedEventCount` (absent ⇒ 0), and `loadSessionScripts` derives a child's entries from `parseSessionLog(text).slice(inheritedEventCount)` — the events at or after the boundary, i.e. the child's own model calls. For a spawn child the cut is 0 and this is a no-op, so spawn scenarios are byte-for-byte unchanged.
 
 This closes the routing correctness gap, and two recorded fork scenarios exercise it end to end — see [Record fork and mixed spawn+fork snapshot scenarios](../../archived/testing/2026-06-22-fork-snapshot-scenarios.md).
 
@@ -40,5 +40,5 @@ This closes the routing correctness gap, and two recorded fork scenarios exercis
 
 ## Consequences
 
-- A new persisted header field spans core and the JSONL provider; the subsystems catalog (`persistence.md`) is updated in the same change (its `SessionHeader` / `CreateSessionOptions` `type-equiv` blocks).
-- Spawn replay is unchanged (`seedLength` 0). Fork replay now routes a child to its own script; covered by a regression in `llm-replay`'s tests (a child fixture whose seeded prefix carries a parent chunk — the derived child script must exclude it, proven red without the slice) and a JSONL persistence round trip through the shared coordinator contract.
+- The lineage bit spans logical Session metadata while the exact cut spans only body-bearing core, persistence, query, and replay values; the v0 physical header remains unchanged.
+- Spawn replay is unchanged (cut 0). Fork replay routes a child to its own script; covered by a regression in `llm-replay`'s tests (a child fixture whose seeded prefix carries a parent chunk — the derived child script must exclude it, proven red without the slice) and a JSONL persistence round trip through the shared coordinator contract.

+ 7 - 7
.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.zh.md

@@ -18,19 +18,19 @@ subagent 脚本由 [`deriveReplayScript`](../../../../packages/test-support/llm-
 
 记录会话**继承**前缀的结束位置,将其持久化,并让回放 harness 仅从子会话**自身**的事件推导脚本。
 
-### 1. 会话头部的 `seedLength`
+### 1. 谱系 metadata 与正文拥有的精确 cut
 
-`SessionHeader` 新增可选字段 `seedLength: number`——表示有多少前导事件是通过 seed 继承而来、而非本会话产生的。fork 后端在创建子会话时设置它(= 播种前缀的长度);全新的 spawn 子会话不设置(等同于 0)。它通过 `CreateSessionOptions.meta`(及 `CreateAgentOptions.meta`)传递,在 `SessionStore.prepare` 中设置
+`SessionHeader.isSeeded` 记录 Session 是否具有继承谱系,而不向仅 header 的 reader 暴露正文坐标。精确的前导事件数量是单独品牌化为 `SessionLogOffset` 的 `inheritedEventCount`;fork 同时提供 `isSeeded: true` 与复制前缀的长度,全新的 spawn 则提供 unseeded header 与零 cut。该 cut 经 `CreateSessionOptions`、`CreateAgentOptions`、持久化 inspection 与恢复后的 Session 状态传递
 
-`seedLength` 是**显式**的,绝不从 `seed.length` 推断。恢复/加载时用会话的完整已存储日志作为 seed,此时 `seed.length` 是全长而非原始边界——恢复路径改为从加载的 header 中取回持久化的 `seedLength`。(做法与 `createdAt` 相同:恢复时显式保留,而非重新默认为当前时间。)
+`inheritedEventCount` 是**显式**的,绝不从 `seed.length` 推断。恢复/加载时用会话的完整已存储日志作为 seed,此时 `seed.length` 是全长而非原始边界——恢复路径改为在 logical header 之外传递解码后的 cut。
 
 ### 2. JSONL 完整往返
 
-JSONL 把 `seedLength` 存在 header 行(`toHeaderLine`/`fromHeaderLine`),并通过共享持久化约定返回它
+v0 JSONL header 为保持字节兼容而继续携带可选数值 `seedLength`。`toHeaderLine`/`fromHeaderLine` 在它与 logical `isSeeded` 加精确 `inheritedEventCount` 之间转换,共享的含正文持久化值再单独返回该 cut
 
 ### 3. 回放从边界之后推导子会话脚本
 
-`dsh-llm-replay` 的 `parseSessionHeader` 现在也读取 `seedLength`(缺失则为 0),`loadSessionScripts` 从 `parseSessionLog(text).slice(seedLength)` 推导子会话条目——即边界及之后的事件,也就是子会话自身的模型调用。对 spawn 子会话而言 `seedLength` 为 0,此操作是空操作,spawn 场景逐字节不变。
+`dsh-llm-replay` 的私有 v0 parser 把物理 `seedLength` 读入 `inheritedEventCount`(缺失则为 0),`loadSessionScripts` 从 `parseSessionLog(text).slice(inheritedEventCount)` 推导子会话条目——即边界及之后的事件,也就是子会话自身的模型调用。对 spawn 子会话而言 cut 为 0,此操作是空操作,spawn 场景逐字节不变。
 
 这弥补了路由正确性的缺口,两个已录制的 fork 场景对其进行端到端验证——见[记录 fork 与混合 spawn+fork 快照场景](../../archived/testing/2026-06-22-fork-snapshot-scenarios.md)。
 
@@ -40,5 +40,5 @@ JSONL 把 `seedLength` 存在 header 行(`toHeaderLine`/`fromHeaderLine`),
 
 ## 后果
 
-- core 与 JSONL provider 新增一个持久化 header 字段;子系统目录(`persistence.md`)在同一变更中更新(其 `SessionHeader` / `CreateSessionOptions` 的 `type-equiv` 块)
-- spawn 回放不变(`seedLength` 为 0)。fork 回放现在将子会话路由到自身的脚本;由 `llm-replay` 测试中的一个回归用例覆盖(一个子会话 fixture,其播种前缀包含父会话的分片——推导出的子会话脚本必须排除它,不做 slice 时该用例会失败),以及通过共享 coordinator 约定执行的 JSONL 持久化往返测试。
+- 谱系 bit 横跨 logical Session metadata,精确 cut 则只横跨含正文的 core、持久化、query 与 replay 值;v0 物理 header 保持不变
+- spawn 回放不变(cut 为 0)。fork 回放将子会话路由到自身的脚本;由 `llm-replay` 测试中的一个回归用例覆盖(一个子会话 fixture,其播种前缀包含父会话的分片——推导出的子会话脚本必须排除它,不做 slice 时该用例会失败),以及通过共享 coordinator 约定执行的 JSONL 持久化往返测试。

+ 0 - 3
apps/cli/composition.md

@@ -136,8 +136,6 @@ flowchart LR
   cfg --> plugin_dsh_base_tool_subagent
   plugin_dsh_base_tool_subagent_fork["tool-subagent-fork<br/>@deepseek-ai/dsh-tool-subagent"]
   cfg --> plugin_dsh_base_tool_subagent_fork
-  plugin_dsh_base_tool_subagent_report["tool-subagent-report<br/>@deepseek-ai/dsh-tool-subagent-report"]
-  cfg --> plugin_dsh_base_tool_subagent_report
   plugin_dsh_base_workflow_worker_thread["workflow-worker-thread<br/>@deepseek-ai/dsh-workflow-worker-thread"]
   cfg --> plugin_dsh_base_workflow_worker_thread
   plugin_dsh_base_tool_workflow["tool-workflow<br/>@deepseek-ai/dsh-tool-workflow"]
@@ -248,7 +246,6 @@ flowchart LR
 | `tool-subagent-list-agents` | `@deepseek-ai/dsh-tool-subagent-control/list-agents` |
 | `tool-subagent` | `@deepseek-ai/dsh-tool-subagent` |
 | `tool-subagent-fork` | `@deepseek-ai/dsh-tool-subagent` |
-| `tool-subagent-report` | `@deepseek-ai/dsh-tool-subagent-report` |
 | `workflow-worker-thread` | `@deepseek-ai/dsh-workflow-worker-thread` |
 | `tool-workflow` | `@deepseek-ai/dsh-tool-workflow` |
 | `timeout-policy` | `@deepseek-ai/dsh-tool-call-timeout-policy` |

+ 0 - 1
apps/cli/package.json

@@ -138,7 +138,6 @@
     "@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:^",
     "@deepseek-ai/dsh-subprocess-local": "workspace:^",
     "@deepseek-ai/dsh-system-prompt": "workspace:^",
-    "@deepseek-ai/dsh-tool-subagent-report": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/dsh-user-approval": "workspace:^",
     "@types/js-yaml": "^4.0.9",

+ 2 - 2
apps/cli/reference/README.i18n.yaml

@@ -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 apps/cli/reference/README.md
-README.md: 9e88d527ebaed9a20b64ea77016fa7af4ca4c216
-README.zh.md: 2d4f330aa46c94e8ebc2798dd678242ee2acb528
+README.md: 78328be4d614ebf647d6f00a6f0d66978919da3e
+README.zh.md: c140c3ba528509611ae17712851c6d406c190972

+ 1 - 1
apps/cli/reference/README.md

@@ -89,7 +89,7 @@ New sessions in base-backed profiles default to the `workspace-write` permission
 
 ## Shared deployment behavior
 
-The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search`, the public-only HTTP fetch provider, and feedback-gated session telemetry. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`. The Web app's `cordis`, `ptc`, and `standard` agent presets expose `web_fetch` in every sandbox and approval mode without per-call confirmation; the provider still rejects non-public destinations before connecting.
+The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search` and `web_fetch`, the public-only HTTP fetch provider, and feedback-gated session telemetry. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`. Enabled fetch calls run in every sandbox and approval mode without per-call confirmation; the provider rejects non-public destinations before connecting. The Web app disables the base tool row and exposes the same tools through its `cordis`, `ptc`, and `standard` agent presets.
 
 Session telemetry defaults to feedback-gated sharing: nothing is uploaded until the user records `/feedback`, and each recorded feedback uploads the session records not yet shared, through that event; a resumed session shares only its current lifecycle. `DSH_TELEMETRY_MODE=FULL` instead streams every projected session event as OTLP/HTTP logs, `DSH_TELEMETRY_MODE=DISABLED` keeps everything local, and any non-empty `DSH_TELEMETRY_DISABLED` remains an authoritative hard opt-out. `DSH_TELEMETRY_OTLP_URL` selects another collector. The shipped base has no telemetry redaction rule, so released exports can contain message text, tool arguments and results, and workspace paths; the [feedback-gated-default Agent Note](../../../.agents/notes/implemented/feature/2026-08-25-feedback-gated-telemetry-default.md) owns that deployment decision.
 

+ 1 - 1
apps/cli/reference/README.zh.md

@@ -89,7 +89,7 @@ dsh web --help
 
 ## 共享部署行为
 
-基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search`、仅限公网的 HTTP fetch 提供方,以及按反馈门控的会话遥测。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env` 和 `$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`。Web app 的 `cordis`、`ptc` 与 `standard` agent preset 会在所有 sandbox 和审批模式下暴露 `web_fetch`,无需逐次确认;提供方仍会在连接前拒绝非公开目的地址
+基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search` 和 `web_fetch`、仅限公网的 HTTP fetch 提供方,以及按反馈门控的会话遥测。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env` 和 `$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`。已启用的抓取调用会在所有 sandbox 与审批模式下执行,无需逐次确认;提供方会在连接前拒绝非公开目的地址。Web app 会禁用 base 工具配置项,再通过 `cordis`、`ptc` 与 `standard` agent preset 暴露相同工具
 
 会话遥测默认按反馈门控共享:在用户记录 `/feedback` 之前不上传任何数据,每条已记录的反馈通过该事件上传尚未共享的会话记录;恢复的会话只共享当前生命周期。`DSH_TELEMETRY_MODE=FULL` 改为将每条已投影会话事件作为 OTLP/HTTP 日志流式发送,`DSH_TELEMETRY_MODE=DISABLED` 让全部数据留在本地,任何非空的 `DSH_TELEMETRY_DISABLED` 仍是具有最终效力的遥测强制关闭开关。`DSH_TELEMETRY_OTLP_URL` 选择其他 collector。随附基础配置没有遥测脱敏规则,因此释放的导出可能包含消息文本、工具参数和结果,以及 workspace 路径;相关部署决策见[反馈门控默认值 Agent Note](../../../.agents/notes/implemented/feature/2026-08-25-feedback-gated-telemetry-default.zh.md)。
 

+ 2 - 2
apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl

@@ -3,11 +3,11 @@
 {"type":"session/end-seed","data":{}}
 {"type":"sandbox/mode","data":{"mode":"workspace-write","source":"delegation"}}
 {"type":"approval/policy","data":{"policy":"never","source":"delegation"}}
-{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call report."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}
+{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message."},{"type":"text","text":"Your parent agent id is \"{{sessionId}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"{{sessionId}}\", message: \"<self-contained result>\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}
 {"type":"turn/start","data":{"turn":1}}
 {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
 {"type":"step/start","data":{"turn":1,"step":1}}
-{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call report."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
+{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message."},{"type":"text","text":"Your parent agent id is \"{{sessionId}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"{{sessionId}}\", message: \"<self-contained result>\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
 {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
 {"type":"session/title","data":{"title":"Reply with exactly CHILD_RESULT and","messageSeqs":[8],"source":{"kind":"fallback"}}}
 {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}

+ 2 - 2
apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/parent.override.json

@@ -3,8 +3,8 @@
     "kind": "chunks",
     "chunks": [
       { "type": "block-start", "index": 0, "blockType": "tool-call" },
-      { "type": "tool-call-delta", "index": 0, "id": "start-child", "name": "subagent", "argumentsDelta": "{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call report.\"}" },
-      { "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "start-child", "name": "subagent", "arguments": "{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call report.\"}" } },
+      { "type": "tool-call-delta", "index": 0, "id": "start-child", "name": "subagent", "argumentsDelta": "{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}" },
+      { "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "start-child", "name": "subagent", "arguments": "{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}" } },
       { "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 5 } },
       { "type": "finish", "reason": { "kind": "tool-calls" } }
     ]

+ 4 - 4
apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl

@@ -9,12 +9,12 @@
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":12,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[7],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":7,\"text\":\"Start one continuable background subagent and answer from its completion notice. Do not call list_agents, send_message, job_output, or job_list.\"}]"}],"source":{"kind":"plugin","plugin":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":64}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":13,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"start-child","name":"subagent","argumentsDelta":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call report.\"}"}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call report.\"}"}}}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"start-child","name":"subagent","argumentsDelta":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":17,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":18,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call report.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[13,14,15,16,17],"surfaceOp":"append"}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":19,"time":0,"data":{"turn":1,"step":1,"callId":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call report.\"}"}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":18,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[13,14,15,16,17],"surfaceOp":"append"}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":19,"time":0,"data":{"turn":1,"step":1,"callId":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":20,"time":0,"data":{"title":"Subagent settlement","messageSeqs":[7],"source":{"kind":"provider","provider":"session-title-first-prompt-llm","model":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":21,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"start-child"},"content":[{"type":"tool-result","toolCallId":"start-child","content":[{"type":"text","text":"started subagent {{sessionId}}"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[19],"surfaceOp":"append"}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":22,"time":0,"data":{"turn":1,"step":1}}}

+ 15 - 4
apps/cli/tests/profiles/headless/tests/keyless-smoke.e2e.ts

@@ -1,22 +1,22 @@
 import { readFile, readdir } from 'node:fs/promises'
-import { zstdDecompress } from 'node:zlib'
-import { promisify } from 'node:util'
+import { zstdDecompressSync } from 'node:zlib'
 import { join } from 'node:path'
 import { fileURLToPath } from 'node:url'
 import { describe, expect, it } from 'vitest'
 import { runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
 import type { SessionEvent } from '@deepseek-ai/dsh-session'
+import { scanZstdFrames } from '@deepseek-ai/dsh-session-persistence-jsonl/src/zstd.js'
 
 const PRODUCTION_PROFILE_PROCESS_TIMEOUT_MS = 60_000
 const PRODUCTION_PROFILE_TEST_TIMEOUT_MS = PRODUCTION_PROFILE_PROCESS_TIMEOUT_MS + 15_000
 const binScript = fileURLToPath(new URL('../../../../../../packages/test-support/loader-smoke/tests/fixtures/headless-driver.ts', import.meta.url))
 const configPath = fileURLToPath(new URL('./fixtures/cli.patch.yml', import.meta.url))
 const tsconfigPath = fileURLToPath(new URL('../../../../../../tsconfig.json', import.meta.url))
-const decompress = promisify(zstdDecompress)
 
 describe('headless-agent keyless smoke', () => {
   it('boots the real Loader tree, runs the production shell tool, and persists the turn', async () => {
     let persistedHeader: Record<string, unknown> | undefined
+    let persistedToolNames: string[] = []
     const { stdout, stderr } = await runLoaderSmoke({
       label: 'headless-agent',
       tempDirPrefix: 'headless-agent-smoke-',
@@ -33,7 +33,17 @@ describe('headless-agent keyless smoke', () => {
         if (relativePath === undefined) return
         const compressed = await readFile(join(sessionsDir, relativePath))
         expect(compressed.subarray(0, 4).toString('hex')).toBe('28b52ffd')
-        persistedHeader = JSON.parse((await decompress(compressed)).toString()) as Record<string, unknown>
+        const { frames, tornStart } = scanZstdFrames(compressed)
+        expect(tornStart).toBeUndefined()
+        const records = frames.flatMap(({ start, end }) =>
+          zstdDecompressSync(compressed.subarray(start, end)).toString().trim().split('\n'))
+          .map(line => JSON.parse(line) as Record<string, unknown>)
+        persistedHeader = records[0]
+        const requestHeader = records.find(record => record.type === 'request/header')
+        const data = requestHeader?.data as Record<string, unknown> | undefined
+        const header = data?.header as Record<string, unknown> | undefined
+        const tools = header?.tools as Array<{ name?: string }> | undefined
+        persistedToolNames = tools?.flatMap(tool => tool.name === undefined ? [] : [tool.name]) ?? []
       },
     })
     const lines = stdout.trimEnd().split('\n').map(line => JSON.parse(line) as Record<string, unknown>)
@@ -50,5 +60,6 @@ describe('headless-agent keyless smoke', () => {
     })
     expect(String(result?.['output'])).toContain('CLI_TOOL_ROUND_TRIP')
     expect(persistedHeader).toMatchObject({ type: 'session' })
+    expect(persistedToolNames).toEqual(expect.arrayContaining(['web_fetch', 'web_search']))
   }, PRODUCTION_PROFILE_TEST_TIMEOUT_MS)
 })

+ 7 - 6
apps/cli/tests/profiles/headless/tests/semantic-checkpoint.expected.e2e.ts

@@ -5,7 +5,7 @@ import { Context } from '@deepseek-ai/cordis'
 import { normalizeSessionSnapshot, type NormalizeContext } from '@deepseek-ai/dsh-session-snapshot'
 import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
 import { createUserMessage, ToolCallId , createMessage } from '@deepseek-ai/dsh-llm'
-import SessionStore, { SESSION_FORMAT_VERSION, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
+import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionSeq, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
 import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
 import { describe, expect, it } from 'vitest'
 
@@ -29,17 +29,18 @@ async function seedInterruptedSession(root: string, cwd: string): Promise<string
     id: sessionId,
     createdAt: 1,
     cwd,
+    isSeeded: false,
     delegationDepth: 0,
   }
   const events: SessionEvent[] = [
-    { type: 'turn/start', seq: 0, time: 10, data: { turn: 1 } },
-    { type: 'user/message', seq: 1, time: 11, data: createUserMessage({
+    { type: 'turn/start', seq: SessionSeq(0), time: 10, data: { turn: 1 } },
+    { type: 'user/message', seq: SessionSeq(1), time: 11, data: createUserMessage({
       content: [{ type: 'text', text: 'Perform one side-effecting remote mutation.' }], source: { kind: 'user' },
     }), surfaceOp: 'append' },
-    { type: 'step/start', seq: 2, time: 12, data: { turn: 1, step: 1 } },
+    { type: 'step/start', seq: SessionSeq(2), time: 12, data: { turn: 1, step: 1 } },
     {
       type: 'assistant/message',
-      seq: 3,
+      seq: SessionSeq(3),
       time: 13,
       data: {
         turn: 1,
@@ -57,7 +58,7 @@ async function seedInterruptedSession(root: string, cwd: string): Promise<string
     },
     {
       type: 'tool/call',
-      seq: 4,
+      seq: SessionSeq(4),
       time: 14,
       data: {
         turn: 1,

+ 5 - 4
apps/cli/tests/profiles/headless/tests/session-format-guard.expected.e2e.ts

@@ -13,6 +13,7 @@ import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-l
 import SessionStore, {
   SESSION_FORMAT_VERSION,
   SessionId,
+  SessionSeq,
   type SessionEvent,
   type SessionHeader,
 } from '@deepseek-ai/dsh-session'
@@ -32,7 +33,7 @@ async function seedSession(root: string, cwd: string, version: number, events: S
   const ctx = new Context()
   await ctx.plugin(SessionStore)
   await ctx.plugin(JsonlSessionPersistence, { root, compression: 'none' })
-  const meta: SessionHeader = { version, id: sessionId, createdAt: 1, cwd }
+  const meta: SessionHeader = { version, id: sessionId, createdAt: 1, cwd, isSeeded: false }
   try {
     await ctx.sessionPersistence.create(meta)
     await ctx.sessionPersistence.append(sessionId, events)
@@ -46,8 +47,8 @@ async function seedSession(root: string, cwd: string, version: number, events: S
 
 function closedTurn(): SessionEvent[] {
   return [
-    { type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
-    { type: 'turn/end', seq: 1, time: 2, data: { turn: 1, reason: { kind: 'completed' } } },
+    { type: 'turn/start', seq: SessionSeq(0), time: 1, data: { turn: 1 } },
+    { type: 'turn/end', seq: SessionSeq(1), time: 2, data: { turn: 1, reason: { kind: 'completed' } } },
   ]
 }
 
@@ -92,7 +93,7 @@ describe('session format guard through the assembled app', () => {
       prepare: async (runCwd) => {
         sessionPath = await seedSession(join(runCwd, '.sessions'), runCwd, SESSION_FORMAT_VERSION, [
           ...closedTurn(),
-          { type: 'future/event', seq: 2, time: 3, data: { payload: 1 } } as unknown as SessionEvent,
+          { type: 'future/event', seq: SessionSeq(2), time: 3, data: { payload: 1 } } as unknown as SessionEvent,
         ])
       },
     })

+ 8 - 6
apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts

@@ -11,7 +11,7 @@ import { Context } from '@deepseek-ai/cordis'
 import { normalizeSessionSnapshot, type NormalizeContext } from '@deepseek-ai/dsh-session-snapshot'
 import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
 import { createUserMessage } from '@deepseek-ai/dsh-llm'
-import SessionStore, { SESSION_FORMAT_VERSION, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
+import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionSeq, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
 import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
 import { describe, expect, it } from 'vitest'
 
@@ -40,12 +40,13 @@ async function seedDescriptorlessChild(root: string, cwd: string): Promise<void>
     id: parentId,
     createdAt: 1,
     cwd,
+    isSeeded: false,
     delegationDepth: 0,
   }
   const parentEvents: SessionEvent[] = [
-    { type: 'turn/start', seq: 0, time: 10, data: { turn: 1 } },
-    { type: 'user/message', seq: 1, time: 11, data: createUserMessage({ content: [{ type: 'text', text: 'Start a background job.' }], source: { kind: 'user' } }), surfaceOp: 'append' },
-    { type: 'turn/end', seq: 2, time: 12, data: { turn: 1, reason: { kind: 'completed' } } },
+    { type: 'turn/start', seq: SessionSeq(0), time: 10, data: { turn: 1 } },
+    { type: 'user/message', seq: SessionSeq(1), time: 11, data: createUserMessage({ content: [{ type: 'text', text: 'Start a background job.' }], source: { kind: 'user' } }), surfaceOp: 'append' },
+    { type: 'turn/end', seq: SessionSeq(2), time: 12, data: { turn: 1, reason: { kind: 'completed' } } },
   ]
   const childMeta: SessionHeader = {
     version: SESSION_FORMAT_VERSION,
@@ -53,12 +54,13 @@ async function seedDescriptorlessChild(root: string, cwd: string): Promise<void>
     createdAt: 2,
     cwd,
     parentSession: parentId,
+    isSeeded: false,
     origin: 'subagent',
     delegationDepth: 1,
   }
   const childEvents: SessionEvent[] = [
-    { type: 'turn/start', seq: 0, time: 20, data: { turn: 1 } },
-    { type: 'turn/end', seq: 1, time: 21, data: { turn: 1, reason: { kind: 'interrupted' } } },
+    { type: 'turn/start', seq: SessionSeq(0), time: 20, data: { turn: 1 } },
+    { type: 'turn/end', seq: SessionSeq(1), time: 21, data: { turn: 1, reason: { kind: 'interrupted' } } },
   ]
   try {
     await ctx.sessionPersistence.create(parentMeta)

+ 7 - 6
apps/cli/tests/profiles/headless/tests/subagent-inheritance.expected.e2e.ts

@@ -10,7 +10,7 @@ import { Context } from '@deepseek-ai/cordis'
 import { normalizeSessionSnapshot, type NormalizeContext } from '@deepseek-ai/dsh-session-snapshot'
 import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
 import { createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
-import SessionStore, { SESSION_FORMAT_VERSION, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
+import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionSeq, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
 import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
 import { describe, expect, it } from 'vitest'
 
@@ -36,15 +36,16 @@ async function seedReadOnlyParent(root: string, cwd: string): Promise<void> {
     id: sessionId,
     createdAt: 1,
     cwd,
+    isSeeded: false,
     delegationDepth: 0,
   }
   const events: SessionEvent[] = [
-    { type: 'turn/start', seq: 0, time: 10, data: { turn: 1 } },
-    { type: 'user/message', seq: 1, time: 11, data: createUserMessage({ content: [{ type: 'text', text: 'Tighten this session to read-only.' }], source: { kind: 'user' } }), surfaceOp: 'append' },
-    { type: 'sandbox/mode', seq: 2, time: 12, data: { mode: 'read-only' } },
+    { type: 'turn/start', seq: SessionSeq(0), time: 10, data: { turn: 1 } },
+    { type: 'user/message', seq: SessionSeq(1), time: 11, data: createUserMessage({ content: [{ type: 'text', text: 'Tighten this session to read-only.' }], source: { kind: 'user' } }), surfaceOp: 'append' },
+    { type: 'sandbox/mode', seq: SessionSeq(2), time: 12, data: { mode: 'read-only' } },
     {
       type: 'request/header',
-      seq: 3,
+      seq: SessionSeq(3),
       time: 13,
       data: {
         header: {
@@ -57,7 +58,7 @@ async function seedReadOnlyParent(root: string, cwd: string): Promise<void> {
         reason: 'initial',
       },
     },
-    { type: 'turn/end', seq: 4, time: 14, data: { turn: 1, reason: { kind: 'completed' } } },
+    { type: 'turn/end', seq: SessionSeq(4), time: 14, data: { turn: 1, reason: { kind: 'completed' } } },
   ]
   try {
     await ctx.sessionPersistence.create(meta)

Some files were not shown because too many files changed in this diff