Просмотр исходного кода

Merge origin/master into feat/remove-str-replace-editor-from-minimal

fz 5 дней назад
Родитель
Сommit
94ff82f57b
100 измененных файлов с 1334 добавлено и 243 удалено
  1. 2 2
      .agents/notes/implemented/architecture/2026-06-18-session-surface.i18n.yaml
  2. 16 20
      .agents/notes/implemented/architecture/2026-06-18-session-surface.md
  3. 16 20
      .agents/notes/implemented/architecture/2026-06-18-session-surface.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml
  5. 5 5
      .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md
  6. 5 5
      .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md
  7. 2 2
      .agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml
  8. 4 2
      .agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md
  9. 4 2
      .agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md
  10. 2 2
      .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml
  11. 5 3
      .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
  12. 5 3
      .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md
  13. 2 2
      .agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml
  14. 5 5
      .agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md
  15. 5 5
      .agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md
  16. 2 2
      .agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.i18n.yaml
  17. 1 1
      .agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md
  18. 1 1
      .agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.zh.md
  19. 6 0
      .agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.i18n.yaml
  20. 47 0
      .agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.md
  21. 47 0
      .agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.zh.md
  22. 2 2
      .agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.i18n.yaml
  23. 22 1
      .agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md
  24. 22 1
      .agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md
  25. 2 2
      .agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.i18n.yaml
  26. 2 0
      .agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.md
  27. 2 0
      .agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.zh.md
  28. 6 0
      .agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.i18n.yaml
  29. 95 0
      .agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md
  30. 95 0
      .agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md
  31. 3 3
      .agents/notes/implemented/architecture/2026-09-06-v3-canonical-session-envelopes.i18n.yaml
  32. 47 0
      .agents/notes/implemented/architecture/2026-09-06-v3-canonical-session-envelopes.md
  33. 47 0
      .agents/notes/implemented/architecture/2026-09-06-v3-canonical-session-envelopes.zh.md
  34. 6 0
      .agents/notes/implemented/architecture/2026-09-07-prebuilt-system-primitives.i18n.yaml
  35. 33 0
      .agents/notes/implemented/architecture/2026-09-07-prebuilt-system-primitives.md
  36. 33 0
      .agents/notes/implemented/architecture/2026-09-07-prebuilt-system-primitives.zh.md
  37. 6 0
      .agents/notes/implemented/bug-fix/2026-09-07-typert-package-local-forwarding-imports.i18n.yaml
  38. 29 0
      .agents/notes/implemented/bug-fix/2026-09-07-typert-package-local-forwarding-imports.md
  39. 29 0
      .agents/notes/implemented/bug-fix/2026-09-07-typert-package-local-forwarding-imports.zh.md
  40. 2 2
      .agents/notes/implemented/feature/2026-06-15-ptc.i18n.yaml
  41. 8 4
      .agents/notes/implemented/feature/2026-06-15-ptc.md
  42. 8 4
      .agents/notes/implemented/feature/2026-06-15-ptc.zh.md
  43. 2 2
      .agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.i18n.yaml
  44. 3 3
      .agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md
  45. 3 3
      .agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.zh.md
  46. 2 2
      .agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml
  47. 1 1
      .agents/notes/implemented/feature/2026-07-06-sandbox.md
  48. 1 1
      .agents/notes/implemented/feature/2026-07-06-sandbox.zh.md
  49. 2 2
      .agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.i18n.yaml
  50. 1 1
      .agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.md
  51. 1 1
      .agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.zh.md
  52. 2 2
      .agents/notes/implemented/feature/2026-07-26-ptc-live-parallel-dispatch.i18n.yaml
  53. 2 2
      .agents/notes/implemented/feature/2026-07-26-ptc-live-parallel-dispatch.md
  54. 2 2
      .agents/notes/implemented/feature/2026-07-26-ptc-live-parallel-dispatch.zh.md
  55. 2 2
      .agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml
  56. 6 6
      .agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
  57. 6 6
      .agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md
  58. 2 2
      .agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.i18n.yaml
  59. 2 2
      .agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.md
  60. 2 2
      .agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.zh.md
  61. 2 2
      .agents/notes/implemented/feature/2026-08-25-promote-open-anywhere-plugin.i18n.yaml
  62. 6 2
      .agents/notes/implemented/feature/2026-08-25-promote-open-anywhere-plugin.md
  63. 6 2
      .agents/notes/implemented/feature/2026-08-25-promote-open-anywhere-plugin.zh.md
  64. 2 2
      .agents/notes/implemented/feature/2026-08-31-cross-process-session-write-lease.i18n.yaml
  65. 3 3
      .agents/notes/implemented/feature/2026-08-31-cross-process-session-write-lease.md
  66. 3 3
      .agents/notes/implemented/feature/2026-08-31-cross-process-session-write-lease.zh.md
  67. 6 0
      .agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.i18n.yaml
  68. 97 0
      .agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.md
  69. 97 0
      .agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.zh.md
  70. 6 0
      .agents/notes/implemented/feature/2026-09-07-session-prose-local-media-display.i18n.yaml
  71. 43 0
      .agents/notes/implemented/feature/2026-09-07-session-prose-local-media-display.md
  72. 43 0
      .agents/notes/implemented/feature/2026-09-07-session-prose-local-media-display.zh.md
  73. 6 0
      .agents/notes/implemented/feature/2026-09-08-file-display-through-filesystem.i18n.yaml
  74. 35 0
      .agents/notes/implemented/feature/2026-09-08-file-display-through-filesystem.md
  75. 35 0
      .agents/notes/implemented/feature/2026-09-08-file-display-through-filesystem.zh.md
  76. 2 2
      .agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.i18n.yaml
  77. 3 2
      .agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.md
  78. 3 2
      .agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.zh.md
  79. 2 2
      .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml
  80. 1 1
      .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md
  81. 1 1
      .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md
  82. 2 2
      .agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.i18n.yaml
  83. 5 1
      .agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.md
  84. 5 1
      .agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.zh.md
  85. 2 2
      .agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml
  86. 2 2
      .agents/notes/implemented/process/2026-08-10-npm-release-sequences.md
  87. 2 2
      .agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md
  88. 2 2
      .agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.i18n.yaml
  89. 2 0
      .agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.md
  90. 3 1
      .agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.zh.md
  91. 6 0
      .agents/notes/implemented/process/2026-09-08-comment-only-review-routing.i18n.yaml
  92. 41 0
      .agents/notes/implemented/process/2026-09-08-comment-only-review-routing.md
  93. 41 0
      .agents/notes/implemented/process/2026-09-08-comment-only-review-routing.zh.md
  94. 19 11
      .agents/notes/implemented/process/2026-09-08-trusted-changed-file-review-routing.md
  95. 0 47
      .agents/notes/implemented/process/2026-09-08-trusted-changed-file-review-routing.zh.md
  96. 6 0
      .agents/notes/implemented/simplification/2026-09-07-file-content-scan.i18n.yaml
  97. 32 0
      .agents/notes/implemented/simplification/2026-09-07-file-content-scan.md
  98. 32 0
      .agents/notes/implemented/simplification/2026-09-07-file-content-scan.zh.md
  99. 2 2
      .agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.i18n.yaml
  100. 3 1
      .agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.md

+ 2 - 2
.agents/notes/implemented/architecture/2026-06-18-session-surface.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-18-session-surface.md
-2026-06-18-session-surface.md: 0139cc4beba766e4e8b936594899649304234eaa
-2026-06-18-session-surface.zh.md: 0596d2a0425890924276265dd9cc6c32fcffb974
+2026-06-18-session-surface.md: 93ea55883dedd943fe1ffac67a9842c962ca6dac
+2026-06-18-session-surface.zh.md: 54ecb1162bc46007dfcbb7d8cb39075d52171567

+ 16 - 20
.agents/notes/implemented/architecture/2026-06-18-session-surface.md

@@ -12,36 +12,32 @@ The event log is authoritative, but history manipulation had no durable shared m
 
 Add a **surface** — a derived, cached order of event sequences (the subset of events that produce LLM messages) — maintained by `surfaceOp` markers in the event log.
 
-### Two new top-level fields on `SessionEvent`
+### Top-level surface metadata on `SessionEvent`
 
-Every `SessionEvent` gains two optional fields (structural metadata, like `seq`/`time`):
+Surface metadata belongs only to the four surface event types (`system/message`, `user/message`, `assistant/message`, `tool/result`):
 
-- **`sourceEventSeqs?: number[]`** — seq numbers of earlier events cited as sources, such as a `tool/call` cited by its result or surface nodes shadowed by a compaction marker. A present list is non-empty, unique, earlier, and known. V2 `assistant/message` embeds its provider stream and cannot carry this field. Without cited seqs, replay cannot validate that a replace-range operation names every event it removed.
-- **`surfaceOp?: SurfaceOp`** — how this event entered the surface. Absent for non-surface events.
+- **`sourceEventSeqs?: SessionSeq[]`** — seq numbers of earlier events cited as sources, such as a `tool/call` cited by its result or surface nodes shadowed by a compaction marker. A present list is non-empty, unique, earlier, and known. `assistant/message` embeds its provider stream and cannot carry this field. Without cited seqs, replay cannot validate that a replace-range operation names every event it removed.
+- **`surfaceOp: SurfaceOp`** — required placement for every surface event. Known log-only events forbid both metadata fields; native unknown or obsolete ignorable envelopes remain opaque.
 
 ### SurfaceOp: two operations
 
-```ts
-export type SurfaceOp =
-  | 'append'                                    // normal tail append
-  | { op: 'replace'; start: number; end: number }  // shadow [start, end] inclusive
-```
+The [source-backed `SurfaceOp` reference](../../../../docs/subsystems/session.md#surface-types) defines the exact union. Replacement objects contain only `op`, `startSeq`, and `endSeq`; endpoints use the `SessionSeq` brand.
 
-1. **Append** — add the new event seq to the tail. Used by `user/message`, `assistant/message`, `tool/result`, `context/message`. The loop passes `surfaceOp: 'append'` on all such appends and records `sourceEventSeqs` where applicable: `tool/result` records its `tool/call` source, while `assistant/message` owns its embedded stream directly.
+1. **Append** — add the new event seq to the tail. Used by `system/message`, `user/message`, `assistant/message`, `tool/result`. The loop passes `surfaceOp: 'append'` on all such appends and records `sourceEventSeqs` where applicable: `tool/result` records its `tool/call` source, while `assistant/message` owns its embedded stream directly.
 
-2. **Replace** — remove entries from `start` through `end` (both inclusive) and insert the new event seq in their place. Both `start` and `end` must be present in the current surface; `start === end` replaces one entry. The event's `sourceEventSeqs` must contain every shadowed surface seq. The shadowed events remain in the log but are no longer on the surface.
+2. **Replace** — remove entries from `startSeq` through `endSeq` (both inclusive) and insert the new event seq in their place. Both `startSeq` and `endSeq` must be present in the current surface; `startSeq === endSeq` replaces one entry. The event's `sourceEventSeqs` must contain every shadowed surface seq. The shadowed events remain in the log but are no longer on the surface.
 
 ### SurfaceManager: delta-based, not full rebuild
 
-A `Session` owns one `SurfaceManager` that maintains an ordered `number[]` of event seqs. The manager validates each seed or append candidate without applying it before commit, then processes only committed events since its previous synchronization rather than rescanning the entire log. `Session.surface` exposes the same manager through the readonly `SessionSurface` contract, so acceptance, derived history, compaction, and workspace context share one incremental state. Replace locates its inclusive endpoints by array position and splices the replacement seq into that range; no second manager, link objects, or seq-to-node map duplicates the order.
+A `Session` owns one `SurfaceManager` that maintains an ordered `SessionSeq[]` of event seqs. The manager validates each seed or append candidate without applying it before commit, then processes only committed events since its previous synchronization rather than rescanning the entire log. `Session.surface` exposes the same manager through the readonly `SessionSurface` contract, so acceptance, derived history, compaction, and workspace context share one incremental state. Replace locates its inclusive endpoints by array position and splices the replacement seq into that range; no second manager, link objects, or seq-to-node map duplicates the order.
 
 Delta processing is O(1) when no new events and O(new events) when new events arrive.
 
-`deriveMessages()` uses the surface when surface markers exist, falling back to the existing linear scan for sessions without markers (backward compatibility).
+`deriveMessages()` walks the surface as its sole derivation path. A surface event without its required marker is invalid, not an implicit append.
 
 ### Persistence
 
-The new fields are serialized as top-level JSON properties. JSONL storage requires no separate column mapping: its lossless JSON boundary preserves both values. Released v0 and v1 share this surface representation, and the identity v0-to-v1 edge preserves it exactly; a future structural representation change increments `SESSION_FORMAT_VERSION` and owns an adjacent migration.
+The fields are serialized as top-level JSON properties. JSONL preserves placement and provenance without a separate column mapping. The [V3 canonical-envelope decision](2026-09-06-v3-canonical-session-envelopes.md) owns exact replacement keys and strict-acceptance rationale; the [V2-to-V3 specification](../../../../packages/session/session-format-v2-to-v3/README.md#canonical-envelopes) owns historical conversion. This note retains ordered-projection ownership and replacement rationale.
 
 ### Crash recovery
 
@@ -51,22 +47,22 @@ The `repair.ts` module synthesizes `tool/result` closers for orphaned tool calls
 
 `Session` validates `sourceEventSeqs` and `surfaceOp` at the always-on seed/append boundary: source lists are non-empty, unique, earlier, and known; `assistant/message` carries no source list; replacement endpoints exist in surface order; and `sourceEventSeqs` covers every shadowed node. These are single-record acceptance and storage-projection rules, not optional invariant-service contributions.
 
-Every surface-eligible event must carry `surfaceOp` or it would disappear from derived history. Typed `append` overloads enforce this for literal event types; runtime checks in `append` and the seed constructor cover widened unions and current loaded logs. Historical v0 validation and normalization belong to the v0-to-v1 edge rather than generic Session code.
+Every surface-eligible event must carry `surfaceOp` or it would disappear from derived history. Typed `append` overloads enforce this for literal event types; runtime checks in `append` and the seed constructor cover widened unions and current loaded logs. Released validation and conversion belong to their versioned migration edges rather than generic Session code; see the [V2-to-V3 placement rules](../../../../packages/session/session-format-v2-to-v3/README.md#canonical-envelopes).
 
 ## Alternatives considered
 
 - **Per-plugin `agent/request` wrapping** (the pre-surface pattern for history manipulation) — listener-ordering fragility, no durable record of what was changed, and every new manipulation forces another change to core `deriveMessages()`.
-- **Half-open `[start, endExclusive)` replace ranges** — rejected: endpoints are named by surface event seqs, and single-entry replacement (`start === end`) reads naturally with inclusive semantics.
+- **Half-open `[start, endExclusive)` replace ranges** — rejected: endpoints are named by surface event seqs, and single-entry replacement (`startSeq === endSeq`) reads naturally with inclusive semantics.
 - **Linked node objects plus a seq map** — rejected: production did not read predecessor links, the only successor use was the next array position, and replacement already required linear `indexOf` lookup. A single seq array preserves the same asymptotic behavior with one representation to validate.
 - **Full rebuild behind a dirty flag** instead of delta processing — O(N²) over a session's lifetime: every single-event append would rescan all prior events.
 
 ## Consequences
 
 - **`packages/core/session`**: `surface.ts` (`SurfaceManager`) maintains one ordered seq array for candidate acceptance and live projection; `SessionSurface` is its readonly public view. `SurfaceOp`/`SurfaceIntent` and the top-level session-event fields record how entries join it. `append()` requires a `SurfaceIntent` for surface events, `deriveMessages()` walks the surface as the sole derivation path, and `repair.ts` emits surface-aware closers. The seed constructor rejects a surface-eligible seed event missing its `surfaceOp` marker (see § Invariants).
-- **`packages/core/agent-loop`**: All surface-capable appends pass surface opts. Each `assistant/message` cites its chunk seqs; each `tool/result` cites its `tool/call` seq.
-- **`packages/session/session-persistence-jsonl`**: No changes required.
-- **`packages/session/session-persistence`**: Abstract interface unchanged.
+- **`packages/core/agent-loop`**: All surface-capable appends pass surface opts. Each `assistant/message` embeds its exact provider stream and forbids `sourceEventSeqs`; each `tool/result` cites its `tool/call` seq.
+- **`packages/session/session-persistence-jsonl`**: Persists canonical surface metadata and restores current events through validated format preparation.
+- **`packages/session/session-persistence`**: Keeps storage ownership separate from the in-memory surface projection.
 
-The surface is the foundation history manipulation ships on — dsh-compaction's compaction rides it. A compaction or tool-result-pruner plugin appends one of the existing message-producing event types (a `user/message` carrying the summary, say) with `surfaceOp: { op: 'replace', start, end }` and `sourceEventSeqs` covering the shadowed entries — the new event takes the range's place on the surface while the plugin's own trace events (e.g. `compaction/start`, `compaction/end`) stay off it. Replay preserves the decision deterministically.
+The surface is the foundation history manipulation ships on — dsh-compaction's compaction rides it. A compaction or tool-result-pruner plugin appends one of the existing message-producing event types (a `user/message` carrying the summary, say) with `surfaceOp: { op: 'replace', startSeq, endSeq }` and `sourceEventSeqs` covering the shadowed entries — the new event takes the range's place on the surface while the plugin's own trace events (e.g. `compaction/start`, `compaction/end`) stay off it. Replay preserves the decision deterministically.
 
 A `tool/result` replacement may rewrite exactly one current `tool/result` and must preserve every data field except `content`. Session acceptance enforces this rule together with positional range and cited source-event validation, independent of optional diagnostic plugins.

+ 16 - 20
.agents/notes/implemented/architecture/2026-06-18-session-surface.zh.md

@@ -12,36 +12,32 @@ Status: implemented
 
 新增一个 **surface**:事件 seq 的派生并缓存的有序投影(即产出 LLM(大语言模型)消息的事件子集),通过事件日志中的 `surfaceOp` 标记维护。
 
-### `SessionEvent` 新增两个顶层字段
+### `SessionEvent` 的顶层 surface 元数据
 
-每个 `SessionEvent` 获得两个可选字段(结构性元数据,与 `seq`/`time` 同级):
+surface 元数据仅属于四种 surface 事件类型(`system/message`、`user/message`、`assistant/message`、`tool/result`):
 
-- **`sourceEventSeqs?: number[]`**:被引用为数据来源的早期事件 seq 编号,例如 result 引用的 `tool/call`,或被 compaction marker 遮蔽的 surface 节点。出现的列表必须非空、唯一、更早且已知。V2 `assistant/message` 嵌入其 provider stream,不能携带该字段。如果没有这些引用的 seq,回放就无法验证 replace-range 操作是否列出了它移除的每个事件。
-- **`surfaceOp?: SurfaceOp`**:该事件如何进入 surface。非 surface 事件不携带此字段
+- **`sourceEventSeqs?: SessionSeq[]`**:被引用为数据来源的早期事件 seq 编号,例如 result 引用的 `tool/call`,或被 compaction marker 遮蔽的 surface 节点。出现的列表必须非空、唯一、更早且已知。`assistant/message` 嵌入其 provider stream,不能携带该字段。如果没有这些引用的 seq,回放就无法验证 replace-range 操作是否列出了它移除的每个事件。
+- **`surfaceOp: SurfaceOp`**:每个 surface 事件必填的位置声明。已知仅日志事件禁止两个元数据字段;原生未知或已退役的可忽略信封保持不透明
 
 ### SurfaceOp:两种操作
 
-```ts
-export type SurfaceOp =
-  | 'append'                                    // normal tail append
-  | { op: 'replace'; start: number; end: number }  // shadow [start, end] inclusive
-```
+[与源码同步的 `SurfaceOp` 参考](../../../../docs/subsystems/session.zh.md#surface-types)定义了精确联合类型。替换对象仅包含 `op`、`startSeq` 和 `endSeq`;端点使用 `SessionSeq` 品牌。
 
-1. **Append**:在尾部追加新事件的 seq。`user/message`、`assistant/message`、`tool/result`、`context/message` 使用此操作。agent loop(智能体循环)在所有此类追加上传入 `surfaceOp: 'append'`,并在适用时记录 `sourceEventSeqs`:`tool/result` 记录其 `tool/call` 来源,`assistant/message` 则直接拥有其嵌入式 stream。
+1. **Append**:在尾部追加新事件的 seq。`system/message`、`user/message`、`assistant/message`、`tool/result` 使用此操作。agent loop(智能体循环)在所有此类追加上传入 `surfaceOp: 'append'`,并在适用时记录 `sourceEventSeqs`:`tool/result` 记录其 `tool/call` 来源,`assistant/message` 则直接拥有其嵌入式 stream。
 
-2. **Replace**:移除从 `start` 到 `end`(两端包含)的条目,并在其位置插入新事件的 seq。`start` 和 `end` 都必须存在于当前 surface;`start === end` 表示替换单个条目。该事件的 `sourceEventSeqs` 必须包含所有被遮蔽的 surface seq。被遮蔽的事件仍留在日志中,但不再出现在 surface 上。
+2. **Replace**:移除从 `startSeq` 到 `endSeq`(两端包含)的条目,并在其位置插入新事件的 seq。`startSeq` 和 `endSeq` 都必须存在于当前 surface;`startSeq === endSeq` 表示替换单个条目。该事件的 `sourceEventSeqs` 必须包含所有被遮蔽的 surface seq。被遮蔽的事件仍留在日志中,但不再出现在 surface 上。
 
 ### SurfaceManager:基于增量,而非全量重建
 
-一个 `Session` 拥有一个 `SurfaceManager`,后者维护事件 seq 的有序 `number[]`。管理器会在提交前校验每个种子或追加候选项而不应用它,然后只处理上次同步之后已经提交的事件,而不重新扫描整个日志。`Session.surface` 通过只读的 `SessionSurface` 约定暴露同一个管理器,因此接纳、派生历史、压缩与工作区上下文共享同一份增量状态。Replace 按数组位置定位两个端点(均包含在范围内),并把替换 seq splice 到该范围;不会用第二个管理器、链接对象或 seq 到节点的 map 来重复表达顺序。
+一个 `Session` 拥有一个 `SurfaceManager`,后者维护事件 seq 的有序 `SessionSeq[]`。管理器会在提交前校验每个种子或追加候选项而不应用它,然后只处理上次同步之后已经提交的事件,而不重新扫描整个日志。`Session.surface` 通过只读的 `SessionSurface` 约定暴露同一个管理器,因此接纳、派生历史、压缩与工作区上下文共享同一份增量状态。Replace 按数组位置定位两个端点(均包含在范围内),并把替换 seq splice 到该范围;不会用第二个管理器、链接对象或 seq 到节点的 map 来重复表达顺序。
 
 无新事件时增量处理为 O(1),有新事件到达时为 O(新事件数)。
 
-`deriveMessages()` 在存在 surface 标记时使用 surface,对没有标记的会话回退到既有的线性扫描(向后兼容)
+`deriveMessages()` 以遍历 surface 作为唯一派生路径。缺少必填标记的 surface 事件无效,不会被视为隐式追加
 
 ### 持久化
 
-新字段作为顶层 JSON 属性序列化。JSONL 存储无需单独列映射:其无损 JSON 边界会保留两个值。已发布 v0 与 v1 共享该 surface 表示,恒等的 v0-to-v1 边会精确保留它;未来结构性表示变更会递增 `SESSION_FORMAT_VERSION` 并拥有一项相邻迁移
+这些字段作为顶层 JSON 属性序列化。JSONL 无需单独列映射即可保留位置与来源。[V3 规范信封决策](2026-09-06-v3-canonical-session-envelopes.zh.md)负责精确替换键与严格准入依据;[V2 到 V3 规范](../../../../packages/session/session-format-v2-to-v3/README.zh.md#canonical-envelopes)负责历史转换。本文继续负责有序投影的所有权与替换依据
 
 ### 崩溃恢复
 
@@ -51,22 +47,22 @@ export type SurfaceOp =
 
 `Session` 在始终启用的 seed/append 边界校验 `sourceEventSeqs` 与 `surfaceOp`:source list 必须非空、唯一、更早且已知;`assistant/message` 不携带 source list;replacement endpoint 必须存在于 surface 顺序中;`sourceEventSeqs` 必须覆盖每个被遮蔽的节点。这些是单记录接纳与存储投影规则,不是由可选 invariant service 提供的规则。
 
-每个可进入 surface 的事件都必须携带 `surfaceOp`,否则它将从派生历史中消失。类型化的 `append` 重载对字面事件类型强制执行此规则;`append` 和种子构造函数中的运行时检查覆盖宽化联合类型和当前已加载日志。历史 v0 的校验与规范化属于 v0-to-v1 边,而不属于通用 Session 代码
+每个可进入 surface 的事件都必须携带 `surfaceOp`,否则它将从派生历史中消失。类型化的 `append` 重载对字面事件类型强制执行此规则;`append` 和种子构造函数中的运行时检查覆盖宽化联合类型和当前已加载日志。已发布格式的校验与转换属于各自版本化迁移边,而不属于通用 Session 代码;参见 [V2 到 V3 位置规则](../../../../packages/session/session-format-v2-to-v3/README.zh.md#canonical-envelopes)
 
 ## 曾考虑的替代方案
 
 - **逐插件的 `agent/request` 包装**(surface 之前的历史操纵模式):监听器排序脆弱、无法持久记录改动内容,且每种新操纵都迫使核心 `deriveMessages()` 再次修改。
-- **半开区间 `[start, endExclusive)` 的 replace 范围**:否决。端点由 surface 事件 seq 命名,单条目替换(`start === end`)在闭区间语义下读起来更自然。
+- **半开区间 `[start, endExclusive)` 的 replace 范围**:否决。端点由 surface 事件 seq 命名,单条目替换(`startSeq === endSeq`)在闭区间语义下读起来更自然。
 - **链接节点对象加 seq map**:否决。生产代码不读取前驱链接,唯一的后继用途就是数组中的下一个位置,而替换本来就需要线性 `indexOf` 查找。单个 seq 数组在保留相同渐进复杂度的同时,只留下一个需要校验的表示。
 - **脏标记后全量重建**替代增量处理:在会话生命周期内为 O(N²),每次单事件追加都要重新扫描所有先前事件。
 
 ## 后果
 
 - **`packages/core/session`**:`surface.ts`(`SurfaceManager`)维护一个用于候选接纳和实时投影的有序 seq 数组;`SessionSurface` 是其只读公共视图。`SurfaceOp`/`SurfaceIntent` 与顶层会话事件字段记录条目如何加入它。`append()` 要求 surface 事件携带 `SurfaceIntent`,`deriveMessages()` 以遍历 surface 作为唯一派生路径,`repair.ts` 则发出 surface 感知的闭合事件。种子构造函数拒绝缺少 `surfaceOp` 标记的可进入 surface 的种子事件(见「不变式」一节)。
-- **`packages/core/agent-loop`**:所有涉及 surface 事件的追加操作都传入 surface 选项。每个 `assistant/message` 都引用产生它的分片 seq;每个 `tool/result` 都引用它的 `tool/call` seq。
-- **`packages/session/session-persistence-jsonl`**:无需改动
-- **`packages/session/session-persistence`**:抽象接口不变
+- **`packages/core/agent-loop`**:所有涉及 surface 事件的追加操作都传入 surface 选项。每个 `assistant/message` 都嵌入精确提供方 stream,并禁止 `sourceEventSeqs`;每个 `tool/result` 都引用其 `tool/call` seq。
+- **`packages/session/session-persistence-jsonl`**:持久化规范 surface 元数据,并通过经过校验的格式准备恢复当前事件
+- **`packages/session/session-persistence`**:存储所有权与内存 surface 投影保持分离
 
-surface 是历史操纵赖以落地的基础——dsh-compaction 的压缩就搭载于其上。压缩或 tool-result-pruner 插件追加一个既有的消息产出事件类型(例如一条携带摘要的 `user/message`),附带 `surfaceOp: { op: 'replace', start, end }` 和覆盖被遮蔽条目的 `sourceEventSeqs`——新事件在 surface 上取代该范围的位置,而插件自身的 trace 事件(如 `compaction/start`、`compaction/end`)不进入 surface。回放以确定性方式保留该决策。
+surface 是历史操纵赖以落地的基础——dsh-compaction 的压缩就搭载于其上。压缩或 tool-result-pruner 插件追加一个既有的消息产出事件类型(例如一条携带摘要的 `user/message`),附带 `surfaceOp: { op: 'replace', startSeq, endSeq }` 和覆盖被遮蔽条目的 `sourceEventSeqs`——新事件在 surface 上取代该范围的位置,而插件自身的 trace 事件(如 `compaction/start`、`compaction/end`)不进入 surface。回放以确定性方式保留该决策。
 
 一次 `tool/result` 替换只能改写当前的一个 `tool/result`,并且必须保留除 `content` 以外的每个数据字段。Session 接纳会与位置范围和引用的源事件校验一起强制这条规则,不依赖可选的诊断插件。

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.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-05-reconstructable-requests.md
-2026-07-05-reconstructable-requests.md: bca93a60bf07484d73f1faf50359b72a0d00b9a3
-2026-07-05-reconstructable-requests.zh.md: c9d2a4a5d05456df8b0bd065bade8a41dd7e4e84
+2026-07-05-reconstructable-requests.md: 2f88675a75a72e7fbf105dfbf4f337a4dd80948a
+2026-07-05-reconstructable-requests.zh.md: 90008502a6651e38c142b7fb88052c05d46dea76

+ 5 - 5
.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md

@@ -22,9 +22,9 @@ Prefix-cache stability is corollary #1, not the headline: an append-only log pro
 
 **Messages.** `Session.deriveMessages()` is cached: each surface entry is projected exactly once, when first seen, through the public per-event function `deriveEventMessage(event)`; a surface rewrite (a compaction `replace` — `SurfaceManager.replaceGeneration`) rebuilds. Callers get a fresh array per call over shared, deep-frozen messages: mutating logged history through a projection is unrepresentable (it throws), replacing the old clone-per-call isolation. External reconstructors fold the same public function over a log prefix, so no two paths can disagree.
 
-`EpochHeader` records the request's non-history state: call config, rendered system prompt, and tool schemas, with empty values canonicalized to absence. Adapter-supplied effort and token defaults retain their `adapterDefaults` provenance; a Web model selection restored from the log omits an adapter-owned effort so the next resolution cannot reclassify the same effective config as an explicit selection and a false change. `request/header` always writes a full snapshot: the first loop instance uses reason `initial`, later instances use `resume`, an in-instance change uses `change`, and an unchanged envelope beginning an explicitly declared message series or following a surface replacement uses `series`. A `change` snapshot carries `startsSeries: true` when the changed request also starts a series, preserving the two independent facts without a duplicate header. Ordinary append-only later Turns, further same-series Steps, and retries inherit the latest snapshot. `foldRequestHeader` selects the latest snapshot. Legacy `request/header-delta` events and the removed `fallback` reason are rejected when appended or loaded.
+`EpochHeader` records the request's non-history state: call config and tool schemas. Writers omit `tools: []` and `adapterDefaults: {}`; current acceptance rejects those fields and any `header.system`, rather than repairing them. Whitespace-only system-message content, `config.stop: []`, and nested extensions remain intact. The [V3 canonical-envelope decision](2026-09-06-v3-canonical-session-envelopes.md) owns historical conversion. The rendered system prompt is derived history — the `system/message` event at surface node 0, per the [surface-node Agent Note](2026-09-02-system-prompt-as-surface-node.md) — so a prompt change is a surface replacement rather than a header change. Adapter-supplied effort and token defaults retain their `adapterDefaults` provenance; a Web model selection restored from the log omits an adapter-owned effort so the next resolution cannot reclassify the same effective config as an explicit selection and a false change. `request/header` always writes a full snapshot: the first loop instance uses reason `initial`, later instances use `resume`, an in-instance change uses `change`, and an unchanged envelope beginning an explicitly declared message series or following a surface replacement uses `series`. A `change` snapshot carries `startsSeries: true` when the changed request also starts a series, preserving the two independent facts without a duplicate header. Ordinary append-only later Turns, further same-series Steps, and retries inherit the latest snapshot. `foldRequestHeader` selects the latest snapshot. Legacy `request/header-delta` events and the removed `fallback` reason are rejected when appended or loaded.
 
-Each proposed step first claims its inbox batch and runs `agent/pre-step`. Rejection opens no step; enter opens `step/start`, records the final message batch as `user/message` events, and may use `startsRequestSeries: true` to declare a distinct series. The step then assembles the system prompt and tools, while `agent/request` may replace only the frozen call-config seed. The loop records the owed initial, resume, change, or series full snapshot, builds `GenerateOptions` from derived messages and that header, and freezes it while leaving `AbortSignal` live. The [request-freeze provenance decision](../simplification/2026-09-06-agent-request-freeze-provenance.md) owns reuse of completed message freezes and per-request local header freezing. The first call config starts from explicit `AgentOptions`, preserving fork overrides and resume reconfiguration; later calls start from the folded header.
+Each proposed step first claims its inbox batch, assembles the system prompt and tools, projects the rendered prompt against the surviving `system/message` node, and runs `agent/pre-step`. Rejection opens no step; enter opens `step/start`, commits a changed prompt as the `system/message` append or node-0 replacement, records the final message batch as `user/message` events, and may use `startsRequestSeries: true` to declare a distinct series. `agent/request` may replace only the frozen call-config seed. The loop records the owed initial, resume, change, or series full snapshot, builds `GenerateOptions` from derived messages (system message first) and that header with no `system` field, and freezes it while leaving `AbortSignal` live. The [request-freeze provenance decision](../simplification/2026-09-06-agent-request-freeze-provenance.md) owns reuse of completed message freezes and per-request local header freezing. The first call config starts from explicit `AgentOptions`, preserving fork overrides and resume reconfiguration; later calls start from the folded header.
 
 **The open step is the reconstruction boundary.** Its entered `user/message` batch and any newly written `request/header` precede request dispatch. Injection after the atomic claim joins a later request, while a listener that must affect this request returns messages through `agent/pre-step`. Header reconstruction selects the step's `request/header`, or carries the prior snapshot when no new header is written.
 
@@ -49,9 +49,9 @@ Like MiniCode, the conversation advances append-only and resets only when model-
 
 - A request that is not explained by the log cannot be constructed by accident — not by the loop, not by a listener; mutating a built request throws; every header change is a durable, diffable log event.
 - Model-visible context uses logged message channels. `agent.inject()` and tool `additionalContexts` enter the inbox for a later claim, while `agent/pre-step` returns context that must settle with the current claimed batch. Each entered value is a durable sourced `user/message`, paid once and prefix-cached thereafter at the price of accumulating in history until compaction.
-- What still costs full price at the provider is inherent and logged: compaction (its `compaction/*` events and replacement entry), a real prompt, tool, or config change (`request/header` with reason `change`), or a process boundary with drift (a differing `resume` snapshot). The provider's own reasoning-content exclusion is managed server-side.
+- What still costs full price at the provider is inherent and logged: compaction (its `compaction/*` events and replacement entry), a real prompt change (a `system/message` replacement of surface node 0), a real tool or config change (`request/header` with reason `change`), or a process boundary with drift (a differing `resume` snapshot). The provider's own reasoning-content exclusion is managed server-side.
 - `agent/pre-step` is the current-request message channel; direct inbox mutation is the eventual later-request channel.
-- Tool-result trimming needs no new mechanism: a logged single-entry surface replace (`start === end`) carrying a trimmed `tool/result` under the same `callId` — compaction-family, replay-correct, cache-bust batched by the same pressure logic.
+- Tool-result trimming needs no new mechanism: a logged single-entry surface replace (`startSeq === endSeq`) carrying a trimmed `tool/result` under the same `callId` — compaction-family, replay-correct, cache-bust batched by the same pressure logic.
 - Unreadable referenced attachment objects still fail model requests; [automatic attachment quarantine](../../proposed/bug-fix/2026-08-20-attachment-read-quarantine.md) records the proposed recovery without weakening byte-exact reconstruction.
-- Session logs grow one `request/header` snapshot per loop instance, real change, and later model-message series. Repeating the full system prompt and tool catalog is larger than a delta codec but small beside chunk-heavy logs and retains one self-contained replay representation. Current v1 retains this single representation; the frozen v0-to-v1 edge explicitly refuses legacy delta events before current Session construction.
+- Session logs grow one `request/header` snapshot per loop instance, real change, and later model-message series. Repeating the full tool catalog is larger than a delta codec but small beside chunk-heavy logs and retains one self-contained replay representation. Current logs retain this single representation; the frozen historical edges explicitly refuse legacy delta events before current Session construction.
 - Snapshot fixtures include each repeated series header. Keyless refresh owns those deterministic log changes, while the snapshot harness pins prompt and tool sidecars only for the initial and actual change revisions and reuses the current revision for `series` snapshots. Filesystem-writing fixtures remain in normalized authored form with cwd-relative tool arguments because replay only round-trips cwd-independent argument paths.

+ 5 - 5
.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md

@@ -22,9 +22,9 @@ Status: implemented
 
 **消息。** `Session.deriveMessages()` 带缓存:每个 surface 条目在首次出现时通过公开的逐事件函数 `deriveEventMessage(event)` 精确投影一次;surface 重写(压缩的 `replace`,即 `SurfaceManager.replaceGeneration`)触发重建。调用方每次获得一个新数组,底层是共享的深度冻结消息:通过投影变异已记录的历史是不可表达的(会抛异常),取代了旧的逐次调用克隆隔离。外部重建器对日志前缀折叠同一个公开函数,因此不可能有两条路径产生分歧。
 
-`EpochHeader` 记录请求的非历史状态:调用配置、渲染后的系统提示词和工具 schema,空值规范化为缺失。适配器提供的推理强度与 token 默认值会保留其 `adapterDefaults` 来源信息;Web 从日志恢复模型选择时会省略适配器持有的推理强度,因此下一次解析不会把相同的有效配置重新归类为显式选择并产生虚假变更。`request/header` 始终写入完整快照:首个循环实例使用 reason `initial`,后续实例使用 `resume`,实例内变更使用 `change`,内容未变的封装显式开启消息序列或跟随表层替换时使用 `series`。如果发生变化的请求同时开启序列,`change` 快照会携带 `startsSeries: true`,无需重复 header 即可保留这两个独立事实。普通的仅追加后续 Turn、同一序列内后续的 Step 与重试沿用最新快照。`foldRequestHeader` 选择最新快照。旧的 `request/header-delta` 事件和已移除的 `fallback` reason 在追加或加载时都会被拒绝。
+`EpochHeader` 记录请求的非历史状态:调用配置和工具 schema。写入方省略 `tools: []` 与 `adapterDefaults: {}`;当前接纳拒绝这些字段以及任何 `header.system`,而不修复它们。仅含空白的系统消息内容、`config.stop: []` 与嵌套扩展保持原样。[V3 规范信封决策](2026-09-06-v3-canonical-session-envelopes.zh.md)负责历史转换。渲染后的系统提示词是派生历史——surface 第 0 号节点上的 `system/message` 事件,见[surface 节点 Agent Note](2026-09-02-system-prompt-as-surface-node.zh.md)——因此提示词变更是 surface 替换而不是 header 变更。适配器提供的推理强度与 token 默认值会保留其 `adapterDefaults` 来源信息;Web 从日志恢复模型选择时会省略适配器持有的推理强度,因此下一次解析不会把相同的有效配置重新归类为显式选择并产生虚假变更。`request/header` 始终写入完整快照:首个循环实例使用 reason `initial`,后续实例使用 `resume`,实例内变更使用 `change`,内容未变的封装显式开启消息序列或跟随表层替换时使用 `series`。如果发生变化的请求同时开启序列,`change` 快照会携带 `startsSeries: true`,无需重复 header 即可保留这两个独立事实。普通的仅追加后续 Turn、同一序列内后续的 Step 与重试沿用最新快照。`foldRequestHeader` 选择最新快照。旧的 `request/header-delta` 事件和已移除的 `fallback` reason 在追加或加载时都会被拒绝。
 
-每个拟议步骤先领取其 inbox 批次,再运行 `agent/pre-step`。reject 不打开步骤;enter 打开 `step/start`,把最终消息批次记录为 `user/message` 事件,并可使用 `startsRequestSeries: true` 声明独立序列。随后步骤组装系统提示词与工具,`agent/request` 只能替换冻结的调用配置种子。循环记录所需的 initial、resume、change 或 series 完整快照,从派生消息与该 header 构建 `GenerateOptions`,冻结请求但保持 `AbortSignal` 活跃。[请求冻结来源证明决策](../simplification/2026-09-06-agent-request-freeze-provenance.zh.md)拥有消息完整冻结的复用规则和每次请求的本地 header 冻结规则。首次调用配置从显式的 `AgentOptions` 出发,保留 fork 覆盖和恢复重配置;后续调用从折叠后的 header 出发。
+每个拟议步骤先领取其 inbox 批次,组装系统提示词与工具,把渲染后的提示词与存活的 `system/message` 节点比对投影,再运行 `agent/pre-step`。reject 不打开步骤;enter 打开 `step/start`,把变化的提示词作为 `system/message` 追加或第 0 号节点替换提交,把最终消息批次记录为 `user/message` 事件,并可使用 `startsRequestSeries: true` 声明独立序列。`agent/request` 只能替换冻结的调用配置种子。循环记录所需的 initial、resume、change 或 series 完整快照,从派生消息(系统消息在先)与该不含 `system` 字段的 header 构建 `GenerateOptions`,冻结请求但保持 `AbortSignal` 活跃。[请求冻结来源证明决策](../simplification/2026-09-06-agent-request-freeze-provenance.zh.md)拥有消息完整冻结的复用规则和每次请求的本地 header 冻结规则。首次调用配置从显式的 `AgentOptions` 出发,保留 fork 覆盖和恢复重配置;后续调用从折叠后的 header 出发。
 
 **已打开步骤是重建边界。** 进入步骤的 `user/message` 批次与任何新写入的 `request/header` 都位于请求分派之前。原子领取后发生的注入加入后续请求;必须影响本次请求的监听器则通过 `agent/pre-step` 返回消息。header 重建选择该步骤的 `request/header`,或在无新 header 写入时沿用前一个快照。
 
@@ -49,9 +49,9 @@ Status: implemented
 
 - 一个日志无法解释的请求不可能被意外构造——无论是循环还是监听器;变异已构建的请求会抛异常;每个 header 变更都是持久的、可 diff 的日志事件。
 - 模型可见上下文使用已记录消息通道。`agent.inject()` 与工具 `additionalContexts` 进入 inbox,等待后续领取;必须与当前已领取批次一起结算的上下文由 `agent/pre-step` 返回。每个进入步骤的值都是带来源的持久 `user/message`,只付出一次代价并在后续成为可缓存前缀,代价是会在历史中累积直至压缩。
-- 在提供方处仍需全价计算的内容是固有的且已记录的:压缩(其 `compaction/*` 事件和替换条目)、真正的提示词、工具或配置变更(reason 为 `change` 的 `request/header`),或带漂移的进程边界(不同的 `resume` 快照)。提供方自身的 reasoning-content 排除由服务端管理。
+- 在提供方处仍需全价计算的内容是固有的且已记录的:压缩(其 `compaction/*` 事件和替换条目)、真正的提示词变更(对 surface 第 0 号节点的 `system/message` 替换)真正的工具或配置变更(reason 为 `change` 的 `request/header`),或带漂移的进程边界(不同的 `resume` 快照)。提供方自身的 reasoning-content 排除由服务端管理。
 - `agent/pre-step` 是当前请求的消息通道;直接修改 inbox 则是最终进入后续请求的通道。
-- 工具结果裁剪无需新机制:一个已记录的单条目 surface replace(`start === end`),携带同一 `callId` 下裁剪后的 `tool/result`——属压缩家族,回放正确,缓存失效由相同的压力逻辑批量处理。
+- 工具结果裁剪无需新机制:一个已记录的单条目 surface replace(`startSeq === endSeq`),携带同一 `callId` 下裁剪后的 `tool/result`——属压缩家族,回放正确,缓存失效由相同的压力逻辑批量处理。
 - 无法读取的被引用附件对象仍会让模型请求失败;[附件自动隔离](../../proposed/bug-fix/2026-08-20-attachment-read-quarantine.zh.md)记录了不削弱字节精确重建的拟议恢复方案。
-- 会话日志会为每个循环实例、真实变更和后续模型消息序列增加一个 `request/header` 快照。重复完整系统提示词与工具目录比 delta 编解码器更大,但相对分片密集型日志仍然很小,并保留一种自包含的回放表示。当前 v1 保留这一种表示;冻结的 v0-to-v1 迁移边会在构造当前 Session 前显式拒绝旧版 delta 事件。
+- 会话日志会为每个循环实例、真实变更和后续模型消息序列增加一个 `request/header` 快照。重复完整工具目录比 delta 编解码器更大,但相对分片密集型日志仍然很小,并保留一种自包含的回放表示。当前日志保留这一种表示;冻结的历史迁移边会在构造当前 Session 前显式拒绝旧版 delta 事件。
 - 快照 fixture 包含每个重复的 series header。无密钥 refresh 负责这些确定性日志变化;快照 harness 只为 initial 与真实 change 修订固定提示词和工具 sidecar,并让 `series` 快照复用当前修订。写入文件系统的 fixture 继续以规范化的撰写形式存储,工具参数使用 cwd 相对路径,因为回放只对 cwd 无关的参数路径做往返。

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.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-08-agent-scope-contexts.md
-2026-07-08-agent-scope-contexts.md: 6a1fd4aed49cb8edef061c8fb6f0edcd0a09c30f
-2026-07-08-agent-scope-contexts.zh.md: 8408c4afff6075c129c6a96c47393c9c812b04b7
+2026-07-08-agent-scope-contexts.md: 45e635b7bc3138d4e90a25a06ff23b3b57a9415e
+2026-07-08-agent-scope-contexts.zh.md: aac860734e744843c3b9e7d55e5bc7a150763090

+ 4 - 2
.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md

@@ -16,6 +16,8 @@ The mechanism also needs a publication boundary. An agent must not become visibl
 
 Every live agent owns one flat registration layer exposed as `agent.ctx`. Code registers through the context that owns a contribution; scope-aware services combine deployment-global registrations with exactly one matching agent layer; operations choose that layer from their real agent; and the layer exists for the agent's complete published lifetime.
 
+`agent.ctx` carries registration ownership and the scope key; it does not expose a reverse `agent` property. Code that needs the domain subject receives it explicitly: `AgentSetup` receives `(agentCtx, agent)`, and scoped events carry their subject in the payload.
+
 Cordis is the plugin framework underneath the SDK. A Cordis **context** is the object plugins use to access services and register effects whose cleanup follows that context. The [Cordis primer](../../../../docs/cordis-primer.md) explains the framework in more detail.
 
 For most contributors, the complete contract is four rules:
@@ -45,7 +47,7 @@ flowchart LR
 
 The missing cross-edges are the isolation rule: Agent A's local registrations do not enter Agent B's view, and a parent's registrations do not enter a child merely because the parent owns the child's lifetime.
 
-The companion [runtime-design Agent Note](2026-07-12-agent-scope-runtime-design.md) explains the implementation and correctness reasoning. The [subagent composition-controls Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) owns the separate `persona`, `toolFilter`, and `maxDepth` feature.
+The companion [runtime-design Agent Note](2026-07-12-agent-scope-runtime-design.md) explains the implementation and correctness reasoning. The [explicit runtime-identity Agent Note](2026-08-31-explicit-agent-runtime-identity.md) owns why lifecycle, event, and transport interfaces pass Agent identity instead of exposing it through Context. The [subagent composition-controls Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) owns the separate `persona`, `toolFilter`, and `maxDepth` feature.
 
 ### Registration origin chooses visibility and cleanup
 
@@ -88,7 +90,7 @@ await handle.dispose()
 ctx.tools.get('review_summary', handle.agent)  // undefined: scope is gone
 ```
 
-Setup receives a full trusted Cordis context so it can compose ordinary plugins and services. Its contract is composition-only: driving or publishing the in-flight agent through casts or internal registry calls is unsupported.
+Setup receives the full trusted Cordis context and unpublished Agent so it can compose ordinary plugins and services while reading the exact child Session when needed. Its contract is composition-only: driving or publishing the in-flight agent through casts or internal registry calls is unsupported.
 
 ### The operation chooses the view
 

+ 4 - 2
.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md

@@ -16,6 +16,8 @@ Status: implemented
 
 每个存活的 agent 拥有一个扁平的注册层,通过 `agent.ctx` 暴露。代码通过拥有某项贡献的上下文进行注册;具备作用域感知的服务将部署全局注册与恰好一个匹配的 agent 层合并;操作从其真实 agent 选择该层;该层在 agent 的完整发布生命周期内存在。
 
+`agent.ctx` 携带注册所有权和作用域键,不暴露反向的 `agent` 属性。需要领域主体的代码会显式接收它:`AgentSetup` 接收 `(agentCtx, agent)`,作用域事件则在 payload 中携带主体。
+
 Cordis 是 SDK 底层的插件框架。Cordis **上下文**是插件用来访问服务和注册效果的对象,效果的清理跟随该上下文。[Cordis 入门](../../../../docs/cordis-primer.zh.md)对该框架有更详细的说明。
 
 对大多数贡献者而言,完整约定是四条规则:
@@ -45,7 +47,7 @@ flowchart LR
 
 缺失的交叉边即隔离规则:Agent A 的本地注册不会进入 Agent B 的视图,父级的注册也不会仅因父级拥有子级的生命周期就进入子级。
 
-配套的[运行时设计 Agent Note](2026-07-12-agent-scope-runtime-design.zh.md) 阐述实现与正确性推理。[subagent 组合控制 Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.zh.md) 负责独立的 `persona`、`toolFilter` 和 `maxDepth` 功能。
+配套的[运行时设计 Agent Note](2026-07-12-agent-scope-runtime-design.zh.md)阐述实现与正确性推理。[显式运行时身份 Agent Note](2026-08-31-explicit-agent-runtime-identity.zh.md)说明生命周期、事件和传输接口为何显式传递 Agent 身份,而不通过 Context 暴露该身份。[subagent 组合控制 Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.zh.md)负责独立的 `persona`、`toolFilter` 和 `maxDepth` 功能。
 
 ### 注册来源决定可见性与清理
 
@@ -88,7 +90,7 @@ await handle.dispose()
 ctx.tools.get('review_summary', handle.agent)  // undefined: scope is gone
 ```
 
-setup 接收一个完整的受信 Cordis 上下文,因此可以组合普通插件和服务。其约定仅限组合:不支持通过 cast 或内部注册表调用来驱动或发布正在构建中的 agent。
+setup 接收完整的受信 Cordis 上下文和未发布的 Agent,因此可以组合普通插件和服务,也能在需要时读取确切的子 Session。其约定仅限组合:不支持通过 cast 或内部注册表调用来驱动或发布正在构建中的 agent。
 
 ### 操作选择视图
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.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-12-agent-scope-runtime-design.md
-2026-07-12-agent-scope-runtime-design.md: b6001a5ef9f2dc69ec21908f8350b765dd00acf1
-2026-07-12-agent-scope-runtime-design.zh.md: be12c53ffa9b89e007888935002a5c484c038fd7
+2026-07-12-agent-scope-runtime-design.md: ca300d4eeeab878a4e41b8e68a669be418617181
+2026-07-12-agent-scope-runtime-design.zh.md: 870690d6ace9fefd859557a2e73e88b9b1da6206

+ 5 - 3
.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md

@@ -42,6 +42,8 @@ All agents share one Cordis service graph. A derived context does not clone `Too
 
 `agent.ctx` is such a derived context. Service calls still reach the shared instances, while a registration can inspect its calling context and store a contribution under the nearest scope key. Ordinary plugin contexts carry no scope key and therefore register globally.
 
+The Agent context is exactly the context returned by `createScope`; it carries no second reverse association to the Agent. Subject-bearing APIs pass the Agent explicitly, leaving one formal scope mechanism for registration ownership and routing.
+
 ### Fibers and effects make cleanup structural
 
 A Cordis fiber is the live instance created when a plugin or child context is activated. Its state records whether that lifecycle is active, unloading, failed, or disposed. `ctx.effect()` and `ctx.on()` return disposers and also attach those disposers to the registering fiber, so unloading a plugin or agent scope removes everything registered through that context without a separate inventory.
@@ -68,7 +70,7 @@ A `ScopeKey` is an opaque object compared by identity. The harness uses the live
 
 `createScope(parent, key)` returns a scope whose `ctx` shares the parent's services and whose effects are tagged with that key. `scopeOf(ctx)` reads the nearest registration key. `scopeTarget(base, key)` creates the event receiver whose filter preserves the base receiver's Cordis service filter, then admits unscoped listeners and listeners with that exact key.
 
-The receiver is a small carrier rather than a transparent proxy for the domain object. Code that needs the agent receives the explicit event argument; code that needs registration ownership receives `agent.ctx`.
+The receiver is a small carrier rather than a transparent proxy for the domain object. Code that needs the agent receives an explicit setup parameter or event argument; code that needs registration ownership receives `agent.ctx`.
 
 ### Registry reads overlay one exact layer
 
@@ -100,11 +102,11 @@ The transaction is installed under both the calling Cordis context and the concr
 
 Create prepares a new Session. Resume loads and validates the persisted Session before preparing the same live session identity. Both paths then build the scope, agent, and driver and invoke the same setup/publication algorithm.
 
-The factory stores concrete trace targets but invokes them through a caller-bound Cordis trace. This preserves dependency origin and caller ownership without stacking trace proxies.
+The factory stores concrete trace targets but invokes them through a caller-bound Cordis trace. A runtime child creator sets `parentAgent` in the create or resume options, and AgentRegistry forwards those options without deriving a parent from the caller Context. This preserves dependency origin and both ownership facts without stacking trace proxies or attaching a domain object to the Context. Scoped Remote event adapters likewise receive the Agent in the request, verify that it is the carrier key, and project its Context and wire identity directly. No scope index reconstructs an Agent from a Context. The [explicit runtime-identity decision](2026-08-31-explicit-agent-runtime-identity.md) owns this separation and the continuable-child ownership rule that follows from it.
 
 ### Setup is trusted composition inside a private world
 
-Setup receives the full child context and may await plugin activation. It can register tools, prompt sections, restrictions, listeners, and other effects, but the public contract does not support driving or publishing the in-flight agent through casts or internal registry calls.
+Setup receives the full child context and the exact unpublished Agent, and may await plugin activation. It can register tools, prompt sections, restrictions, listeners, and other effects, and consumers that need the child's Session read it from the Agent parameter. The public contract does not support driving or publishing the in-flight agent through casts or internal registry calls.
 
 The transaction races asynchronous load and setup against deactivation rather than waiting forever for a promise owned by external code. If cancellation or owner unload wins, public creation rejects after transaction-owned cleanup even when the external promise never settles.
 

+ 5 - 3
.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md

@@ -42,6 +42,8 @@ Status: implemented
 
 `agent.ctx` 就是这样一个派生上下文。服务调用仍然到达共享实例,而注册操作可以检查其调用上下文并将贡献存储在最近的作用域键下。普通的插件上下文不携带作用域键,因此注册到全局。
 
+Agent 上下文就是 `createScope` 返回的上下文,不携带第二份指回 Agent 的关联。需要主体的 API 显式传递 Agent,因此注册所有权与路由只依赖一种正式的作用域机制。
+
 ### Fiber 与 effect 使清理成为结构性的
 
 Cordis fiber 是插件或子上下文被激活时创建的活跃实例。其状态记录该生命周期是 active、unloading、failed 还是 disposed。`ctx.effect()` 和 `ctx.on()` 返回 disposer,同时将这些 disposer 附加到注册所在的 fiber,因此卸载一个插件或 agent 作用域会移除通过该上下文注册的一切,无需单独的清单。
@@ -70,7 +72,7 @@ scope 包实现了 Cordis 路由所需的最小对象。其载体仅持有一个
 
 `createScope(parent, key)` 返回一个作用域,其 `ctx` 共享父级的服务,其 effect 被标记为该键。`scopeOf(ctx)` 读取最近的注册键。`scopeTarget(base, key)` 创建事件接收器,其过滤器保留 base receiver 的 Cordis 服务过滤器,然后接纳无作用域的监听器和具有该确切键的监听器。
 
-Receiver 是一个小型载体而非领域对象的透明代理。需要 agent 的代码接收显式的事件参数;需要注册所有权的代码接收 `agent.ctx`。
+Receiver 是一个小型载体而非领域对象的透明代理。需要 agent 的代码接收显式的 setup 参数或事件参数;需要注册所有权的代码接收 `agent.ctx`。
 
 ### 注册表读取叠加一个精确 layer
 
@@ -102,11 +104,11 @@ detach 闭包捕获其确切注册表条目。它仅在映射仍指向该注册
 
 创建准备一个新 Session。恢复加载并验证持久化的 Session,然后准备相同的活跃会话标识。两条路径随后构建作用域、agent 和 driver,并调用相同的 setup/发布算法。
 
-工厂存储具体的 trace 目标,但通过调用方绑定的 Cordis trace 调用它们。这保留了依赖来源和调用方所有权,而不堆叠 trace 代理
+工厂存储具体的 trace 目标,但通过调用方绑定的 Cordis trace 调用它们。运行时子 Agent 的创建方在 create 或 resume options 中设置 `parentAgent`,AgentRegistry 转交这些 options,不从调用方 Context 推导父级。这既保留了依赖来源和两种所有权事实,又不堆叠 trace 代理,也不把领域对象附着到 Context。作用域 Remote 事件适配器同样从 request 接收 Agent,校验它就是 carrier key,再直接投影其 Context 与 wire identity。系统不会通过作用域索引从 Context 重建 Agent。[显式运行时身份决策](2026-08-31-explicit-agent-runtime-identity.zh.md)拥有这项分离原则及由此确定的可续跑子级归属规则
 
 ### Setup 是私有世界内的可信组合
 
-Setup 接收完整的子上下文,可以等待插件激活。它可以注册工具、提示词段、限制、监听器和其他 effect,但公开约定不支持通过强制转换或内部注册表调用来驱动或发布正在创建中的 agent。
+Setup 接收完整的子上下文和确切的未发布 Agent,可以等待插件激活。它可以注册工具、提示词段、限制、监听器和其他 effect;需要子 Session 的消费者从 Agent 参数读取它。公开约定不支持通过强制转换或内部注册表调用来驱动或发布正在创建中的 agent。
 
 事务将异步加载和 setup 与停用进行竞争,而非无限等待外部代码拥有的 promise。如果取消或所有者卸载获胜,即使外部 promise 永不结算,公开创建也会在事务拥有的清理之后拒绝。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.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-15-agent-initiator-scope.md
-2026-07-15-agent-initiator-scope.md: 63540c0ec6b29a10613e01f1ed9ced24e8f2d277
-2026-07-15-agent-initiator-scope.zh.md: 3ea893aa5f6992bf09965436c1db3144d2fae5ac
+2026-07-15-agent-initiator-scope.md: ab11da116a463cd706418e797eb58f1bc4ab9b1c
+2026-07-15-agent-initiator-scope.zh.md: 343acba5f99379b6d2c3af41368e8fbe90b20611

+ 5 - 5
.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md

@@ -6,7 +6,7 @@ English | [中文](2026-07-15-agent-initiator-scope.zh.md)
 
 ## Problem
 
-The harness has two useful but different notions of context. A Cordis `Context` selects services, registration ownership, and lifetime; `agent.ctx` is the flat registration scope owned by one live Agent. Agent and Session identity instead describe the subject of an asynchronous operation. Changing a root `ctx.agent` to mean “whichever Agent is running” would conflate those meanings and fail when one process drives Agents concurrently.
+The harness has two useful but different notions of context. A Cordis `Context` selects services, registration ownership, and lifetime; `agent.ctx` is the flat registration scope owned by one live Agent. Agent and Session identity instead describe the subject of an asynchronous operation. A dynamic `ctx.agent` meaning “whichever Agent is running” would conflate those meanings and fail when one process drives Agents concurrently.
 
 Deep process-local infrastructure sometimes needs a trusted initiating Agent below explicit loop, tool, and request parameters—for example, a host-aware transport, tracing helper, logger, or gateway client. Requiring every private helper to forward `agent` adds repetition, while a process-global mutable slot is incorrect across `await`. Model-visible arguments are unsuitable because a model must not choose a trusted Session or routing header. The carrier belongs to the Agent service rather than optional model-visible context.
 
@@ -18,9 +18,9 @@ The mandatory `ctx.agents` service uses Node `AsyncLocalStorage` to carry the in
 
 `AgentLoop` already injects `ctx.agents` and wraps each concrete driver's complete `runLoop` lifetime in `agents.withInitiator(agent, ...)`. Its package-private loop, turn, step, and tool-call orchestration entries recover the exact Agent from `ctx.agents`, derive `agent.session` once, and let operation-local helpers capture it instead of forwarding the concrete driver or `Session` through shallow interfaces. A leaf helper keeps a narrow `Session` parameter when that is its actual interface rather than accepting a broader `Context` only for an ambient lookup.
 
-Concurrent drivers receive independent stores. A child driver's continuations carry the child, while the caller resumes in its prior store as soon as `withInitiator()` returns; active-run tracking keeps the returned Promise in the teardown drain until it settles. Creation, persistence load, and unpublished `setup(agentCtx)` remain outside the child's driver boundary: creation initiated by a parent runs under the parent identity, while `agentCtx.agent` explicitly identifies the child.
+Concurrent drivers receive independent stores. A child driver's continuations carry the child, while the caller resumes in its prior store as soon as `withInitiator()` returns; active-run tracking keeps the returned Promise in the teardown drain until it settles. Creation, persistence load, and unpublished `setup(agentCtx, childAgent)` remain outside the child's driver boundary: creation initiated by a parent runs under the parent identity, while the explicit `childAgent` parameter identifies the child.
 
-Ambient identity does not replace explicit contracts. `ToolExecution.agent`, `AssembleContext.agent`, `GenerateOptions.sessionId`, job ownership, parent/child requests, `ctx.agent`, `agentCtx.agent`, approval and hook subjects, `cwd` selection, cancellation, worker/process messages, persistence records, and wire identity remain explicit. A remote boundary materializes the identity it needs into its typed request because ALS is process-local.
+Ambient identity does not replace explicit contracts. `ToolExecution.agent`, `AssembleContext.agent`, the Agent parameter of `AgentSetup`, `GenerateOptions.sessionId`, job ownership, parent/child requests, approval and hook subjects, `cwd` selection, cancellation, worker/process messages, persistence records, and wire identity remain explicit. A remote boundary materializes the identity it needs into its typed request because ALS is process-local.
 
 `AgentRegistry` owns an ordered initiator lifecycle. Teardown first rejects new boundaries; removing `ctx.agents` then drains injected dependents such as AgentLoop, and the registry waits for active returned-Promise boundaries before calling `AsyncLocalStorage.disable()`. If a boundary's inherited async chain starts an owning Cordis fiber's unload, the private run-token lineage releases that nested boundary chain from the drain, which prevents teardown from waiting on itself while unrelated boundaries still drain. `currentInitiator()` and `requireInitiator()` remain usable through a retained in-flight service reference while the ordinary drain runs; after disposal, initiator methods throw `agent initiator scope is disposed`. Root Context disposal may start sibling fiber teardown concurrently, so active-boundary counting remains necessary in addition to Cordis dependency ordering.
 
@@ -28,7 +28,7 @@ Initiator scope does not own detached work: registry drain tracks only the Promi
 
 A host-aware transport may derive a deployment-owned header such as `X-Harness-Session-Id` from `ctx.agents.requireInitiator().session.id`; the header is absent from model-visible schema and arguments. No production MCP or Web transport adopts such a header in this decision. A test-double transport proves the trusted boundary without assigning host routing policy to an existing provider-neutral seam.
 
-This decision extends the [Agent registration-scope contract](2026-07-08-agent-scope-contexts.md) and its [runtime design](2026-07-12-agent-scope-runtime-design.md); it does not change their static `agent.ctx` meaning.
+This decision extends the [Agent registration-scope contract](2026-07-08-agent-scope-contexts.md) and its [runtime design](2026-07-12-agent-scope-runtime-design.md); it does not change their static `agent.ctx` meaning. The [explicit runtime-identity decision](2026-08-31-explicit-agent-runtime-identity.md) keeps initiator scope limited to private asynchronous chains while lifecycle, ownership, event, and wire interfaces carry their subjects directly.
 
 ## Verification
 
@@ -40,7 +40,7 @@ A test-double host-aware transport derives `X-Harness-Session-Id` internally and
 
 **Pass Agent through every function.** Public, worker, process, persistence, and wire boundaries continue to do this, but requiring every process-local private helper to carry Agent adds repetitive forwarding without improving trust. ALS is confined to the asynchronous chain inside those explicit boundaries.
 
-**Make `ctx.agent` dynamic.** `ctx.agent` already means the static Agent associated with an Agent-scoped Cordis context. Changing the root meaning would mix registration and execution scopes and make concurrent behavior surprising.
+**Expose a dynamic `ctx.agent`.** Context carries registration ownership, not a domain subject. Adding an accessor for the executing Agent would mix registration and execution scopes and make concurrent behavior surprising.
 
 **Add a separate `ctx.agentExecution` service.** The carrier has no independent backend, configuration, or identity type: it stores the same `Agent` that `ctx.agents` already owns, and AgentLoop already depends on that service. A second mandatory provider would add package, composition, lifecycle, generated-catalog, and test-harness wiring without separating a real capability.
 

+ 5 - 5
.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md

@@ -6,7 +6,7 @@ Status: implemented
 
 ## 问题
 
-harness 中存在两种有用但不同的上下文概念。Cordis `Context` 负责选择服务、注册归属和生命周期;`agent.ctx` 是一个存活 Agent 所拥有的扁平注册作用域。Agent 与会话身份描述的则是异步操作主体。若把根 `ctx.agent` 改成「当前正在运行的 Agent」,就会混淆这两种含义,并在单进程并发驱动多个 Agent 时失效。
+harness 中存在两种有用但不同的上下文概念。Cordis `Context` 负责选择服务、注册归属和生命周期;`agent.ctx` 是一个存活 Agent 所拥有的扁平注册作用域。Agent 与会话身份描述的则是异步操作主体。若提供表示「当前正在运行的 Agent」的动态 `ctx.agent`,就会混淆这两种含义,并在单进程并发驱动多个 Agent 时失效。
 
 进程内深层基础设施有时需要在显式传递的循环、工具及请求参数之下获取可信的发起 Agent,例如宿主感知传输层、追踪辅助函数、日志器或网关客户端。要求每个私有辅助函数都转发 `agent` 会造成重复,而进程级可变槽会在跨 `await` 时发生并发错误。模型可见参数也不适用,因为模型不得选择可信的会话或路由请求头。该载体归 Agent 服务所有,而非模型可见的可选上下文。
 
@@ -18,9 +18,9 @@ harness 中存在两种有用但不同的上下文概念。Cordis `Context` 负
 
 `AgentLoop` 已经注入 `ctx.agents`,并用 `agents.withInitiator(agent, ...)` 包裹每个具体驱动的完整 `runLoop` 生命周期。循环、轮次、步骤和工具调用的包内私有入口从 `ctx.agents` 恢复同一个 Agent,一次推导 `agent.session`,再由操作内辅助函数捕获该值,避免在浅层接口中转发具体驱动或 `Session`。若 `Session` 本身就是底层辅助函数的实际接口,该函数会保留狭窄的 `Session` 参数,而不会只为隐式查找而接收更宽泛的 `Context`。
 
-因此,并发驱动使用彼此独立的存储。子驱动的异步延续携带子 Agent;`withInitiator()` 返回后,调用方立即恢复之前的存储,而活动运行计数仍持续跟踪返回的 Promise,直到其结束。创建、持久化加载和尚未发布的 `setup(agentCtx)` 位于子驱动边界之外:由父 Agent 发起的创建使用父身份,而 `agentCtx.agent` 显式标识子 Agent。
+因此,并发驱动使用彼此独立的存储。子驱动的异步延续携带子 Agent;`withInitiator()` 返回后,调用方立即恢复之前的存储,而活动运行计数仍持续跟踪返回的 Promise,直到其结束。创建、持久化加载和尚未发布的 `setup(agentCtx, childAgent)` 位于子驱动边界之外:由父 Agent 发起的创建使用父身份,而显式的 `childAgent` 参数标识子 Agent。
 
-隐式身份不会取代显式约定。`ToolExecution.agent`、`AssembleContext.agent`、`GenerateOptions.sessionId`、任务归属、父子请求、`ctx.agent`、`agentCtx.agent`、审批与 hook 主体、`cwd` 选择、取消、worker 和进程消息、持久化记录及协议身份都保持显式传递。远程边界会把所需身份写入类型化请求,因为 ALS 只在进程内有效。
+隐式身份不会取代显式约定。`ToolExecution.agent`、`AssembleContext.agent`、`AgentSetup` 的 Agent 参数、`GenerateOptions.sessionId`、任务归属、父子请求、审批与 hook 主体、`cwd` 选择、取消、worker 和进程消息、持久化记录及协议身份都保持显式传递。远程边界会把所需身份写入类型化请求,因为 ALS 只在进程内有效。
 
 `AgentRegistry` 管理一个有序的发起方生命周期。teardown 会先拒绝新边界;移除 `ctx.agents` 后,AgentLoop 等注入方开始排空,注册表随后等待活动的返回 Promise 边界,最后调用 `AsyncLocalStorage.disable()`。如果某个边界继承的异步调用链启动所属 Cordis fiber 的卸载,私有运行标记谱系会从排空范围中释放该嵌套边界链,从而避免 teardown 等待自身完成,同时继续排空无关边界。在普通排空期间,进行中代码可通过保留的服务引用继续调用 `currentInitiator()` 和 `requireInitiator()`;dispose(资源释放)后,发起方方法会抛出 `agent initiator scope is disposed`。根 Context dispose 可能并发启动同级 fiber 的 teardown,因此除 Cordis 依赖顺序外仍必须统计活动边界。
 
@@ -28,7 +28,7 @@ harness 中存在两种有用但不同的上下文概念。Cordis `Context` 负
 
 宿主感知的传输层可以从 `ctx.agents.requireInitiator().session.id` 推导由部署方拥有的 `X-Harness-Session-Id` 等请求头;模型可见 schema 和参数中不包含该请求头。本决策不让现有生产 MCP 或 Web 传输层采用此请求头。测试替身传输层用于证明可信边界,而不会把宿主路由策略分配给现有的提供方无关 seam。
 
-本决策扩展 [Agent 注册作用域约定](2026-07-08-agent-scope-contexts.zh.md)及其[运行时设计](2026-07-12-agent-scope-runtime-design.zh.md),不会改变其中 `agent.ctx` 的静态含义。
+本决策扩展 [Agent 注册作用域约定](2026-07-08-agent-scope-contexts.zh.md)及其[运行时设计](2026-07-12-agent-scope-runtime-design.zh.md),不会改变其中 `agent.ctx` 的静态含义。[显式运行时身份决策](2026-08-31-explicit-agent-runtime-identity.zh.md)把发起方作用域限制在私有异步调用链内,同时让生命周期、归属、事件和协议接口直接携带各自的主体。
 
 ## 验证
 
@@ -40,7 +40,7 @@ Agent 服务测试锁定可选与必需读取、同步值及跨 realm Promise 
 
 **在每个函数中传递 Agent。** 公开、worker、进程、持久化和协议边界继续显式传递,但要求每个进程内私有辅助函数都携带 Agent 只会造成重复转发,不会提高可信度。ALS 仅限于这些显式边界内部的异步调用链。
 
-**让 `ctx.agent` 变成动态值。** `ctx.agent` 已经表示与 Agent 作用域 Cordis 上下文静态关联的 Agent。改变根上下文的含义会混合注册作用域与执行作用域,并让并发行为变得意外。
+**暴露动态的 `ctx.agent`。** Context 携带注册所有权,而非领域主体。为正在执行的 Agent 新增 accessor 会混合注册作用域与执行作用域,并让并发行为变得意外。
 
 **新增独立的 `ctx.agentExecution` 服务。** 该载体没有独立后端、配置或身份类型:它存储的是 `ctx.agents` 已经管理的同一个 `Agent`,而 AgentLoop 本就依赖该服务。第二个必需提供方会增加包、组合、生命周期、生成目录及测试 harness 接线,却没有拆出真实能力。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.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-20-canonical-tool-output-contract.md
-2026-07-20-canonical-tool-output-contract.md: f2c17325f77b93675086c39dd5a7693854b8521a
-2026-07-20-canonical-tool-output-contract.zh.md: d81c1730aab66df2dcf4eea4515f260df917ffb4
+2026-07-20-canonical-tool-output-contract.md: 4dbcac3da8381e69809b15a653cdb4987af4e0e7
+2026-07-20-canonical-tool-output-contract.zh.md: c2f4ebdfc7266495570766c69b3aa264f91172cc

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md

@@ -34,7 +34,7 @@ type ToolExecutionResult =
 
 `tools/post-execute` has two mutually exclusive successful projections. Replacing `content` changes only Native/model presentation and preserves the canonical value and metadata. Replacing `value` revalidates the replacement and recomputes both presentation projections. A block removes the value and becomes a failure. Content replacement is therefore not a confidentiality mechanism: policy that must prevent programmatic access blocks the call or replaces the value.
 
-Canonical values are execution-local. The agent loop persists `tool/result` with only `content`, `error`, and optional `meta`; PTC mode's `tool/code-dispatch` persists the sub-call's rendered `content` and `isError`. Neither event stores the canonical intermediate value, so replay reproduces presentation but cannot reconstruct the programmatic result. When a tool declares `presentationMeta`, it is computed only for a direct surface call; a nested Code dispatch gets no metadata. The Client can derive [nested terminal cards](../bug-fix/2026-09-05-nested-terminal-cards.md) from raw arguments and rendered content without that metadata. The outer `run_code` card instead reads final post-policy content and declares no presentation metadata. Generic and tool-owned spill projections similarly skip nested dispatches, whose canonical value never enters model context.
+Canonical values are execution-local. The agent loop persists `tool/result` with only `content`, `error`, and optional `meta`; PTC mode's `tool/ptc-dispatch` persists the sub-call's rendered `content` and `isError`. Neither event stores the canonical intermediate value, so replay reproduces presentation but cannot reconstruct the programmatic result. When a tool declares `presentationMeta`, it is computed only for a direct surface call; a nested Code dispatch gets no metadata. The Client can derive [nested terminal cards](../bug-fix/2026-09-05-nested-terminal-cards.md) from raw arguments and rendered content without that metadata. The outer `run_code` card instead reads final post-policy content and declares no presentation metadata. Generic and tool-owned spill projections similarly skip nested dispatches, whose canonical value never enters model context.
 
 The first-party tools preserve their existing Native text while returning domain DTOs:
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.zh.md

@@ -34,7 +34,7 @@ type ToolExecutionResult =
 
 `tools/post-execute` 为成功结果提供两种互斥的投影方式。替换 `content` 只改变 Native/模型展示,并保留规范值和元数据。替换 `value` 会重新校验替代值,并重新计算两份展示投影。阻止操作会移除值并转为失败。因此,替换内容并不是保密机制:必须阻止程序化访问的策略,应当阻止调用或替换值。
 
-规范值仅存在于执行期间。agent loop(智能体循环)持久化的 `tool/result` 只包含 `content`、`error` 和可选的 `meta`;PTC mode 的 `tool/code-dispatch` 持久化子调用渲染后的 `content` 与 `isError`。两个事件都不存储规范中间值,因此回放可以重现展示,却无法重建程序化结果。当工具声明 `presentationMeta` 时,系统只会为直接的外层调用计算它;嵌套 Code 分发没有元数据。Client 可以从原始参数与渲染后的内容派生[嵌套 terminal 卡片](../bug-fix/2026-09-05-nested-terminal-cards.zh.md),无需这些元数据。外层 `run_code` 卡片则读取最终的 post-policy 内容,并且不声明展示元数据。通用以及工具自有的 spill 投影同样跳过嵌套分发,因为它们的规范值永远不会进入模型上下文。
+规范值仅存在于执行期间。agent loop(智能体循环)持久化的 `tool/result` 只包含 `content`、`error` 和可选的 `meta`;PTC mode 的 `tool/ptc-dispatch` 持久化子调用渲染后的 `content` 与 `isError`。两个事件都不存储规范中间值,因此回放可以重现展示,却无法重建程序化结果。当工具声明 `presentationMeta` 时,系统只会为直接的外层调用计算它;嵌套 Code 分发没有元数据。Client 可以从原始参数与渲染后的内容派生[嵌套 terminal 卡片](../bug-fix/2026-09-05-nested-terminal-cards.zh.md),无需这些元数据。外层 `run_code` 卡片则读取最终的 post-policy 内容,并且不声明展示元数据。通用以及工具自有的 spill 投影同样跳过嵌套分发,因为它们的规范值永远不会进入模型上下文。
 
 第一方工具在保持现有 Native 文本不变的同时返回领域 DTO:
 

+ 6 - 0
.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.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-explicit-agent-runtime-identity.md
+2026-08-31-explicit-agent-runtime-identity.md: f52b8ec116c312a27306fe73dc0bd5b99fcd9039
+2026-08-31-explicit-agent-runtime-identity.zh.md: 6b6fd2f2ea1f1645069264f09fd53c3f71e1d02f

+ 47 - 0
.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.md

@@ -0,0 +1,47 @@
+# Agent Note: Explicit Agent identity at runtime boundaries
+
+Status: implemented
+
+English | [中文](2026-08-31-explicit-agent-runtime-identity.zh.md)
+
+## Problem
+
+An Agent's Cordis Context owns registrations and their cleanup. Agent identity instead selects the Session, runtime owner, event subject, authority decision, or wire identity for one operation. A reverse Agent property on Context made those two facts appear interchangeable: a caller could choose a Context for effect ownership and accidentally let that choice determine domain identity.
+
+The reverse association also required compensating mechanisms after type erasure. Host Remote forwarding inspected a routed subject for its Context, creation inferred runtime parentage from the caller Context, and adapters maintained reverse identity scans. These mechanisms duplicated identity already present in typed requests and obscured which caller owned an Agent at runtime.
+
+Without an explicit owner, `SubagentContinuationManager` creates and resumes children through its private plugin Context, so Context-based inference classifies every continuable child as a runtime root even though the manager holds its exact parent. Root-only consumers could then attach scheduling tools, grant direct-human goal authority, or route user questions as if the child were top-level.
+
+## Decision
+
+Runtime interfaces carry Agent identity at the point that owns it. `AgentSetup` receives `(agentCtx, agent)`; Agent creation and resume options carry `parentAgent` for a runtime child; scoped events carry their Agent in the payload; Remote forwarding verifies that `request.agent` is the carrier key; and Host Typert Context resolution maps wire identity to a live Agent Context without a reverse scan. `agent.ctx` remains the registration and lifecycle owner and exposes no reverse Agent property.
+
+Scope-aware registries continue to use the opaque scope key only for registration membership. Tool-subagent does not classify that key or resolve an Agent from Context. A direct `AgentSetup` passes the unpublished Session explicitly and installs through the supplied Context before publication. For a settings-backed standing preset, the event payload supplies the Agent, its Session supplies the policy target, and its Context owns the registrations.
+
+`SubagentContinuationManager` puts the exact parent in both fresh-creation and cold-resume options. A live continuable child is therefore excluded from `AgentRegistry.roots()` and satisfies `isOwnedBy(child.id, parent)`. Durable `parentSession` metadata does not substitute for this relation: a fork or resumed Session may be a runtime root when no live Agent owns it.
+
+The [Agent registration-scope decision](2026-07-08-agent-scope-contexts.md), its [runtime design](2026-07-12-agent-scope-runtime-design.md), and the [initiator-scope decision](2026-07-15-agent-initiator-scope.md) retain their independent registration, lifecycle, and private-chain rationale. This decision supersedes only the reverse Context association and implicit runtime-owner derivation described there.
+
+## Verification
+
+Agent creation tests pin explicit root and child ownership. Continuation integration tests keep a real child live long enough to assert both `roots()` exclusion and `isOwnedBy()` membership. Existing Schedule tests verify that root-only registrations stay absent from an explicitly owned child.
+
+Remote-event tests reject a missing or mismatched Agent before forwarding a scoped waterfall. Tool-subagent tests verify that direct setup installs before Session publication; standing-preset tests verify per-Session policy sampling and inheritance.
+
+## Alternatives considered
+
+**Keep `Context.agent`.** A reverse accessor makes registration ownership look like operation identity and requires every Context derivation, adapter, and test double to preserve an association unrelated to Cordis service selection or effect cleanup.
+
+**Infer runtime ownership from the caller Context.** A private manager Context, an Agent Context, and a standing preset Context can all call the same factory. Context ancestry therefore does not state which live Agent owns the result; the creator must put the parent it already knows in the request options.
+
+**Classify Agent scope keys.** An opaque scope key states routing membership, not domain identity. Classifying it would make Agent the center of composition and would still couple a plugin's effect owner to the Session whose policy it needs.
+
+**Use the initiating Agent as creation ownership.** Initiator scope records causal asynchronous execution, not lifetime ownership. A parent may initiate work that intentionally creates a root, and setup remains outside the child's driver boundary.
+
+**Use durable Session lineage.** `parentSession` records conversation ancestry across process lifetimes. Runtime ownership controls live roots and teardown, so equating the two would prevent a legitimately resumed fork from becoming a top-level Agent.
+
+## Consequences
+
+Lifecycle options, events, service requests, and transport requests carry explicit Agent identities, so each operation states the identity it uses and TypeScript checks both sides. Context remains reusable for dependency access and effect ownership without becoming an alternate domain-object locator.
+
+Continuable children have the same runtime parent relation as one-shot in-process children. Root-only consumers exclude them, parent teardown can reason from one live ownership graph, and durable lineage remains free to describe history rather than process-local lifetime.

+ 47 - 0
.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.zh.md

@@ -0,0 +1,47 @@
+# Agent Note: 运行时边界显式携带 Agent 身份
+
+Status: implemented
+
+[English](2026-08-31-explicit-agent-runtime-identity.md) | 中文
+
+## 问题
+
+Agent 的 Cordis Context 拥有注册及其清理。Agent 身份则为某项操作选择会话、运行时所属方、事件主体、权限决策或协议身份。Context 上反向的 Agent 属性让这两个事实看起来可以互换:调用方选择用于管理 effect 所有权的 Context 时,可能意外地让该选择决定领域身份。
+
+类型信息被擦除后,这项反向关联还需要补偿机制。Host Remote 转发会从已路由主体检查其 Context,创建流程会从调用方 Context 推断运行时父级,适配器则维护反向身份扫描。这些机制重复类型化请求中已有的身份,也掩盖了哪个调用方在运行时拥有 Agent。
+
+若没有显式所属方,`SubagentContinuationManager` 会通过私有插件 Context 创建和恢复子级,因此基于 Context 的推断会把每个可续跑子级归类为 runtime root,尽管管理器持有其确切父级。仅限根级的消费方随后可能附加调度工具、授予直接人类输入对应的 Goal 权限,或像处理顶层 Agent 一样路由用户问题。
+
+## 决策
+
+运行时接口在拥有身份的位置携带 Agent 身份。`AgentSetup` 接收 `(agentCtx, agent)`;创建与恢复 Agent 的 options 通过 `parentAgent` 标识运行时子级;作用域事件在 payload 中携带 Agent;Remote 转发校验 `request.agent` 就是 carrier key;Host Typert Context 解析则把协议身份映射到存活 Agent Context,不执行反向扫描。`agent.ctx` 继续拥有注册和生命周期,不暴露反向 Agent 属性。
+
+感知作用域的注册表继续仅使用不透明作用域键判断注册成员关系。tool-subagent 不会分类该键,也不会从 Context 解析 Agent。直接 `AgentSetup` 显式传入尚未发布的 Session,并在发布前通过所给 Context 完成安装。对于由设置控制的常驻 preset,事件 payload 提供 Agent,其 Session 提供策略目标,其 Context 拥有注册项。
+
+`SubagentContinuationManager` 会把确切父级放进全新创建与冷恢复的 options。因此,存活的可续跑子级不会出现在 `AgentRegistry.roots()` 中,并且满足 `isOwnedBy(child.id, parent)`。持久化 `parentSession` 元数据不能代替这项关系:没有存活 Agent 拥有 fork 或已恢复会话时,它仍可成为 runtime root。
+
+[Agent 注册作用域决策](2026-07-08-agent-scope-contexts.zh.md)、其[运行时设计](2026-07-12-agent-scope-runtime-design.zh.md)和[发起方作用域决策](2026-07-15-agent-initiator-scope.zh.md)继续拥有各自独立的注册、生命周期及私有调用链理由。本决策只取代其中描述的反向 Context 关联和隐式运行时所属方推导。
+
+## 验证
+
+Agent 创建测试锁定显式的根级与子级归属。continuation 集成测试让一个真实子级保持存活,直到断言其既不属于 `roots()`、又满足 `isOwnedBy()`。现有 Schedule 测试验证仅限根级的注册项不会出现在显式归属的子级中。
+
+Remote 事件测试会在转发作用域 waterfall 前拒绝缺失或不匹配的 Agent。tool-subagent 测试验证 direct setup 会在 Session 发布前完成安装;常驻 preset 测试验证逐 Session 的策略读取与继承。
+
+## 考虑过的替代方案
+
+**保留 `Context.agent`。** 反向 accessor 会让注册所有权看起来等同于操作身份,还要求每个 Context 派生、适配器和测试替身保留一项与 Cordis 服务选择或 effect 清理无关的关联。
+
+**从调用方 Context 推断运行时归属。** 私有管理器 Context、Agent Context 和常驻 preset Context 都能调用同一个工厂。因此,Context 祖先关系无法说明由哪个存活 Agent 拥有结果;创建方必须把它已知的父级放进请求 options。
+
+**分类 Agent 作用域键。** 不透明作用域键表达路由成员关系,而不是领域身份。分类该键会让 Agent 成为组合中心,也仍会把插件的 effect 所有者与策略所需的 Session 耦合起来。
+
+**使用发起 Agent 作为创建归属。** 发起方作用域记录异步执行的因果关系,而非生命周期归属。父级可能发起有意创建根级 Agent 的工作,而 setup 仍位于子级驱动边界之外。
+
+**使用持久化会话谱系。** `parentSession` 跨进程生命周期记录对话祖先关系。运行时归属控制存活根级和 teardown,因此把二者等同会阻止合法恢复的 fork 成为顶层 Agent。
+
+## 后果
+
+生命周期 options、事件、服务请求和传输请求会携带显式 Agent 身份,因此每项操作都会声明自身使用的身份,TypeScript 也会检查两侧。Context 可以继续复用于依赖访问与 effect 所有权,而不会成为另一种领域对象定位器。
+
+可续跑子级与一次性进程内子级使用同一种运行时父级关系。仅限根级的消费方会排除这些子级,父级 teardown 可以依据唯一的存活归属图推理,而持久化谱系仍可描述历史,不必承担进程内生命周期语义。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.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-31-released-session-format-migrations.md
-2026-08-31-released-session-format-migrations.md: 592322c0e4c1b2fa52dcf71652f3878f43a8c8ca
-2026-08-31-released-session-format-migrations.zh.md: ba2317903845739cda8da1c01c2f959c7a2ccd50
+2026-08-31-released-session-format-migrations.md: eb5eb14f28a6459bd388caa2ea106ec01d1ebbe6
+2026-08-31-released-session-format-migrations.zh.md: b06bfac059541e9c397ff46fe7d169690c5b1213

+ 22 - 1
.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md

@@ -58,16 +58,33 @@ JSONL record
   → released physical row decoder
   → v0-to-v1 stage
   → v1-to-v2 stage
+  → v2-to-v3 stage
   → current event collector
 ```
 
 The chain contains no `flatMap`, spread expansion, intermediate event array, or scheduler. The final event collector expands a compact run only after every migration stage has had the opportunity to consume it directly.
 
+### Adjacent version ownership
+
+The [V2-to-V3 delivery guards](../../../../packages/session/session-format-v2-to-v3/README.md#delivery-guards) prevent a marker ignored in the source generation from becoming an active upload watermark merely because the header changes. Python release smoke checks generated logs against the source `SESSION_FORMAT_VERSION` independently of generation-neutral golden comparison, so coherent filenames and headers cannot conceal an outdated writer.
+
+The [V2-to-V3 README](../../../../packages/session/session-format-v2-to-v3/README.md#v2-to-v3-specification) is the single specification for that edge's transformations, preservation, and refusal; its separate [native admission section](../../../../packages/session/session-format-v2-to-v3/README.md#native-v3-admission) prevents current-only capabilities from being mistaken for historical transformations. The released V2 codec remains owned by V1→V2 and is reused, not copied. The [system-prompt](2026-09-02-system-prompt-as-surface-node.md), [PTC](../feature/2026-06-15-ptc.md), and [canonical-envelope](2026-09-06-v3-canonical-session-envelopes.md) notes retain their independent rationale, not duplicate conversion specifications. The [format-version cookbook](../../../../docs/cookbook/adding-a-session-format-version.md) owns package wiring, current consumers, snapshot successors, and validation commands.
+
+Historical content admission belongs to the incoming edge, not native V3 extension validation. Preserving an unknown block without understanding its fields cannot establish that migration preserves its meaning. The [source audit](../../../../packages/session/session-format-v2-to-v3/README.md#source-audit) therefore uses one historical kind set across its explicitly owned content positions, including partial streams. It inspects admitted content without rewriting it and leaves owner-opaque JSON uninterpreted. Narrowing native acceptance or editing frozen predecessor validators would change independent promises rather than establish safe conversion.
+
+Preset renames cover the creation header and every selection event because the latest selection controls resume while earlier selections control historical forks. Rewriting only the last selection loses that distinction. The released `code` id denotes the legacy built-in preset; migration is independent of the installed roster so the same bytes produce the same result on every host. Native V3 custom ids remain available without a global runtime alias.
+
+A source inherited count can be unknown before EOF: V2 derives it from seed markers, and V1→V2 can change cardinality. The chain passes that absence to the next stage instead of fabricating a count. The [V2-to-V3 inheritance rules](../../../../packages/session/session-format-v2-to-v3/README.md#sequence-references) support this case; older stages that require a header-supplied count still refuse when it is absent. This permits seeded multi-hop restoration without retaining an intermediate artifact array.
+
+All structural changes compose in the one unreleased V2→V3 edge; feature or review order does not allocate extra Session format versions. V0, V1, and V2 generations remain byte-frozen, and migration publishes only the final V3 successor. The unreleased target can evolve until release, but an already-written V3 file does not rerun its incoming migration. Integration tests therefore require isolated disposable homes and unchanged historical inputs rather than rewriting committed generations.
+
+The [committed-corpus inventory](../../../../packages/test-support/llm-replay/tests/session-format-corpus-inventory.ts) identifies deliberately unsupported historical conversions by source path, generation, and exact refusal reason. Retaining those artifacts must not force chronology-changing migration or permit a blanket skip: every listed artifact must still raise the typed migration refusal, and unlisted artifacts must restore. Native current-generation fixtures cannot be classified as unsupported, because they do not traverse an incoming edge. Headerless test-harness protocol examples remain a separate explicit class. The corpus test checks source bytes after both successful and refused restoration; it does not rewrite historical evidence to satisfy the current reader.
+
 ### Physical codecs and packed runs
 
 Each released codec creates a row decoder with explicit `strict` or `recoverable` recovery. The decoder validates and emits one event or one codec-owned `SessionFormatEventRun` at a time through separate context methods. v0-to-v1 and v1-to-v2 implement both `transformEvent()` and `transformRun()`, so packed Assistant chunks can reach the folding edge without first becoming millions of ordinary events.
 
-The v0-to-v1 edge preserves logical headers, sequence numbers, references, timestamps, and payloads except for bounded released-v0 normalizations. It translates the retired `steering/message` and `compact/*` event names, accepts a released `llm/retry` after its matching `step/end`, deterministically supplies a missing `llm/retry.retryId` per turn/step/provider/policy chain, and supplies one deterministic `compactionId` across a legacy compaction group that omitted it. The v1-to-v2 edge owns attempt folding and reference remapping, and emits only settled current events. It splits a legacy goal-sourced user message into `goal/change` plus the original model-visible message. It also inserts an interrupted `turn/end` for the bounded released restart in which an open turn with no open step is followed by a non-empty `next-turn` inbox splice and the next numbered `turn/start`.
+The v0-to-v1 edge preserves logical headers, sequence numbers, references, timestamps, and payloads except for bounded released-v0 normalizations. It translates the retired `steering/message` and `compact/*` event names, accepts a released `llm/retry` after its matching `step/end`, deterministically supplies a missing `llm/retry.retryId` per turn/step/provider/policy chain, and supplies one deterministic `compactionId` across a legacy compaction group that omitted it. The v1-to-v2 edge owns attempt folding and reference remapping, and emits only settled v2 events. It splits a legacy goal-sourced user message into `goal/change` plus the original model-visible message. It also inserts an interrupted `turn/end` for the bounded released restart in which an open turn with no open step is followed by a non-empty `next-turn` inbox splice and the next numbered `turn/start`.
 
 The catalog exposes one `createRestore()` operation for production, Worker, fixture, and replay callers. Recovery policy and final validation policy are chosen once at restore creation. Historical production uses recoverable source parsing with transformed-current validation; this validates the released current result after migration, while input that is already current receives only codec validation. Worker and fixture verification use strict parsing with full installed current restoration. A migration-stage or transformed-current validation refusal remains `SessionFormatUnsupportedMigrationError`; physical decoding failures remain corruption. Test support keeps only fixture-specific token and envelope materialization.
 
@@ -105,6 +122,10 @@ Existing write handles retain the process-local claim and kernel-backed cross-pr
 
 ## Verification
 
+The migration specification requires evidence for transformations, preservation, and refusal separately. Direct-edge and native V3 tests cannot establish seeded multi-hop publication: preceding assistant-stream folding changes source coordinates before V3 inserts system events. Tests through the real catalog and JSONL provider therefore need raw and compressed V0/V1 inputs, mapped references and inherited cuts, publish/reopen equivalence, unchanged predecessor bytes, and no intermediate generations. Coverage percentages alone cannot prove those cross-stage relationships; combined assertions must compare the resulting history and refusal effects.
+
+Content-admission evidence must cover every position named in the specification, nested results, partial starts, and malformed known blocks, with source-coordinate diagnostics. Successful migration must preserve admitted content and opaque values. Refusal through real persistence must leave the source unchanged and publish no successor. Native V3 tests must independently retain extension acceptance under both catalog validation policies; historical refusal is not evidence of native rejection.
+
 ### Benchmark input and meanings
 
 The benchmark uses Node v24.18.0 and one 116,228,655-byte v0 Zstandard log containing 317,540 frames and 454,151 physical rows. The old reader restores 9,143,111 expanded v0 events. Migration produces 72,784 current v2 events with artifact SHA-256 `fa16ff9472ca350595a3112c20a3db79655bc2673973469987ecaf2a57ebd17c`.

+ 22 - 1
.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md

@@ -58,16 +58,33 @@ JSONL record
   → released physical row decoder
   → v0-to-v1 stage
   → v1-to-v2 stage
+  → v2-to-v3 stage
   → current event collector
 ```
 
 Chain 中不存在 `flatMap`、spread expansion、中间 event array 或 scheduler。只有在每个 migration stage 都已获得直接消费 compact run 的机会后,最终 event collector 才会展开它。
 
+### 相邻版本所有权
+
+[V2 到 V3 投递保护](../../../../packages/session/session-format-v2-to-v3/README.zh.md#delivery-guards)防止源代中被忽略的标记仅因头部变化就成为有效上传水位。Python 发布冒烟测试独立于跨代 golden 比较,按源代码中的 `SESSION_FORMAT_VERSION` 检查生成日志,因此文件名与 header 自洽不能掩盖过期 writer。
+
+[V2 到 V3 README](../../../../packages/session/session-format-v2-to-v3/README.zh.md#v2-to-v3-specification)是该迁移边转换、保留与拒绝规则的单一规范真源;单列的[原生准入章节](../../../../packages/session/session-format-v2-to-v3/README.zh.md#native-v3-admission)避免将仅当前版本支持的能力误认为历史转换。已发布 V2 codec 仍归 V1→V2 所有,并被复用而非复制。[系统提示词](2026-09-02-system-prompt-as-surface-node.zh.md)、[PTC](../feature/2026-06-15-ptc.zh.md)和[规范信封](2026-09-06-v3-canonical-session-envelopes.zh.md)记录保留各自独立依据,而非重复转换规范。[格式版本实操手册](../../../../docs/cookbook/adding-a-session-format-version.zh.md)负责包接线、当前消费方、快照后继代际与验证命令。
+
+历史内容准入归入边所有,而非原生 V3 扩展校验。在不了解字段的情况下保留未知块,不能证明迁移保留了其含义。因此,[源审计](../../../../packages/session/session-format-v2-to-v3/README.zh.md#source-audit)在明确归其所有的内容位置(包括未完成的流)使用同一历史种类集合。它检查已接纳的内容而不改写,并且不解释归其他所有者所有的不透明 JSON。收紧原生准入或修改冻结的前代校验器,会改变独立承诺,而非证明转换安全。
+
+预设更名覆盖创建头部和每条选择事件,因为最新选择决定恢复时的预设,而更早的选择决定历史 fork 的预设。只改写最后一条选择会丢失这种区别。已发布的 `code` 标识表示旧内置预设;迁移不依赖已安装的预设列表,因此相同字节在每台主机上产生相同结果。原生 V3 的自定义标识仍可使用,无需全局运行时别名。
+
+源继承数量在 EOF 前可能未知:V2 从种子标记推导它,而 V1→V2 可以改变事件数量。迁移链将这种缺失传递给下一个 Stage,而不伪造数量。[V2 到 V3 继承规则](../../../../packages/session/session-format-v2-to-v3/README.zh.md#sequence-references)支持此情况;需要 header 提供数量的旧 Stage 仍在数量缺失时拒绝。这使有种子的多跳恢复无需保留中间产物数组。
+
+所有结构变更组合在唯一且尚未发布的 V2→V3 迁移边中;功能或评审顺序不分配额外 Session 格式版本。V0、V1、V2 代际保持字节冻结,迁移只发布最终 V3 后继代际。未发布的目标可以持续演化至发布,但已经写出的 V3 文件不会重新执行入边迁移。因此,集成测试必须使用隔离、可丢弃的 home 和未变更的历史输入,而非改写已提交代际。
+
+[已提交语料清单](../../../../packages/test-support/llm-replay/tests/session-format-corpus-inventory.ts) 按源路径、代际与精确拒绝原因标识有意不支持的历史转换。保留这些产物不能迫使迁移改变时序,也不能允许统一跳过:每个清单中的产物仍必须抛出类型化迁移拒绝,未列入的产物必须还原。原生当前代际 fixture 不经过入边,因此不能被归为不支持。没有版本 header 的测试框架协议示例保持为独立的显式类别。语料测试在还原成功和拒绝后都检查源字节;它不通过改写历史证据来满足当前 reader。
+
 ### Physical codec 与 packed run
 
 每个 released codec 会用显式 `strict` 或 `recoverable` 策略创建 row decoder。Decoder 每次通过不同的 context 方法校验并 emit 一个 event 或 codec-owned `SessionFormatEventRun`。v0-to-v1 与 v1-to-v2 都实现 `transformEvent()` 和 `transformRun()`,因此 packed Assistant chunk 可以直接到达 folding edge,无需先变成数百万个普通事件。
 
-v0-to-v1 除了有限的 released-v0 归一化外,会保留逻辑 header、seq、引用、时间戳与 payload。它转换已移除的 `steering/message` 与 `compact/*` 事件名称,接受出现在对应 `step/end` 之后的已发布 `llm/retry`,按 turn/step/provider/policy chain 为缺失的 `llm/retry.retryId` 确定性补值,并为省略 id 的旧 compaction group 确定性补充同一个 `compactionId`。v1-to-v2 负责 attempt folding 与引用重写,并且只 emit 已结算的 current event。它会把旧的 goal 来源 user message 拆成 `goal/change` 与原本的模型可见 message。它还会为一种有限的已发布 restart 插入 interrupted `turn/end`:一个没有 open step 的 open turn 后出现非空 `next-turn` inbox splice,随后直接开始编号连续的下一轮。
+v0-to-v1 除了有限的 released-v0 归一化外,会保留逻辑 header、seq、引用、时间戳与 payload。它转换已移除的 `steering/message` 与 `compact/*` 事件名称,接受出现在对应 `step/end` 之后的已发布 `llm/retry`,按 turn/step/provider/policy chain 为缺失的 `llm/retry.retryId` 确定性补值,并为省略 id 的旧 compaction group 确定性补充同一个 `compactionId`。v1-to-v2 负责 attempt folding 与引用重写,并且只 emit 已结算的 v2 event。它会把旧的 goal 来源 user message 拆成 `goal/change` 与原本的模型可见 message。它还会为一种有限的已发布 restart 插入 interrupted `turn/end`:一个没有 open step 的 open turn 后出现非空 `next-turn` inbox splice,随后直接开始编号连续的下一轮。
 
 Catalog 为 production、Worker、fixture 与 replay 暴露同一个 `createRestore()`。Recovery policy 与最终 validation policy 在 restore 创建时一次确定。Historical production 使用 recoverable source parsing 与 transformed-current validation;这种策略会在迁移后校验已发布 current 结果,而已经是 current 的输入只接受 codec 校验。Worker 与 fixture verification 使用 strict parsing 与已安装 current 格式的完整 restoration。Migration stage 或 transformed-current validation 的拒绝会保持为 `SessionFormatUnsupportedMigrationError`;物理解码失败仍是 corruption。Test support 只保留 fixture 自身需要的 token 和 envelope materialization。
 
@@ -105,6 +122,10 @@ POSIX publication 使用 hard-link creation 加目录 sync;Windows 使用 no-o
 
 ## 验证
 
+迁移规范要求分别提供转换、保留与拒绝的证据。直接迁移边和原生 V3 测试不能证明有种子的多跳发布:前代 assistant 流折叠会在 V3 插入系统事件前改变源坐标。因此,经过真实目录与 JSONL 提供方的测试需要原始及压缩的 V0/V1 输入、映射后的引用和继承切点、发布/重新打开等价性、前代字节不变,以及不产生中间代。覆盖率百分比本身不能证明这些跨阶段关系;组合断言必须比较结果历史与拒绝效果。
+
+内容准入证据必须覆盖规范列出的每个位置、嵌套结果、未完成的起始记录和已知种类的畸形块,并验证诊断使用源坐标。成功迁移必须保留已接纳的内容与不透明值。经真实持久化路径拒绝时,必须保持源不变且不发布后继代。原生 V3 测试必须独立证明两种目录校验策略均保留扩展准入;历史拒绝不能证明原生输入也被拒绝。
+
 ### Benchmark 输入与口径
 
 Benchmark 使用 Node v24.18.0 和一份 116,228,655-byte 的 v0 Zstandard 日志,其中包含 317,540 个 frame 与 454,151 个 physical row。老 reader 会恢复 9,143,111 个展开后的 v0 event;migration 会生成 72,784 个 current v2 event,artifact SHA-256 为 `fa16ff9472ca350595a3112c20a3db79655bc2673973469987ecaf2a57ebd17c`。

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.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-09-01-v2-embedded-assistant-streams.md
-2026-09-01-v2-embedded-assistant-streams.md: bee4d50fb830caa277bb700f3415e6d7f98ff64b
-2026-09-01-v2-embedded-assistant-streams.zh.md: 9116e94111b78af68d33f6f01dc289ee9f349b7e
+2026-09-01-v2-embedded-assistant-streams.md: 98207749028e2182c5e60073fc07985688ecfa30
+2026-09-01-v2-embedded-assistant-streams.zh.md: 90dff5a385cf83071ec52a2fc2b57a893107b861

+ 2 - 0
.agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.md

@@ -14,6 +14,8 @@ Changing event cardinality also changes Session sequence numbers. A released mig
 
 ## Decision
 
+The [V3 canonical-envelope decision](2026-09-06-v3-canonical-session-envelopes.md) owns current replacement-key and header-acceptance rules. It preserves the embedded streams, attempt settlements, and frozen v1-to-v2 conversion described here.
+
 Session format v2 has no top-level `assistant/chunk` event. Each model attempt commits one durable settlement containing `stream: AssistantStreamRecord[]`:
 
 - `assistant/message` is the surface settlement for a successful response or a cancelled response with visible assembled content. It embeds the exact compact timed stream beside the assembled message, optional usage, and optional `interrupted: true` marker.

+ 2 - 0
.agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.zh.md

@@ -14,6 +14,8 @@ Token 粒度的 `assistant/chunk` 事件会保留精确的 stream 顺序、时
 
 ## 决策
 
+[V3 规范信封决策](2026-09-06-v3-canonical-session-envelopes.zh.md)负责当前替换键与请求头接纳规则。它保留本文的嵌入式 stream、尝试结算与冻结的 v1-to-v2 转换。
+
 Session format v2 没有顶层 `assistant/chunk` 事件。每个模型 attempt 提交一个包含 `stream: AssistantStreamRecord[]` 的持久 settlement:
 
 - `assistant/message` 是成功响应或具有可见组装内容的已取消响应所对应的 surface settlement。它在组装 message 旁嵌入精确的紧凑带时间 stream、可选 usage 与可选 `interrupted: true` marker。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.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-09-02-system-prompt-as-surface-node.md
+2026-09-02-system-prompt-as-surface-node.md: dc0d22b2fb927ad288415346bea9d0c2793cf000
+2026-09-02-system-prompt-as-surface-node.zh.md: 368684d85cb7ddf5d0be63bce905ce48e86cb49f

+ 95 - 0
.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md

@@ -0,0 +1,95 @@
+# Agent Note: The system prompt is surface node 0
+
+Status: implemented
+
+English | [中文](2026-09-02-system-prompt-as-surface-node.zh.md)
+
+## Problem
+
+A system prompt held outside the surface has a different durable representation from every other message the model reads. Conversation messages are surface events (`user/message`, `assistant/message`, `tool/result`) folded in seq order by `Session.deriveMessages()`; a prompt stored as a `system` field of the log-only `request/header` snapshot has to be prepended by each serializer as wire message 0. The [reconstructable-requests Agent Note](2026-07-05-reconstructable-requests.md) made both halves durable, but that layout leaves one model-visible fact with two homes: the surface owns the messages, the header owns the message in front of them.
+
+That split forces every reader of "what did the model see" to join two sources: the compaction summarizer copies the header prompt in front of the region's derived messages, `dsh-token-meter` estimates the system prompt from the header while pricing every other message from the surface, and the Web request-prompt card, the trajectory view, and the snapshot normalizer's `{{system}}` placeholder each read the header on their own. Change detection is split the same way: a `headerEquals` that compares `system` byte-for-byte beside `config` and `tools` makes a prompt change and a tool change indistinguishable in the log (`request/header` reason `change`) even though they are different operations on the conversation.
+
+The split also blocks the next step. A model that accepts a mid-conversation `system` message as a prompt replacement needs the harness to append a system-role message to history; with the prompt living in the header there is no surface representation to append, and the header would have to be frozen by special case. The [in-history replacement decision](../feature/2026-09-02-in-history-system-prompt-replacement.md) depends on this note.
+
+## Decision
+
+The system prompt lives on the surface. It is an ordinary surface event, `system/message`, and every prompt lifecycle operation is one of the two existing `SurfaceOp` variants applied to that event type. The wire request is unchanged: the surface fold yields the message list the serializers send, with the system message first.
+
+### The event
+
+`system/message` is a member of `SurfaceEventType` beside `user/message`, `assistant/message`, and `tool/result` (`packages/core/session/src/types.ts`). Its payload mirrors `tool/result`: `{ turn, step, message }`, where `message` is a `SystemMessage` with `role: 'system'`, one text block holding the rendered prompt, and source `{ kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' }`. Empty `content` records "no system prompt": the node keeps its surface position and `deriveEventMessage` projects it to `null`, so it contributes no wire message. A non-empty node projects verbatim, so `deriveMessages()` returns the system message at its surface position and the DeepSeek serializers, which pass a `role: 'system'` history message through unchanged, emit it as wire message 0. `EpochHeader` is `{ config, adapterDefaults?, tools? }`; `canonicalHeader` and `headerEquals` in `packages/core/session/src/request-header.ts` compare config, adapter defaults, and tools only.
+
+### The operations
+
+| Situation | Surface operation |
+|---|---|
+| No `system/message` survives on the surface (including an empty rendered prompt) | append `system/message`; on the session's first step it is surface node 0, before the first `user/message` of the step |
+| A `system/message` survives and the rendered prompt differs from its text (including a prompt that becomes empty) | replace exactly that node: `surfaceOp: { op: 'replace', startSeq: <seq of the node>, endSeq: <same> }`, `sourceEventSeqs: [<seq of the node>]`; an empty prompt produces an empty-content node that projects to no message |
+| The rendered prompt equals the surviving node's text | no operation |
+
+When the initial rendered prompt is empty, the loop reserves an empty system head before the initial admitted user messages so a prompt that first becomes non-empty later still replaces node 0. Omitting that empty node would append the later prompt behind user history, where pi-ai converts it to a user message rather than its `systemPrompt`. Replacing node 0 is a head rewrite expressed on the surface: the provider prefix changes from the first token, the log records the shadowed node through `sourceEventSeqs`, and `replaceGeneration` advances as it does for a compaction replacement. The loop's `startsSeries` detection (`requestSurfaceGeneration !== surfaceGeneration`) therefore covers the prompt change without a `system` comparison in `headerEquals`. `request/header` keeps reasons `initial`, `resume`, `change`, and `series`; `change` means config or tools changed, and the unchanged header that follows a prompt replacement logs as `series`.
+
+`packages/core/session/src/surface.ts` enforces the head invariant in `assertSystemHeadRewrite`: a replacement whose range covers surface node 0 while node 0 is a `system/message` is rejected unless the replacing event is itself a `system/message` covering exactly that node. System nodes at later positions carry no such protection; a compaction range may shadow them.
+
+### Ownership in the loop
+
+`dsh-agent-loop` owns `SystemPromptProjection` beside `RuntimeContextProjection` in `packages/core/agent-loop/src/runtime-context.ts`. It reads the surviving `system/message` nodes from the current surface on every projection, so a compaction or replacement that ran earlier in the same step is already reflected. `project(rendered, { inHistory, startsSeries })` returns `{ message, intent }` — `intent` is `{ surfaceOp: 'append' }` when no system node survives or when the [in-history rule](../feature/2026-09-02-in-history-system-prompt-replacement.md) applies, otherwise a replacement of exactly the latest surviving system node — or `undefined` when the latest node already holds the rendered text.
+
+In `packages/core/agent-loop/src/agent.ts`, `preStep` renders the prompt with `renderPrompt(assembly)` and projects it after the `agent/pre-step` waterfall, so a compaction provider's replacement inside that waterfall is visible to the decision; `turn()` commits the `system/message` immediately after `step/start` and before the step's `user/message` events, so log order is wire order. `buildRequest` sets no `system` on the request: the request is `header.config`, `session.deriveMessages()` (system message first), and `header.tools`. The loop step order is: claim inbox → `systemPrompt.assemble()` → project runtime context → `agent/pre-step` waterfall → project system prompt → `step/start` → commit `system/message` (when changed) → commit `user/message`s → `agent/request` waterfall → `request/header` → `request/context` → stream. The `dsh-agent-loop/invariant` companion (`packages/core/agent-loop/src/invariant.ts`) asserts that a loop-built request has `system === undefined` and `messages` equal to `deriveMessages()`.
+
+`dsh-token-meter` anchors usage to the priced surface immediately before the successful `assistant/message`, not to `step/start`. The loop admits the system prompt and user messages after step start, and retry recovery can replace nodes before rebuilding the request. Capturing that current surface includes every admitted input once; the embedded provider output remains separately priced so durable assistant rewrites retain their signed delta. The open step stores only turn and step for lifecycle validation, not a second node snapshot.
+
+### Consumers
+
+| Consumer | Reads |
+|---|---|
+| DeepSeek serializers (`serializeRequest`, `serializeRequestWithImages`) | `options.messages`, passing the `role: 'system'` history message through as wire message 0; `GenerateOptions.system` remains for direct one-shot callers such as title providers |
+| `dsh-llm-pi-ai` | a leading system history message maps to pi-ai's `systemPrompt` |
+| `compaction-basic` `buildSummarizationInput` | node 0's derived message prepended to the region in `SummarizationInput.messages`, with no separate `system` field; an empty-content head projects to no message while staying protected from compaction |
+| `compaction-basic` `selectCompactableRange` | anchors at the first non-system node; node 0 is never inside a compaction range |
+| `dsh-token-meter` | the system node is priced as a surface node under the `systemTokens` breakdown |
+| Web request-prompt card, trajectory request node, request inspection | the `system/message` node; a replaced node 0 is shown as a prompt change and an appended in-history node as a prompt update, each in a collapsed inspectable card, never a chat bubble |
+| Snapshot normalizer `{{system}}` placeholder, plan-mode tests | the system node's text |
+| TypeScript and Python SDK expected outputs | include the `system/message` event |
+| Human transcript projections | skip `system/message`; it is model history, not conversation |
+
+`RuntimeContextProjection` and `SystemPromptProjection` both hand the loop an uncommitted message that `turn()` commits. They differ in how they observe the surface and in their operation set: runtime context follows `session/event` for its owned user-role snapshots and appends only, while the system prompt scans the current surface for system nodes on each projection because its decision depends on how many survive, and it appends or replaces per the route.
+
+### V2-to-V3 structural conversion
+
+The [V2-to-V3 specification](../../../../packages/session/session-format-v2-to-v3/README.md#system-head) owns system-head conversion and message identities; its [reference rules](../../../../packages/session/session-format-v2-to-v3/README.md#sequence-references) and [source refusal](../../../../packages/session/session-format-v2-to-v3/README.md#source-audit) define preservation and unsupported inputs. The migrated layout is semantically equivalent to native requests, not byte-identical to a native recording. A valid V2 source can lack an order-preserving conversion under the current step invariant; refusing it is preferable to moving history or relaxing ownership. Historical acceptance coordinates must not become acknowledgements of the transformed log.
+
+The [released-format policy](2026-08-31-released-session-format-migrations.md) keeps V0, V1, and V2 generations byte-frozen and publishes only V3 successors. V3 is one unreleased target, not a new version per feature; it can evolve before release, so integration requires disposable homes. An existing V3 generation does not rerun V2-to-V3. Projection-cache version 4 is independent of the Session format and does not imply Session V4.
+
+The [canonical-envelope specification](../../../../packages/session/session-format-v2-to-v3/README.md#canonical-envelopes) defines composition with the structural conversion; the [canonical-envelope decision](2026-09-06-v3-canonical-session-envelopes.md) owns the strict-acceptance rationale.
+
+## Alternatives considered
+
+**Keep `header.system` and add `system/message` only for updates.** Two homes for one fact: every consumer above would read the header for message 0 and the surface for later messages, and the loop would need a special case that ignores `system` in `headerEquals` while a surface system node exists. Rejected because the point of the change is one representation.
+
+**A dedicated log-only `system-prompt/change` event that rewrites the header.** Preserves the header as the home of the prompt and records changes as their own event kind, but still cannot express a system message inside history, so the in-history proposal would need a second mechanism anyway. Rejected.
+
+**Synthesize the system message inside the adapter from consecutive headers.** The adapter is stateless per request and never sees the log; a wire history that depends on adapter state is not reconstructable from the surface fold. Rejected.
+
+**Express the prompt as a `user/message` snapshot like runtime context.** Reuses an existing event type but sends the wrong role, so a model that treats a system message as authoritative would not. Rejected.
+
+## Consequences
+
+- One representation: every reader of "what did the model see" folds the surface; no consumer joins the header to the message list. `EpochHeader` has no `system` field, so a reader that expects one fails at compile time.
+- A prompt change and a tool or config change are distinguishable in the log: the former is a `system/message` replacement of node 0 followed by a `series` header, the latter a `request/header` with reason `change`.
+- Compaction carries an invariant: node 0 is never compacted. The `dsh-session` surface manager enforces it in the replace operation itself, so a compaction provider other than `compaction-basic` cannot shadow the prompt by anchoring at `surfaceNodes[0]`. Later system nodes are unprotected by design.
+- `replaceGeneration` advances for a prompt replacement as well as for compaction; a reader that needs to distinguish them inspects the replacement event's type.
+- A mid-history system node has a surface representation, which is what the [in-history replacement decision](../feature/2026-09-02-in-history-system-prompt-replacement.md) builds on.
+- An initially empty prompt occupies the protected head without contributing a wire message; in replacement mode, a later non-empty prompt replaces it and remains the leading system message.
+- Recorded snapshot fixtures carry the `system/message` event instead of a header `system` field. The snapshot normalizer tokenizes that event's text to `{{system}}`, the prompt sidecar is harvested from the `system/message` sequence (one section per prompt version, declared as `header.promptChanges`), and `request/header` pins compare config and tools only.
+
+## Testing
+
+- `packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts` pins zero post-call surface delta with provider usage through initial, growing, shrinking, and empty prompts, same-step retry replacement, request middleware, and fresh replay.
+- `packages/core/session/tests/surface.spec.ts` (`system/message surface node` block) pins the leading system-role projection, the empty-content `null` projection, `assertSystemHeadRewrite`'s acceptance and rejection paths, the unprotected later system nodes, and the rejection of a seeded `system/message` with a non-system role or non-plugin source.
+- `packages/core/agent-loop/tests/system-prompt-projection.spec.ts` pins the append on first render (including empty), the later non-empty prompt at the derived head in replacement mode, the no-op on an unchanged prompt, the replacement of the latest surviving node on change, the tail append after a replacement shadowed a non-head system node, and the in-history append and re-baseline rules.
+- `packages/core/agent-loop/tests/request-reconstruction.spec.ts` (`a system-prompt change replaces surface node 0 and starts a new series under the same header`) pins the `series` header that follows a prompt replacement.
+- `packages/core/agent-loop/tests/invariant.spec.ts` pins the companion's rejection of a loop request carrying a `system` field and its `messages` equality check against the boundary derivation.
+- `packages/llm/llm-deepseek/tests/serialize.spec.ts` (`serializes a leading system message byte-for-byte like the same prompt passed as options.system`) pins wire identity. `packages/llm/llm-pi-ai/tests/context.spec.ts` compares both system sources on text and image paths. `packages/compaction/compaction-basic/tests/compaction-basic.spec.ts` pins the derived prefix, routed tools, absent separate `system` option, and protected non-empty or empty head through the region transaction and default summarizer.
+- The recorded snapshots under `snapshots/` pin the model-visible wire request of every shipped profile; a recorded session that renders a prompt carries the `system/message` event at surface node 0 in its `session.jsonl`, and a session with a mid-session prompt change carries the replacement of node 0 or, on an in-history route, the appended node.

+ 95 - 0
.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md

@@ -0,0 +1,95 @@
+# Agent Note: 系统提示词是 surface 的第 0 号节点
+
+Status: implemented
+
+[English](2026-09-02-system-prompt-as-surface-node.md) | 中文
+
+## Problem
+
+放在 surface 之外的系统提示词,其持久化表示与模型读到的其他所有消息都不同。对话消息是 surface 事件(`user/message`、`assistant/message`、`tool/result`),由 `Session.deriveMessages()` 按 seq 顺序折叠;而存放在仅记日志的 `request/header` 快照 `system` 字段中的提示词,必须由每个序列化器前置为协议消息 0。[可重建请求 Agent Note](2026-07-05-reconstructable-requests.zh.md) 让两半都成为持久数据,但这种布局让一个模型可见的事实拥有两个归属:surface 拥有消息,header 拥有排在这些消息之前的那条消息。
+
+这种拆分迫使每个想知道「模型看到了什么」的读取方都要合并两个来源:压缩(compaction)摘要器把 header 中的提示词复制到区域派生消息之前,`dsh-token-meter` 从 header 估算系统提示词却从 surface 为其他每条消息计价,Web 请求提示词卡片、轨迹视图和快照归一化器的 `{{system}}` 占位符各自单独读取 header。变更检测同样被拆开:在 `config` 和 `tools` 旁边逐字节比较 `system` 的 `headerEquals`,让提示词变更与工具变更在日志中无法区分(`request/header` 的 reason 都是 `change`),尽管它们是对对话的两种不同操作。
+
+这种拆分还阻塞了下一步。一个把对话中途的 `system` 消息当作提示词替换来接受的模型,需要 harness 向历史追加一条 system 角色消息;当提示词住在 header 里时,没有可追加的 surface 表示,header 也只能靠特例被冻结。[历史内替换决定](../feature/2026-09-02-in-history-system-prompt-replacement.zh.md) 依赖本 Agent Note。
+
+## Decision
+
+系统提示词住在 surface 上。它是一个普通的 surface 事件 `system/message`,提示词生命周期中的每个操作都是对该事件类型施加现有两种 `SurfaceOp` 变体之一。协议请求不变:surface 折叠产出的就是序列化器发送的消息列表,系统消息在最前面。
+
+### 事件
+
+`system/message` 是 `SurfaceEventType` 的成员,与 `user/message`、`assistant/message`、`tool/result` 并列(`packages/core/session/src/types.ts`)。它的载荷与 `tool/result` 对称:`{ turn, step, message }`,其中 `message` 是 `role: 'system'` 的 `SystemMessage`,一个文本块承载渲染后的提示词,source 为 `{ kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' }`。空的 `content` 记录「没有系统提示词」:该节点保持其 surface 位置,`deriveEventMessage` 把它投影为 `null`,因此不贡献任何协议消息。非空节点逐字投影,因此 `deriveMessages()` 在其 surface 位置返回系统消息,而原样透传 `role: 'system'` 历史消息的 DeepSeek 序列化器把它作为协议消息 0 发出。`EpochHeader` 是 `{ config, adapterDefaults?, tools? }`;`packages/core/session/src/request-header.ts` 中的 `canonicalHeader` 与 `headerEquals` 只比较 config、适配器默认值和工具。
+
+### 操作
+
+| 情形 | surface 操作 |
+|---|---|
+| surface 上没有存活的 `system/message`(包括渲染后的提示词为空时) | 追加 `system/message`;在会话的首个步骤中它是 surface 第 0 号节点,位于该步骤首条 `user/message` 之前 |
+| 有存活的 `system/message` 且渲染后的提示词与其文本不同(包括提示词变为空) | 恰好替换该节点:`surfaceOp: { op: 'replace', startSeq: <该节点的 seq>, endSeq: <同一值> }`,`sourceEventSeqs: [<该节点的 seq>]`;空提示词产生一个投影为无消息的空内容节点 |
+| 渲染后的提示词与存活节点的文本相同 | 无操作 |
+
+当初始渲染的提示词为空时,循环在初始接纳的用户消息之前预留空系统头部,使稍后首次变为非空的提示词仍替换第 0 号节点。省略该空节点会让后来的提示词追加在用户历史之后,pi-ai 会将其转换为用户消息,而不是 `systemPrompt`。替换第 0 号节点是头部重写在 surface 上的表达:提供方前缀从第一个 token 起改变,日志通过 `sourceEventSeqs` 记录被遮蔽的节点,`replaceGeneration` 与压缩替换时一样推进。因此循环的 `startsSeries` 检测(`requestSurfaceGeneration !== surfaceGeneration`)无需在 `headerEquals` 中比较 `system` 即可覆盖提示词变更。`request/header` 保留 `initial`、`resume`、`change`、`series` 四种 reason;`change` 表示 config 或 tools 变更,提示词替换之后跟随的未变 header 记为 `series`。
+
+`packages/core/session/src/surface.ts` 在 `assertSystemHeadRewrite` 中强制头部不变量:当第 0 号节点是 `system/message` 时,范围覆盖第 0 号节点的替换会被拒绝,除非替换事件本身是恰好覆盖该节点的 `system/message`。位于更后位置的系统节点没有此类保护;压缩范围可以遮蔽它们。
+
+### 循环中的归属
+
+`dsh-agent-loop` 在 `packages/core/agent-loop/src/runtime-context.ts` 中与 `RuntimeContextProjection` 并列拥有 `SystemPromptProjection`。它在每次投影时从当前 surface 读取存活的 `system/message` 节点,因此同一步骤中更早运行的压缩或替换已经反映在内。`project(rendered, { inHistory, startsSeries })` 返回 `{ message, intent }`——没有系统节点存活或[历史内规则](../feature/2026-09-02-in-history-system-prompt-replacement.zh.md)适用时 `intent` 为 `{ surfaceOp: 'append' }`,否则是对最新存活系统节点的精确替换——最新节点已持有渲染文本时返回 `undefined`。
+
+在 `packages/core/agent-loop/src/agent.ts` 中,`preStep` 用 `renderPrompt(assembly)` 渲染提示词,并在 `agent/pre-step` waterfall 之后投影它,因此压缩提供者在该 waterfall 内做出的替换对决定可见;`turn()` 紧接在 `step/start` 之后、该步骤的 `user/message` 事件之前提交 `system/message`,因此日志顺序即协议顺序。`buildRequest` 不在请求上设置 `system`:请求由 `header.config`、`session.deriveMessages()`(系统消息在先)和 `header.tools` 构成。循环步骤顺序为:领取收件箱 → `systemPrompt.assemble()` → 投影运行时上下文 → `agent/pre-step` waterfall → 投影系统提示词 → `step/start` → 提交 `system/message`(有变化时) → 提交各条 `user/message` → `agent/request` waterfall → `request/header` → `request/context` → 流式请求。`dsh-agent-loop/invariant` 伴随组件(`packages/core/agent-loop/src/invariant.ts`)断言循环构建的请求满足 `system === undefined` 且 `messages` 等于 `deriveMessages()`。
+
+`dsh-token-meter` 把用量锚定到成功的 `assistant/message` 之前的已计价 surface,而不是 `step/start`。循环在步骤开始之后接纳系统提示词与用户消息,重试恢复还可能在重建请求之前替换节点。捕获当前 surface 会让每个已接纳输入恰好计入一次;内嵌的提供方输出仍单独计价,因此持久 assistant 改写保留其带符号增量。开放步骤只保存 turn 与 step 以验证生命周期,不保存第二份节点快照。
+
+### 消费方
+
+| 消费方 | 读取内容 |
+|---|---|
+| DeepSeek 序列化器(`serializeRequest`、`serializeRequestWithImages`) | `options.messages`,把 `role: 'system'` 的历史消息作为协议消息 0 透传;`GenerateOptions.system` 为标题提供方等直接单次调用方保留 |
+| `dsh-llm-pi-ai` | 开头的 system 历史消息映射为 pi-ai 的 `systemPrompt` |
+| `compaction-basic` 的 `buildSummarizationInput` | 第 0 号节点的派生消息前置于 `SummarizationInput.messages` 中的区域消息,无单独的 `system` 字段;空内容头节点不投影为消息,但仍受保护而不能被压缩 |
+| `compaction-basic` 的 `selectCompactableRange` | 锚定在首个非系统节点;第 0 号节点永不落入压缩范围 |
+| `dsh-token-meter` | 系统节点作为 surface 节点计价,归入 `systemTokens` 明细 |
+| Web 请求提示词卡片、轨迹请求节点、请求检视 | `system/message` 节点;被替换的第 0 号节点显示为提示词变更,追加的历史内节点显示为提示词更新,各自以折叠可检视的卡片呈现,永不作为聊天气泡 |
+| 快照归一化器的 `{{system}}` 占位符、plan-mode 测试 | 系统节点的文本 |
+| TypeScript 与 Python SDK 预期输出 | 包含 `system/message` 事件 |
+| 人类 transcript(文本记录)投影 | 跳过 `system/message`;它是模型历史,不是对话 |
+
+`RuntimeContextProjection` 与 `SystemPromptProjection` 都把一条未提交的消息交给循环由 `turn()` 提交。两者在观察 surface 的方式与操作集上不同:运行时上下文跟随 `session/event` 观察自己拥有的 user 角色快照且只做追加,而系统提示词在每次投影时扫描当前 surface 上的系统节点,因为它的决定取决于有多少节点存活,并按路由追加或替换。
+
+### V2-to-V3 结构转换
+
+[V2 到 V3 规范](../../../../packages/session/session-format-v2-to-v3/README.zh.md#system-head)负责系统头节点转换与消息身份;其[引用规则](../../../../packages/session/session-format-v2-to-v3/README.zh.md#sequence-references)和[源拒绝](../../../../packages/session/session-format-v2-to-v3/README.zh.md#source-audit)定义保留内容与不支持的输入。迁移布局与原生请求语义等价,而非与原生录制逐字节相同。有效 V2 源在当前步骤不变量下可能没有保持顺序的转换方式;拒绝它优于移动历史或放宽归属。历史接收坐标不得变为对转换后日志的确认。
+
+[已发布格式策略](2026-08-31-released-session-format-migrations.zh.md)保持 V0、V1、V2 代际字节冻结,并且只发布 V3 后继代际。V3 是一个尚未发布的目标,而不是每个功能一个新版本;它在发布前可以演化,因此集成必须使用可丢弃的 home。已有 V3 代际不会重跑 V2-to-V3。投影缓存版本 4 独立于 Session 格式,并不意味着 Session V4。
+
+[规范信封规范](../../../../packages/session/session-format-v2-to-v3/README.zh.md#canonical-envelopes)定义与结构转换的组合;[规范信封决策](2026-09-06-v3-canonical-session-envelopes.zh.md)负责严格准入的依据。
+
+## Alternatives considered
+
+**保留 `header.system`,只为更新添加 `system/message`。** 一个事实两个归属:上述每个消费方都要从 header 读消息 0、从 surface 读后续消息,循环还需要一个在 surface 存在系统节点时让 `headerEquals` 忽略 `system` 的特例。被否决,因为本次变更的目的就是单一表示。
+
+**用专门的仅记日志事件 `system-prompt/change` 重写 header。** 保留 header 作为提示词归属,并把变更记录为独立事件种类,但仍无法表达历史内部的系统消息,历史内替换提案还是需要第二套机制。被否决。
+
+**在适配器内根据相邻 header 合成系统消息。** 适配器逐请求无状态且从不接触日志;依赖适配器状态的协议历史无法从 surface 折叠重建。被否决。
+
+**像运行时上下文那样用 `user/message` 快照表达提示词。** 复用了现有事件类型,却发送了错误的角色,因此把系统消息视为权威的模型不会这样对待它。被否决。
+
+## Consequences
+
+- 单一表示:每个想知道「模型看到了什么」的读取方都折叠 surface;没有消费方需要把 header 与消息列表合并。`EpochHeader` 没有 `system` 字段,因此期望该字段的读取方在编译期失败。
+- 提示词变更与工具或 config 变更在日志中可以区分:前者是对第 0 号节点的 `system/message` 替换加随后的 `series` header,后者是 reason 为 `change` 的 `request/header`。
+- 压缩带有一条不变量:第 0 号节点永不被压缩。`dsh-session` 的 surface 管理器在替换操作本身中强制它,因此除 `compaction-basic` 以外的压缩提供方无法通过锚定在 `surfaceNodes[0]` 来遮蔽提示词。更后位置的系统节点按设计不受保护。
+- `replaceGeneration` 在提示词替换时和压缩时一样推进;需要区分两者的读取方检查替换事件的类型。
+- 历史中途的系统节点拥有 surface 表示,这正是[历史内替换决定](../feature/2026-09-02-in-history-system-prompt-replacement.zh.md)所依赖的基础。
+- 初始空提示词占据受保护的头部,但不贡献协议消息;在替换模式下,后来的非空提示词替换它,并保持为开头的系统消息。
+- 录制的快照 fixture 携带 `system/message` 事件而非 header 的 `system` 字段。快照归一化器把该事件的文本标记化为 `{{system}}`,提示词伴随文件从 `system/message` 序列采集(每个提示词版本一节,以 `header.promptChanges` 声明),`request/header` 的 pin 只比较 config 与 tools。
+
+## Testing
+
+- `packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts` 钉住提供方用量下调用后的表面增量为零,覆盖初始、增长、缩短与空提示词、同一步骤中的重试替换、请求中间件和全新回放。
+- `packages/core/session/tests/surface.spec.ts`(`system/message surface node` 块)钉住开头 system 角色的投影、空内容的 `null` 投影、`assertSystemHeadRewrite` 的接受与拒绝路径、更后位置系统节点不受保护,以及对 seed 中非 system 角色或非插件 source 的 `system/message` 的拒绝。
+- `packages/core/agent-loop/tests/system-prompt-projection.spec.ts` 钉住首次渲染时的追加(包括空提示词)、替换模式下后来非空提示词位于派生历史头部、提示词未变时的无操作、变更时对最新存活节点的替换、替换遮蔽了非头部系统节点之后的尾部追加,以及历史内追加与重新基线规则。
+- `packages/core/agent-loop/tests/request-reconstruction.spec.ts`(`a system-prompt change replaces surface node 0 and starts a new series under the same header`)钉住提示词替换之后跟随的 `series` header。
+- `packages/core/agent-loop/tests/invariant.spec.ts` 钉住伴随组件对携带 `system` 字段的循环请求的拒绝,以及其 `messages` 与边界派生结果的相等性检查。
+- `packages/llm/llm-deepseek/tests/serialize.spec.ts`(`serializes a leading system message byte-for-byte like the same prompt passed as options.system`)钉住协议一致性。 `packages/llm/llm-pi-ai/tests/context.spec.ts` 在文本与图片路径上比较两种系统提示词来源。`packages/compaction/compaction-basic/tests/compaction-basic.spec.ts` 通过区域事务与默认摘要器钉住派生前缀、已路由工具、不携带单独 `system` 选项,以及非空或空头节点的保护。
+- `snapshots/` 下的录制快照钉住每个随发 profile 的模型可见协议请求;渲染了提示词的录制会话在其 `session.jsonl` 中于 surface 第 0 号节点携带 `system/message` 事件,会话中途发生提示词变更的会话则携带对第 0 号节点的替换,或在历史内路由上携带追加的节点。

+ 3 - 3
.agents/notes/implemented/process/2026-09-08-trusted-changed-file-review-routing.i18n.yaml → .agents/notes/implemented/architecture/2026-09-06-v3-canonical-session-envelopes.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/process/2026-09-08-trusted-changed-file-review-routing.md
-2026-09-08-trusted-changed-file-review-routing.md: 246c223d976e312e5712d7baa150bd3e84f1363e
-2026-09-08-trusted-changed-file-review-routing.zh.md: 397d4174340fc9c6002419b21436b5c33542f7e4
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-06-v3-canonical-session-envelopes.md
+2026-09-06-v3-canonical-session-envelopes.md: 17ec331cc1ea21f31d0b51eab86d22725b6deef8
+2026-09-06-v3-canonical-session-envelopes.zh.md: a18bb3b4d71c164e3fba7288cc686a1cb7c7b4df

+ 47 - 0
.agents/notes/implemented/architecture/2026-09-06-v3-canonical-session-envelopes.md

@@ -0,0 +1,47 @@
+# Agent Note: Canonical V3 Session event envelopes
+
+Status: implemented
+
+English | [中文](2026-09-06-v3-canonical-session-envelopes.zh.md)
+
+## Problem
+
+A Session event can cross in-memory, durable, and browser-wire readers. If its type permits missing placement or unrelated surface metadata, a reader can silently omit a message or disagree about which fields affect reconstruction. Multiple spellings for replacement endpoints and empty request-header optionals also allow different stored records to describe the same request. Contradictory tool failure metadata can make model history and diagnostics report different outcomes.
+
+## Decision
+
+Session format V3 uses one canonical event envelope. Every `system/message`, `user/message`, `assistant/message`, and `tool/result` requires `surfaceOp`. Known log-only events permit only `type`, `seq`, `time`, `data`, and optional `ignorable: true`; their TypeScript variants declare both surface metadata fields as optional `never`. Native unknown or obsolete ignorable envelopes remain opaque, including their metadata. Assistant messages embed their exact provider stream and alone forbid `sourceEventSeqs`. System, user, and tool messages may cite a non-empty, unique set of earlier source sequences.
+
+`SurfaceOp` is exactly `'append'` or `{ op: 'replace', startSeq, endSeq }`, with `SessionSeq` endpoints and no aliases or extra keys. Both endpoints precede the replacing event and identify an inclusive span in current surface order, not numeric sequence order. Session acceptance additionally verifies current membership, ordered endpoints, complete cited coverage, and content-only single-node tool-result replacement. Compaction payload fields such as `shadowedRange.start/end` and fold-result fields retain their own names; this is not a recursive payload rename.
+
+Current acceptance rejects every `request/header.header.system` and exactly empty `tools: []` or `adapterDefaults: {}`. System prompts belong to `system/message`; `request/header` remains the non-history request snapshot. Writers omit the two empty optionals. Whitespace-only system content, `config.stop: []`, nested header/source/data extras, and nested tool schema values remain intact. A `tool/result` with `data.error` requires `message.content[0].isError === true`; a failed result need not carry error identity. Neither current reads nor migration infer an error outcome from contradictory metadata.
+
+### Validation ownership
+
+[Core Session](../../../../packages/core/session/src/surface.ts) owns event-local placement, header-empty-field, and tool-error rules, while its surface manager owns relationships that need the event log. Seed, append, and restoration apply these rules before accepting events. They do not create a general schema for plugin-owned payloads or eagerly expand embedded provider streams.
+
+The generic Gateway client returns raw outputs without validating them. The existing [SessionEventStream](../../../../packages/api/session-controller/src/client/transport.ts) therefore checks follow snapshots, live durable entries, and history pages before publishing them. Its private [wire-event checker](../../../../packages/api/session-controller/src/client/session-wire-event.ts) validates the exact envelope and delegates event-local rules to the browser-safe core validators. It does not add a generic Gateway schema or validate unrelated plugin payloads. Surface membership and source existence remain Host-owned because a browser window may omit earlier events.
+
+### Released V2 to V3 conversion
+
+The [V2-to-V3 specification](../../../../packages/session/session-format-v2-to-v3/README.md#v2-to-v3-specification) owns the complete historical conversion, its [canonicalization rules](../../../../packages/session/session-format-v2-to-v3/README.md#canonical-envelopes), and [native admission and recovery](../../../../packages/session/session-format-v2-to-v3/README.md#native-v3-admission). Keeping these rules together prevents a cardinality-preserving canonicalization step from being mistaken for an identity migration. Frozen relationship validation uses private views rather than runtime aliases; the original V3 artifact remains authoritative.
+
+## Alternatives considered
+
+**Default missing placement to append.** This invents a model-history decision absent from the stored record and admits invalid V2 artifacts. Required placement keeps all readers accountable to the same evidence.
+
+**Accept both replacement spellings in current readers.** This preserves two durable representations and makes validation depend on which reader receives them. Only the adjacent edge interprets released keys; current readers accept V3 keys exclusively.
+
+**Normalize all empty values or repair tool outcomes.** Empty stop lists, whitespace, and plugin payloads can be meaningful. Removing them or setting `isError` from diagnostics changes recorded facts. The edge performs only named, semantics-preserving conversions and refuses contradictions.
+
+**Copy historical validators or pass V3 events directly to them.** Copying duplicates relationship semantics; direct reuse would accept obsolete envelope spellings and misinterpret system nodes and repair identities. Strict V3 validation followed by composed private views reuses frozen relationships without widening current acceptance.
+
+## Consequences
+
+Typed events, persistence, and browser history agree on required placement and event-local failure semantics. Malformed records fail before projection rather than disappearing from model history. Migration gives up best-effort recovery of contradictory records; retained source generations remain untouched under the [released-format publication policy](2026-08-31-released-session-format-migrations.md).
+
+This decision partially supersedes envelope representation details in the [session surface](2026-06-18-session-surface.md) and [reconstructable requests](2026-07-05-reconstructable-requests.md) notes. They remain active for ordered projection and logged request ownership. The [system-prompt surface-node decision](2026-09-02-system-prompt-as-surface-node.md) retains prompt ownership, protected-head semantics, and migration rationale. The [V2 embedded-stream decision](2026-09-01-v2-embedded-assistant-streams.md) remains active for attempt settlement, exact stream evidence, and cardinality-changing migration; V3 preserves those decisions.
+
+## Verification
+
+[Core acceptance tests](../../../../packages/core/session/tests/canonical-envelopes.spec.ts) pin invalid seed/append/restore records, typed surface variants, optional failure identity, and unchanged derived state after rejection. [Browser transport tests](../../../../packages/api/session-controller/tests/transport.client.spec.ts) exercise strict follow/page admission before publication. [Migration tests](../../../../packages/session/session-format-v2-to-v3/tests/canonical-envelopes.spec.ts) cover conversion and restoration; frozen adjacent-edge suites preserve historical semantics. Required coverage also includes codec admission, whitespace and empty stop-list preservation, opaque payload retention, and numerically descending replacement endpoints in valid surface order.

+ 47 - 0
.agents/notes/implemented/architecture/2026-09-06-v3-canonical-session-envelopes.zh.md

@@ -0,0 +1,47 @@
+# Agent Note: 规范的 V3 Session 事件信封
+
+Status: implemented
+
+[English](2026-09-06-v3-canonical-session-envelopes.md) | 中文
+
+## 问题
+
+一个 Session 事件会经过内存、持久化与浏览器协议读取器。如果其类型允许缺少位置声明或携带无关 surface 元数据,读取器就可能静默遗漏消息,或对哪些字段影响重建产生分歧。替换端点的多种拼写与空请求头可选字段,也使不同存储记录能够描述同一请求。相互矛盾的工具失败元数据会让模型历史与诊断报告不同结果。
+
+## 决策
+
+Session 格式 V3 使用一种规范事件信封。每个 `system/message`、`user/message`、`assistant/message` 与 `tool/result` 都要求 `surfaceOp`。已知仅日志事件仅允许 `type`、`seq`、`time`、`data` 与可选的 `ignorable: true`;其 TypeScript 变体将两个 surface 元数据字段声明为可选 `never`。原生未知或已退役的可忽略信封(包括其元数据)保持不透明。assistant 消息嵌入精确提供方 stream,且只有此类消息禁止 `sourceEventSeqs`。system、user 与 tool 消息可以引用非空、唯一的较早来源序号集合。
+
+`SurfaceOp` 恰好为 `'append'` 或 `{ op: 'replace', startSeq, endSeq }`,端点使用 `SessionSeq`,不接受别名或额外键。两个端点都早于替换事件,并按当前 surface 顺序而非数值序号顺序标识闭区间。Session 接纳还验证当前成员关系、端点顺序、完整引用覆盖与仅修改内容的单节点工具结果替换。`shadowedRange.start/end` 等压缩(compaction)载荷字段与折叠结果字段保留各自名称;这不是对载荷进行递归重命名。
+
+当前接纳拒绝任何 `request/header.header.system` 以及恰好为空的 `tools: []` 或 `adapterDefaults: {}`。系统提示词属于 `system/message`;`request/header` 仍是请求非历史状态的快照。写入方省略两个空可选字段。仅含空白的系统内容、`config.stop: []`、嵌套 header/source/data 扩展与嵌套工具 schema 值保持原样。带有 `data.error` 的 `tool/result` 要求 `message.content[0].isError === true`;失败结果不必携带错误身份。当前读取与迁移均不会根据矛盾元数据推断错误结果。
+
+### 校验所有权
+
+[核心 Session](../../../../packages/core/session/src/surface.ts)负责事件本地的位置、请求头空字段与工具错误规则,其 surface 管理器负责需要事件日志的关系。seed、append 与恢复会在接纳事件前应用这些规则。它们不会为插件自有载荷创建通用 schema,也不会提前展开嵌入式提供方 stream。
+
+通用 Gateway 客户端返回未经校验的原始输出。因此,现有 [SessionEventStream](../../../../packages/api/session-controller/src/client/transport.ts) 会在发布前检查 follow 快照、实时持久条目与历史页。其私有[协议事件检查器](../../../../packages/api/session-controller/src/client/session-wire-event.ts)验证精确信封,并将事件本地规则委托给可在浏览器中使用的核心校验器。它不添加通用 Gateway schema,也不校验无关插件载荷。surface 成员关系与来源是否存在仍由 Host 负责,因为浏览器窗口可能未包含较早事件。
+
+### 已发布 V2 到 V3 的转换
+
+[V2 到 V3 规范](../../../../packages/session/session-format-v2-to-v3/README.zh.md#v2-to-v3-specification)负责完整历史转换、[规范化规则](../../../../packages/session/session-format-v2-to-v3/README.zh.md#canonical-envelopes)及[原生准入与恢复](../../../../packages/session/session-format-v2-to-v3/README.zh.md#native-v3-admission)。将这些规则集中在一起,可以避免把保持事件数量的规范化步骤误认为恒等迁移。冻结的关系校验使用私有视图而非运行时别名;原始 V3 产物仍具权威性。
+
+## 曾考虑的替代方案
+
+**将缺失的位置默认为 append。** 这会凭空添加存储记录中不存在的模型历史决策,并接纳无效 V2 产物。要求位置声明,使所有读取器必须依据同一证据。
+
+**当前读取器接受两种替换拼写。** 这会保留两种持久表示,并使校验取决于接收记录的读取器。只有相邻迁移边解释已发布的键;当前读取器只接受 V3 键。
+
+**规范化所有空值或修复工具结果。** 空 stop 列表、空白与插件载荷可能有意义。删除它们或根据诊断设置 `isError` 会改变已记录事实。迁移边只执行具名且保持语义的转换,并拒绝矛盾。
+
+**复制历史校验器或直接向其传入 V3 事件。** 复制会重复关系语义;直接复用则会接受旧信封拼写,并误解系统节点与修复身份。严格的 V3 校验加组合后的私有视图,可以在不扩大当前接纳范围的前提下复用冻结关系。
+
+## 后果
+
+类型化事件、持久化与浏览器历史对必填位置和事件本地失败语义保持一致。畸形记录在投影前失败,而不会从模型历史中消失。迁移放弃对矛盾记录的尽力恢复;[已发布格式的发布策略](2026-08-31-released-session-format-migrations.zh.md)保证保留的源代次不被修改。
+
+本决策部分取代[会话 surface](2026-06-18-session-surface.zh.md)与[可重建请求](2026-07-05-reconstructable-requests.zh.md)说明中的信封表示细节。它们继续负责有序投影与已记录请求的所有权。[系统提示词 surface 节点决策](2026-09-02-system-prompt-as-surface-node.zh.md)保留提示所有权、受保护头节点语义与迁移依据。[V2 嵌入式 stream 决策](2026-09-01-v2-embedded-assistant-streams.zh.md)继续负责尝试结算、精确 stream 证据与改变事件数量的迁移;V3 保留这些决策。
+
+## 验证
+
+[核心接纳测试](../../../../packages/core/session/tests/canonical-envelopes.spec.ts)固定无效 seed/append/restore 记录、类型化 surface 变体、可选失败身份与拒绝后派生状态不变。[浏览器传输测试](../../../../packages/api/session-controller/tests/transport.client.spec.ts)检验发布前的严格 follow/page 接纳。[迁移测试](../../../../packages/session/session-format-v2-to-v3/tests/canonical-envelopes.spec.ts)覆盖转换与恢复;冻结的相邻迁移边测试保留历史语义。必需覆盖还包括编解码器接纳、空白与空 stop 列表保留、不透明载荷保留,以及按合法 surface 顺序排列但数值递减的替换端点。

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-07-prebuilt-system-primitives.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-09-07-prebuilt-system-primitives.md
+2026-09-07-prebuilt-system-primitives.md: 183cd3ea984cd779417493f0de17a770f38fc001
+2026-09-07-prebuilt-system-primitives.zh.md: f051bba92910086601aa48a877f360e276df524d

+ 33 - 0
.agents/notes/implemented/architecture/2026-09-07-prebuilt-system-primitives.md

@@ -0,0 +1,33 @@
+# Agent Note: Prebuilt system primitives
+
+Status: implemented
+
+English | [中文](2026-09-07-prebuilt-system-primitives.zh.md)
+
+## Problem
+
+The JSONL writer's `fs-ext` dependency compiled a NAN addon during consumer installation. Native compiler availability and Node module ABI changes therefore affected ordinary installs, including Node 26. The repository already maintained the Landlock launcher and its per-platform publication workflow.
+
+## Decision
+
+The independently versioned `@deepseek-ai/node-addon-system` family in [native/system](../../../../native/system/README.md) distributes the existing `landlock-run` executable and a stable Node-API v8 `system.node` addon. Platform packages select OS and CPU; Linux carries distinct glibc and musl addon files. macOS carries the addon without a Landlock executable. Neither the entry nor platform packages compile during installation.
+
+The package has no root export. The `./landlock-run` JavaScript entry retains Landlock's API and [CLI protocol](../../../../native/system/docs/cli-contract.md). The `./flock` entry loads its addon only when `tryLockExclusive(fd)` is called. It runs `flock(fd, LOCK_EX | LOCK_NB)` in asynchronous native work and captures errno on that worker. The caller owns the descriptor through completion and releases its lock by closing it. Missing bindings reject acquisition rather than granting an unprotected lock.
+
+The [Session write-lease decision](../feature/2026-08-31-cross-process-session-write-lease.md) continues to own acquisition timing, inode checks, close ownership, and crash semantics. Windows retains its existing koffi semaphore. The browser worker substitutes only the flock subpath; it uses the unchanged `./landlock-run` JavaScript API.
+
+Source builds explicitly compile the host addon before repository tests and builds that need it. Native CI builds the complete platform payload and tests the same addon bytes across Node releases; Linux also exercises the musl payload in Alpine. Platform prepack rejects malformed or incomplete binaries, and an offline npm install rehearsal checks installed bytes and real lock behavior. Native [tests](../../../../native/system/test/flock.test.js) cover descriptor/process contention, close and crash release, independent errno values, and worker teardown.
+
+## Alternatives considered
+
+**Keep NAN and publish one build per Node ABI.** This retains a Node-major build matrix for a binding that needs only stable Node-API operations. The evaluated `fs-ext-extra-prebuilt@2.2.14` selected a Node 25 ABI 141 binary under Node 26 ABI 147; its default-install fallback also exited without building when NAN was hoisted.
+
+**Bundle fs-ext into the parent tarball.** npm normally still runs bundled dependency installation hooks. Bundling alone neither suppresses compilation nor makes one binary portable across operating systems, CPUs, libc implementations, or Node ABIs.
+
+**Replace flock with OFD/fcntl locks.** On ordinary Linux filesystems these locks do not necessarily exclude existing flock holders. A tmpfs probe admitted an OFD lock while fs-ext held flock, so this is not a behavior-preserving replacement.
+
+**Use koffi for the POSIX call.** A synchronous call changes event-loop blocking behavior; reading errno after its asynchronous callback reads the wrong thread's value. A native async-work adapter keeps the syscall result and errno together without another FFI coordination layer.
+
+## Consequences
+
+The family owns a small C binding, platform builds, and installed-artifact verification rather than an entire filesystem-extension API. Node-API removes the per-Node-major binary requirement, not OS/CPU/libc requirements. The shared native release includes both capabilities, but importing or using one does not load the other. Landlock binary semantics, Windows locking, and released Session data formats remain unchanged.

+ 33 - 0
.agents/notes/implemented/architecture/2026-09-07-prebuilt-system-primitives.zh.md

@@ -0,0 +1,33 @@
+# Agent Note: 预编译系统原语
+
+Status: implemented
+
+[English](2026-09-07-prebuilt-system-primitives.md) | 中文
+
+## Problem
+
+JSONL 写入方依赖的 `fs-ext` 在用户安装时编译 NAN addon。因此,原生编译器是否可用以及 Node 模块 ABI 的变化会影响普通安装,包括 Node 26。仓库已经维护了 Landlock 启动器及其按平台发布的工作流。
+
+## Decision
+
+[native/system](../../../../native/system/README.zh.md) 中独立版本的 `@deepseek-ai/node-addon-system` 包族分发既有 `landlock-run` 可执行文件和使用稳定 Node-API v8 的 `system.node` addon。平台包按操作系统和 CPU 选择;Linux 分别携带 glibc 与 musl addon 文件。macOS 携带 addon,但不包含 Landlock 可执行文件。入口包和平台包都不在安装期间编译。
+
+包不提供根导出。`./landlock-run` JavaScript 入口保留 Landlock API 和 [CLI 协议](../../../../native/system/docs/cli-contract.md)。`./flock` 入口仅在调用 `tryLockExclusive(fd)` 时加载 addon。它在异步原生工作中执行 `flock(fd, LOCK_EX | LOCK_NB)`,并在该工作线程保存 errno。调用方在完成前持有描述符,并通过关闭它释放锁。绑定缺失时拒绝获取锁,不授予没有保护的锁。
+
+[Session 写租约决策](../feature/2026-08-31-cross-process-session-write-lease.zh.md) 继续负责获取时机、inode 校验、关闭所有权和崩溃语义。Windows 保留既有 koffi 信号量。浏览器 worker 仅替换 flock 子路径,使用未经修改的 `./landlock-run` JavaScript API。
+
+源码构建在需要 addon 的仓库测试与构建之前显式编译当前宿主 addon。Native CI 构建完整平台产物,并让相同 addon 字节跨 Node 版本测试;Linux 还在 Alpine 中执行 musl 产物。平台 prepack 拒绝格式错误或不完整的二进制,离线 npm 安装演练检查安装字节与真实锁行为。Native [测试](../../../../native/system/test/flock.test.js) 覆盖描述符与进程竞争、关闭和崩溃释放、独立 errno 值及 worker 清理。
+
+## Alternatives considered
+
+**保留 NAN,为每个 Node ABI 发布构建。** 这会为仅需稳定 Node-API 操作的绑定保留 Node 主版本构建矩阵。已评估的 `fs-ext-extra-prebuilt@2.2.14` 在 Node 26 ABI147 下选中 Node 25 ABI141 二进制;默认安装回退还会在 NAN 被提升安装时提前退出而不编译。
+
+**将 fs-ext 打入父包 tarball。** npm 默认仍执行 bundled 依赖的安装钩子。仅打包既不能禁止编译,也不能让一个二进制跨操作系统、CPU、libc 实现或 Node ABI 通用。
+
+**将 flock 换成 OFD/fcntl 锁。** 在普通 Linux 文件系统上,这些锁不一定排斥既有 flock 持有者。tmpfs 探针在 fs-ext 持有 flock 时仍取得 OFD 锁,因此这不是保持行为的替换。
+
+**通过 koffi 执行 POSIX 调用。** 同步调用改变事件循环的阻塞行为;在异步回调后读取 errno 会读到错误线程的值。原生 async-work 适配器把系统调用结果和 errno 保存在一起,无须另加 FFI 协调层。
+
+## Consequences
+
+包族维护小型 C 绑定、平台构建和安装产物验证,而不是整套文件系统扩展 API。Node-API 消除按 Node 主版本分发二进制的要求,但不消除操作系统、CPU 和 libc 要求。统一原生发布包含两项能力,但导入或使用其中一项不会加载另一项。Landlock 二进制语义、Windows 锁和已发布 Session 数据格式保持不变。

+ 6 - 0
.agents/notes/implemented/bug-fix/2026-09-07-typert-package-local-forwarding-imports.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-09-07-typert-package-local-forwarding-imports.md
+2026-09-07-typert-package-local-forwarding-imports.md: f50dc7bfc8c9d83c2b6f2b584e1d1119b8df817b
+2026-09-07-typert-package-local-forwarding-imports.zh.md: 7e012d220df3e7356b35a105784f89e4df148802

+ 29 - 0
.agents/notes/implemented/bug-fix/2026-09-07-typert-package-local-forwarding-imports.md

@@ -0,0 +1,29 @@
+# Agent Note: Follow package-local forwarding modules in Typert references
+
+Status: implemented
+
+English | [中文](2026-09-07-typert-package-local-forwarding-imports.zh.md)
+
+## Problem
+
+`WorkspaceAnalyzer` resolves every type reference to its original declaration before classifying it, then reads only the referencing file's own `import` statement to decide whether the reference crossed a package through a public export. A package that re-exports another package's type from one of its own modules, and imports that module by relative path elsewhere, therefore fails with `crosses a package without an explicit package import` although the package import exists one hop away. The failure is deterministic for every batch size and package order; it surfaces in whichever analysis selects the referencing package as a root, which is why [issue 3525](https://github.com/deepseek-harness/deepseek-harness/issues/3525) observed it as batch-dependent.
+
+## Decision
+
+[`targetForReference`](../../../../packages/typert/generator/src/analyzer.ts) resolves a relative specifier through the face's shared compiler host and module-resolution cache and follows it only while the resolved file stays inside the referencing package. In each forwarding module it collects the `export` edges that carry the requested name: a named re-export with a specifier, an `export { local }` backed by that module's `import`, and star re-exports whose module exports the same symbol. Explicit edges are tried before star edges, matching TypeScript's shadowing of star exports, and each resolved module and requested export-name pair is entered once, so circular star re-exports terminate while distinct renamed routes through one module remain available. The walk stops at the first package specifier and feeds that identity and export name to the existing `packageExportName` check, so a forwarded type must still be public at the package subpath the forwarding module names, and a package name without a registration is refused there. The reference model is unchanged: the target remains `declaration` for a same-face owner and `cross-face` for another face.
+
+The walk yields no package import, and the reference fails as before, when a relative specifier resolves outside the referencing package, when the only edge carrying the name is a namespace re-export or a re-exported namespace import, or when every edge loops back to a module and requested-name pair already entered.
+
+## Alternatives considered
+
+**Treat a relative import whose alias chain ends in another package as implicitly public.** Rejected: it would accept `../../other/src/file.ts` and any forwarding module that itself reaches the other package by relative path, removing the public-export check the generated Remote declarations rely on to name an importable subpath.
+
+**Record the forwarding module as the reference target.** Rejected: emitters and cross-face links need the original declaration's package and public subpath; a package-local module has no public identity of its own.
+
+**Select edges in source order without symbol checks.** Rejected: a star re-export that loops back to an earlier module can precede the explicit re-export that actually carries the type, and TypeScript itself lets explicit exports shadow star exports; ordering explicit edges first and continuing past an entered module and requested-name pair keeps such modules accepted without an unbounded walk.
+
+**Make batched and whole-workspace analysis select the same roots.** Rejected as a fix: root selection does not change the verdict on a reference, only whether the reference is visited, so aligning the callers would hide the incorrect classification rather than remove it.
+
+## Consequences
+
+Packages may keep one forwarding module for foreign types and import it relatively, matching how their own modules are organized. Each cross-package relative reference costs one module resolution per hop through the face's shared resolution cache; `reachableFiles` now resolves through the same cache. [`type-model.spec.ts`](../../../../packages/typert/generator/tests/type-model.spec.ts) pins named, renamed multi-hop, import-then-export, star, and namespace-import forwarding, an explicit re-export beside a looping star edge, distinct renamed routes through one shared module, a forwarded private export, a forwarding module that crosses by relative path, a cycle whose only exit crosses by relative path, a namespace re-export, a re-exported namespace import, cross-face forwarding, and equality of whole and batched analysis for the forwarding fixture across batch sizes and package orders.

+ 29 - 0
.agents/notes/implemented/bug-fix/2026-09-07-typert-package-local-forwarding-imports.zh.md

@@ -0,0 +1,29 @@
+# Agent Note: Typert 引用追踪包内转发模块
+
+Status: implemented
+
+[English](2026-09-07-typert-package-local-forwarding-imports.md) | 中文
+
+## Problem
+
+`WorkspaceAnalyzer` 先把每个类型引用解析到原始声明再分类,然后只读引用所在文件自己的 `import` 语句来判断该引用是否经由公开导出跨包。一个包若在自己的某个模块里重新导出另一个包的类型,并在别处用相对路径导入该模块,就会报 `crosses a package without an explicit package import`,尽管包导入只隔一跳。这个失败在任何批次大小和包顺序下都会稳定出现;它出现在哪次分析里,取决于哪次分析把引用方的包选为根,因此 [issue 3525](https://github.com/deepseek-harness/deepseek-harness/issues/3525) 观察到的现象像是与批次相关。
+
+## Decision
+
+[`targetForReference`](../../../../packages/typert/generator/src/analyzer.ts) 通过该 face 共享的编译器宿主及其模块解析缓存来解析相对说明符,且只在解析到的文件仍位于引用方包内时继续追踪。在每个转发模块里,它收集承载所请求名字的 `export` 边:带说明符的具名重新导出、由该模块自身 `import` 支撑的 `export { local }`,以及导出同一符号的星号重新导出。显式边先于星号边尝试,与 TypeScript 中显式导出遮蔽星号导出的规则一致;解析后的模块与请求导出名组成的每个组合只进入一次,因此循环的星号重新导出能够终止,经同一模块转发的不同改名路径仍可继续尝试。追踪在遇到第一个包说明符时停止,并把该包身份和导出名交给现有的 `packageExportName` 检查,因此被转发的类型仍必须在转发模块所写的包子路径上公开,没有登记的包名也在此被拒绝。引用模型不变:同 face 的所有者仍是 `declaration`,另一 face 仍是 `cross-face`。
+
+当相对说明符解析到引用方包之外、承载该名字的唯一边是命名空间重新导出或被重新导出的命名空间导入,或所有边都回到已进入的模块与请求名组合时,追踪得不到包导入,引用照旧失败。
+
+## Alternatives considered
+
+**把别名链终点在另一个包的相对导入视为隐式公开。** 已拒绝:这会接受 `../../other/src/file.ts`,也会接受自身用相对路径抵达另一个包的转发模块,从而取消公开导出检查,而生成的 Remote 声明依赖该检查来命名可导入的子路径。
+
+**把转发模块记为引用目标。** 已拒绝:发射器和跨 face 链接需要原始声明的包和公开子路径,包内模块没有自己的公开身份。
+
+**按源码顺序选边且不校验符号。** 已拒绝:回到更早模块的星号重新导出可能排在真正承载该类型的显式重新导出之前,而 TypeScript 本身允许显式导出遮蔽星号导出;显式边优先并跳过已进入的模块与请求名组合,既能接受这类模块,又不会无限追踪。
+
+**让分批分析与全工作区分析选择相同的根。** 作为修复方案已拒绝:根的选择不改变对一个引用的判定,只决定该引用是否被访问,对齐调用方只会掩盖错误分类,不能消除它。
+
+## Consequences
+
+包可以为外部类型保留一个转发模块并用相对路径导入它,与自身模块的组织方式一致。每个跨包相对引用每跳付出一次经该 face 共享解析缓存的模块解析;`reachableFiles` 现在也通过同一缓存解析。[`type-model.spec.ts`](../../../../packages/typert/generator/tests/type-model.spec.ts) 固定了具名、改名多跳、先导入再导出、星号和命名空间导入这几种转发,与回环星号边并存的显式重新导出,经同一模块转发的不同改名路径,被转发的私有导出,用相对路径跨包的转发模块,唯一出口用相对路径跨包的循环,命名空间重新导出,被重新导出的命名空间导入,跨 face 转发,以及转发 fixture 在不同批次大小和包顺序下全量分析与分批分析相等。

+ 2 - 2
.agents/notes/implemented/feature/2026-06-15-ptc.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-15-ptc.md
-2026-06-15-ptc.md: 96dda525c677af638654cef042b583803d948707
-2026-06-15-ptc.zh.md: 1da45205c62eb054fd534e4f395e570244066c5a
+2026-06-15-ptc.md: 43bd4a4fd5c49a449ceccb7f1889b80b0844214d
+2026-06-15-ptc.zh.md: a6aaf203186ad3023ce39a9df04fc1b233db88eb

+ 8 - 4
.agents/notes/implemented/feature/2026-06-15-ptc.md

@@ -42,7 +42,7 @@ This note owns PTC mode's presentation, composition, isolation, and settlement f
 
 Under `'ptc'` and `'both'` the registry owns `run_code` as a reserved presentation transport with two required parameters, `{ code: string; description: string }` (the description labels the call in UIs, the bash precedent). It is represented by a normal `ToolDefinition` for dispatch but stays outside the filterable capability layers, so restrictions cannot accidentally remove PTC mode's only entry point. Calls traverse the complete tool pipeline — `tools/pre-execute` → monotonic guards → `tools/execute` around dispatch → `tools/post-execute` → optional definition-owned `finalizeContent` → immutable `tools/result` notification — exactly like native calls; a permission plugin can inspect the program text before it runs, and final-result observers see the normalized outer outcome. Its `execute(args, exec)`:
 
-1. **Build bindings.** One run-scoped signal follows outer cancellation and is aborted whenever the run settles. Each visible tool binding snapshots lossless-JSON arguments, enters the native-contract dispatch pool (the [live-parallel note](2026-07-26-ptc-live-parallel-dispatch.md) owns the scheduling design), executes with a deterministic call id and the outer token as `parent`, defers returned contexts through the outer execution, and logs the `tool/code-dispatch-start`/`tool/code-dispatch` pair, the settle side carrying the full rendered result content. Success returns the tool's final canonical JSON value; failure becomes the program-visible `ToolCallError`. Every sub-call retains its own immutable execution identity and traverses the full tool pipeline.
+1. **Build bindings.** One run-scoped signal follows outer cancellation and is aborted whenever the run settles. Each visible tool binding snapshots lossless-JSON arguments, enters the native-contract dispatch pool (the [live-parallel note](2026-07-26-ptc-live-parallel-dispatch.md) owns the scheduling design), executes with a deterministic call id and the outer token as `parent`, defers returned contexts through the outer execution, and logs the `tool/ptc-dispatch-start`/`tool/ptc-dispatch` pair, the settle side carrying the full rendered result content. Success returns the tool's final canonical JSON value; failure becomes the program-visible `ToolCallError`. Every sub-call retains its own immutable execution identity and traverses the full tool pipeline.
 2. **Runs the program**: `ctx.codeRuntime.run({ program: args.code, bindings: [{ global: 'tools', functions }], signal: runController.signal })`. The runtime receives the run-scoped signal, not only the caller's outer signal, so any way the outer run settles also aborts work inside the runtime.
 3. **Settle after quiescence.** When the runtime settles, the bridge aborts outstanding work and drains the dispatch queue before returning. Success returns captured logs and the completion value as canonical output; the registry renders that value into durable `tool/result.content`, which the result card reads directly. A runtime failure becomes `CodeRunFailedError`; backend rejection uses the registry's normal error boundary. Both produce structured error results, and no sub-call can append after `run_code` settles.
 
@@ -52,9 +52,13 @@ Under `'ptc'` and `'both'` the registry owns `run_code` as a reserved presentati
 
 **Presentation.** `run_code`'s render intent is decided here per the [render-intent Agent Note](../architecture/2026-07-02-tool-render-intent-union.md): `presentCall` creates a `generic` card with `kind: 'execute'`, the program text as its title, and the same program text as `rawInput`; `run_code` intentionally declares no `presentResult`, so the TUI and host/client runtime (Web) complete that card through their generic raw-content fallback using the final durable `tool/result.content`, including captured logs plus the returned value, failure, or post-policy spill preview. This is not a `terminal` card: that card's semantics are "a shell command in a working directory", which a program is not. See the [result-card completeness note](../../archived/bug-fix/2026-07-20-code-mode-result-card-completeness.md).
 
-### Observability: `tool/code-dispatch`
+### Observability: `tool/ptc-dispatch`
 
-Each sub-dispatch appends a log-only `tool/code-dispatch-start` event at pool entry and a `tool/code-dispatch` settle event containing parent and child call ids, tool identity, normalized arguments, and the complete rendered `content`/`isError` outcome. It remains outside model history but available to persistence and UIs. Appends occur inside the open `run_code` turn. Direct executions without an agent still run but cannot log the event.
+Each sub-dispatch appends a log-only `tool/ptc-dispatch-start` event at pool entry and a `tool/ptc-dispatch` settle event containing parent and child call ids, tool identity, normalized arguments, and the complete rendered `content`/`isError` outcome. It remains outside model history but available to persistence and UIs. Appends occur inside the open `run_code` turn. Direct executions without an agent still run but cannot log the event.
+
+New sub-calls use `<parent>:ptc:<n>` ids, numbered in submission order. All call ids are opaque to consumers: migration preserves every historical id byte-for-byte, including `:code:` substrings, so dispatch pairs, spill references, and other correlations remain intact. The bridge attributes forwarded image context to `{ kind: 'plugin', plugin: 'tools-ptc' }`.
+
+The [V2-to-V3 PTC specification](../../../../packages/session/session-format-v2-to-v3/README.md#ptc-vocabulary) owns exact historical tag and attribution conversion; [native V3 admission](../../../../packages/session/session-format-v2-to-v3/README.md#native-v3-admission) owns predecessor-tag refusal. These are not runtime aliases: an opaque extension must not acquire PTC lifecycle meaning merely through a version change.
 
 ### The code-runtime seam
 
@@ -98,7 +102,7 @@ Deployments switching to `'ptc'` must update any native-only `toolOrder`. Assemb
 - **Worker runtime:** Real-worker tests cover typed binding values and failures, every lossless JSON completion root, invalid and over-limit output, exact combined ledger boundaries, compute and wall budgets, hostile binding traffic, empty environment, and disposal to quiescence. A built-package test runs the worker entry under plain Node.
 - **Registry integration:** Tests cover code generation, all presentation modes, reserved-name and restriction rules, scoped visibility, authoritative assembly rewrites, `toolOrder`, runtime compatibility failures, full-pipeline sub-dispatch, parent-token correlation, serialization, cancellation and queue drain, JSON normalization, error propagation, log events, ordered context deferral across successful and failed programs, outer-block suppression, and HMR cleanup.
 - **With-key e2e:** A real model composes two bash calls in one program; another discovers nested workspace instructions through a PTC mode fs dispatch. The tests verify collapsed request headers, correlated dispatch events, resulting files, deferred context, and model behavior.
-- **Snapshot:** The `ptc-turn`, `both-mode-turn`, and `ptc-workspace-context` fixtures pin SDK text, header tool lists, dispatch events, deferred context, and result cards.
+- **Snapshot:** The `ptc-turn`, `both-mode-turn`, and `ptc-workspace-context` fixtures pin SDK text, header tool lists, dispatch events, deferred context, and result cards. The TypeScript SDK PTC scenario mounts its worker runtime through an explicit test-owned profile patch and pins Session events and JSON-RPC notifications; its expected response and completed-turn checks run before refresh writes.
 
 ## Alternatives considered
 

+ 8 - 4
.agents/notes/implemented/feature/2026-06-15-ptc.zh.md

@@ -42,7 +42,7 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一
 
 在 `'ptc'` 和 `'both'` 下,注册表拥有 `run_code` 作为保留的呈现传输通道,带两个必需参数 `{ code: string; description: string }`(description 为 UI 标注该调用,沿用 bash 的先例)。它由一个正常的 `ToolDefinition` 表示以供分发,但位于可过滤的能力层之外,因此限制规则不会意外移除 PTC mode 的唯一入口。调用遍历完整的工具流水线——`tools/pre-execute` → 单调性守卫 → `tools/execute` 包裹分发 → `tools/post-execute` → 由定义拥有的可选 `finalizeContent` → 不可变的 `tools/result` 通知——与原生调用完全一致;权限插件可以在程序运行前检查程序文本,最终结果观察者看到的是规范化的外层结果。其 `execute(args, exec)`:
 
-1. **构建绑定。** 一个 run 级别的 signal 跟随外层取消,并在 run 结算时被 abort。每个可见工具绑定都会对无损 JSON 参数创建快照,进入原生约定的分发池(调度设计由[实时并行 Agent Note](2026-07-26-ptc-live-parallel-dispatch.zh.md) 负责),以确定性的 call id 和外层 token 作为 `parent` 执行,通过外层 execution 延后返回的上下文,并记录 `tool/code-dispatch-start`/`tool/code-dispatch` 事件对,其中结算侧携带完整渲染后的结果内容。成功时返回工具最终的规范 JSON 值;失败则变为程序可见的 `ToolCallError`。每个子调用保留自己不可变的执行标识,并遍历完整的工具流水线。
+1. **构建绑定。** 一个 run 级别的 signal 跟随外层取消,并在 run 结算时被 abort。每个可见工具绑定都会对无损 JSON 参数创建快照,进入原生约定的分发池(调度设计由[实时并行 Agent Note](2026-07-26-ptc-live-parallel-dispatch.zh.md) 负责),以确定性的 call id 和外层 token 作为 `parent` 执行,通过外层 execution 延后返回的上下文,并记录 `tool/ptc-dispatch-start`/`tool/ptc-dispatch` 事件对,其中结算侧携带完整渲染后的结果内容。成功时返回工具最终的规范 JSON 值;失败则变为程序可见的 `ToolCallError`。每个子调用保留自己不可变的执行标识,并遍历完整的工具流水线。
 2. **运行程序**:`ctx.codeRuntime.run({ program: args.code, bindings: [{ global: 'tools', functions }], signal: runController.signal })`。运行时接收的是 run 级别的 signal 而非仅调用方的外层 signal,因此外层 run 以任何方式结算都会同时 abort 运行时内部的工作。
 3. **完全停稳后结算。** 运行时结算后,桥 abort 未完成的工作并排空分发队列后再返回。成功时返回捕获的日志和完成值,将其作为规范输出;注册表再把该值渲染为持久化的 `tool/result.content`,供结果卡片直接读取。运行时失败变为 `CodeRunFailedError`;后端拒绝使用注册表的正常错误边界。两者都产生结构化的错误结果,且 `run_code` 结算后不允许子调用追加。
 
@@ -52,9 +52,13 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一
 
 **呈现。** `run_code` 的 render intent 按[呈现意图 Agent Note](../architecture/2026-07-02-tool-render-intent-union.zh.md)在此决定:`presentCall` 创建一个 `generic` 卡片,`kind: 'execute'`,以程序文本作为标题,并将同一程序文本作为 `rawInput`;`run_code` 有意不声明 `presentResult`,因此 TUI 和宿主/客户端运行时(Web)会通过通用原始内容回退机制,使用最终持久化的 `tool/result.content` 补全该卡片,其中包括捕获的日志,以及返回值、失败信息或 post-policy spill 预览。这不是 `terminal` 卡片:该卡片的语义是「工作目录中的 shell 命令」,程序不是。参见[结果卡片完整性说明](../../archived/bug-fix/2026-07-20-code-mode-result-card-completeness.md)。
 
-### 可观测性:`tool/code-dispatch`
+### 可观测性:`tool/ptc-dispatch`
 
-每次子分发在进入分发池时追加一个仅日志的 `tool/code-dispatch-start` 事件,并以一个 `tool/code-dispatch` 结算事件收尾,后者包含父子 call id、工具标识、规范化参数以及完整渲染后的 `content`/`isError` 结果。它不进入模型历史,但可供持久化和 UI 使用。追加发生在开放的 `run_code` 轮次内。没有 agent 的直接执行仍然运行,但无法记录该事件。
+每次子分发在进入分发池时追加一个仅日志的 `tool/ptc-dispatch-start` 事件,并以一个 `tool/ptc-dispatch` 结算事件收尾,后者包含父子 call id、工具标识、规范化参数以及完整渲染后的 `content`/`isError` 结果。它不进入模型历史,但可供持久化和 UI 使用。追加发生在开放的 `run_code` 轮次内。没有 agent 的直接执行仍然运行,但无法记录该事件。
+
+新子调用使用 `<parent>:ptc:<n>` 标识,按提交顺序编号。消费者将所有 call id 视为不透明值:迁移逐字节保留每个历史标识,包括 `:code:` 子串,因此分发事件对、spill 引用及其他关联保持完整。桥接层将转发图片上下文的来源标记为 `{ kind: 'plugin', plugin: 'tools-ptc' }`。
+
+[V2 到 V3 PTC 规范](../../../../packages/session/session-format-v2-to-v3/README.zh.md#ptc-vocabulary)负责精确的历史标签与归属转换;[原生 V3 准入](../../../../packages/session/session-format-v2-to-v3/README.zh.md#native-v3-admission)负责前代标签拒绝。这些不是运行时别名:不透明扩展不能仅因版本变化就获得 PTC 生命周期含义。
 
 ### code-runtime seam
 
@@ -98,7 +102,7 @@ SDK 指示模型编写一个所加载运行时语言的异步函数体(默认
 - **Worker 运行时:** 真实 worker 测试覆盖类型化的绑定值与失败、每一种无损 JSON 根类型的完成值、无效和超限输出、精确的组合账本边界、compute 和 wall 预算、恶意绑定流量、空环境以及 dispose 至完全停稳。一个构建后包测试在纯 Node 下运行 worker 入口。
 - **注册表集成:** 测试覆盖代码生成、所有呈现模式、保留名称和限制规则、scoped 可见性、权威组装重写、`toolOrder`、运行时兼容性失败、完整流水线子分发、parent-token 关联、序列化、取消和队列排空、JSON 规范化、错误传播、日志事件、成功与失败程序中的有序上下文延后、外层阻止抑制以及 HMR(热模块替换)清理。
 - **带密钥 e2e:** 真实模型在一个程序中组合两次 bash 调用;另一个模型通过 PTC mode fs 分发发现嵌套的工作区指令。测试验证折叠的请求头、关联的分发事件、结果文件、延后上下文和模型行为。
-- **快照:** `ptc-turn`、`both-mode-turn` 和 `ptc-workspace-context` fixture(测试前置数据)固定 SDK 文本、请求头工具列表、分发事件、延后上下文和结果卡片。
+- **快照:** `ptc-turn`、`both-mode-turn` 和 `ptc-workspace-context` fixture(测试前置数据)固定 SDK 文本、请求头工具列表、分发事件、延后上下文和结果卡片。TypeScript SDK PTC 场景通过测试拥有的显式 profile patch 挂载 worker 运行时,并固定 Session 事件与 JSON-RPC 通知;预期回复和完成轮次检查在 refresh 写入前执行。
 
 ## 曾考虑的替代方案
 

+ 2 - 2
.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.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-18-compaction-capability-seam.md
-2026-06-18-compaction-capability-seam.md: 770960498b0d873009ff2a5fc63af59c21259398
-2026-06-18-compaction-capability-seam.zh.md: a05df813cb573ee7adea60723063719f1d104cd9
+2026-06-18-compaction-capability-seam.md: 7d8b4f5011385f306aec4c6374d3402427fb3cc4
+2026-06-18-compaction-capability-seam.zh.md: 6a255bb05e0b3c90077af051bd24744305f3833e

+ 3 - 3
.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md

@@ -8,7 +8,7 @@ English | [中文](2026-06-18-compaction-capability-seam.zh.md)
 
 A long-running agent conversation grows without bound. As the event log accumulates turns, the derived message history eventually approaches the model's context window — the model then truncates mid-response (`max-tokens`) or degrades. **Compaction** is the mitigation: replace a run of older history with a concise summary, keeping recent context intact.
 
-The [session surface](../architecture/2026-06-18-session-surface.md) was built as the foundation for exactly this — an ordered projection over the event log with a `surfaceOp: { op: 'replace', start, end }` operation purpose-built to shadow a range of entries and insert a replacement, with `sourceEventSeqs` listing every source event so replay can validate that the replacement cites every event it removes. What remained was the plugin that *decides what to compact and produces the summary*.
+The [session surface](../architecture/2026-06-18-session-surface.md) was built as the foundation for exactly this — an ordered projection over the event log with a `surfaceOp: { op: 'replace', startSeq, endSeq }` operation purpose-built to shadow a range of entries and insert a replacement, with `sourceEventSeqs` listing every source event so replay can validate that the replacement cites every event it removes. What remained was the plugin that *decides what to compact and produces the summary*.
 
 Two forces shape the design. First, compaction policy and reusable token measurement vary independently: measurement belongs to the LLM-family [`ctx.tokenMeter` service](../../archived/architecture/2026-07-15-replay-token-meter-service.md), while summarization can be a model call, a template, or a remote service. Second, `SurfaceEventType` is closed to the message-producing event types (`user/message`, `assistant/message`, `tool/result`); only those may carry `surfaceOp`. A bespoke `compaction/*` event therefore **cannot** itself appear on the surface — the compiler and Session's always-on append/seed boundary reject `surfaceOp` on it.
 
@@ -71,13 +71,13 @@ Auto-compaction always starts at the surface head, merging the prior checkpoint
 
 ### Surface replacement: `compaction/*` events are log-only; one `user/message` carries the summary
 
-Because `SurfaceEventType` is closed, the summary cannot ride on a `compaction/*` event. The backend instead appends a **single `user/message`** with `source: COMPACT_CHECKPOINT_SOURCE` and `surfaceOp: { op: 'replace', start, end }` whose `content` is the (framed) summary and whose `sourceEventSeqs` covers the shadowed entries *and* the bookkeeping events. The interface exports that source and `isCompactCheckpointSource()` so consumers recognize a persisted or cloned checkpoint without depending on backend package identity. The `compaction/*` events record the lock, summary, selected range, shadowed seqs, token count, and model call without joining the surface. The surface mutation sits **inside** the lock — `compaction/end` is the last event appended:
+Because `SurfaceEventType` is closed, the summary cannot ride on a `compaction/*` event. The backend instead appends a **single `user/message`** with `source: COMPACT_CHECKPOINT_SOURCE` and `surfaceOp: { op: 'replace', startSeq, endSeq }` whose `content` is the (framed) summary and whose `sourceEventSeqs` covers the shadowed entries *and* the bookkeeping events. The interface exports that source and `isCompactCheckpointSource()` so consumers recognize a persisted or cloned checkpoint without depending on backend package identity. The `compaction/*` events record the lock, summary, selected range, shadowed seqs, token count, and model call without joining the surface. The surface mutation sits **inside** the lock — `compaction/end` is the last event appended:
 
 ```
 compaction/start    → log-only. Acquires the lock.
 [summarize older range via the backend]
 compaction/summary  → log-only. Records the raw summary, local-call marker, range, shadowed seqs, and token count.
-user/message     → canonical checkpoint source + surfaceOp { op:'replace', start, end }.
+user/message     → canonical checkpoint source + surfaceOp { op:'replace', startSeq, endSeq }.
                    THE surface mutation (framed summary).
                    deriveMessages() renders it as a user-role message.
 compaction/end      → log-only. Releases the lock (carries `error` on a recoverable failure).

+ 3 - 3
.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.zh.md

@@ -8,7 +8,7 @@ Status: implemented
 
 长时间运行的 agent(智能体)对话会无限增长。随着事件日志不断累积轮次,派生出的消息历史最终逼近模型的上下文窗口,模型随即在响应中途停止生成(`max-tokens`),或表现退化。**上下文压缩(context compaction)** 是对此的缓解手段:用一段简洁的摘要替换一批较早的历史,保持近期上下文完整。
 
-[会话接口面](../architecture/2026-06-18-session-surface.zh.md)正是为此而构建的基础设施:一份建立在事件日志之上的有序投影,带有专门设计的 `surfaceOp: { op: 'replace', start, end }` 操作,用于遮蔽一段条目并插入替换内容,`sourceEventSeqs` 列出每个来源事件,使回放可以验证替换是否引用了它移除的每个事件。剩下的是那个*决定压缩什么、并产出摘要*的插件。
+[会话接口面](../architecture/2026-06-18-session-surface.zh.md)正是为此而构建的基础设施:一份建立在事件日志之上的有序投影,带有专门设计的 `surfaceOp: { op: 'replace', startSeq, endSeq }` 操作,用于遮蔽一段条目并插入替换内容,`sourceEventSeqs` 列出每个来源事件,使回放可以验证替换是否引用了它移除的每个事件。剩下的是那个*决定压缩什么、并产出摘要*的插件。
 
 两股力量塑造了设计。第一,压缩策略与可复用的 token 测量独立变化:测量归 LLM(大语言模型)系列的 [`ctx.tokenMeter` 服务](../../archived/architecture/2026-07-15-replay-token-meter-service.md)所有,摘要生成则可以使用模型调用、模板或远程服务。第二,`SurfaceEventType` 封闭为产生消息的事件类型(`user/message`、`assistant/message`、`tool/result`);只有这些类型可以携带 `surfaceOp`。因此一个专用的 `compaction/*` 事件**不能**出现在 surface 上,编译器与 Session 始终启用的 append/seed 边界都会拒绝在其上附加 `surfaceOp`。
 
@@ -71,13 +71,13 @@ retry → next numbered step/start      ⟵ derives from the replacement surface
 
 ### Surface 替换:`compaction/*` 事件仅存在于日志;一条 `user/message` 承载摘要
 
-由于 `SurfaceEventType` 是封闭的,摘要不能搭载在 `compaction/*` 事件上。后端改为追加**单条 `user/message`**,带有 `source: COMPACT_CHECKPOINT_SOURCE` 和 `surfaceOp: { op: 'replace', start, end }`;其 `content` 是(带框架的)摘要,`sourceEventSeqs` 覆盖被遮蔽的条目*和*簿记事件。接口导出该来源和 `isCompactCheckpointSource()`,使消费方无需依赖后端包身份,即可识别持久化或克隆得到的检查点。`compaction/*` 事件记录锁、摘要、选中区间、被遮蔽的 seq、token 数和模型调用,但不加入 surface。surface 变更位于锁**内部**,`compaction/end` 是最后追加的事件:
+由于 `SurfaceEventType` 是封闭的,摘要不能搭载在 `compaction/*` 事件上。后端改为追加**单条 `user/message`**,带有 `source: COMPACT_CHECKPOINT_SOURCE` 和 `surfaceOp: { op: 'replace', startSeq, endSeq }`;其 `content` 是(带框架的)摘要,`sourceEventSeqs` 覆盖被遮蔽的条目*和*簿记事件。接口导出该来源和 `isCompactCheckpointSource()`,使消费方无需依赖后端包身份,即可识别持久化或克隆得到的检查点。`compaction/*` 事件记录锁、摘要、选中区间、被遮蔽的 seq、token 数和模型调用,但不加入 surface。surface 变更位于锁**内部**,`compaction/end` 是最后追加的事件:
 
 ```
 compaction/start    → log-only. Acquires the lock.
 [summarize older range via the backend]
 compaction/summary  → log-only. Records the raw summary, local-call marker, range, shadowed seqs, and token count.
-user/message     → canonical checkpoint source + surfaceOp { op:'replace', start, end }.
+user/message     → canonical checkpoint source + surfaceOp { op:'replace', startSeq, endSeq }.
                    THE surface mutation (framed summary).
                    deriveMessages() renders it as a user-role message.
 compaction/end      → log-only. Releases the lock (carries `error` on a recoverable failure).

+ 2 - 2
.agents/notes/implemented/feature/2026-07-06-sandbox.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-06-sandbox.md
-2026-07-06-sandbox.md: a6e5639e21ca7140cb0314c9918319e99b1495f6
-2026-07-06-sandbox.zh.md: 5144b3fa719465707d7fb2870d45094b8c07661a
+2026-07-06-sandbox.md: 31d96836ad2f932f2abf7d1d76242a711f0de2f6
+2026-07-06-sandbox.zh.md: fdf5f4b1691b4a55fffbd206847c99307e12c9da

+ 1 - 1
.agents/notes/implemented/feature/2026-07-06-sandbox.md

@@ -64,7 +64,7 @@ Left open, for the phase that needs them: whether network restriction arrives as
 
 The launcher is a ~300-line C program (plain C11 over the raw Landlock UAPI — no libraries beyond a statically linked musl, so the audit surface is that one file plus the kernel's stable syscall contract): `--ro <path>` / `--rw <path>` grants, `--`, the wrapped argv; it installs the ruleset on itself and `exec`s (rulesets are inherited across `execve`, and it sets `no_new_privs` before restricting); `--probe` enforces a maximal ruleset in a short-lived child and exits 0 only when the kernel actually enforces; every launcher failure exits 125 without running the child and prints a fatal `landlock-run:` line. A successfully exec'd child may also return 125, so status alone is not launcher evidence. An older ABI prints the exact `landlock-run: partial enforcement (older Landlock ABI)` notice before it executes the child, so that line is not fatal evidence.
 
-The Landlock launcher source and package family live at `native/landlock-run`, next to the harness consumers and inside the root pnpm workspace. The [`native/` README](../../../../native/README.md) owns the shared lockfile, native build, pack rehearsal, and npm publication boundary. Platform binaries are selected by npm, and the entry package owns path resolution, probing, CLI flags, the fatal prefix, and the partial-enforcement notice while the harness maps sandbox modes to grants. Versioning the entry point with its binaries keeps probe parsing and launch syntax aligned.
+The Landlock launcher source and package family live at `native/system`, next to the harness consumers and inside the root pnpm workspace. The [`native/` README](../../../../native/README.md) owns the shared lockfile, native build, pack rehearsal, and npm publication boundary. Platform binaries are selected by npm, and the entry package owns path resolution, probing, CLI flags, the fatal prefix, and the partial-enforcement notice while the harness maps sandbox modes to grants. Versioning the entry point with its binaries keeps probe parsing and launch syntax aligned.
 
 Backend profiles share the mode contract but differ in necessary host grants. Landlock and Seatbelt allow only `/dev/null` in read-only mode; workspace-write also permits their required host temp roots. Each wrap carries backend-specific denial signatures. Landlock reports partial enforcement on older ABIs that cannot govern every operation, while successful bwrap and Seatbelt profiles report full enforcement.
 

+ 1 - 1
.agents/notes/implemented/feature/2026-07-06-sandbox.zh.md

@@ -64,7 +64,7 @@ OS 子进程约束适用于 bash 执行器(包括钩子命令),后续还
 
 launcher 是一个约 300 行的 C 程序(纯 C11,直接使用 Landlock UAPI——除静态链接的 musl 外无其他库,因此审计面仅为该文件加内核的稳定 syscall 约定):`--ro <path>` / `--rw <path>` 授权,`--`,被包装的 argv;它为自身安装规则集并执行 `exec`(规则集跨 `execve` 继承,且它在限制前设置 `no_new_privs`);`--probe` 在一个短生命周期子进程中强制最大规则集,仅当内核确实强制时才以 0 退出;所有 launcher 失败都会以 125 退出且不运行子进程,并打印一行致命的 `landlock-run:` 诊断。成功完成 exec 的子进程也可能返回 125,因此仅凭退出状态不能作为 launcher 失败的证据。较旧的 ABI 会在执行子进程之前打印精确的 `landlock-run: partial enforcement (older Landlock ABI)` 通知,因此该行不是致命证据。
 
-Landlock launcher 源码和包家族位于 `native/landlock-run`,与 harness 消费方同仓,并属于根 pnpm workspace。[`native/` README](../../../../native/README.zh.md)负责共享锁文件、原生构建、打包演练和 npm 发布边界。平台二进制由 npm 选择,入口包拥有路径解析、探测、CLI(命令行界面)参数、致命前缀和部分强制执行通知,而 harness 将沙箱模式映射为授权。将入口点与其二进制一起版本化,使探测解析和启动语法保持对齐。
+Landlock launcher 源码和包家族位于 `native/system`,与 harness 消费方同仓,并属于根 pnpm workspace。[`native/` README](../../../../native/README.zh.md)负责共享锁文件、原生构建、打包演练和 npm 发布边界。平台二进制由 npm 选择,入口包拥有路径解析、探测、CLI(命令行界面)参数、致命前缀和部分强制执行通知,而 harness 将沙箱模式映射为授权。将入口点与其二进制一起版本化,使探测解析和启动语法保持对齐。
 
 后端 profile 共享模式约定但在必要的主机授权上有所不同。Landlock 和 Seatbelt 在 read-only 模式下仅允许 `/dev/null`;workspace-write 还允许各自所需的主机临时目录根。每次包装携带后端特定的拒绝签名。Landlock 在较旧的 ABI 无法管控所有操作时报告 partial enforcement,而成功的 bwrap 和 Seatbelt profile 报告 full enforcement。
 

+ 2 - 2
.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.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-20-ptc-typed-tool-returns.md
-2026-07-20-ptc-typed-tool-returns.md: 04ef9f7da4cd59ba07632684541dd6962008b810
-2026-07-20-ptc-typed-tool-returns.zh.md: c197d3131cc0f7fa326a9a47d945b2b7730f01c0
+2026-07-20-ptc-typed-tool-returns.md: b7d7cc56210b229d7e36f8face888a34c9d1ac3e
+2026-07-20-ptc-typed-tool-returns.zh.md: 20e14cdac6151ceead084ff6a78eb5f7a6277af1

+ 1 - 1
.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.md

@@ -73,7 +73,7 @@ Temporary Cordis Plugins follow the same rule: `cordis_mount` returns `{ id, plu
 
 ### Persistence, metadata, and spill
 
-Nested dispatch logs the sub-call's full rendered `content`/`isError` on `tool/code-dispatch` but does not persist canonical values. `tool/result` continues to persist only rendered content, error, and optional metadata. A successful final content sequence containing an image is also wrapped in a source-attributed user message and deferred through the outer result; the normal session event makes that model-visible input reconstructable. This feature did not itself require a structural Session-format change; the released v0-to-v1 identity edge preserves these records, and replay still cannot recreate intermediate canonical program values.
+Nested dispatch logs the sub-call's full rendered `content`/`isError` on `tool/ptc-dispatch` but does not persist canonical values. `tool/result` continues to persist only rendered content, error, and optional metadata. A successful final content sequence containing an image is also wrapped in a source-attributed user message and deferred through the outer result; the normal session event makes that model-visible input reconstructable. The [PTC mode note](2026-06-15-ptc.md) owns durable event names and historical identity preservation; replay cannot recreate intermediate canonical program values.
 
 The opaque `exec.parent` token marks nested calls. Presentation metadata and generic or tool-owned spill projections skip those calls; their canonical values never enter context. The Client can derive [nested terminal cards](../bug-fix/2026-09-05-nested-terminal-cards.md) from raw dispatch events without metadata. The outer `run_code` call produces the model-facing result and may spill its final post-policy presentation; `run_code` intentionally declares neither a result presenter nor presentation metadata, so UI adapters complete the card through their generic raw-content fallback using durable `tool/result.content`.
 

+ 1 - 1
.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.zh.md

@@ -73,7 +73,7 @@ PTC mode 通过运行时请求中的 `{ name: "ToolCallError", memberNamePropert
 
 ### 持久化、元数据与 spill
 
-嵌套分发在 `tool/code-dispatch` 上记录子调用完整渲染后的 `content`/`isError`,但不会持久化规范值。`tool/result` 继续只持久化渲染后的内容、错误和可选元数据。包含图片的成功最终内容序列还会包装成带来源归属的用户消息,并经外层结果延后;普通会话事件使该模型可见输入可以重建。该功能本身不要求结构性 Session 格式变更;已发布的 v0-to-v1 恒等边会保留这些记录,回放仍无法重建程序的规范中间值。
+嵌套分发在 `tool/ptc-dispatch` 上记录子调用完整渲染后的 `content`/`isError`,但不会持久化规范值。`tool/result` 继续只持久化渲染后的内容、错误和可选元数据。包含图片的成功最终内容序列还会包装成带来源归属的用户消息,并经外层结果延后;普通会话事件使该模型可见输入可以重建。[PTC mode note](2026-06-15-ptc.zh.md) 负责持久事件名称与历史标识保留规则;回放无法重建程序的规范中间值。
 
 不透明的 `exec.parent` token 用于标识嵌套调用。展示元数据以及通用或工具自有的 spill 投影都会跳过这些调用;它们的规范值永远不会进入上下文。Client 可以从原始分发事件派生[嵌套 terminal 卡片](../bug-fix/2026-09-05-nested-terminal-cards.zh.md),无需元数据。外层 `run_code` 调用产生面向模型的结果,并且可能对 post-policy 处理后的最终展示执行 spill;`run_code` 有意既不声明结果展示器,也不声明展示元数据,因此 UI 适配器会通过通用的原始内容回退机制,使用持久化的 `tool/result.content` 补全该卡片。
 

+ 2 - 2
.agents/notes/implemented/feature/2026-07-26-ptc-live-parallel-dispatch.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-26-ptc-live-parallel-dispatch.md
-2026-07-26-ptc-live-parallel-dispatch.md: 53388e4b4cc067527bd1ed04d6c66054e84ecd44
-2026-07-26-ptc-live-parallel-dispatch.zh.md: 8eb8e6d743be65db50a4685eafb2496bc791141c
+2026-07-26-ptc-live-parallel-dispatch.md: f064314098af9c074f7cd06dc9ceea3df4c91c76
+2026-07-26-ptc-live-parallel-dispatch.zh.md: 70cbebcdc30c5771a2c29f36b588232822e19269

+ 2 - 2
.agents/notes/implemented/feature/2026-07-26-ptc-live-parallel-dispatch.md

@@ -4,7 +4,7 @@ Status: implemented
 
 English | [中文](2026-07-26-ptc-live-parallel-dispatch.zh.md)
 
-> Scope: the `tool/code-dispatch-start` event, per-sub-call running state in the web chat, and the bridge's scheduler reusing the native concurrency contract. Builds on the [host foundation](../../archived/feature/2026-07-26-ptc-dispatch-ui-foundation.md) and [chat sub-call rows](../../archived/feature/2026-07-26-ptc-chat-subcall-rows.md); the native contract itself is owned by the [parallel tool-call note](2026-07-10-parallel-tool-call-execution.md).
+> Scope: the `tool/ptc-dispatch-start` event, per-sub-call running state in the web chat, and the bridge's scheduler reusing the native concurrency contract. Builds on the [host foundation](../../archived/feature/2026-07-26-ptc-dispatch-ui-foundation.md) and [chat sub-call rows](../../archived/feature/2026-07-26-ptc-chat-subcall-rows.md); the native contract itself is owned by the [parallel tool-call note](2026-07-10-parallel-tool-call-execution.md).
 
 ## Problem
 
@@ -14,7 +14,7 @@ Two gaps remained after the host foundation and chat sub-call rows shipped. Sub-
 
 **One lifecycle pair, one scheduling contract, shared with native.**
 
-- **Event pair**: `tool/code-dispatch-start` (parent/sub ids, name, normalized args) is appended when the scheduler actually starts a call — not at submission, so a queued call abandoned by run settlement logs nothing. The existing `tool/code-dispatch` settles the pair (same `subCallId`); every started call settles exactly once (aborts settle as `isError` outcomes through the pipeline). Timing = the two events' `time` fields. Both stay log-only; model context is untouched; format stays v0.
+- **Event pair**: `tool/ptc-dispatch-start` (parent/sub ids, name, normalized args) is appended when the scheduler actually starts a call — not at submission, so a queued call abandoned by run settlement logs nothing. The existing `tool/ptc-dispatch` settles the pair (same `subCallId`); every started call settles exactly once (aborts settle as `isError` outcomes through the pipeline). Timing = the two events' `time` fields. Both stay log-only; model context is untouched.
 - **Bridge scheduler**: submitted calls are classified at start time via `registry.executionMode` (the SAME fail-closed `isConcurrencySafe` contract the loop uses) and start strictly in submission order. One single-lane driver owns every ORDERED stage — the start append, `prepare` (pre-execute/guards), the head-of-line `finalize`/`finish` commit (post-execute + context deferral + settle append) — so ordered policy stages never overlap each other and only the around-dispatch/body stage runs concurrently, exactly the native loop's sequencing (`fillPool` awaits `startCall` then `commitReady`). Consecutive parallel-classified calls overlap up to `maxParallelSubCalls` (a `Config` field validated by the Loader schema AND re-validated at direct construction, default 10 — the loop scheduler's own default; `1` restores serial dispatch); an exclusive call drains the pool, runs alone, and holds its barrier until its COMMIT completes (post-execute included), like a native exclusive group. Run settlement aborts in-flight dispatches and abandons queued-unstarted ones (binding rejection, no events), then drains to quiescence — including a commit already mid-flight when the program returned — before the outer result closes the turn.
 - **Client**: Runtime's `ToolCallTree` stores a start event as a `RunningToolCall` child and projects it through the parent's recursive `subCalls` (rows derive the running ring from that shape, exactly as for native in-flight calls). Its settle replaces the private-index entry in place, preserving start order under parallel completion and carrying the start's `time` as `callTime` (duration source). A settle with no observed start (window cut mid-pair, or a pre-start-event log) appends directly, so old logs keep rendering.
 - **SDK prompt**: the model-facing "calls execute sequentially" sentence is replaced with the true contract (independent safe calls may overlap under `Promise.all`; dependent work sequences with `await`) — a model-visible change, re-recorded across every ptc snapshot.

+ 2 - 2
.agents/notes/implemented/feature/2026-07-26-ptc-live-parallel-dispatch.zh.md

@@ -4,7 +4,7 @@ Status: implemented
 
 [English](2026-07-26-ptc-live-parallel-dispatch.md) | 中文
 
-> 范围:`tool/code-dispatch-start` 事件、Web chat 中每个子调用的运行状态,以及桥接层调度器对原生并发约定的复用。构建在[宿主侧基础](../../archived/feature/2026-07-26-ptc-dispatch-ui-foundation.md)与 [chat 子调用行](../../archived/feature/2026-07-26-ptc-chat-subcall-rows.md)之上;原生约定本身归[并行工具调用 Agent Note](2026-07-10-parallel-tool-call-execution.zh.md) 所有。
+> 范围:`tool/ptc-dispatch-start` 事件、Web chat 中每个子调用的运行状态,以及桥接层调度器对原生并发约定的复用。构建在[宿主侧基础](../../archived/feature/2026-07-26-ptc-dispatch-ui-foundation.md)与 [chat 子调用行](../../archived/feature/2026-07-26-ptc-chat-subcall-rows.md)之上;原生约定本身归[并行工具调用 Agent Note](2026-07-10-parallel-tool-call-execution.zh.md) 所有。
 
 ## 问题
 
@@ -14,7 +14,7 @@ Status: implemented
 
 **一对生命周期事件,一份调度约定,与原生共用。**
 
-- **事件对**:`tool/code-dispatch-start`(父/子 id、名称、规范化参数)在调度器真正启动某个调用时才追加,而非在提交时,因此因 run 结算而被放弃的排队调用不会留下任何日志。既有的 `tool/code-dispatch` 结算该事件对(`subCallId` 相同);每个已启动的调用恰好结算一次(中止也会作为 `isError` 结果经由流水线结算)。计时即这两个事件的 `time` 字段。两个事件仍仅用于日志;模型上下文不受影响;格式保持 v0
+- **事件对**:`tool/ptc-dispatch-start`(父/子 id、名称、规范化参数)在调度器真正启动某个调用时才追加,而非在提交时,因此因 run 结算而被放弃的排队调用不会留下任何日志。既有的 `tool/ptc-dispatch` 结算该事件对(`subCallId` 相同);每个已启动的调用恰好结算一次(中止也会作为 `isError` 结果经由流水线结算)。计时即这两个事件的 `time` 字段。两个事件仍仅用于日志;模型上下文不受影响。
 - **桥接层调度器**:已提交的调用在启动那一刻经 `registry.executionMode` 分类(与 loop 所用完全相同、故障时默认判为不安全的 `isConcurrencySafe` 约定),并严格按提交顺序启动。所有有序阶段——start 事件追加、`prepare`(pre-execute/守卫)、队首 `finalize`/`finish` 提交(post-execute + 上下文延迟提交 + settle 事件追加)——由单通道驱动器独占执行,因此有序策略阶段彼此绝不重叠,只有 around-dispatch/工具体阶段并发运行,与原生 loop 的时序完全一致(`fillPool` 先 await `startCall` 再 `commitReady`)。连续被分类为可并行的调用可以重叠执行,上限为 `maxParallelSubCalls`(`Config` 字段,Loader schema 校验之外直接构造时也重新校验,默认值 10,即 loop 调度器自身的默认值;设为 `1` 即恢复串行分发);独占调用则先排空池、独自运行,且其屏障保持到自身提交(含 post-execute)完成为止,与原生独占分组一致。run 结算时会中止仍在运行的分发,并放弃已排队未启动的分发(绑定调用被拒绝,不产生事件),随后排空到完全停稳——包括程序返回时已在途的提交——之后外层结果才结束该轮次。
 - **客户端侧**:运行时的 `ToolCallTree` 把 start 事件存为 `RunningToolCall` 子级,并通过父级递归的 `subCalls` 投影出来(行组件从该形状推导出运行指示环,与原生运行中的调用处理完全一致)。其结算事件会原位替换私有索引中的条目,即使并行完成也保持启动顺序不变,并把 start 事件的 `time` 作为 `callTime`(时长来源)带入。未观察到对应 start 的结算事件(窗口切在事件对中间,或日志录制于 start 事件引入之前)会直接追加,因此旧日志仍能照常渲染。
 - **SDK 提示词**:面向模型的「调用按顺序执行」一句替换为真实约定(相互独立的安全调用可以在 `Promise.all` 下重叠执行;相互依赖的工作以 `await` 顺序衔接);这是模型可见的变更,每一份 PTC mode 快照都已重新录制。

+ 2 - 2
.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.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-04-claude-code-and-codex-subagent-backends.md
-2026-08-04-claude-code-and-codex-subagent-backends.md: 52665a44b654a50b8dc28f4bbd530a0606f8e2d9
-2026-08-04-claude-code-and-codex-subagent-backends.zh.md: f16d2412826db5d0fd3dcaad1281ccd92acdd26e
+2026-08-04-claude-code-and-codex-subagent-backends.md: d11b200e95cbb0769ad30ae88cb976c7921c80f8
+2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 5899a27bb8ad592d9b83b7d19e681bef375525fb

+ 6 - 6
.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md

@@ -34,21 +34,21 @@ configured tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> pro
 
 ## Codex provider
 
-`@deepseek-ai/dsh-subagent-codex` registers a Profile-selected provider name that defaults to `codex`, resolves the `codex` bin declared by its pinned `@openai/codex@0.149.1` package, and starts that wrapper through the current Node executable with `app-server --stdio`. The wrapper selects the private native platform payload; the provider neither resolves nor falls back to a host `codex`. Its public configuration contains a non-empty `providerName`, an optional non-empty `model`, an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a three-value native `permissionMode` that defaults to `never`. Each named instance retains those resolved values for its own runs. An explicit model is passed unchanged on every ephemeral `thread/start`; omission leaves native Codex settings authoritative. Installation, login, `CODEX_HOME`, model discovery or fallback, base URL, and product-session settings remain native Codex or deployment responsibilities; the selected mode owns only the thread approval/reviewer/sandbox fields described by the non-interactive permissions decision.
+`@deepseek-ai/dsh-subagent-codex` registers a Profile-selected provider name that defaults to `codex`, resolves the `codex` bin declared by its pinned `@openai/codex@0.153.4` package, and starts that wrapper through the current Node executable with `app-server --stdio`. The wrapper selects the private native platform payload; the provider neither resolves nor falls back to a host `codex`. Its public configuration contains a non-empty `providerName`, an optional non-empty `model`, an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a three-value native `permissionMode` that defaults to `never`. Each named instance retains those resolved values for its own runs. An explicit model is passed unchanged on every ephemeral `thread/start`; omission leaves native Codex settings authoritative. Installation, login, `CODEX_HOME`, model discovery or fallback, base URL, and product-session settings remain native Codex or deployment responsibilities; the selected mode owns only the thread approval/reviewer/sandbox fields described by the non-interactive permissions decision.
 
 Before publication, the provider validates a non-empty text-only task, starts the managed app-server in the parent workspace, completes `initialize` → `initialized`, maps the optional model and resolved mode into official `thread/start` fields, and creates an `ephemeral: true` thread. The fixed app-server argv contains no model, mode, or task text. The published run owns exactly one `turn/start`; its thread and turn ids remain private and are never persisted in the parent Session.
 
 `turn/completed` is the authoritative remote terminal fact. The latest `agentMessage` with `phase: "final_answer"` wins, and that selected message must contain nonblank text. When the product emits no explicit final phase, the latest message with `phase: null` is the compatibility fallback and must likewise be nonblank; commentary never replaces either answer. The [minimal-diagnostics decision](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns Codex action categories, HTTP status, lifecycle stages, process outcomes, and stop-reason preservation. Local cancellation remains `aborted` without a failure diagnostic.
 
-For command and file approvals, the unattended wire selects a non-approval decision offered by the request, preferring `cancel`; the stable 0.149.1 request shape without an offered-decision list falls back to `decline`. It grants no requested permissions for the turn, answers user-input requests with no answers, and declines MCP elicitation. It records safe categories for those requests, declined command/file items, and structured `sandboxError` terminals. Product stderr is forwarded unchanged to the Host but is neither classified nor copied into the diagnostic. A request with no legal unattended response, or any unknown server request, fails the run instead of waiting for a user interface the provider does not supply.
+For command and file approvals, the unattended wire selects a non-approval decision offered by the request, preferring `cancel`; a request without an offered-decision list falls back to `decline`. It grants no requested permissions for the turn, answers user-input requests with no answers, and declines MCP elicitation. It records safe categories for those requests, declined command/file items, and structured `sandboxError` terminals. Product stderr is forwarded unchanged to the Host but is neither classified nor copied into the diagnostic. A request with no legal unattended response, or any unknown server request, fails the run instead of waiting for a user interface the provider does not supply.
 
 An unpublished startup failure closes the wire, terminates the acquired process tree, waits for exit, detaches the stderr observer, and then rejects `start()` with its fixed operation stage. Published disposal best-effort interrupts a known turn, closes the wire, ends stdin, invokes the shared termination escalation, waits for whole-tree exit, and detaches the observer. Independent cleanup failure reports `teardown`; when startup and rollback both fail, the aggregate's top message retains both safe stage lines while the underlying causes remain internal.
 
-Codex 0.149.1 speaks the Responses protocol, while DeepSeek's public OpenAI-compatible endpoint speaks Chat Completions. The credentialed Codex e2e therefore uses a loopback-only, test-private bridge for one no-tool nonce request: real Codex sends Responses to the bridge, the bridge forwards the received bearer credential and extracted task to the fixed official DeepSeek endpoint, and it wraps the real text in the minimal Responses SSE lifecycle. The bridge is neither a production proxy nor evidence that Codex connects to DeepSeek Chat Completions natively.
+Codex 0.153.4 speaks the Responses protocol, while DeepSeek's public OpenAI-compatible endpoint speaks Chat Completions. The credentialed Codex e2e therefore uses a loopback-only, test-private bridge for one no-tool nonce request: real Codex sends Responses to the bridge, the bridge forwards the received bearer credential and extracted task to the fixed official DeepSeek endpoint, and it wraps the real text in the minimal Responses SSE lifecycle. The bridge is neither a production proxy nor evidence that Codex connects to DeepSeek Chat Completions natively.
 
 ## Claude Code provider
 
-`@deepseek-ai/dsh-subagent-claude-code` registers a Profile-selected provider name that defaults to `claude-code` and invokes `@anthropic-ai/claude-agent-sdk@0.3.241`. The provider omits `pathToClaudeCodeExecutable`, so the SDK selects Claude Code 2.1.241 from the matching OS, CPU, and Linux-libc platform package in its own optional dependency closure. The provider does not resolve or fall back to a host `claude`; an omitted, unsupported, missing, or damaged platform payload fails the first delegation at the SDK startup boundary. The provider uses the official `query()` entrypoint and passes the SDK's native `claude` or `claude.exe` command, arguments, cwd, environment, and forwarded signal from `spawnClaudeCodeProcess` to `dsh-subprocess`; its private `SpawnedProcess` adapter exposes only the stream, event, kill, and exit facts the SDK requires.
+`@deepseek-ai/dsh-subagent-claude-code` registers a Profile-selected provider name that defaults to `claude-code` and invokes `@anthropic-ai/claude-agent-sdk@0.3.263`. The provider omits `pathToClaudeCodeExecutable`, so the SDK selects Claude Code 2.1.263 from the matching OS, CPU, and Linux-libc platform package in its own optional dependency closure. The provider does not resolve or fall back to a host `claude`; an omitted, unsupported, missing, or damaged platform payload fails the first delegation at the SDK startup boundary. The provider uses the official `query()` entrypoint and passes the SDK's native `claude` or `claude.exe` command, arguments, cwd, environment, and forwarded signal from `spawnClaudeCodeProcess` to `dsh-subprocess`; its private `SpawnedProcess` adapter exposes only the stream, event, kill, and exit facts the SDK requires.
 
 The public configuration contains a non-empty `providerName`, an optional non-empty `model`, an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a five-value native `permissionMode` that defaults to `dontAsk`. Each named instance retains those resolved values for its own runs. An explicit model is passed unchanged through `Options.model`; omission leaves that field absent so native settings choose the model. Each run creates its own `AbortController`, sets `persistSession: false`, disables `AskUserQuestion`, and passes the resolved mode to the SDK; only `bypassPermissions` receives the SDK's explicit dangerous confirmation. The provider deliberately omits `settingSources`, so the SDK reads the host's normal user, project, and local Claude settings relative to the parent Session cwd. It neither copies nor filters those settings and does not create or modify login state. Remaining permission prompts are denied, MCP elicitation is declined, and blocking dialogs fail closed instead of waiting for a user interface the provider does not own.
 
@@ -62,11 +62,11 @@ The credentialed Claude Code e2e uses the official DeepSeek Claude Code contract
 
 Each product owns branch-complete package tests, a required keyless real-product spec, a Loader composition e2e, and a credentialed DeepSeek e2e. The keyless product tier uses the exact official distribution under test, a non-empty fake product key, an isolated temporary workspace and product home, and a loopback fixed-answer model. Missing product requests, wrong authentication, altered task text, a non-exact answer, a skipped real product, or a surviving managed handle fails the required test. The Codex Loader fixture exposes two named Codex instances and tools; the Claude Code Loader fixture exposes the default Codex tool plus two named Claude Code instances and tools. Both fixtures include generic Job controls and start neither product process. The credentialed tier starts the same production provider and real product with a runtime-only key, requires a unique nonce from the fixed official DeepSeek service, and proves quiescence again; it self-skips only when a local operator supplied no key, while trusted CI preflights the secret.
 
-The Codex evidence pins `@openai/codex@0.149.1`, `codex-cli 0.149.1`, and all six optional platform aliases. Its generated schema proves optional `ThreadStartParams.model`; the real-product spec observes omitted-model inheritance, two explicit instance models, the package-local wrapper argv, exact Bearer key, original task, byte-exact final answer, native permission modes, explicit dangerous-bypass writing in suite-owned temporary storage, and wrapper/native whole-tree exit. An isolated wrapper fixture proves missing-payload failure without host fallback, named instances retain separate models, environments, and modes, and production never resolves a host `codex` from `PATH`. The [minimal-diagnostics decision](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns failure, process-outcome, and final presentation evidence.
+The Codex evidence pins `@openai/codex@0.153.4`, `codex-cli 0.153.4`, and all six optional platform aliases. Its generated schema proves optional `ThreadStartParams.model`; the real-product spec observes omitted-model inheritance, two explicit instance models, the package-local wrapper argv, exact Bearer key, original task, byte-exact final answer, native permission modes, explicit dangerous-bypass writing in suite-owned temporary storage, and wrapper/native whole-tree exit. An isolated wrapper fixture proves missing-payload failure without host fallback, named instances retain separate models, environments, and modes, and production never resolves a host `codex` from `PATH`. The [minimal-diagnostics decision](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns failure, process-outcome, and final presentation evidence.
 
 The Codex credentialed e2e registers the production provider, starts the same real app-server, and requests one random nonce through the test-private bridge described above. It fixes the external endpoint and model, stores no credential or request payload, requires exactly one completed upstream response, compares the trimmed product answer byte-for-byte with the nonce, and waits for every managed handle to exit.
 
-The Claude Code evidence pins Agent SDK 0.3.241, Claude Code 2.1.241, and all eight SDK platform packages. Its real-product spec lets the SDK select the installed payload, asserts that the shared subprocess argv begins with that package's native CLI, and observes omitted-model inheritance, two explicit instance models, the exact `x-api-key`, original task, byte-exact final answer, native permission modes, suite-owned denied and bypassed writes, and whole-tree exit. Package tests prove that production never resolves host `PATH`, omits the executable override, and forwards the SDK-selected Windows `claude.exe` without a batch shim. This evidence proves the pinned official SDK/CLI integration rather than compatibility with independently installed Claude versions; the [minimal-diagnostics decision](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns failure and process-outcome evidence. Loader coverage resolves both products through their optional Bundle patches while starting neither product.
+The Claude Code evidence pins Agent SDK 0.3.263, Claude Code 2.1.263, and all eight SDK platform packages. Its real-product spec lets the SDK select the installed payload, asserts that the shared subprocess argv begins with that package's native CLI, and observes omitted-model inheritance, two explicit instance models, the exact `x-api-key`, original task, byte-exact final answer, native permission modes, suite-owned denied and bypassed writes, and whole-tree exit. Package tests prove that production never resolves host `PATH`, omits the executable override, and forwards the SDK-selected Windows `claude.exe` without a batch shim. This evidence proves the pinned official SDK/CLI integration rather than compatibility with independently installed Claude versions; the [minimal-diagnostics decision](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns failure and process-outcome evidence. Loader coverage resolves both products through their optional Bundle patches while starting neither product.
 
 The Claude Code credentialed e2e maps the key and fixed official endpoint only in the provider's in-memory environment, uses the documented `deepseek-v4-pro[1m]` and `deepseek-v4-flash` model variables, and traverses the production provider, official SDK, and real CLI. It compares the trimmed result with a random nonce and proves whole-tree exit without calling the Messages API directly from the test.
 

+ 6 - 6
.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md

@@ -34,21 +34,21 @@ configured tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> pro
 
 ## Codex 提供方
 
-`@deepseek-ai/dsh-subagent-codex` 注册由 Profile 选择、默认值为 `codex` 的提供方名称,解析锁定的 `@openai/codex@0.149.1` 包所声明的 `codex` bin,并使用当前 Node 可执行文件加 `app-server --stdio` 启动该 wrapper。Wrapper 会选择私有原生平台载荷;提供方既不解析也不回退宿主 `codex`。其公开配置包含非空的 `providerName`、可选的非空 `model`、显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`,以及默认使用 `never` 的三值原生 `permissionMode`。每个命名实例会为自己的运行保留这些已解析值。显式模型会原样传给每个临时 `thread/start`;省略时仍以 Codex 原生设置为权威。安装、登录、`CODEX_HOME`、模型发现或 fallback、基础 URL 和产品会话设置仍由 Codex 原生机制或部署环境负责;所选模式只拥有非交互权限决策中描述的线程 approval/reviewer/sandbox 字段。
+`@deepseek-ai/dsh-subagent-codex` 注册由 Profile 选择、默认值为 `codex` 的提供方名称,解析锁定的 `@openai/codex@0.153.4` 包所声明的 `codex` bin,并使用当前 Node 可执行文件加 `app-server --stdio` 启动该 wrapper。Wrapper 会选择私有原生平台载荷;提供方既不解析也不回退宿主 `codex`。其公开配置包含非空的 `providerName`、可选的非空 `model`、显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`,以及默认使用 `never` 的三值原生 `permissionMode`。每个命名实例会为自己的运行保留这些已解析值。显式模型会原样传给每个临时 `thread/start`;省略时仍以 Codex 原生设置为权威。安装、登录、`CODEX_HOME`、模型发现或 fallback、基础 URL 和产品会话设置仍由 Codex 原生机制或部署环境负责;所选模式只拥有非交互权限决策中描述的线程 approval/reviewer/sandbox 字段。
 
 发布前,提供方会验证非空的纯文本任务,在父级工作区中启动受管的 app-server,完成 `initialize` → `initialized` 握手,把可选模型与已解析模式映射为官方 `thread/start` 字段,并创建一个 `ephemeral: true` 线程。固定 app-server argv 不包含模型、模式或任务文本。已发布的运行只拥有一次 `turn/start`;其线程 ID 与轮次 ID 保持私有,绝不会持久化到父会话。
 
 `turn/completed` 是权威的远端终止事实。以最后一条带有 `phase: "final_answer"` 的 `agentMessage` 为准,且选中的消息必须包含非空白文本。若产品没有发出明确的最终阶段,则以最后一条 `phase: null` 的消息作为兼容性回退,该消息也必须包含非空白文本;过程说明绝不会取代上述任一答案。[最小诊断决策](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md)负责 Codex 行动类别、HTTP status、生命周期阶段、进程结果与终止原因保持。本地取消仍是 `aborted` 且不附带失败诊断。
 
-对于命令与文件审批,无人值守的协议连接会从请求给出的决策选项中选择一项不予批准的决策,并优先选择 `cancel`;稳定的 0.149.1 请求形态没有决策选项列表,因此回退到 `decline`。它不授予该轮次请求的任何权限,不向用户输入请求提供任何答案,并拒绝 MCP elicitation。它会记录这些请求、被拒绝的命令/文件 item 与结构化 `sandboxError` 终态的安全类别。产品 stderr 会原样转发给 Host,但既不会被分类,也不会复制进诊断。若请求在无人值守模式下没有合法响应,或是未知服务器请求,此次运行就会失败,而不会等待本提供方没有提供的用户界面。
+对于命令与文件审批,无人值守的协议连接会从请求给出的决策选项中选择一项不予批准的决策,并优先选择 `cancel`;若请求没有决策选项列表,则回退到 `decline`。它不授予该轮次请求的任何权限,不向用户输入请求提供任何答案,并拒绝 MCP elicitation。它会记录这些请求、被拒绝的命令/文件 item 与结构化 `sandboxError` 终态的安全类别。产品 stderr 会原样转发给 Host,但既不会被分类,也不会复制进诊断。若请求在无人值守模式下没有合法响应,或是未知服务器请求,此次运行就会失败,而不会等待本提供方没有提供的用户界面。
 
 若启动在发布前失败,提供方会关闭协议连接、终止已获取的进程树、等待其退出、移除 stderr observer,然后用固定操作阶段拒绝 `start()`。对已发布的运行执行资源释放时,提供方会尽力中断已知轮次、关闭协议连接、结束标准输入、调用共享的逐级终止机制,等待整棵进程树退出,并移除 observer。独立清理失败会报告 `teardown`;启动与回滚同时失败时,聚合的顶层消息会保留两条安全阶段说明,而底层 cause 仍只在内部可见。
 
-Codex 0.149.1 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端点使用 Chat Completions。因此,带密钥 Codex e2e 会采用一个仅限回环、仅供测试内部使用的桥接层来处理一次不使用工具的随机数请求:真实 Codex 将 Responses 发送到桥接层,桥接层把收到的 Bearer 凭据与提取出的任务转发到固定的 DeepSeek 官方端点,再将真实文本包装进最小化的 Responses SSE(Server-Sent Events)生命周期。该桥接层既不是生产代理,也不能作为 Codex 原生连接 DeepSeek Chat Completions 的证据。
+Codex 0.153.4 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端点使用 Chat Completions。因此,带密钥 Codex e2e 会采用一个仅限回环、仅供测试内部使用的桥接层来处理一次不使用工具的随机数请求:真实 Codex 将 Responses 发送到桥接层,桥接层把收到的 Bearer 凭据与提取出的任务转发到固定的 DeepSeek 官方端点,再将真实文本包装进最小化的 Responses SSE(Server-Sent Events)生命周期。该桥接层既不是生产代理,也不能作为 Codex 原生连接 DeepSeek Chat Completions 的证据。
 
 ## Claude Code 提供方
 
-`@deepseek-ai/dsh-subagent-claude-code` 注册由 Profile 选择、默认值为 `claude-code` 的提供方名称,并调用 `@anthropic-ai/claude-agent-sdk@0.3.241`。提供方会省略 `pathToClaudeCodeExecutable`,因此 SDK 会从自己的 optional dependency 闭包中,按操作系统、CPU 与 Linux libc 选择携带 Claude Code 2.1.241 的匹配平台包。提供方既不会解析也不会回退宿主 `claude`;省略 optional dependency、不受支持的平台,以及缺失或损坏的平台载荷,都会在第一次委派的 SDK 启动边界失败。提供方使用官方 `query()` 入口点,并把 SDK 的 `spawnClaudeCodeProcess` 给出的原生 `claude` 或 `claude.exe` 命令、参数、cwd、环境和转发的信号交给 `dsh-subprocess`;其私有 `SpawnedProcess` 适配器只公开 SDK 所需的流、事件、终止和退出事实。
+`@deepseek-ai/dsh-subagent-claude-code` 注册由 Profile 选择、默认值为 `claude-code` 的提供方名称,并调用 `@anthropic-ai/claude-agent-sdk@0.3.263`。提供方会省略 `pathToClaudeCodeExecutable`,因此 SDK 会从自己的 optional dependency 闭包中,按操作系统、CPU 与 Linux libc 选择携带 Claude Code 2.1.263 的匹配平台包。提供方既不会解析也不会回退宿主 `claude`;省略 optional dependency、不受支持的平台,以及缺失或损坏的平台载荷,都会在第一次委派的 SDK 启动边界失败。提供方使用官方 `query()` 入口点,并把 SDK 的 `spawnClaudeCodeProcess` 给出的原生 `claude` 或 `claude.exe` 命令、参数、cwd、环境和转发的信号交给 `dsh-subprocess`;其私有 `SpawnedProcess` 适配器只公开 SDK 所需的流、事件、终止和退出事实。
 
 公开配置包含非空的 `providerName`、可选的非空 `model`、显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`,以及默认使用 `dontAsk` 的五值原生 `permissionMode`。每个命名实例会为自己的运行保留这些已解析值。显式模型会原样传入 `Options.model`;省略时不设置该字段,由原生设置选择模型。每次运行都会创建自己的 `AbortController`,设置 `persistSession: false`、禁用 `AskUserQuestion`,并把已解析模式传给 SDK;只有 `bypassPermissions` 会取得 SDK 的显式危险确认。提供方故意省略 `settingSources`,因此 SDK 会相对于父会话 cwd 读取宿主机常规的用户、项目和本地 Claude 设置。它既不复制也不过滤这些设置,也不会创建或修改登录状态。其余权限提示会被拒绝,MCP elicitation 会被拒绝,阻塞对话会快速失败,而不会等待本提供方不负责的用户界面。
 
@@ -62,11 +62,11 @@ Codex 0.149.1 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端
 
 每个产品都负责覆盖所有分支的包测试、一项必跑的无密钥真实产品测试、一项 Loader 组合 e2e 和一项带密钥 DeepSeek e2e。无密钥产品层级使用被测的确切官方发行版、非空的伪产品密钥、隔离的临时工作区与产品主目录,以及能返回固定答案的回环模型。产品请求缺失、身份验证错误、任务文本被改动、答案不完全一致、真实产品被跳过或受管句柄仍存活,都会使这项必跑测试失败。Codex Loader fixture 会公开两个命名 Codex 实例与工具;Claude Code Loader fixture 会公开默认 Codex 工具以及两个命名 Claude Code 实例与工具。两个 fixture 都包含通用 Job 控制工具,而且不会启动任何产品进程。带密钥层级会使用仅在运行时提供的密钥启动同一生产提供方与真实产品,要求从固定的 DeepSeek 官方服务取得唯一随机数,并再次证明完全停稳;仅当本地操作者未提供密钥时才会自行跳过,而受信任的 CI 会预检该 secret。
 
-Codex 证据会锁定 `@openai/codex@0.149.1`、`codex-cli 0.149.1` 与六个平台 alias。生成 schema 会证明可选的 `ThreadStartParams.model`;真实产品测试会观测省略模型继承、两个显式实例模型、包内 wrapper argv、确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、原生权限模式、测试拥有临时存储中的显式危险绕过写入,以及 wrapper/原生整棵进程树退出。独立 wrapper fixture 会证明载荷缺失时不回退宿主命令,命名实例会保留彼此独立的模型、环境与模式,生产环境也不会从 `PATH` 解析宿主 `codex`。[最小诊断决策](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md)负责失败、进程结果与最终呈现证据。
+Codex 证据会锁定 `@openai/codex@0.153.4`、`codex-cli 0.153.4` 与六个平台 alias。生成 schema 会证明可选的 `ThreadStartParams.model`;真实产品测试会观测省略模型继承、两个显式实例模型、包内 wrapper argv、确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、原生权限模式、测试拥有临时存储中的显式危险绕过写入,以及 wrapper/原生整棵进程树退出。独立 wrapper fixture 会证明载荷缺失时不回退宿主命令,命名实例会保留彼此独立的模型、环境与模式,生产环境也不会从 `PATH` 解析宿主 `codex`。[最小诊断决策](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md)负责失败、进程结果与最终呈现证据。
 
 带密钥 Codex e2e 会注册生产提供方,启动同样的真实 app-server,并通过上述测试专用桥接层请求一个随机数。该测试固定外部端点与模型,不存储任何凭据或请求载荷,要求上游恰好完成一次响应,将去除首尾空白后的产品答案与该随机数逐字节比较,并等待所有受管句柄退出。
 
-Claude Code 证据会锁定 Agent SDK 0.3.241、Claude Code 2.1.241 与八个 SDK 平台包。真实产品测试会让 SDK 选择已安装载荷,断言共享子进程 argv 以该包的原生 CLI 开头,并观测省略模型继承、两个显式实例模型、确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、原生权限模式、测试拥有范围内的拒绝写入与 bypass 写入,以及整棵进程树退出。包测试还会证明生产运行从不解析宿主 `PATH`、省略可执行文件覆盖,并直接转发 SDK 所选的 Windows `claude.exe` 而不经过 batch shim。这项证据证明锁定的官方 SDK/CLI 集成,而不证明与独立安装的 Claude 版本兼容;[最小诊断决策](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md)负责失败与进程结果证据。Loader 覆盖会通过各自的可选 Bundle patch 解析两个产品,且不会启动任一产品。
+Claude Code 证据会锁定 Agent SDK 0.3.263、Claude Code 2.1.263 与八个 SDK 平台包。真实产品测试会让 SDK 选择已安装载荷,断言共享子进程 argv 以该包的原生 CLI 开头,并观测省略模型继承、两个显式实例模型、确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、原生权限模式、测试拥有范围内的拒绝写入与 bypass 写入,以及整棵进程树退出。包测试还会证明生产运行从不解析宿主 `PATH`、省略可执行文件覆盖,并直接转发 SDK 所选的 Windows `claude.exe` 而不经过 batch shim。这项证据证明锁定的官方 SDK/CLI 集成,而不证明与独立安装的 Claude 版本兼容;[最小诊断决策](../../archived/simplification/2026-08-21-product-subagent-minimal-diagnostics.md)负责失败与进程结果证据。Loader 覆盖会通过各自的可选 Bundle patch 解析两个产品,且不会启动任一产品。
 
 带密钥 Claude Code e2e 仅在提供方的内存环境中映射密钥与固定的官方端点,把模型变量设为文档所示的 `deepseek-v4-pro[1m]` 与 `deepseek-v4-flash`,并实际经过生产提供方、官方 SDK 与真实 CLI。它将去除首尾空白后的结果与一个随机数比较,并证明整棵进程树退出,且测试不会直接调用 Messages API。
 

+ 2 - 2
.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.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-15-product-subagent-noninteractive-permissions.md
-2026-08-15-product-subagent-noninteractive-permissions.md: 1bdca6214bf730386e27e922361bb45a3c120ecf
-2026-08-15-product-subagent-noninteractive-permissions.zh.md: 9377da8d5f3622f5faafce631ed2c6095d22560c
+2026-08-15-product-subagent-noninteractive-permissions.md: 1ec45f9adafa2364f9d0f37f3914d97054942226
+2026-08-15-product-subagent-noninteractive-permissions.zh.md: 80e99d9ee47da859456faf66560d42e73c88cf3d

+ 2 - 2
.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.md

@@ -32,7 +32,7 @@ Every query disables `AskUserQuestion`. Non-bypass permission callbacks deny ins
 
 ### Codex
 
-Codex defaults to `never` and accepts the three native non-interactive modes exposed by Codex 0.149.1. The Provider starts the fixed app-server command, then maps the selected mode into official `thread/start` fields because CLI-global permission flags do not configure threads created later by an app-server client:
+Codex defaults to `never` and accepts the three native non-interactive modes exposed by Codex 0.153.4. The Provider starts the fixed app-server command, then maps the selected mode into official `thread/start` fields because CLI-global permission flags do not configure threads created later by an app-server client:
 
 | Value | `thread/start` fields | Native behavior |
 | --- | --- | --- |
@@ -63,7 +63,7 @@ The foreground consumer presents the stop-reason headline, then the optional dia
 
 ## Verification
 
-Package tests pin every allowed and rejected Config value, the exact SDK and app-server field mappings, dangerous confirmations, unattended terminal responses, diagnostic sanitization and UTF-8 bound, successful-result omission, concurrent-run isolation, foreground ordering, Job detail, stderr observer disposal, and process cleanup. The real Claude Agent SDK 0.3.241 and Claude Code 2.1.241 fixture proves its safe default, restricted denial, explicit bypass, and whole-tree quiescence. The real Codex 0.149.1 app-server fixture proves that thread-level `never` overrides ambient `on-request`, automatic review starts, dangerous bypass writes only inside suite-owned temporary storage, a rejected escalation leaves no side effect or raw command or path in the diagnostic, stderr remains Host-only, and the wrapper/native tree exits. Loader composition proves non-default modes can be published without starting either product, and the keyless ACP snapshot records each product's failure diagnostic through foreground and Job presentation while the model-facing product tool schemas contain no permission parameter.
+Package tests pin every allowed and rejected Config value, the exact SDK and app-server field mappings, dangerous confirmations, unattended terminal responses, diagnostic sanitization and UTF-8 bound, successful-result omission, concurrent-run isolation, foreground ordering, Job detail, stderr observer disposal, and process cleanup. The real Claude Agent SDK 0.3.263 and Claude Code 2.1.263 fixture proves its safe default, restricted denial, explicit bypass, and whole-tree quiescence. The real Codex 0.153.4 app-server fixture proves that thread-level `never` overrides ambient `on-request`, automatic review starts, dangerous bypass writes only inside suite-owned temporary storage, a rejected escalation leaves no side effect or raw command or path in the diagnostic, stderr remains Host-only, and the wrapper/native tree exits. Loader composition proves non-default modes can be published without starting either product, and the keyless ACP snapshot records each product's failure diagnostic through foreground and Job presentation while the model-facing product tool schemas contain no permission parameter.
 
 ## Alternatives considered
 

+ 2 - 2
.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.zh.md

@@ -32,7 +32,7 @@ Claude Code 默认使用 `dontAsk`,而且只接受锁定版本 Agent SDK 支
 
 ### Codex
 
-Codex 默认使用 `never`,并接受 Codex 0.149.1 公开的三种原生非交互模式。提供方启动固定的 app-server 命令,再把所选模式映射为官方 `thread/start` 字段,因为 CLI 全局权限 flag 不会配置之后由 app-server 客户端创建的线程:
+Codex 默认使用 `never`,并接受 Codex 0.153.4 公开的三种原生非交互模式。提供方启动固定的 app-server 命令,再把所选模式映射为官方 `thread/start` 字段,因为 CLI 全局权限 flag 不会配置之后由 app-server 客户端创建的线程:
 
 | 值 | `thread/start` 字段 | 原生行为 |
 | --- | --- | --- |
@@ -63,7 +63,7 @@ Codex 默认使用 `never`,并接受 Codex 0.149.1 公开的三种原生非交
 
 ## Verification
 
-包测试固定所有允许与拒绝的 Config 值、准确的 SDK 与 app-server 字段映射、危险确认、无人值守终态、诊断脱敏与 UTF-8 上限、成功结果不携带诊断、并发运行隔离、前台顺序、Job detail、stderr observer 释放和进程清理。真实 Claude Agent SDK 0.3.241 与 Claude Code 2.1.241 fixture 证明其安全默认、受限拒绝、显式 bypass 与整棵进程树完全停稳。真实 Codex 0.149.1 app-server fixture 证明线程级 `never` 覆盖环境中的 `on-request`、自动评审可以启动、危险绕过只在测试拥有的临时存储中写入、被拒绝的提权不会留下副作用且诊断不含原始命令或路径、stderr 只供 Host 观测,而且 wrapper/native 进程树会退出。Loader 组装证明非默认模式可以在不启动任一产品的情况下发布;无密钥 ACP snapshot 则记录每个产品的失败诊断如何经过前台与 Job 呈现,同时面向模型的产品工具 schema 不包含权限参数。
+包测试固定所有允许与拒绝的 Config 值、准确的 SDK 与 app-server 字段映射、危险确认、无人值守终态、诊断脱敏与 UTF-8 上限、成功结果不携带诊断、并发运行隔离、前台顺序、Job detail、stderr observer 释放和进程清理。真实 Claude Agent SDK 0.3.263 与 Claude Code 2.1.263 fixture 证明其安全默认、受限拒绝、显式 bypass 与整棵进程树完全停稳。真实 Codex 0.153.4 app-server fixture 证明线程级 `never` 覆盖环境中的 `on-request`、自动评审可以启动、危险绕过只在测试拥有的临时存储中写入、被拒绝的提权不会留下副作用且诊断不含原始命令或路径、stderr 只供 Host 观测,而且 wrapper/native 进程树会退出。Loader 组装证明非默认模式可以在不启动任一产品的情况下发布;无密钥 ACP snapshot 则记录每个产品的失败诊断如何经过前台与 Job 呈现,同时面向模型的产品工具 schema 不包含权限参数。
 
 ## Alternatives considered
 

+ 2 - 2
.agents/notes/implemented/feature/2026-08-25-promote-open-anywhere-plugin.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-25-promote-open-anywhere-plugin.md
-2026-08-25-promote-open-anywhere-plugin.md: ee83c424d1454b26c1ce6cf6954105cdbfbb7419
-2026-08-25-promote-open-anywhere-plugin.zh.md: f1696cec10a683d44dcaa3db454d343821fc13c9
+2026-08-25-promote-open-anywhere-plugin.md: 83888cb548046cd8e023cd2b7c87f123cda0ed0f
+2026-08-25-promote-open-anywhere-plugin.zh.md: 4ef476ce7043d2dcee05dec9f604849741ce5e96

+ 6 - 2
.agents/notes/implemented/feature/2026-08-25-promote-open-anywhere-plugin.md

@@ -12,6 +12,8 @@ The community plugin `@dsh-plugins/open-anywhere` (gitlab.deepseek.com/Ciyou/dsh
 
 The first-party feature is named `open-in-app`: it selects the application that opens a workspace directory on the Harness host, not another machine or destination.
 
+The shared `launchedThroughSsh()` predicate in [launch-environment](../../../../packages/util/launch-environment/README.md) reads non-empty `SSH_CONNECTION` or `SSH_TTY` only from the inherited process layer. An SSH launch produces an empty application catalog before any probe. Project and user `.env` values cannot establish an SSH launch; Web browser handoff and the adaptive directory picker use the same predicate. The client hides the action even when it remembers a choice, and the existing availability checks reject icon and launch requests. SSH port forwarding changes HTTP reachability, not which machine owns the workspace or applications.
+
 The feature's first-party owners are `@deepseek-ai/dsh-host-open-in-app` at `packages/host/open-in-app/` (the probe, catalog, and launch routes) and `@deepseek-ai/dsh-client-ui-open-in-app` at `packages/client/ui-open-in-app/` (the split button), mounted in the Web profile by the `dsh-web-app` bundle rows `open-in-app` and `ui-open-in-app`. The promotion is a rewrite, not a vendoring:
 
 - **A host/client package pair, following the `directory-picker-browse`/`ui-directory-picker-browse` pairing**: the host package's `src/index.ts` registers the three HTTP routes on `ctx.webServer` (`GET /open-in-app/apps`, `GET /open-in-app/icon/<id>`, `POST /open-in-app/open`); the ui package's `src/client/index.ts` registers the split button into `conversation.session.header.utilities` through the standard slot/inject currency, with copy in a typed `open-in-app` locale namespace and styling in CSS Modules over `--dsw-*` tokens (the original's hand-injected style tag and inline dropdown are replaced by the `Menu` primitive), over an empty-apply node half that keeps the plugin on the host roster. Route paths and wire payload types have one home, the host package's browser-safe `./shared` subpath (constants and types only); the client bundle inlines it through an `INLINE_SAFE` entry in the client tsdown preset, the same channel `dsh-session`'s wire slices use. The host root exports only the Loader-required plugin values and types; catalog, resolver, launcher, and icon helpers remain source-internal.
@@ -28,6 +30,8 @@ The pair lives in `packages/host/` and `packages/client/` because that is what t
 
 ## Alternatives considered
 
+**Offer VS Code's remote CLI during SSH sessions.** Its installed executable does not prove a usable editor connection: the inherited IPC socket belongs to a live VS Code connection and can disappear while Harness keeps running. Browser-side SSH-target configuration and local editor handoff remain outside this host-application feature.
+
 **Vendor the plugin's `lib/` as-is under `packages/`.** Fastest, but the hand-authored JavaScript fails typecheck, coverage, i18n, JSDoc, and invariant gates wholesale; keeping it exempt would create a package class the repository deliberately does not have.
 
 **A Typert Remote instead of raw webServer routes.** The apps/open calls fit the Remote RPC shape, but the icon route serves binary PNGs, which the JSON RPC vocabulary does not carry; splitting icons onto a raw route while apps/open ride Remote gives two transports for one feature. Raw routes also match the original's client, and `webhook-github` establishes the validated-raw-route pattern.
@@ -50,8 +54,8 @@ The pair lives in `packages/host/` and `packages/client/` because that is what t
 
 ## Consequences
 
-- The Web profile gains the header button wherever the host probes at least one installed catalog application on macOS, Windows, or Linux, with zero rendering elsewhere (empty probed catalog → the component returns null).
+- Outside SSH sessions, the Web profile gains the header button wherever the host probes at least one installed catalog application on macOS, Windows, or Linux, with zero rendering elsewhere (empty probed catalog → the component returns null).
 - The community plugin's install path remains valid but redundant; its original routes and browser choice key are separate from `open-in-app`, so installations using the first-party feature should remove the community plugin to avoid duplicate header controls.
 - Resolution and icons run lazily, once per host process, so an application installed while dsh runs appears only after restart — accepted; the uninstall direction self-heals through the `ENOENT` single-entry refresh.
 - The catalog is compile-time fixed; extending it means editing `OPEN_IN_APP_CATALOG` and both locale dictionaries together (README Known Limitations). Platform coverage is uneven — several Git GUIs and terminals are macOS-only entries, Windows icons are limited to the 32px stock .NET extraction, Linux follows hicolor rather than the active theme, and CLI-only entries without a desktop record keep the generic icon.
-- Coverage: resolver logic (every locator kind over temp filesystems, registry-dump and desktop-entry fixtures, an injected env/home/PATH table), per-platform icon extraction, the three routes (real Loader + real WebServer composition, including the one-pass cache, the `ENOENT` refresh, and HMR-safety disposal), controller wire behavior, and component presentation are unit-tested to the per-file 100% gate; no snapshot is added because the shipped keyless snapshot fixtures assert session-driven output, which this browser-side control never touches. The web ARIA goldens disable the `open-in-app` and `ui-open-in-app` rows, and the Host-only preset e2e composition disables the host row: the button reflects whatever applications the running machine has installed, so its presence and label are host facts no cross-platform golden can pin.
+- Resolver, icon, route, controller, and component tests cover platform discovery, launch outcomes, the availability cache, and HMR disposal. The [SSH Web snapshot](../../../../snapshots/web/open-in-app-ssh/snapshot.yml) renders the shared recorded conversation with both Open In rows enabled and a remembered app choice, capturing only the Session header; composer and statistics output belong to their own snapshots. Inherited SSH markers make the empty catalog deterministic across platforms. Ordinary Web snapshots keep host-dependent application discovery disabled.

+ 6 - 2
.agents/notes/implemented/feature/2026-08-25-promote-open-anywhere-plugin.zh.md

@@ -12,6 +12,8 @@ Status: implemented
 
 第一方功能命名为 `open-in-app`:它选择在 Harness 主机上打开 workspace 目录的应用,不表示另一台机器或目的位置。
 
+[launch-environment](../../../../packages/util/launch-environment/README.zh.md) 中共用的 `launchedThroughSsh()` 只从继承的进程层读取非空 `SSH_CONNECTION` 或 `SSH_TTY`。SSH 启动时会在任何探测开始前返回空应用目录。项目与用户 `.env` 中的值不能作为 SSH 启动的依据;Web 浏览器唤起和自适应目录选择器共用此判断。即使客户端记住了应用选择,也会隐藏操作入口;已有的可用性检查会拒绝图标和启动请求。SSH 端口转发只改变 HTTP 可达性,不改变工作区或应用所属的机器。
+
 该功能的第一方归属是一对包:`@deepseek-ai/dsh-host-open-in-app` 位于 `packages/host/open-in-app/`(探测、目录与启动路由),`@deepseek-ai/dsh-client-ui-open-in-app` 位于 `packages/client/ui-open-in-app/`(分体按钮),由 `dsh-web-app` bundle 的 `open-in-app` 与 `ui-open-in-app` 两行挂载进 Web profile。转正是重写,不是 vendoring:
 
 - **一对 host/client 包,沿用 `directory-picker-browse`/`ui-directory-picker-browse` 的配对结构**:host 包的 `src/index.ts` 在 `ctx.webServer` 上注册三条 HTTP 路由(`GET /open-in-app/apps`、`GET /open-in-app/icon/<id>`、`POST /open-in-app/open`);ui 包的 `src/client/index.ts` 经标准 slot/inject 通货把分体按钮注册进 `conversation.session.header.utilities`,文案在类型化的 `open-in-app` locale 命名空间中,样式为 `--dsw-*` token 上的 CSS Modules(原插件手工注入的 style 标签与内联下拉被 `Menu` 原语替代),节点半边是让插件出现在主机名册上的空 apply。路由路径与 wire 载荷类型只有一个家:host 包浏览器安全的 `./shared` 子路径(只有常量与类型);client bundle 经 client tsdown preset 的 `INLINE_SAFE` 条目将其内联,与 `dsh-session` 各 wire 切片同一通道。host 根入口只导出 Loader 所需的插件实体与类型;目录、resolver、launcher 与图标 helper 保持源码内部可见。
@@ -28,6 +30,8 @@ Status: implemented
 
 ## 考虑过的替代方案
 
+**在 SSH 会话中提供 VS Code 的远端 CLI。** 已安装的可执行文件不能证明编辑器连接可用:继承的 IPC socket 属于一个仍在运行的 VS Code 连接,Harness 继续运行时它也可能消失。浏览器侧的 SSH 目标配置与本地编辑器唤起不属于这个主机应用功能。
+
 **将插件的 `lib/` 原样 vendor 进 `packages/`。** 最快,但手写 JavaScript 会整体不过 typecheck、覆盖率、i18n、JSDoc 和 invariant 门禁;为其保留豁免会造出仓库刻意不设的包类别。
 
 **用 Typert Remote 而非裸 webServer 路由。** apps/open 调用符合 Remote RPC 形态,但 icon 路由提供二进制 PNG,JSON RPC 词汇承载不了;把 icon 拆去裸路由而 apps/open 走 Remote 会让一个功能有两种传输。裸路由也匹配原插件的客户端,且 `webhook-github` 已确立带校验裸路由的先例。
@@ -50,8 +54,8 @@ Status: implemented
 
 ## 后果
 
-- 只要主机在 macOS、Windows 或 Linux 上探测到至少一个已安装的目录应用,Web profile 就会出现头部按钮;其余情况零渲染(探测目录为空 → 组件返回 null)。
+- 非 SSH 会话中,只要主机在 macOS、Windows 或 Linux 上探测到至少一个已安装的目录应用,Web profile 就会出现头部按钮;其余情况零渲染(探测目录为空 → 组件返回 null)。
 - 社区插件的安装路径仍然有效但已冗余;其原始路由与浏览器选择键独立于 `open-in-app`,因此使用第一方功能的安装应移除社区插件,避免出现重复的头部控件。
 - 解析与图标每主机进程惰性执行一次,dsh 运行期间安装的应用要重启后才出现——接受;卸载方向经 `ENOENT` 单条目刷新自愈。
 - 目录在编译期固定;扩展它意味着同时编辑 `OPEN_IN_APP_CATALOG` 与两份 locale 词典(README 已知限制)。平台覆盖不均——若干 Git GUI 与终端仅有 macOS 条目;Windows 图标受限于 .NET 标准接口的 32px 提取,Linux 跟随 hicolor 而非当前主题,没有 desktop 记录的纯 CLI 条目则保留通用图标。
-- 覆盖:resolver 逻辑(每种 locator 在临时文件系统上、注册表转储与 desktop 条目 fixture、注入的 env/home/PATH 表)、逐平台图标提取、三条路由(真实 Loader + 真实 WebServer 组合,含单趟缓存、`ENOENT` 刷新与 HMR 安全处置)、controller wire 行为和组件呈现都以逐文件 100% 门禁做了单元测试;不新增 snapshot,因为随仓库发布的免密 snapshot fixture 断言会话驱动的输出,而这个纯浏览器侧控件不触及它。Web ARIA golden 禁用 `open-in-app` 与 `ui-open-in-app` 两行,Host-only 的 preset e2e 组合禁用 host 行:按钮反映运行机器实际安装了哪些应用,其出现与否和标签都是主机事实,跨平台 golden 无法钉住
+- 解析器、图标、路由、控制器与组件测试覆盖平台探测、启动结果、可用性缓存和 HMR 处置。[SSH Web 快照](../../../../snapshots/web/open-in-app-ssh/snapshot.yml) 在启用两个 Open In 配置项并记住应用选择的条件下渲染共享的录制会话,并仅捕获会话头部;输入框和统计栏由各自的快照负责。继承的 SSH 标记使空应用目录在不同平台上保持确定。普通 Web 快照仍禁用依赖主机的应用探测

+ 2 - 2
.agents/notes/implemented/feature/2026-08-31-cross-process-session-write-lease.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-31-cross-process-session-write-lease.md
-2026-08-31-cross-process-session-write-lease.md: 174c5152ea62e01e30ade9a68b6786638acb8ada
-2026-08-31-cross-process-session-write-lease.zh.md: e4246f12f7ed8d8b304ca7f7514117f03f32267b
+2026-08-31-cross-process-session-write-lease.md: ef8ebe2de6b231075dee31a9184bcc0a5b323011
+2026-08-31-cross-process-session-write-lease.zh.md: 8a79b6d5327c7bd5d0ca12ac435140bc0951e993

+ 3 - 3
.agents/notes/implemented/feature/2026-08-31-cross-process-session-write-lease.md

@@ -10,7 +10,7 @@ The JSONL backend's write-handle claim excluded a second writer only inside one
 
 ## Decision
 
-`SessionWriteLease` (packages/session/session-persistence-jsonl/src/lease.ts) holds a kernel lock on `session.lock` beside the log for the whole life of a write handle: POSIX takes a non-blocking `flock(2)` through the pinned native dependency `fs-ext`, and Windows holds a named kernel semaphore (count 1) derived from the canonical lock path (`CreateSemaphoreW` in src/win32.ts beside the existing koffi bindings) — a kernel object with no filesystem footprint, destroyed with its last handle. Contention maps to `SessionAlreadyOwnedError`; the kernel releases the lock when the holder's descriptor or handle closes, including on any process death, so a crashed holder never blocks a successor and no expiry bookkeeping exists. A live but wedged holder keeps the lock until its process exits: expropriating a stalled writer was rejected because its resumed appends would tear the log, and on POSIX removing the lock file remains the explicit forfeit for that case. Because a POSIX lock names an inode rather than a path, acquisition verifies the locked inode is still the file at the lock path and retries otherwise. The lock is taken at write-open of an existing artifact and, for a created session, only right before its first materializing write — an unmaterialized session leaves no filesystem footprint, and a handle that acquired the lock keeps it through close even when materialization fails; release never removes the lock file, preserving the stable inode later lockers verify against. The browser worker deployment stubs fs-ext to immediate success: it is single-process, so the in-process write claim already excludes every writer.
+`SessionWriteLease` (packages/session/session-persistence-jsonl/src/lease.ts) holds a kernel lock on `session.lock` beside the log for the whole life of a write handle: POSIX takes a non-blocking `flock(2)` through the prebuilt `@deepseek-ai/node-addon-system/flock` binding, and Windows holds a named kernel semaphore (count 1) derived from the canonical lock path (`CreateSemaphoreW` in src/win32.ts beside the existing koffi bindings) — a kernel object with no filesystem footprint, destroyed with its last handle. Contention maps to `SessionAlreadyOwnedError`; the kernel releases the lock when the holder's descriptor or handle closes, including on any process death, so a crashed holder never blocks a successor and no expiry bookkeeping exists. A live but wedged holder keeps the lock until its process exits: expropriating a stalled writer was rejected because its resumed appends would tear the log, and on POSIX removing the lock file remains the explicit forfeit for that case. Because a POSIX lock names an inode rather than a path, acquisition verifies the locked inode is still the file at the lock path and retries otherwise. The lock is taken at write-open of an existing artifact and, for a created session, only right before its first materializing write — an unmaterialized session leaves no filesystem footprint, and a handle that acquired the lock keeps it through close even when materialization fails; release never removes the lock file, preserving the stable inode later lockers verify against. The browser worker deployment stubs the flock entry to immediate success: it is single-process, so the in-process write claim already excludes every writer.
 
 ## Alternatives considered
 
@@ -22,8 +22,8 @@ The JSONL backend's write-handle claim excluded a second writer only inside one
 
 **Windows exclusive-open sharing mode (`CreateFileW` denying `FILE_SHARE_WRITE`)** — leaves readers untouched but pins the lock file's name and directory while held: CI showed dozens of suites failing their temp-root cleanup with EBUSY because a still-open handle blocks recursive removal, and users deleting a session directory would hit the same wall. The named semaphore keeps kernel arbitration with zero filesystem footprint.
 
-**Hand-rolled ffi for POSIX too (`flock(2)` via koffi)** — avoids the node-gyp install-time build, but means owning both platform lock implementations plus their error mapping; `fs-ext` ships the POSIX code maintained and pinned, and the Windows side reuses the koffi bindings `win32.ts` already owns.
+**Hand-rolled ffi for POSIX too (`flock(2)` via koffi)** — binding selection and asynchronous errno handling are governed by the [prebuilt system primitives decision](../architecture/2026-09-07-prebuilt-system-primitives.md). The Windows side retains the koffi bindings `win32.ts` already owns.
 
 ## Consequences
 
-Cross-process exclusion costs a node-gyp-compiled native dependency (`fs-ext`, allow-listed in `pnpm-workspace.yaml` `allowBuilds`), one lock file per materialized session that release deliberately leaves in place, and the wedged-holder rule: a stuck process blocks that session's writers until it exits. It buys immediate crash recovery (no waiting period), no renewal traffic, and the removal of every takeover race the TTL design managed rather than prevented. Advisory `flock` is unreliable on some network filesystems (NFSv3); a root on such a mount degrades toward in-process-only exclusion. Deleting a live session's lock file forfeits exclusion on POSIX by design — the harness never does so; the agent-loop resume test uses it deliberately to simulate a wedged first lifecycle, and skips on Windows, where the lock is a kernel object no file operation can forfeit.
+Cross-process exclusion requires the platform's prebuilt system binding, one lock file per materialized session that release deliberately leaves in place, and the wedged-holder rule: a stuck process blocks that session's writers until it exits. It buys immediate crash recovery (no waiting period), no renewal traffic, and the removal of every takeover race the TTL design managed rather than prevented. Advisory `flock` is unreliable on some network filesystems (NFSv3); a root on such a mount degrades toward in-process-only exclusion. Deleting a live session's lock file forfeits exclusion on POSIX by design — the harness never does so; the agent-loop resume test uses it deliberately to simulate a wedged first lifecycle, and skips on Windows, where the lock is a kernel object no file operation can forfeit.

+ 3 - 3
.agents/notes/implemented/feature/2026-08-31-cross-process-session-write-lease.zh.md

@@ -10,7 +10,7 @@ JSONL 后端的写句柄认领只在单个后端实例内部排除第二个写
 
 ## Decision
 
-`SessionWriteLease`(packages/session/session-persistence-jsonl/src/lease.ts)在日志旁的 `session.lock` 上持有内核锁,贯穿写句柄的整个生命期:POSIX 经由固定版本的原生依赖 `fs-ext` 以非阻塞 `flock(2)` 加锁,Windows 持有由规范锁路径派生的命名内核信号量(计数 1,`CreateSemaphoreW`,实现在 src/win32.ts 既有 koffi 绑定旁)——零文件系统足迹的内核对象,随最后一个句柄关闭而销毁。竞争映射为 `SessionAlreadyOwnedError`;持有者的描述符或句柄关闭时内核释放锁,包括任何形式的进程死亡,因此崩溃的持有者从不阻塞后继者,也不存在任何过期簿记。活着但卡死的持有者保有锁直到其进程退出:剥夺停顿写入方的所有权被否决,因为其复活后的追加会撕坏日志;POSIX 上删除锁文件仍是该场景的显式放弃手段。由于 POSIX 锁指向 inode 而非路径,获取后会校验所锁 inode 仍是锁路径上的文件,否则重试。锁在写打开既有工件时立即获取,新建会话则仅在首次物化写入之前获取——未物化的会话不留任何文件系统足迹,已取得锁的句柄即使物化失败也保有锁直到关闭;释放从不删除锁文件,保住后续加锁者用于校验的稳定 inode。浏览器 worker 部署将 fs-ext 存根为立即成功:它是单进程部署,进程内写认领已排除所有写入方。
+`SessionWriteLease`(packages/session/session-persistence-jsonl/src/lease.ts)在日志旁的 `session.lock` 上持有内核锁,贯穿写句柄的整个生命期:POSIX 经由预编译 `@deepseek-ai/node-addon-system/flock` 绑定 以非阻塞 `flock(2)` 加锁,Windows 持有由规范锁路径派生的命名内核信号量(计数 1,`CreateSemaphoreW`,实现在 src/win32.ts 既有 koffi 绑定旁)——零文件系统足迹的内核对象,随最后一个句柄关闭而销毁。竞争映射为 `SessionAlreadyOwnedError`;持有者的描述符或句柄关闭时内核释放锁,包括任何形式的进程死亡,因此崩溃的持有者从不阻塞后继者,也不存在任何过期簿记。活着但卡死的持有者保有锁直到其进程退出:剥夺停顿写入方的所有权被否决,因为其复活后的追加会撕坏日志;POSIX 上删除锁文件仍是该场景的显式放弃手段。由于 POSIX 锁指向 inode 而非路径,获取后会校验所锁 inode 仍是锁路径上的文件,否则重试。锁在写打开既有工件时立即获取,新建会话则仅在首次物化写入之前获取——未物化的会话不留任何文件系统足迹,已取得锁的句柄即使物化失败也保有锁直到关闭;释放从不删除锁文件,保住后续加锁者用于校验的稳定 inode。浏览器 worker 部署将 flock 入口存根为立即成功:它是单进程部署,进程内写认领已排除所有写入方。
 
 ## Alternatives considered
 
@@ -22,8 +22,8 @@ JSONL 后端的写句柄认领只在单个后端实例内部排除第二个写
 
 **Windows 共享模式独占打开(`CreateFileW` 拒绝 `FILE_SHARE_WRITE`)** —— 读者不受影响,但持有期间钉住锁文件的名字与目录:CI 显示数十个套件的临时根清理因仍打开的句柄阻塞递归删除而报 EBUSY,用户删除会话目录也会撞上同一堵墙。命名信号量保住内核仲裁,且文件系统足迹为零。
 
-**POSIX 也手写 ffi(经 koffi 调 `flock(2)`)** —— 免去 node-gyp 安装期编译,但意味着自有两个平台的锁实现及其错误映射;`fs-ext` 交付了有维护、可固定版本的 POSIX 侧,Windows 侧复用 `win32.ts` 已自有的 koffi 绑定。
+**POSIX 也手写 ffi(经 koffi 调 `flock(2)`)** —— 绑定选择与异步 errno 处理由[预编译系统原语决策](../architecture/2026-09-07-prebuilt-system-primitives.zh.md)规定。Windows 侧保留 `win32.ts` 已有的 koffi 绑定。
 
 ## Consequences
 
-跨进程排他的代价是一个 node-gyp 编译的原生依赖(`fs-ext`,已在 `pnpm-workspace.yaml` 的 `allowBuilds` 列入允许)、每个物化会话一个由释放刻意留下的锁文件,以及卡死持有者规则:卡住的进程阻塞该会话的写入方直到其退出。它换来的是即时崩溃恢复(无等待期)、零续约流量,以及删除了 TTL 设计只能"管理"而非"消除"的全部接管竞态。咨询式 `flock` 在部分网络文件系统(NFSv3)上不可靠;位于此类挂载上的根目录会退化为仅进程内排他。POSIX 上删除活跃会话的锁文件按设计即放弃排他——harness 自身从不这样做;agent-loop 的 resume 测试刻意用它模拟卡死的第一个生命周期,并在 Windows 上跳过:那里的锁是任何文件操作都无法放弃的内核对象。
+跨进程排他需要对应平台的预编译系统绑定、每个物化会话一个由释放刻意留下的锁文件,以及卡死持有者规则:卡住的进程阻塞该会话的写入方直到其退出。它换来的是即时崩溃恢复(无等待期)、零续约流量,以及删除了 TTL 设计只能"管理"而非"消除"的全部接管竞态。咨询式 `flock` 在部分网络文件系统(NFSv3)上不可靠;位于此类挂载上的根目录会退化为仅进程内排他。POSIX 上删除活跃会话的锁文件按设计即放弃排他——harness 自身从不这样做;agent-loop 的 resume 测试刻意用它模拟卡死的第一个生命周期,并在 Windows 上跳过:那里的锁是任何文件操作都无法放弃的内核对象。

+ 6 - 0
.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.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-02-in-history-system-prompt-replacement.md
+2026-09-02-in-history-system-prompt-replacement.md: b34a0d2b3591c1b62aba16d79963940be787f373
+2026-09-02-in-history-system-prompt-replacement.zh.md: 296932a977ae852c4ef32de1c23b2c5d9e4a1bbf

+ 97 - 0
.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.md

@@ -0,0 +1,97 @@
+# Agent Note: In-history system prompt replacement for cache-stable prompt changes
+
+Status: implemented
+
+English | [中文](2026-09-02-in-history-system-prompt-replacement.zh.md)
+
+## Problem
+
+Every system prompt change costs the whole provider prefix cache. The loop renders the prompt on every step; when the bytes differ — a plan-mode section entering or leaving, a skill or tool guidance section registering, an agent-scoped persona shadow, a changed `{{model}}` variable — the request's message 0 changes and the DeepSeek context cache misses from the first token. Long agentic sessions pay this repeatedly, and the [runtime-context snapshot design](../../archived/feature/2026-07-30-current-sandbox-policy-context.md) exists precisely because moving a changing fact out of the prompt was the only way to keep the prefix stable.
+
+A DeepSeek model, recorded here as a model fact supplied for this work, removes that constraint: it accepts a `system` message at any position of the conversation and treats the latest one as the complete effective system prompt, replacing the leading one. Tool schemas remain part of the cached prefix, so a tool-set change still invalidates the cache. With that model the harness can append the new prompt after the cached history instead of rewriting message 0, and the prefix stays warm.
+
+The harness has the representation for this because the [system prompt is surface node 0](../architecture/2026-09-02-system-prompt-as-surface-node.md): a prompt change is an operation on `system/message` surface nodes, and the choice between "replace the latest system node" and "append a new node" is a per-route decision.
+
+## Decision
+
+For a model route that declares the capability, the loop appends a new `system/message` surface node instead of replacing the latest system node when the rendered prompt changes and the prefix would otherwise survive. Everything else in the [surface-node decision](../architecture/2026-09-02-system-prompt-as-surface-node.md) is unchanged: the event type, the projection owner, the serializers, and the node 0 head protection.
+
+### Capability
+
+`dsh-llm` defines `SystemPromptUpdate = 'in-history'` and carries it as an optional sibling field, `systemPromptUpdate`, on `LlmResolvedModelInfo` and `PreparedLlmCall`; `normalizeModelInfo` rejects any other value with an `LlmError` whose code is `INVALID_MODEL_INFO`. The DeepSeek adapter's catalog model (`DeepSeekCatalogModel.systemPromptUpdate`, validated by zod at load) and the replay provider's `ReplayModelConfig.systemPromptUpdate` declare it per model; absence means the model needs message 0 rewritten. No default catalog entry declares it; a deployment enables it through the `models` list in `cordis.yml`, and every `dsh-llm-pi-ai` route keeps the replace behaviour.
+
+The loop records the mode in the session: `RequestContext.systemPromptUpdate` joins provider, model, and capacity as a `request/context` field, logged whenever any of them differs from the latest snapshot. Admission reads `PreparedLlmCall.systemPromptUpdate` from the actual call prepared after `agent/request`; the preceding snapshot is not an admission input. First requests, resumed sessions, route changes, and same-route capability changes therefore use the capability of the bound adapter that will serve the call.
+
+### The decision rule
+
+`SystemPromptProjection.project(rendered, { inHistory, startsSeries })` in `packages/core/agent-loop/src/runtime-context.ts` scans the surviving `system/message` nodes of the current surface on every call. It returns ordered per-node commits. With no surviving system node it reserves the head even for an empty rendering. Effective text comes from the latest non-empty system node, falling back to the head; dormant empty tails neither supply effective text nor need another empty replacement. An empty rendering clears every active system node, regardless of route or series state. For a non-empty rendering on an incapable route or at a new request series, consolidation applies even when the effective text is unchanged. Otherwise matching effective text emits nothing. The operations are:
+
+| Route capability | Prefix state | Operation |
+|---|---|---|
+| none | non-empty rendering, any prefix state | log an empty replacement for each non-empty later system node, then rewrite the first system node with the rendering if needed |
+| `in-history` | the current request series continues | append a new `system/message` before the step's `user/message` events; the append alone needs no `request/header` |
+| `in-history` | non-empty rendering, a new series starts | log empty replacements for non-empty later system nodes, then rewrite the first system node if needed, even when the latest effective text is unchanged |
+| any | the rendered prompt is empty | log empty replacements for non-empty later system nodes, then empty the head if needed; no prompt version remains in derived messages |
+
+`startsSeries` is true when the `agent/pre-step` decision declares `startsRequestSeries`, when the surface replace generation moved since the last request (a compaction or any other replacement), or when the visible tool-schema set changed. A provider or model swap alone is not a series start for this rule: on a capable destination route the changed prompt is appended, which costs nothing because the route change already misses the cache. A series start already costs the cache, so consolidation keeps only the current prompt in model history. Logged per-node empty replacements remove later prompts from derived messages without a surface delete operation or any replacement of intervening conversation nodes. This also keeps compaction recovery from appending a system update after users already admitted by the failed attempt.
+
+The first attempt admits the prompt after assembly, an accepted `agent/pre-step` decision, `step/start`, the `agent/request` waterfall, and `prepareCall()`. A rejected or empty first input opens no step. Neither async request phase commits the pending system prompt or accepted users, and cancellation during either commits neither. Every attempt synchronously reconciles the same rendered assembly after its own `agent/request` and `prepareCall()`, appends the accepted user batch only on the first attempt, logs header/context as needed, and derives and freezes the request before streaming through the same prepared call. Retries do not repeat assembly, `agent/pre-step`, or user admission. Reconciliation sees both pre-step compaction (`compaction-basic` with `auto: true`) and recovery compaction, and consolidates non-empty prompt text at the head when either starts a new series. Resume is series-continuing — the `resume` header is not a series start — so a prompt that changed across a restart is appended; the provider cache may still be warm across a process boundary.
+
+An empty head with no active later system node represents no prompt. Dormant empty tails do not supply effective text, so repeated clearing and resume cannot resurrect an older prompt. Restoring non-empty text uses the same admission rule: a continuing capable route may append it; an incapable route or a new series refills the head. Clearing uses ordinary per-node replacements, not a surface delete or an initial empty-head creation.
+
+### Presentation and accounting
+
+Web presents an appended in-history node at its own position. `SystemPromptNode` carries `{ seq, time, turn, step, text, update }`, `update` being true for an appended `system/message` that follows an earlier system node in the loaded window. Chat renders a non-empty update as a collapsed `system-prompt` card titled by the locale key `message.systemPromptUpdate`, and a `request/header` in the same turn and step does not repeat the prompt card; `inspectRequestPrompt` reports no system change for a header that follows an update. Trajectory folds an update following a loaded request header into a synthetic request-header fact with `promptChange.kind = 'system'`, so later requests show the effective prompt without a real header change. When the loaded window lacks the earlier system node, the update is presented as an initial prompt. Transcript projections skip it like every `system/message`.
+
+`dsh-token-meter` prices the last nonempty surviving system node in surface order as `contextBreakdown.systemTokens`; every other visible node, including superseded prompts, contributes to `messageTokens`. Empty dormant nodes are ignored. The sum equals the fixed-heuristic surface total after every replacement, whether or not a shadow-price claim exists. Compact retained entries reuse the measurement surface planner: state and transitions cost O(current retained surface), not O(1) or O(total historical log). Replaced entries and message bodies are discarded, and state version 4 rejects scalar checkpoints. `cacheReadTokens` on subsequent assistant usage remains the observable provider-cache effect.
+
+Trajectory chooses the newer of the preceding real header and the preceding synthetic system header as the comparison state. A real header owns configuration and tools; an appended prompt can advance that state without another real header. Comparing only real headers would report A rather than B as the previous prompt for an A → B → C sequence.
+
+Chat and Trajectory interpret the effective prompt through the pure `uiConversation.inspectSystemPrompt` operation. Each target keeps immutable prefix states for system events and positional replacements, with only surviving system nodes and a map of surviving replacement sequences to inherited surface positions. Each replacement copies that map and removes shadowed entries; historical prefix maps remain immutable. Ordinary appends and streaming updates require no prompt fold. Surface order, rather than event order or provenance citations, determines which nodes survive: a compaction can restore an older prompt without another system event, and a head rewrite can have a greater sequence than a later active prompt. Empty nodes remain addressable but do not override a nonempty prompt. An endpoint older than the earliest relevant loaded event has unknown order unless its replacement position is indexed. The interpreter withholds all subsequent prompt text after such an endpoint until prepend replay resolves the missing prefix; numeric event order cannot establish surface order. Historical cards keep their own prefix state rather than reading the final surface.
+
+### Compaction
+
+`compaction-basic` is unchanged. `selectCompactableRange` still anchors at the first non-system node, so node 0 is never shadowed and later in-history nodes can be; `buildSummarizationInput` prepends the derived head to `messages`, followed by every shadowed node's derived message in surface order, so a mid-region system node is replayed in place and the summarization call remains a genuine prefix of the conversation.
+
+## Alternatives considered
+
+**Send only the changed sections as a delta.** The model treats the latest system message as the complete prompt, so a delta would silently drop every unchanged section. Rejected on the model contract.
+
+**Enable in-history mode by plugin config instead of a model capability.** A deployment flag could pair a non-capable model with appended system messages, which such a model would read as ordinary history at best. The capability belongs to the route that honours it; the adapter catalog already carries per-model capacities. Rejected.
+
+**Always append, never re-baseline.** One rule, but node 0 would stay stale for the life of the session and every request after compaction would carry the stale head plus the replacement. Re-baselining at a series start costs nothing extra because the cache is already lost there. Rejected.
+
+**Re-baseline on every resume.** Accepts one cache miss per process restart for a simpler resume path. The cache persists across restarts for hours to days, and the log already carries what resume needs. Rejected.
+
+**Place the system message after the step's user messages.** Both positions sit after the cached prefix, but the model then reads the instructions after the input it must apply them to; system-before-user matches the leading position's ordering. Rejected.
+
+**Project the prompt before the `agent/pre-step` waterfall.** The projection would not see a compaction performed inside the waterfall, so a just-appended node could be shadowed in the same step and the request would carry node 0's stale prompt as the only system message. Projecting after the waterfall keeps the rule a pure function of the surface the request is built from. Rejected.
+
+**Use the preceding request context for admission.** It describes the previous call, not the adapter bound after request middleware. It can select the wrong prompt representation on the first call, after resume, or after a route or capability change. Resolving before prompt and user commits also keeps cancellation from admitting unsent content. Rejected.
+
+**Treat a provider or model swap as a series start.** It would fold the prompt into node 0 on every route change, matching the tools case. The header already records the change and the cache misses either way, so the extra rule bought nothing but a special case in the loop. Rejected.
+
+**Clear only the latest system node.** Empty nodes project to no message, so an older prompt would become effective again. Clearing all active versions preserves the meaning of an empty rendering without deleting conversation history. Rejected.
+
+**Keep only scalar totals or prompt ancestry.** A scalar shadow price cannot identify which bucket lost the newest prompt or restore its predecessor. Prompt-only entries cannot locate arbitrary nonprompt replacement endpoints; `sourceEventSeqs` may also cite surviving prompts, and event sequence order differs from surface order after rewrites. Retaining compact current surface entries reuses the existing planner without full-log access, a second validator, or consumer-specific durable events. Summing all surviving prompts as system tokens would change the intended effective-prompt meaning rather than fix classification.
+
+## Consequences
+
+- A prompt change on a capable route keeps the provider prefix cache; the appended node costs its own tokens on every request in the series until compaction shadows it. A deployment whose prompt changes on most steps is better served by moving that fact into runtime context.
+- The request head is not the only place a system prompt can live: readers of "what did the model see" fold the surface and take the latest system node, and the breakdown's system figure follows the same rule.
+- A `request/context` snapshot records the prepared route and declared mode; it describes admission rather than deciding it. Incapable-route consolidation is logged per system node, preserving intervening user, assistant, and tool history.
+- The model contract is recorded as supplied. If a released model narrows it — for example honouring only the latest system message within a bounded window — the rule needs a re-baseline trigger beyond series starts.
+- A proxy that rewrites or reorders system messages breaks the replacement semantics silently; the real-API e2e's cache-hit assertion is the detector.
+
+## Testing
+
+Lifecycle verification requires no event for an unchanged prompt and an appended changed prompt after resume on a capable route. Both TypeScript and Python SDK expected outputs must include the typed appended `system/message` event, as required by the [SDK snapshot policy](../../../../docs/testing.md). The [TypeScript SDK notifications](../../../../snapshots/sdk/system-prompt-in-history/notifications.expected.jsonl) and [Python SDK prompt history](../../../../scripts/snapshots/python-sdk-single-exe/minimal-in-history/prompt-history.json) record the appended prompt event and retained prompt versions.
+
+- `packages/core/agent-loop/tests/system-prompt-admission.spec.ts` covers capable-to-incapable routing with changed or unchanged text, incapable-to-capable routing, resumed-route admission, cancellation in request middleware or preparation, and a concurrent selection change while the prepared route stays bound. Retry-compaction cases shadow the latest prompt with or without an earlier surviving update and verify reuse of the admitted assembly, one user admission, and no extra series header on an unchanged retry. Clear cases on capable and incapable routes remove three active prompt versions, keep repeated requests and seeded resume empty without extra prompt events, and restore only the new text; log reconstruction and the pi converter retain no old instructions. Focused coverage of `src/agent.ts` and `src/runtime-context.ts` reaches 100% for statements, branches, functions, and lines.
+- `packages/core/agent-loop/tests/system-prompt-projection.spec.ts` pins the append on a continuing series, the re-baseline at a series start with or without surviving later nodes and with changed or unchanged effective text, the empty-prompt clearing of all active versions, and the replace-only behaviour without the capability.
+- `packages/core/agent-loop/tests/request-reconstruction.spec.ts` pins the appended node under an inherited header with `request/context` carrying `systemPromptUpdate`, the series-start fold into node 0, the compaction-driven re-baseline, and the tool-schema change re-baseline under a `change` header that starts a series.
+- `packages/llm/llm/tests/service.spec.ts`, `packages/llm/llm-deepseek/tests/adapter.spec.ts`, and `packages/test-support/llm-replay/tests/llm-replay.spec.ts` pin the declared mode on resolved model info and the load-time rejection of any other value.
+- `packages/llm/token-meter/tests/context-breakdown-projection.spec.ts` pins newest/middle prompt removal, exact heuristic totals, surface ordering after head rewrites, extra provenance citations, dormant empties and fallback clears, immutable transitions, compact retained checkpoints, late registration, replay, and version invalidation.
+- `packages/client/ui-conversation`, `ui-chat`, and `ui-trajectory` client specs pin the update card, the same-step header dedupe, the absent system change after an update, and the synthetic trajectory header.
+- The keyless authored snapshot `snapshots/session/system-prompt-in-history/` declares the capability on the replay route, changes the prompt after the first tool call through a fixture section, and pins the appended `system/message`, the untouched node 0, the single `request/header`, and the `request/context` mode.
+- `packages/llm/llm-deepseek/tests/adapter.e2e.ts` runs a two-step prompt change against the model named by `DEEPSEEK_IN_HISTORY_MODEL`, asserts that the reply follows the appended prompt, and asserts that the appended request reads more cached tokens than the same conversation with a rewritten leading prompt; it skips when the variable is unset.

+ 97 - 0
.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.zh.md

@@ -0,0 +1,97 @@
+# Agent Note: 历史内系统提示词替换,实现缓存稳定的提示词变更
+
+Status: implemented
+
+[English](2026-09-02-in-history-system-prompt-replacement.md) | 中文
+
+## Problem
+
+每一次系统提示词变更都要付出整个提供方前缀缓存的代价。循环在每个步骤渲染提示词;一旦字节不同——plan 模式片段进入或退出、某个 skill 或工具指引片段完成注册、agent 作用域的 persona 遮蔽、`{{model}}` 变量改变——请求的消息 0 随之改变,DeepSeek 上下文缓存从第一个 token 起失效。长时间的 agent 会话反复为此付费,而[运行时上下文快照设计](../../archived/feature/2026-07-30-current-sandbox-policy-context.md)之所以存在,正是因为把会变化的事实移出提示词是保持前缀稳定的唯一办法。
+
+一个 DeepSeek 模型——在此按为本项工作提供的模型事实记录——移除了这一限制:它接受对话任意位置的 `system` 消息,并把最新一条视为完整的有效系统提示词,替换最前面那条。工具 schema 仍属于被缓存的前缀,因此工具集变更仍会使缓存失效。有了这样的模型,harness 可以把新提示词追加到已缓存的历史之后而不是重写消息 0,前缀就能保持热态。
+
+因为[系统提示词是 surface 第 0 号节点](../architecture/2026-09-02-system-prompt-as-surface-node.zh.md),harness 拥有实现这一点的表示:提示词变更是对 `system/message` surface 节点的操作,而「替换最新的系统节点」与「追加新节点」之间的选择是逐路由的决定。
+
+## Decision
+
+对于声明了该能力的模型路由,当渲染后的提示词变化且前缀本可存活时,循环追加一个新的 `system/message` surface 节点而不是替换最新的系统节点。[surface 节点决策](../architecture/2026-09-02-system-prompt-as-surface-node.zh.md)中的其他一切不变:事件类型、投影的拥有者、序列化器,以及第 0 号节点的头部保护。
+
+### 能力
+
+`dsh-llm` 定义 `SystemPromptUpdate = 'in-history'`,并把它作为可选的并列字段 `systemPromptUpdate` 放在 `LlmResolvedModelInfo` 与 `PreparedLlmCall` 上;`normalizeModelInfo` 用代码为 `INVALID_MODEL_INFO` 的 `LlmError` 拒绝任何其他值。DeepSeek 适配器的目录模型(`DeepSeekCatalogModel.systemPromptUpdate`,加载时由 zod 校验)与回放提供者的 `ReplayModelConfig.systemPromptUpdate` 逐模型声明它;缺省表示该模型需要重写消息 0。没有默认目录条目声明它;部署方通过 `cordis.yml` 的 `models` 列表启用,所有 `dsh-llm-pi-ai` 路由保持替换行为。
+
+循环把该模式记录进会话:`RequestContext.systemPromptUpdate` 与 provider、model、容量并列成为 `request/context` 的字段,其中任一项与最新快照不同时就记录一次。准入读取 `agent/request` 之后实际准备调用的 `PreparedLlmCall.systemPromptUpdate`;先前快照不是准入输入。因此首次请求、恢复的会话、路由变更以及同一路由的能力变更,都使用将服务该调用的绑定适配器的能力。
+
+### 决策规则
+
+`packages/core/agent-loop/src/runtime-context.ts` 中的 `SystemPromptProjection.project(rendered, { inHistory, startsSeries })` 每次调用都扫描当前 surface 上存活的 `system/message` 节点。它返回有序的逐节点提交。没有存活的系统节点时,即使渲染文本为空也预留头节点。有效文本取自最新的非空系统节点,没有时回退到头节点;未生效的空尾节点既不提供有效文本,也无需再次以空内容替换。无论路由或序列状态如何,空渲染文本都会清除每个生效的系统节点。不具备能力的路由或新请求序列面对非空渲染文本时,即使有效文本未变也执行归并。除此之外,有效文本相同时不产生事件。具体操作如下:
+
+| 路由能力 | 前缀状态 | 操作 |
+|---|---|---|
+| 无 | 非空渲染文本,任意前缀状态 | 为每个非空的后续系统节点记录空内容替换,随后按需用渲染文本重写首个系统节点 |
+| `in-history` | 当前请求序列延续 | 在该步骤的 `user/message` 事件之前追加新的 `system/message`;仅追加本身不需要记录 `request/header` |
+| `in-history` | 非空渲染文本,新序列开始 | 为非空的后续系统节点记录空内容替换,再按需重写首个系统节点,即使最新有效文本未变也执行 |
+| 任意 | 渲染后的提示词为空 | 为非空的后续系统节点记录空内容替换,再按需清空头节点;派生消息中不保留任何提示词版本 |
+
+`startsSeries` 在以下情况为真:`agent/pre-step` 决定声明了 `startsRequestSeries`、surface 的替换代数自上次请求以来发生了移动(压缩或任何其他替换)、可见工具 schema 集合发生了变化。仅 provider 或 model 切换对本规则不算序列开始:目标路由具备能力时,变更后的提示词被追加,这不花任何代价,因为路由变更本身已经使缓存未命中。序列开始已经付出了缓存代价,因此归并让模型历史只保留当前提示词。有日志记录的逐节点空内容替换会从派生消息中移除后续提示词,无需 surface 删除操作,也不替换其间的对话节点。这也使压缩恢复不会在失败尝试已接纳的用户消息之后追加系统更新。
+
+首次尝试在组装、被接纳的 `agent/pre-step` 决策、`step/start`、`agent/request` waterfall 与 `prepareCall()` 之后才接纳提示词。被拒绝或为空的首次输入不打开步骤。两个异步请求阶段都不提交待处理的系统提示词与已接纳用户消息,在任一阶段取消都不会提交这两者。每次尝试都在各自的 `agent/request` 与 `prepareCall()` 之后同步协调同一份已渲染组装结果、仅在首次尝试追加已接纳用户批次、按需记录 header/context、派生并冻结请求,再通过同一个已准备调用发起流式请求。重试不重复组装、`agent/pre-step` 或用户消息准入。协调过程可见 pre-step 压缩(`auto: true` 的 `compaction-basic`)与恢复压缩,并在任一种压缩开启新序列时将非空提示词文本归并到头部。恢复属于序列延续——`resume` header 不是序列开始——因此跨重启发生变化的提示词被追加;提供方缓存在进程边界之后可能仍是热的。
+
+空头节点且没有生效的后续系统节点表示没有提示词。未生效的空尾节点不提供有效文本,因此重复清除与恢复会话都不会使旧提示词重新生效。重新提供非空文本使用同一准入规则:延续中的具备能力路由可以追加它;不具备能力的路由或新序列则重新填充头节点。清除使用普通的逐节点替换,而非 surface 删除或初次创建空头节点。
+
+### 呈现与记账
+
+Web 在追加的历史内节点自己的位置呈现它。`SystemPromptNode` 携带 `{ seq, time, turn, step, text, update }`,其中 `update` 对已加载窗口内跟在更早系统节点之后的追加 `system/message` 为真。Chat 把非空的更新渲染为一张折叠的 `system-prompt` 卡片,标题取自 locale 键 `message.systemPromptUpdate`,同一 turn 与 step 内的 `request/header` 不会重复提示词卡片;`inspectRequestPrompt` 对跟在更新之后的 header 不报告系统变更。Trajectory 把跟在已加载请求 header 之后的更新折叠为一条合成的请求 header 事实,`promptChange.kind = 'system'`,因此之后的请求无需真实的 header 变更就能显示有效提示词。已加载窗口缺少更早的系统节点时,更新按初始提示词呈现。转录投影像对待所有 `system/message` 一样跳过它。
+
+`dsh-token-meter` 把 surface 顺序中最后一个非空且存活的系统节点计入 `contextBreakdown.systemTokens`;其余可见节点(包括被取代的提示词)计入 `messageTokens`。休眠空节点被忽略。每次替换后,两者之和都等于固定启发式 surface 总量,无论是否存在影子价 claim。紧凑的保留条目复用测量服务的 surface 规划器:状态和转换成本为 O(当前保留 surface),不是 O(1) 或 O(完整历史日志)。被替换条目和消息正文被丢弃,状态版本 4 拒绝标量检查点。后续 assistant 用量中的 `cacheReadTokens` 仍是可观察的提供方缓存效果。
+
+Trajectory 选择前一条真实 header 与前一条合成系统 header 中较新的一个作为比较状态。真实 header 拥有配置与工具;追加的提示词可以在没有另一条真实 header 时推进该状态。只比较真实 header 会在 A → B → C 序列中把 A 而不是 B 报告为先前提示词。
+
+Chat 与 Trajectory 通过纯操作 `uiConversation.inspectSystemPrompt` 解释有效提示词。每个 target 为系统事件与位置替换保留不可变的前缀状态,其中只包含存活系统节点,以及将存活替换序号映射到继承 surface 位置的映射表。每次替换复制该表并删除被遮蔽的条目;历史前缀映射表保持不可变。普通追加与流式更新无需折叠提示词。节点是否存活由 surface 顺序决定,而不是事件顺序或来源引用:压缩可以在没有另一个系统事件的情况下恢复更早的提示词,头部重写的序号也可能大于更后位置的有效提示词。空节点仍可被定位,但不会覆盖非空提示词。早于最早已加载相关事件的端点,其顺序未知,除非已有替换位置索引。遇到这样的端点后,解释器会暂停公开之后的所有提示词文本,直到向前补页回放解析缺失的前缀;事件序号顺序不能确定 surface 顺序。历史卡片保留自己的前缀状态,而不是读取最终 surface。
+
+### 压缩
+
+`compaction-basic` 不变。`selectCompactableRange` 仍锚定在第一个非系统节点,因此第 0 号节点永不被遮蔽,更后的历史内节点则可能被遮蔽;`buildSummarizationInput` 将派生的头节点前置到 `messages`,再按 surface 顺序加入每个被遮蔽节点的派生消息,因此区域中途的系统节点在原位被回放,摘要调用仍是对话的真实前缀。
+
+## Alternatives considered
+
+**只发送变化的片段作为增量。** 模型把最新的系统消息当作完整提示词,因此增量会静默丢掉每个未变化的片段。基于模型约定被否决。
+
+**用插件配置而不是模型能力启用历史内模式。** 部署标志可能把不具备能力的模型与追加的系统消息配对,这样的模型最多把它们当作普通历史。该能力属于兑现它的路由;适配器目录已经承载逐模型的容量信息。被否决。
+
+**永远追加,从不重新基线化。** 规则单一,但第 0 号节点会在会话整个生命周期内保持过时,压缩之后的每个请求都要携带过时的头部加替换消息。在序列开始处重新基线化不花额外代价,因为缓存在那里已经丢失。被否决。
+
+**每次恢复都重新基线化。** 为更简单的恢复路径接受每次进程重启一次缓存未命中。缓存跨重启持续数小时到数天,而日志已经承载恢复所需的一切。被否决。
+
+**把系统消息放在该步骤的用户消息之后。** 两个位置都在已缓存前缀之后,但模型会在读到必须应用指令的输入之后才读到指令;system 在 user 之前与最前位置的顺序一致。被否决。
+
+**在 `agent/pre-step` waterfall 之前投影提示词。** 投影将看不到在该 waterfall 内执行的压缩,刚追加的节点可能在同一步骤内被遮蔽,请求就会把第 0 号节点的过时提示词作为唯一的系统消息携带。在 waterfall 之后投影让规则保持为构建请求所用 surface 的纯函数。被否决。
+
+**用先前的请求上下文决定准入。** 它描述上一次调用,而非请求中间件之后绑定的适配器。在首次调用、恢复之后、路由或能力变更之后,它可能选错提示词表示。在提交提示词与用户消息之前解析,还能防止取消时接纳未发送的内容。被否决。
+
+**把 provider 或 model 切换视为序列开始。** 它会在每次路由变更时把提示词折回第 0 号节点,与 tools 的情形一致。header 已经记录了该变更,缓存无论如何都会未命中,因此这条额外规则除了在循环中多一个特例之外没有任何收益。被否决。
+
+**仅清除最新系统节点。** 空节点不投影为消息,因此更早的提示词会重新生效。清除所有生效版本才能保留空渲染文本的含义,同时不删除对话历史。被否决。
+
+**只保留标量总量或提示词祖先链。** 标量影子价无法判断最新提示词从哪个分类消失,也无法恢复其前一个版本。仅有提示词条目无法定位任意非提示词替换端点;`sourceEventSeqs` 还可能引用存活提示词,改写后的事件序号顺序也不同于 surface 顺序。保留紧凑的当前 surface 条目可以复用现有规划器,无需完整日志访问、第二套验证器或消费方专用持久事件。把所有存活提示词之和归入系统数字会改变有效提示词的含义,而不是修复分类。
+
+## Consequences
+
+- 具备能力的路由上的提示词变更保住提供方前缀缓存;追加的节点在该序列的每个请求上付出自身的 token 开销,直到压缩遮蔽它。提示词在多数步骤都变化的部署,更适合把那个事实移入运行时上下文。
+- 请求头部不是系统提示词唯一可能的位置:「模型看到了什么」的读者折叠 surface 并取最新的系统节点,明细的系统数字遵循同一规则。
+- `request/context` 快照记录已准备的路由与声明模式;它描述准入结果,而不决定准入。不具备能力的路由逐系统节点记录归并,保留其间的用户、assistant 与工具历史。
+- 模型约定按所提供的内容记录。若发布的模型收窄了约定——例如只在有界窗口内兑现最新的系统消息——规则需要序列开始之外的重新基线化触发条件。
+- 重写或重排系统消息的代理会静默破坏替换语义;真实 API e2e 的缓存命中断言是探测器。
+
+## Testing
+
+生命周期验证要求:提示词未变更时不产生事件,具备能力的路由在恢复后追加变更后的提示词。TypeScript 与 Python SDK 的期望输出都必须包含带类型的追加 `system/message` 事件,遵循 [SDK 快照策略](../../../../docs/testing.zh.md)。[TypeScript SDK 通知](../../../../snapshots/sdk/system-prompt-in-history/notifications.expected.jsonl)与 [Python SDK 提示词历史](../../../../scripts/snapshots/python-sdk-single-exe/minimal-in-history/prompt-history.json)记录了追加的提示词事件与保留的提示词版本。
+
+- `packages/core/agent-loop/tests/system-prompt-admission.spec.ts` 覆盖文本变化或未变时从具备能力切换到不具备能力的路由、反向路由切换、恢复时的路由准入、请求中间件或准备阶段取消,以及已准备路由保持绑定时并发选择发生变化。重试压缩用例覆盖遮蔽最新提示词后有或没有更早更新存活的情况,并验证复用已接纳的组装结果、用户消息仅接纳一次,以及未变的后续重试不会多记序列 header。具备和不具备能力路由的清除用例会移除三个生效提示词版本,验证重复请求与带 seed 的恢复保持为空且不多记提示词事件,并仅恢复新文本;日志重建与 pi 转换器都不保留旧指令。`src/agent.ts` 与 `src/runtime-context.ts` 的聚焦覆盖率在语句、分支、函数和行四项均达到 100%。
+- `packages/core/agent-loop/tests/system-prompt-projection.spec.ts` 钉住序列延续时的追加、序列开始时无论是否存在后续存活节点、有效文本是否变化都执行的重新基线化、空提示词对所有生效版本的清除,以及不具备能力时只做替换的行为。
+- `packages/core/agent-loop/tests/request-reconstruction.spec.ts` 钉住继承 header 下追加的节点及携带 `systemPromptUpdate` 的 `request/context`、序列开始时折回第 0 号节点、由压缩驱动的重新基线化,以及在开启序列的 `change` header 下由工具 schema 变更驱动的重新基线化。
+- `packages/llm/llm/tests/service.spec.ts`、`packages/llm/llm-deepseek/tests/adapter.spec.ts` 与 `packages/test-support/llm-replay/tests/llm-replay.spec.ts` 钉住已解析模型信息上声明的模式,以及加载时对任何其他值的拒绝。
+- `packages/llm/token-meter/tests/context-breakdown-projection.spec.ts` 钉住最新与中间提示词移除、精确启发式总量、头部改写后的 surface 顺序、额外来源引用、休眠空节点与回退清空、不可变转换、紧凑保留检查点、延迟注册、重放和版本失效。
+- `packages/client/ui-conversation`、`ui-chat` 与 `ui-trajectory` 的客户端测试钉住更新卡片、同一步骤 header 的去重、更新之后不存在系统变更,以及合成的轨迹 header。
+- 无密钥的手写快照 `snapshots/session/system-prompt-in-history/` 在回放路由上声明该能力,通过 fixture 片段在第一次工具调用之后改变提示词,钉住追加的 `system/message`、未被触及的第 0 号节点、唯一一条 `request/header` 以及 `request/context` 中的模式。
+- `packages/llm/llm-deepseek/tests/adapter.e2e.ts` 针对 `DEEPSEEK_IN_HISTORY_MODEL` 指定的模型运行两个步骤并夹带一次提示词变更,断言回复遵循追加的提示词,并断言追加后的请求比同一对话在重写最前提示词时读取更多的缓存 token;该变量未设置时跳过。

+ 6 - 0
.agents/notes/implemented/feature/2026-09-07-session-prose-local-media-display.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-07-session-prose-local-media-display.md
+2026-09-07-session-prose-local-media-display.md: ed740acd0d0d6eaf7f8834dc8d6280a33305aecd
+2026-09-07-session-prose-local-media-display.zh.md: de8a7f99d7aabc4474f525f9f37f50a465a80840

+ 43 - 0
.agents/notes/implemented/feature/2026-09-07-session-prose-local-media-display.md

@@ -0,0 +1,43 @@
+# Agent Note: Session prose local media paths display through a same-origin file route
+
+Status: implemented
+
+English | [中文](2026-09-07-session-prose-local-media-display.zh.md)
+
+## Problem
+
+Assistant prose can reference an image by its filesystem path, but browsers cannot read Host files. A renderer limited to absolute HTTP(S) destinations leaves those references as inert alt text. Issue #3662 records this display gap.
+
+## Decision
+
+Local media paths in Session prose render through a same-origin file route. This note owns the renderer vocabulary and its placement; [authenticated filesystem reads](2026-09-08-file-display-through-filesystem.md) owns the current serving policy and supersedes the workspace/media restrictions described below.
+
+`ui-primitives` owns the `MarkdownPathImages` vocabulary on `MarkdownText`. Like `fileMentions`, it applies only after a message settles so frozen streaming blocks cannot cache a vocabulary handler. The settled pass rewrites image destinations outside the remote-URL allowlist and emits only absolute `http(s)`, `blob`, or `data` results. Without a vocabulary, local destinations retain inert alt text. Failed loads replace the image with authored alt text, or its original destination when alt is empty; a different source can load again.
+
+`ui-chat` supplies a page-stable `localPathMediaUrl` vocabulary through `AssistantMarkdown`. It maps absolute POSIX paths to `/api/file?path=…` on the page's origin. Relative and protocol-relative paths, Windows-style paths, and non-HTTP page transports such as Electron `file://` remain inert.
+
+`session-controller` owns the `SessionMediaReferences` contribution beside `SessionFileReferences`. It registers through `connection.fetch`, which applies the same browser authentication and trust checks as `/api` RPC. The fixed same-origin endpoint gives the synchronous renderer a stable URL without an asynchronous capability negotiation.
+
+## Alternatives considered
+
+**Typert gateway or workspace controller ownership.** The gateway owns Remote RPC dispatch, while the workspace controller owns registry lifecycle. Neither owns file-byte presentation; Session Controller is the consumer serving Session prose.
+
+**Session RPC followed by blob/data URLs.** Attachment images can use an asynchronous fetch, but this Markdown vocabulary must synchronously resolve a destination during a memoized render pass.
+
+**Image-only endpoints.** One file route can serve images, audio, and video without separate URL vocabularies. The current implementation returns complete bounded files; Markdown audio/video player nodes remain independent work.
+
+**Byte-signature validation in the route.** The model-facing `read_image` tool owns image admission checks. Display responses describe content by MIME lookup and let browser decoding reject corrupt payloads, avoiding a duplicate signature checker.
+
+**Workspace/media-only access (superseded).** The original policy restricted canonical paths to registered workspace roots and allowed image/video/audio MIME categories except SVG. Regular-file checks before opening rejected pipes and devices; an opened-handle identity comparison narrowed replacement races. These restrictions bounded authenticated access and avoided a per-request interactive authorization flow. They also excluded temporary screenshots and remote files; the successor note records the replacement policy and why those restrictions are not retained.
+
+## Consequences
+
+The Client vocabulary cannot bypass Host authentication or the filesystem provider. The original restricted route distinguished an existing outside-workspace path from an absent path, exposing existence even while refusing its bytes; the successor policy instead permits ordinary provider-readable files.
+
+Windows-style authored paths remain unsupported by the Client vocabulary. Trajectory and tool-card Markdown consumers do not supply this vocabulary, and audio/video Markdown nodes do not render players. These are renderer limitations, independent of the file route's readable MIME types.
+
+The archived [model-readable image paths](../../archived/feature/2026-08-21-model-readable-image-paths.md) note owns the model-facing behavior; this note owns user-facing display and does not supersede it.
+
+## Testing
+
+Renderer tests cover settled and streaming gates, reference-style images, protocol rechecks, failed-load fallback, and replacement sources. Chat tests cover the vocabulary and component wiring. The browser scenario in `apps/web/tests/markdown-images.e2e.ts` boots the shipped Web composition with a seeded Session and checks actual loading and fallback text. A model-driven recorded Session round trip remains separate from this UI expectation; the successor note names current route coverage.

+ 43 - 0
.agents/notes/implemented/feature/2026-09-07-session-prose-local-media-display.zh.md

@@ -0,0 +1,43 @@
+# Agent Note: 会话正文本地媒体路径通过同源文件路由显示
+
+Status: implemented
+
+[English](2026-09-07-session-prose-local-media-display.md) | 中文
+
+## Problem
+
+Assistant 正文可能通过文件系统路径引用图片,但浏览器无法读取 Host 文件。仅允许绝对 HTTP(S) 目标的渲染器会把这些引用保留为静态 alt 文本。Issue #3662 记录了这一展示缺口。
+
+## Decision
+
+Session 正文中的本地媒体路径通过同源文件路由渲染。本记录拥有渲染器词表及其归属;[鉴权文件系统读取](2026-09-08-file-display-through-filesystem.zh.md)拥有当前文件服务策略,并取代下文的工作区与媒体限制。
+
+`ui-primitives` 拥有 `MarkdownText` 上的 `MarkdownPathImages` 词表。与 `fileMentions` 一样,它只在消息稳定后生效,使冻结的流式块无法缓存词表处理函数。稳定渲染过程重写远程 URL 白名单之外的图片目标,并只输出绝对 `http(s)`、`blob` 或 `data` 结果。没有词表时,本地目标保留静态 alt 文本。加载失败会把图片替换为作者提供的 alt 文本;alt 为空时显示原始目标路径;不同来源仍可重新加载。
+
+`ui-chat` 通过 `AssistantMarkdown` 提供页面稳定的 `localPathMediaUrl` 词表。它把绝对 POSIX 路径映射到页面同源的 `/api/file?path=…`。相对路径、协议相对路径、Windows 风格路径,以及 Electron `file://` 等非 HTTP 页面传输保持静态回退。
+
+`session-controller` 在 `SessionFileReferences` 旁拥有 `SessionMediaReferences` 贡献。它通过 `connection.fetch` 注册;该通道执行与 `/api` RPC 相同的浏览器鉴权和信任检查。固定同源端点让同步渲染器获得稳定 URL,无需异步能力协商。
+
+## Alternatives considered
+
+**由 Typert gateway 或 workspace controller 拥有。** gateway 拥有 Remote RPC 分发,workspace controller 拥有注册表生命周期。两者都不拥有文件字节展示;Session Controller 是服务 Session 正文的消费方。
+
+**先经 Session RPC 获取,再使用 blob/data URL。** 附件图片可以异步获取,但此 Markdown 词表必须在记忆化渲染过程中同步解析目标。
+
+**图片专用端点。** 单一文件路由即可服务图片、音频和视频,无需独立 URL 词表。当前实现返回有界完整文件;Markdown 音视频播放器节点仍是独立工作。
+
+**路由中的字节签名校验。** 面向模型的 `read_image` 工具拥有图片准入检查。展示响应通过 MIME 查询描述内容,由浏览器解码拒绝损坏载荷,避免重复实现签名检查器。
+
+**仅限工作区与媒体的访问(已取代)。** 原策略把规范路径限制在已注册工作区根目录内,并允许除 SVG 外的 image/video/audio MIME 类别。打开前的普通文件检查拒绝管道与设备;已打开句柄的身份比较收窄替换竞态。这些限制约束了鉴权后的访问范围,并避免每次请求的交互授权流程。它们也排除了临时截图与远程文件;后续记录说明替代策略及不保留这些限制的理由。
+
+## Consequences
+
+客户端词表无法绕过 Host 鉴权或文件系统提供方。原受限路由区分了工作区外已存在路径与缺失路径,即使拒绝其字节仍暴露存在性;后续策略则允许提供方可读的普通文件。
+
+客户端词表仍不支持作者提供的 Windows 风格路径。轨迹与工具卡片 Markdown 消费方不提供此词表,音视频 Markdown 节点也不渲染播放器。这些属于渲染器限制,与文件路由可读的 MIME 类型无关。
+
+已归档的[模型可读图片路径](../../archived/feature/2026-08-21-model-readable-image-paths.md)记录拥有模型侧行为;本记录拥有用户侧展示,不取代它。
+
+## Testing
+
+渲染器测试覆盖稳定与流式门禁、引用式图片、协议复查、加载失败回退和来源替换。聊天测试覆盖词表与组件连接。`apps/web/tests/markdown-images.e2e.ts` 浏览器场景使用已播种 Session 启动交付的 Web 组合,检查实际加载与回退文本。模型驱动的记录 Session 往返仍独立于此 UI 期望;后续记录说明当前路由覆盖。

+ 6 - 0
.agents/notes/implemented/feature/2026-09-08-file-display-through-filesystem.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-08-file-display-through-filesystem.md
+2026-09-08-file-display-through-filesystem.md: b1f93f8c9fbd245ad69bc732b2a599c6e0abeb0d
+2026-09-08-file-display-through-filesystem.zh.md: faa3fdb757c9a3fc60cae37692a559158e02ceee

+ 35 - 0
.agents/notes/implemented/feature/2026-09-08-file-display-through-filesystem.md

@@ -0,0 +1,35 @@
+# Agent Note: Authenticated file display reuses filesystem byte reads
+
+Status: implemented
+
+English | [中文](2026-09-08-file-display-through-filesystem.zh.md)
+
+## Problem
+
+Session prose can reference screenshots in temporary directories or files stored by a remote filesystem provider. A Host-local workspace allowlist cannot serve those paths. An image response without a byte limit can also make the browser download a 1 GiB image before attempting to decode it.
+
+## Decision
+
+The authenticated `/api/file` route reads ordinary files through `ctx.fs`. Authentication and the composed provider's read policy govern access; directory and MIME allowlists do not. This supersedes the serving policy in [the local-media display note](2026-09-07-session-prose-local-media-display.md), which retains renderer ownership and its rationale.
+
+GET calls the existing `readBytes(target, signal, maxBytes)`: providers reject known oversized files before content I/O and enforce the limit while reading. HEAD uses metadata without reading content. `FS_TOO_LARGE` becomes 413. MIME lookup supplies response metadata without sniffing file contents; unknown extensions use `application/octet-stream`. A sandbox CSP prevents directly opened HTML/SVG from executing with the authenticated API origin.
+
+All files use the resolved `ctx.attachments.imageLimits.maxImageBytes` limit, normally 20 MiB. The attachment service owns this deployment setting. All responses contain complete files; Range is ignored and no range support is advertised.
+
+## Alternatives considered
+
+**Workspace and media allowlists.** They limit which authenticated bytes can be read, but exclude ordinary screenshot locations and remote files. The chosen policy permits every regular file the composed provider can read.
+
+**A new filesystem byte-stream API.** Efficient large-file delivery and audio/video seeking would require implementations in every provider, including remote range handling. Complete bounded reads satisfy the current display scope without widening that interface. Streaming and Range can be added when those use cases justify the provider work.
+
+**Duplicate size checks in the route.** GET needs no additional stat/read loop: `readBytes` already owns preflight limits, growth detection, and cancellation. HEAD checks size separately because it must not read the body.
+
+## Consequences
+
+Temporary and remote files use the same filesystem provider as `read_image`, without adding model-facing events. The local sandbox provider constrains mutations and permits reads; an authenticated client therefore has broader access than registered workspace roots. Files remain subject to the provider's permissions and the route's byte limits.
+
+Each GET buffers the complete file in Host memory. Audio/video work as complete responses without incremental transfer or guaranteed seeking. Encoded byte limits do not bound decoded pixel dimensions. Failed image loads show authored alt text or the original destination when alt is empty.
+
+## Testing
+
+Route tests cover sparse 1 GiB rejection before content I/O, post-stat growth, the shared attachment byte limit, ordinary MIME types, temporary paths and symlinks, opaque remote targets, provider failures, metadata-only HEAD, ignored Range, and disposal. Browser expectations cover rendered images, 413/404 and corrupt-image fallbacks, and an image outside the workspace. Remote byte transfer remains owned by the existing filesystem provider tests.

+ 35 - 0
.agents/notes/implemented/feature/2026-09-08-file-display-through-filesystem.zh.md

@@ -0,0 +1,35 @@
+# Agent Note: 鉴权文件展示复用文件系统字节读取
+
+Status: implemented
+
+[English](2026-09-08-file-display-through-filesystem.md) | 中文
+
+## Problem
+
+会话正文可能引用临时目录中的截图或远程文件系统提供方中的文件。Host 本地工作区白名单无法提供这些路径。没有字节上限的图片响应还可能让浏览器先下载一张 1 GiB 图片,再尝试解码。
+
+## Decision
+
+鉴权 `/api/file` 路由通过 `ctx.fs` 读取普通文件。鉴权和所组合提供方的读取策略决定访问权限;目录与 MIME 白名单不参与准入。这取代了[本地媒体展示记录](2026-09-07-session-prose-local-media-display.zh.md)中的文件服务策略;该记录保留渲染器归属及其理由。
+
+GET 调用现有 `readBytes(target, signal, maxBytes)`:提供方在内容 I/O 前拒绝已知超限文件,并在读取过程中执行上限。HEAD 使用元数据,不读取内容。`FS_TOO_LARGE` 转换为 413。MIME 查询提供响应元数据,不嗅探文件内容;未知扩展名使用 `application/octet-stream`。sandbox CSP 阻止直接打开的 HTML/SVG 以鉴权 API 源身份执行脚本。
+
+所有文件均使用已解析的 `ctx.attachments.imageLimits.maxImageBytes` 上限,通常为 20 MiB。附件服务拥有此部署配置。所有响应均包含完整文件;忽略 Range,也不声明支持 Range。
+
+## Alternatives considered
+
+**工作区和媒体白名单。** 它们限制鉴权后能读取哪些字节,却排除了常见截图位置和远程文件。所选策略允许读取所组合提供方可读的任意普通文件。
+
+**新增文件系统字节流 API。** 高效的大文件传输和音视频跳转需要每个提供方实现,包括远端 Range 处理。有界完整读取满足当前展示范围,无需扩展该接口。相关用例足以支持这项提供方工作时,可以加入流式传输与 Range。
+
+**在路由重复实现大小检查。** GET 无需额外的 stat/read 循环:`readBytes` 已经负责读取前上限、增长检测和取消。HEAD 单独检查大小,因为它不能读取正文。
+
+## Consequences
+
+临时与远程文件使用与 `read_image` 相同的文件系统提供方,不增加模型可见事件。本地沙箱提供方约束变更操作并允许读取,因此鉴权客户端的访问范围大于已注册工作区根目录。文件仍受提供方权限和路由字节上限约束。
+
+每个 GET 都会在 Host 内存中缓存完整文件。音视频使用完整响应,不支持增量传输,也不保证跳转播放。编码字节上限不限制解码后的像素尺寸。图片加载失败后展示作者提供的 alt 文本;alt 为空时展示原始目标路径。
+
+## Testing
+
+路由测试覆盖内容 I/O 前拒绝稀疏 1 GiB 文件、stat 后增长、共用附件字节上限、普通 MIME 类型、临时路径与符号链接、不透明远程目标、提供方失败、仅元数据 HEAD、忽略 Range 和释放。浏览器期望覆盖图片渲染、413/404 及损坏图片回退,以及工作区之外的图片。远程字节传输仍由现有文件系统提供方测试负责。

+ 2 - 2
.agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.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/process/2026-07-04-doc-tiers-and-budgets.md
-2026-07-04-doc-tiers-and-budgets.md: 378da8f8fddafa32dc7450bfac1c5376f2c7a065
-2026-07-04-doc-tiers-and-budgets.zh.md: 1d92ed7fbbec8a9a15bf94a2d320ee88f65a9fa8
+2026-07-04-doc-tiers-and-budgets.md: 209504218d18e97ae6da65bed9a22da40d2a7681
+2026-07-04-doc-tiers-and-budgets.zh.md: 9c866424d84b4fefa5ffe95efa21a3cf7d3c321a

+ 3 - 2
.agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.md

@@ -13,14 +13,14 @@ Standing docs accumulated repeated rules, retold incidents, duplicated package m
 - **Structure follows the documentation tree.** [docs/AGENTS.md](../../../../docs/AGENTS.md) is the documentation standard: a document owns detail about its subject, summarizes only the purpose, responsibility, and high-level behavior of direct children, and links to deeper owners. [Agent Notes](../../README.md) remain outside this structural contract. Every human-facing document is a tutorial with an ordered outcome or a reference with an explicit lookup scope; a [postmortem](../../../../docs/postmortem/README.md) is an incident-scoped reference whose chronology records evidence. Tutorials introduce concepts in prerequisite order for the reader's starting knowledge.
 - **A tier taxonomy with one home per fact.** The standard assigns every Markdown tier one job, forbids restating a fact outside its home tier, and carries the slop checklist used when writing or reviewing any doc.
 - **One product onboarding path.** The root README owns the recommended package-run path, the source-run alternative, and compact `dsh plugin --profile` usage. The published user guide starts with tasks inside the running Web UI, then links to distinct tutorials or reference owners for other interfaces, plugin development, and advanced configuration instead of repeating Web startup.
-- **A narrow, hard budget gate.** [scripts/verify-doc-budgets.ts](../../../../scripts/verify-doc-budgets.ts) joins `doc-sync`: every doc listed in [scripts/doc-budgets.manifest.json](../../../../scripts/doc-budgets.manifest.json) must stay under its word ceiling (`wc -w` semantics, whole file), and a budgeted file that is missing fails the gate so a rename cannot silently orphan its budget. Scope is deliberately only the accretion-prone standing docs — the root and subtree `AGENTS.md` files, `architecture.md`, `packages/README.md`, and the standing policy docs they evict content into (`docs/testing.md`, `docs/defensive-patterns.md`). Reference docs, Agent Notes, and package READMEs are unbudgeted: length is legitimate there when every row is a fact, and review plus the slop checklist govern them.
+- **Narrow, hard budget gates.** [scripts/verify-doc-budgets.ts](../../../../scripts/verify-doc-budgets.ts) joins `doc-sync`: every doc listed in [scripts/doc-budgets.manifest.json](../../../../scripts/doc-budgets.manifest.json) must stay under its word ceiling (`wc -w` semantics, whole file), and a budgeted file that is missing fails the gate so a rename cannot silently orphan its budget. Its scope is deliberately only the accretion-prone standing docs — the root and subtree `AGENTS.md` files, `architecture.md`, `packages/README.md`, and the standing policy docs they evict content into (`docs/testing.md`, `docs/defensive-patterns.md`). Reference docs, Agent Notes, and complete package READMEs remain unbudgeted because exhaustive facts can be long. The separate [package Summary gate](../../../../scripts/verify-package-readme-summaries.ts) caps only each English package entry paragraph at 100 words and directs failures to `dsh-doc` and the selected kind template.
 - **Ceilings are an enforcement frontier that ratchets.** A doc at or below its target keeps at least 5% headroom as its ceiling ratchets down; a doc above target keeps a frozen ceiling that prevents growth until it reaches the target (root `AGENTS.md` ≤ 1,600 words; `architecture.md` ≤ 1,800; subtree `AGENTS.md` ≤ 600 except `packages/AGENTS.md` ≤ 650 and `docs/AGENTS.md` ≤ 1,250; `packages/README.md` ≤ 600). When the gate goes red, relocate or condense; raise a ceiling only with explicit PR justification.
 - **A thin workflow skill, contracts in docs.** [.agents/skills/dsh-doc](../../../skills/dsh-doc/SKILL.md) carries the placement, audit, budget, and website workflow and defers to the standard as its source of truth, the same split as [dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md) over the i18n contract.
 
 ## Alternatives considered
 
 - **Skill and review discipline without a gate** — rejected: the accretion above happened while the current-state rule and reviewer attention already existed; a prose rule with no mechanical backstop demonstrably does not hold here, and this repo's own [quality-gates stance](2026-06-11-quality-gates.md) says invariants worth keeping are worth encoding.
-- **A broad gate over every doc tier** — rejected: a blanket ceiling punishes exactly the right kind of long doc (a feature matrix or type catalog where every row is a fact) and generates per-file override churn that trains contributors to rubber-stamp raises.
+- **A broad gate over every complete doc** — rejected: a blanket ceiling punishes exactly the right kind of long doc (a feature matrix or type catalog where every row is a fact) and generates per-file override churn that trains contributors to rubber-stamp raises. The package Summary limit instead bounds one common entry paragraph without constraining its owning reference sections.
 - **Independent onboarding tutorials for each documentation entry point** — rejected: duplicated setup steps drift in command order, first outcome, and product identity. A short README path followed by task-focused guides keeps the transition explicit without maintaining competing tutorials.
 - **Housing the standard inside the skill** — rejected: contracts live in docs and workflows in skills; a standard packed into SKILL.md is invisible to an agent that edits docs without invoking the skill, and `docs/AGENTS.md` already loads as subtree instructions for anyone working under `docs/`.
 
@@ -30,4 +30,5 @@ Standing docs accumulated repeated rules, retold incidents, duplicated package m
 - Structural review starts with ownership and document form before sentence-level editing, so lower-level detail moves to its owner instead of being polished in the wrong place.
 - Readers reach a running Web UI before encountering headless execution, SDK embedding, custom profiles, or direct settings files; those interfaces remain available from their reference owners.
 - Budgeted docs that remain above target cannot grow; reaching the target restores the 5% working headroom.
+- Package references retain exhaustive owned facts below their entry paragraph, while every package Summary stays within the same 100-word retrieval budget.
 - Word count is a crude proxy accepted deliberately: it cannot judge quality, but it forces the relocation decision at exactly the moment content is being added, which is when the author has the context to place it correctly.

+ 3 - 2
.agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.zh.md

@@ -13,14 +13,14 @@ Status: implemented
 - **结构遵循文档树。**[docs/AGENTS.md](../../../../docs/AGENTS.md) 是文档标准:文档负责承载其主题的详细内容,仅概述直接子项的目的、职责和高层行为,并链接到更深层内容的归属文档。[Agent Note](../../README.zh.md) 仍不受这一结构约定约束。每份面向人的文档要么是按顺序引导读者达成结果的教程(tutorial),要么是查阅范围明确的参考文档(reference);[事故复盘(postmortem)](../../../../docs/postmortem/README.zh.md) 是范围限定于单起事故的参考文档,其时间线记录证据。教程结合读者的起始知识,按前置依赖顺序介绍概念。
 - **每项事实只归属一处的层级分类。**文档标准为每种 Markdown 层级分配单一职责,禁止在事实归属层级之外重复陈述,并包含编写或评审任何文档时使用的赘余检查清单。
 - **单一产品入门路径。**根 README 负责推荐的包运行路径、从源码运行的备选路径和简要的 `dsh plugin --profile` 用法。已发布的用户指南从运行中的 Web UI 内部任务开始,再链接到其他界面的独立教程或插件开发与进阶配置的参考文档归属处,而不会重复介绍 Web 启动步骤。
-- **范围窄且严格的预算门禁。**[scripts/verify-doc-budgets.ts](../../../../scripts/verify-doc-budgets.ts) 接入 `doc-sync`:[scripts/doc-budgets.manifest.json](../../../../scripts/doc-budgets.manifest.json) 列出的每份文档都必须低于其词数上限(采用 `wc -w` 语义,统计整个文件);预算内文件缺失也会使门禁失败,使重命名无法悄然遗落其预算。范围刻意只涵盖容易膨胀的常设文档——根目录和子树中的 `AGENTS.md` 文件、`architecture.md`、`packages/README.md`,以及它们将内容移入的常设策略文档(`docs/testing.md`、`docs/defensive-patterns.md`)。参考文档、Agent Note 和包 README 不设预算:只要每一行都是事实,长度在这些位置就是合理的;评审和赘余检查清单负责约束它们
+- **范围窄且严格的预算门禁。**[scripts/verify-doc-budgets.ts](../../../../scripts/verify-doc-budgets.ts) 接入 `doc-sync`:[scripts/doc-budgets.manifest.json](../../../../scripts/doc-budgets.manifest.json) 列出的每份文档都必须低于其词数上限(采用 `wc -w` 语义,统计整个文件);预算内文件缺失也会使门禁失败,使重命名无法悄然遗落其预算。该门禁的范围刻意只涵盖容易膨胀的常设文档——根目录和子树中的 `AGENTS.md` 文件、`architecture.md`、`packages/README.md`,以及它们将内容移入的常设策略文档(`docs/testing.md`、`docs/defensive-patterns.md`)。参考文档、Agent Note 和完整的包 README 仍不设预算,因为穷尽式事实可能很长。单独的[包 Summary 门禁](../../../../scripts/verify-package-readme-summaries.ts)只把每个英文包入口段落限制为 100 词,并引导失败项阅读 `dsh-doc` 和所选 kind 模板
 - **上限是只进不退的执行红线。** 达到或低于目标的文档在上限逐步下调时保留至少 5% 的余量;高于目标的文档则维持冻结的上限,在达到目标之前不得增长(根 `AGENTS.md` ≤ 1,600 词;`architecture.md` ≤ 1,800;子树 `AGENTS.md` ≤ 600,但 `packages/AGENTS.md` ≤ 650、`docs/AGENTS.md` ≤ 1,250;`packages/README.md` ≤ 600)。门禁变红时,迁移或压缩内容;只有在 PR(Pull Request)描述中给出明确理由时才提高上限。
 - **精简的工作流 skill(技能),约定归文档。**[.agents/skills/dsh-doc](../../../skills/dsh-doc/SKILL.md) 承载文档放置、审计、预算与站点发布工作流,并以文档标准为真源,与 [dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md) 和 i18n 约定之间的分工相同。
 
 ## 曾考虑的替代方案
 
 - **仅靠 skill 和评审纪律,不设门禁**:否决。上述膨胀正是在现行规则和评审注意力已经存在的情况下发生的;一条没有自动化保障的行文规则在此处已被证明无法维持,而本仓库自身的[质量门禁立场](2026-06-11-quality-gates.zh.md)认为值得保持的不变式就值得编码。
-- **对所有文档层级全面设限**:否决。一刀切的上限恰好惩罚了那些正当的长文档(如功能矩阵或类型目录,每一行都是事实),并产生逐文件的例外变更,训练贡献者机械地批准提限。
+- **对每份完整文档全面设限**:否决。一刀切的上限恰好惩罚了那些正当的长文档(如功能矩阵或类型目录,每一行都是事实),并产生逐文件的例外变更,训练贡献者机械地批准提限。包 Summary 上限只约束共同的入口段落,不限制其归属参考章节。
 - **为每个文档入口维护独立入门教程**:否决。重复的设置步骤会在命令顺序、首个结果和产品定位上产生分歧。简短的 README 路径接上面向任务的指南,可明确衔接两者,且不需要维护相互竞争的教程。
 - **将标准放在 skill 内部**:否决。约定归文档,工作流归 skill;如果标准被塞进 SKILL.md,那些不调用该 skill 而直接编辑文档的 agent(智能体)就看不到它,而 `docs/AGENTS.md` 已经作为子树指令被任何在 `docs/` 下工作的人加载。
 
@@ -30,4 +30,5 @@ Status: implemented
 - 结构评审先检查归属关系和文档形式,再进行句子层面的编辑,使较低层级的细节迁移到其归属文档,而不是在错误的位置加以润色。
 - 读者会先进入可运行的 Web UI,再遇到 headless 执行、SDK 嵌入、自定义 profile 或直接 settings 文件;这些入口仍可从各自的参考文档归属处访问。
 - 仍高于目标的受预算约束文档不得增长;达到目标后,将恢复 5% 的工作余量。
+- 包参考可在入口段落之后保留穷尽式归属事实,而每个包 Summary 都遵守相同的 100 词检索预算。
 - 词数是一个粗糙的代理指标,这是有意接受的:它无法判断质量,但它在内容被添加的那一刻强制触发迁移决策,而那正是作者拥有足够上下文来正确放置内容的时刻。

+ 2 - 2
.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.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/process/2026-08-08-native-windows-pull-request-ci.md
-2026-08-08-native-windows-pull-request-ci.md: ba63af5a8f1d32035e116b3900eb9d5905f326d0
-2026-08-08-native-windows-pull-request-ci.zh.md: 3a0c8f510f2f8881833633f69d8ac5d7330d5195
+2026-08-08-native-windows-pull-request-ci.md: 690f8e6f9b13fa7e72240a42ff482bd83f9088b0
+2026-08-08-native-windows-pull-request-ci.zh.md: 9efa3cbcf33b6c12e4eed253b6a0546c79b768fe

+ 1 - 1
.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md

@@ -32,7 +32,7 @@ Windows durable JSONL paths keep drive roots in native spelling and apply the ex
 
 Post-boot profile watcher setup proceeds only while the root fiber and Loader are both live. A concurrent setup error is contained only when the same invocation's recorded signal already owns shutdown; unrelated HMR failures remain loud. The [process-shutdown controller](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md) lets a successful one-shot completion drain Node's remaining handles after root disposal, while teardown failure, deadline, and signal escalation retain forced exit. The vendored Include serializes debounced writes, retries only transient access or busy failures with bounded backoff, and observes every timer rejection. A terminal persistence failure remains on the queue and is rethrown to the teardown owner, while successful teardown drains the latest write.
 
-Shiki disables lazy TextMate-regex compilation and warms each boot grammar before user content enters the unchanged per-line tokenization budget, so scheduler contention cannot publish a partial highlighted stream. The Codex real-product fixture is pinned to stable 0.149.1 schemas and selects an actually advertised command tool and argument shape, preserving the provider-owned protocol while proving unattended rejection and whole-tree exit on each host.
+Shiki disables lazy TextMate-regex compilation and warms each boot grammar before user content enters the unchanged per-line tokenization budget, so scheduler contention cannot publish a partial highlighted stream. The Codex real-product fixture is pinned to stable 0.153.4 schemas and selects an actually advertised command tool and argument shape, preserving the provider-owned protocol while proving unattended rejection and whole-tree exit on each host.
 
 ## Alternatives considered
 

+ 1 - 1
.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md

@@ -32,7 +32,7 @@ Windows 的持久 JSONL 路径会保留驱动器根目录的原生写法,并
 
 启动后,只有根 fiber 与 Loader 均处于活跃状态时,系统才会继续设置 profile watcher。只有当同一次调用所记录的信号已取得关闭流程所有权时,系统才会隔离并发设置错误;无关 HMR 故障仍会响亮失败。[进程关闭控制器](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.zh.md)会在根级 dispose 成功后让单次任务的正常完成流程排空 Node 剩余句柄,同时让拆卸失败、截止时间到期和信号升级继续强制退出。vendored Include 会串行化防抖写入,只对瞬时访问或忙碌故障执行有界退避重试,并确保每个由计时器触发的拒绝都得到观察。持久化最终失败后,该故障会保留在队列中,并重新抛给拆卸责任方;成功拆卸则会排空最新写入。
 
-Shiki 会禁用 TextMate 正则的延迟编译,并在用户内容进入保持不变的逐行 tokenization(词元化)预算前预热每种启动语法,从而避免调度器争用发布不完整的高亮流。Codex 真实产品 fixture 固定使用稳定版 0.149.1 schema,并选择实际提供的命令工具与对应参数形态;这样既保留由提供方负责的协议,也能在每种宿主上证明无人值守拒绝和整棵进程树退出。
+Shiki 会禁用 TextMate 正则的延迟编译,并在用户内容进入保持不变的逐行 tokenization(词元化)预算前预热每种启动语法,从而避免调度器争用发布不完整的高亮流。Codex 真实产品 fixture 固定使用稳定版 0.153.4 schema,并选择实际提供的命令工具与对应参数形态;这样既保留由提供方负责的协议,也能在每种宿主上证明无人值守拒绝和整棵进程树退出。
 
 ## 曾考虑的替代方案
 

+ 2 - 2
.agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.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/process/2026-08-08-unified-github-label-taxonomy.md
-2026-08-08-unified-github-label-taxonomy.md: 625c5c1cac951bdc97187c17c964d677f31131c7
-2026-08-08-unified-github-label-taxonomy.zh.md: 855a2b98f44d517abe1f7718ae4e81262cb031b6
+2026-08-08-unified-github-label-taxonomy.md: 1748f9b77ed2922035c5e75ac4a2eee047f413d3
+2026-08-08-unified-github-label-taxonomy.zh.md: 3b221a5f3db43597656f46dfc505cdfda38c75db

+ 5 - 1
.agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.md

@@ -47,6 +47,8 @@ The area set is intentionally extensible. When no existing description honestly
 
 Issues use native Issue Type instead of `kind/*`; their `area/*` labels remain optional. `source/*` labels record how an Issue was created and do not apply to pull requests. Priority, GitHub defaults, and workflow triggers remain independent operational metadata.
 
+The repository lifecycle removes pull request `kind/*` labels and reserved aliases from an Issue before auditing it. Policy comments report only violations whose intended value cannot be derived from the Issue, such as a missing native Type or an unsupported Priority.
+
 Label migrations preserve meaning before removing aliases: add the canonical replacement, verify the labelable, then remove the obsolete assignment. A label is deleted only after no pull request or Issue still uses it, and unrelated labels are never replaced as a set.
 
 ## Alternatives considered
@@ -65,8 +67,10 @@ Label migrations preserve meaning before removing aliases: add the canonical rep
 
 **Kinds on Issues.** Native Issue Type already owns that classification; duplicating it as a label creates drift.
 
+**Comment-only Issue enforcement.** A comment preserves invalid metadata and requires human cleanup even when the only valid result is removal. The lifecycle applies that removal and retains comments for choices it cannot infer.
+
 **Exactly one area per pull request.** Coherent changes can materially affect several independent APIs or behaviors, and dropping secondary areas hides affected scope.
 
 ## Consequences
 
-Reviewers and automation can query intent, semantic scope, how an Issue was created, priority, and operational triggers independently. Maintainers must read the change and the live label descriptions instead of inferring classification from title prefixes or paths. The live catalog, this rationale, and policy enforcement must move together when a kind or a non-obvious area boundary changes, and taxonomy migrations carry an explicit historical backfill and verification cost.
+Reviewers and automation can query intent, semantic scope, how an Issue was created, priority, and operational triggers independently. Invalid Issue labels disappear without a policy comment, and the label event records the repair; when no other violation remains, the lifecycle deletes any earlier policy comment. Maintainers must read the change and the live label descriptions instead of inferring classification from title prefixes or paths. The live catalog, this rationale, and policy enforcement must move together when a kind or a non-obvious area boundary changes, and taxonomy migrations carry an explicit historical backfill and verification cost.

+ 5 - 1
.agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.zh.md

@@ -47,6 +47,8 @@ Issue 已有原生 Issue Type 和独立的来源分类体系。在这两类对
 
 Issue 使用原生 Issue Type,而不是 `kind/*`;其 `area/*` 标签仍然可选。`source/*` 标签记录 Issue 的创建方式,不适用于 PR。优先级、GitHub 默认标签和工作流触发器仍是相互独立的管理元数据。
 
+仓库生命周期会先从 Issue 中移除 PR `kind/*` 标签和保留别名,再执行审计。政策评论只报告无法从 Issue 推导预期值的违规项,例如缺失原生 Issue Type 或使用不受支持的优先级。
+
 迁移标签时,须先保留语义,再移除别名:先添加规范替代标签,核验可加标签对象,再移除废弃的标签关系。只有在所有 PR 和 Issue 都不再使用某个标签后才能将其删除,且绝不整组替换无关标签。
 
 ## 考虑过的替代方案
@@ -65,8 +67,10 @@ Issue 使用原生 Issue Type,而不是 `kind/*`;其 `area/*` 标签仍然
 
 **在 Issue 上使用类型标签。** 原生 Issue Type 已负责这项分类;再用标签复制会造成漂移。
 
+**仅用评论执行 Issue 政策。** 评论会保留无效元数据;即使唯一有效结果是移除,仍要求人工清理。生命周期会直接执行这类移除,只对无法推断的选择保留评论。
+
 **每个 PR 恰好一个领域。** 内聚的变更可能对多个独立 API 或行为产生实质影响,丢弃次要领域会隐藏受影响范围。
 
 ## 后果
 
-评审人和自动化流程可以分别查询意图、语义范围、Issue 的创建方式、优先级和工作流触发条件。维护者必须阅读变更内容和现行标签说明,而不能根据标题前缀或路径推断分类。当某种类型或某条非显然的领域边界发生变化时,现行标签清单、本记录中的决策依据和政策执行必须同步更新;分类体系迁移还会产生明确的历史回填和验证成本。
+评审人和自动化流程可以分别查询意图、语义范围、Issue 的创建方式、优先级和工作流触发条件。无效的 Issue 标签会直接消失,不会产生政策评论;标签事件会记录该修复。如果不存在其他违规项,生命周期会删除更早的政策评论。维护者必须阅读变更内容和现行标签说明,而不能根据标题前缀或路径推断分类。当某种类型或某条非显然的领域边界发生变化时,现行标签清单、本记录中的决策依据和政策执行必须同步更新;分类体系迁移还会产生明确的历史回填和验证成本。

+ 2 - 2
.agents/notes/implemented/process/2026-08-10-npm-release-sequences.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/process/2026-08-10-npm-release-sequences.md
-2026-08-10-npm-release-sequences.md: 6729b506a9cfe7f6d4410dbf0750621901e07293
-2026-08-10-npm-release-sequences.zh.md: 18a30d6dd57c17b8e13cc3a1f71bc91242a391ff
+2026-08-10-npm-release-sequences.md: c403964d8de1c158e5949b5e112a038832709874
+2026-08-10-npm-release-sequences.zh.md: d03da4c4485c4807497dfb28b61ab342bb4c8d12

+ 2 - 2
.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md

@@ -8,7 +8,7 @@ English | [中文](2026-08-10-npm-release-sequences.zh.md)
 
 This repository held three unrelated groups of publishable packages and no channel that sent any of them to a registry.
 
-`packages/*/*` and `apps/*` form the runtime surface of `@deepseek-ai/dsh`; `vendor/*` holds nine rescoped Cordis framework packages, each carrying its upstream version; `native/landlock-run/packages/*` holds Linux platform packages with their own workflow. The three differ in version baseline, change rate, and build requirements: dsh moves with the product, vendor moves only when upstream is re-synced or a local modification changes, and native needs a musl toolchain and one build per architecture. Forcing them through one pipeline means every product release republishes the framework and the native binaries.
+`packages/*/*` and `apps/*` form the runtime surface of `@deepseek-ai/dsh`; `vendor/*` holds nine rescoped Cordis framework packages, each carrying its upstream version; `native/system/packages/*` holds Linux platform packages with their own workflow. The three differ in version baseline, change rate, and build requirements: dsh moves with the product, vendor moves only when upstream is re-synced or a local modification changes, and native needs a musl toolchain and one build per architecture. Forcing them through one pipeline means every product release republishes the framework and the native binaries.
 
 Two hard blockers sat in the way. All 217 workspace manifests set `private: true`, which npm refuses to publish. The subtler one was 933 hand-written `peerDependencies: "^0.0.1"` entries between sibling dsh packages: `pnpm pack` substitutes the `workspace:` protocol but leaves semver ranges alone, and `^0.0.1` means `>=0.0.1 <0.0.2` — it excludes `0.0.2`, and semver excludes prereleases from a range without a prerelease of its own, so it excluded `0.0.1-rc.1` too. Those entries never failed only because the version never left `0.0.1`.
 
@@ -24,7 +24,7 @@ Two hard blockers sat in the way. All 217 workspace manifests set `private: true
 |---|---|---|---|---|
 | dsh | Publish set: non-experimental `packages/*/*` + `apps/*`; private experimental packages join only the shared version bump | one version for the publish set, private dsh packages, and workspace root, `0.0.x` | `dsh-v<version>` | `release.yml` (pack) / `release-publish.yml` (publish) |
 | vendored framework | the nine `vendor/*` packages | each package on its own version line | `vendor-<package>-v<version>` (one per package) | `release-vendor.yml` (pack) / `release-vendor-publish.yml` (publish) |
-| native | `native/landlock-run/packages/*` | its own `0.0.x` | `landlock-run-v<version>` | `landlock-run-release.yml` |
+| native | `native/system/packages/*` | its own `0.0.x` | `node-addon-system-v<version>` | `node-addon-system-release.yml` |
 
 All three publish to the `@deepseek-ai` scope on npmjs.com, and access is per sequence rather than per scope: the vendored framework and the native packages are `public`, and the dsh family has been `public` since its own sequence went public on 2026-08-13 ([rationale](../../archived/process/2026-08-13-public-vendor-and-native-sequences.md)). No publish path passes `--access`, because one flag cannot serve sequences that disagree and would override the manifest that owns the level.
 

+ 2 - 2
.agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md

@@ -8,7 +8,7 @@ Status: implemented
 
 这个仓库有三组互不相干的可发布包,却没有任何发布通道把它们送上 registry。
 
-`packages/*/*` 与 `apps/*` 组成 `@deepseek-ai/dsh` 的运行面;`vendor/*` 是九个 rescope 过的 Cordis 框架包,各自带着上游的版本号;`native/landlock-run/packages/*` 是 Linux 平台包,有自己的 workflow。三组的版本基线、变更节奏和构建要求都不同:dsh 随产品迭代,vendor 只在同步上游或改动本地修改时才动,native 需要 musl 工具链和逐架构构建。把它们塞进一条发布流水线,等于每次产品发版都要重发框架和原生二进制。
+`packages/*/*` 与 `apps/*` 组成 `@deepseek-ai/dsh` 的运行面;`vendor/*` 是九个 rescope 过的 Cordis 框架包,各自带着上游的版本号;`native/system/packages/*` 是 Linux 平台包,有自己的 workflow。三组的版本基线、变更节奏和构建要求都不同:dsh 随产品迭代,vendor 只在同步上游或改动本地修改时才动,native 需要 musl 工具链和逐架构构建。把它们塞进一条发布流水线,等于每次产品发版都要重发框架和原生二进制。
 
 挡路的还有两处硬门。全部 217 个 workspace manifest 都是 `private: true`,`npm publish` 直接拒绝。更隐蔽的是 933 条 dsh 兄弟包之间硬写的 `peerDependencies: "^0.0.1"`:`pnpm pack` 只替换 `workspace:` 协议,不动语义范围,而 `^0.0.1` 等于 `>=0.0.1 <0.0.2`——发 `0.0.2` 落不进去,发 `0.0.1-rc.1` 也落不进去(semver 规定不带预发布段的范围排除预发布版本)。这些条目至今没出事,只因为版本一直停在 `0.0.1`。
 
@@ -24,7 +24,7 @@ Status: implemented
 |---|---|---|---|---|
 | dsh | 发布集:非 experimental 的 `packages/*/*` + `apps/*`;私有实验性包仅加入共享版本 bump | 发布集、私有 dsh 包与 workspace 根共用一个 `0.0.x` | `dsh-v<版本>` | `release.yml`(pack)/ `release-publish.yml`(发布) |
 | vendored framework | `vendor/*` 九个包 | 每包各自一条版本线 | `vendor-<包名>-v<版本>`(每包一个) | `release-vendor.yml`(pack)/ `release-vendor-publish.yml`(发布) |
-| native | `native/landlock-run/packages/*` | 自己的 `0.0.x` | `landlock-run-v<版本>` | `landlock-run-release.yml` |
+| native | `native/system/packages/*` | 自己的 `0.0.x` | `node-addon-system-v<版本>` | `node-addon-system-release.yml` |
 
 三组一律发到 npmjs.com 的 `@deepseek-ai` scope,且 access 按序列而非按 scope 区分:vendored 框架与 native 包是 `public`,dsh 族自 2026-08-13 其自身序列公开发布起即为 `public`([理由](../../archived/process/2026-08-13-public-vendor-and-native-sequences.md))。没有任何发布路径传 `--access`——一个选项无法服务级别互不相同的序列,且会覆盖真正拥有该级别的 manifest。
 

+ 2 - 2
.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.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/process/2026-08-18-in-job-partitioned-coverage.md
-2026-08-18-in-job-partitioned-coverage.md: 8a3eb202255ecbf680dfa0af656fd5b89e958001
-2026-08-18-in-job-partitioned-coverage.zh.md: e7ca2692485fa4f9f0135821963e64347c3cdfdd
+2026-08-18-in-job-partitioned-coverage.md: 8ac4635101504419c976a5ab740b691a090e548f
+2026-08-18-in-job-partitioned-coverage.zh.md: 55dee82662c7174d661c253463153ca78e3099b2

+ 2 - 0
.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.md

@@ -14,6 +14,8 @@ The optimization must retain every test and the merged per-file 100% thresholds.
 
 The ordinary `pnpm run test:coverage` command remains one Vitest invocation. Linux coverage CI fixes `DSH_COVERAGE_PARTITIONS=4`; native Windows now also fixes it at 4 to reduce process-creation pressure under high self-hosted concurrency. No elapsed-time trigger changes either count while a run is in progress. The [coverage-exempt heavy suite](../../archived/process/2026-07-31-coverage-exempt-heavy-suites.md) remains a separate uninstrumented gate beside the instrumented work.
 
+The entire `packages/typert/` group is exempt from source coverage and runs in the uninstrumented gate. Its compiler fixtures, catalog reproduction, loader, protocol, and registry assertions remain required. The shared [exempt roster](../../../../scripts/coverage-exempt.ts) selects every Typert package, including nested tests; both project exclusions and partition inventory consume that roster. Exemption removes coverage collection, not test or hook failures.
+
 When partitioning is enabled, `scripts/run-gates.ts` selects `pnpm run test:coverage:partitioned` for the instrumented gate. `scripts/coverage-partitions.ts` starts the configured Vitest children concurrently, each with one worker. The coordinator collects the instrumented inventory from a `vitest list --filesOnly` run (caller filters narrow it; exempt heavy suites are removed because list does not apply their exclusion), reads recorded per-file durations from a coordinator-persisted gitignored file (restored and saved through the GitHub cache on the windows-coverage job, because checkout removes it and Vitest's own cache never survives CI), and assigns files to partitions by longest-processing-time by way of a min-heap, so the heavy subprocess-bound suites spread across children instead of piling into whichever shard a path hash lands them in. Each partition receives a temporary Vitest config whose include is its file list per project (command-line files exceeded the Windows CreateProcess limit; the mutually exclusive thread-safe and process-bound projects keep only their own files so nothing runs twice), an empty partition is rejected before any child starts, the heaviest partition starts first so its verdict lands earliest (fail-fast), and the duration history is restored and saved through the GitHub cache with per-run keys (cache entries are immutable). Partition mode suppresses thresholds and coverage reporters in each child, gives every child a separate report directory, and writes one blob report per process.
 
 The coordinator waits for every child, validates that the blob directory contains exactly the expected files, and then runs one `vitest --merge-reports ... --coverage` command. Only that merged command applies the repository's per-file statement, branch, function, and line thresholds, so a partition is never judged against an intentionally partial inventory.

+ 3 - 1
.agents/notes/implemented/process/2026-08-18-in-job-partitioned-coverage.zh.md

@@ -14,6 +14,8 @@ Status: implemented
 
 普通的 `pnpm run test:coverage` 命令仍只启动一次 Vitest。Linux 覆盖率 CI 将 `DSH_COVERAGE_PARTITIONS` 固定为 4;原生 Windows 现在也固定为 4,以降低自托管高并发下的进程创建压力。运行期间不会由任何耗时触发器改变这两个数量。[覆盖率豁免重型套件](../../archived/process/2026-07-31-coverage-exempt-heavy-suites.md)仍作为独立的无插桩门禁与插桩工作并排运行。
 
+整个 `packages/typert/` 组豁免源码覆盖率,并在无插桩门禁中运行。其编译器 fixture(测试前置数据)、目录复现、loader、协议及注册表断言仍为必需检查。共享的[豁免清单](../../../../scripts/coverage-exempt.ts)选择每个 Typert 包及其嵌套测试;project 排除规则与分区清单都使用该清单。豁免只移除覆盖率采集,不忽略测试或钩子失败。
+
 启用分区后,`scripts/run-gates.ts` 为插桩门禁选择 `pnpm run test:coverage:partitioned`。`scripts/coverage-partitions.ts` 按配置数量并发启动 Vitest 子进程,每个进程只用 1 个 worker。协调器通过 `vitest list --filesOnly` 收集插桩清单(调用方过滤器会先收窄清单;exempt 重型套件需在此剔除,因为 list 不应用其排除),从协调器持久化的 gitignore 文件读取逐文件耗时(windows-coverage job 通过 GitHub cache 恢复并保存该文件,因为 checkout 会删除它且 Vitest 自身缓存无法在 CI 存活),并借最小堆按最长处理时间把文件分配到各分区,使重量级子进程密集型套件分散到不同子进程,而不是全部落入路径 hash 恰好命中的那一个分片。每个分区获得一个临时 Vitest 配置,其 include 按 project 拆分(命令行传文件会超过 Windows CreateProcess 上限;互斥的 thread-safe 与 process-bound project 只保留各自的文件,避免任何文件跑两次);空分区会在任何子进程启动前被拒绝,最重的分区最先启动使其结论最早落地(fail-fast),耗时历史通过 GitHub cache 以每 run 唯一键恢复与保存(cache 条目不可变)。分区模式会在各子进程中关闭阈值与覆盖率报告器,为每个子进程分配独立报告目录,并让每个进程写出 1 份 blob 报告。
 
 协调器等待全部子进程结束,验证 blob 目录只包含预期文件,然后执行一次 `vitest --merge-reports ... --coverage`。只有这条合并命令应用仓库的逐文件语句、分支、函数与行阈值,因此系统不会拿有意不完整的测试清单单独判定任一分区。
@@ -36,7 +38,7 @@ Status: implemented
 
 **使用工作流级分片。** 不予采用,因为多个 job 会重复设置工作,并需要上传、下载产物以及合并依赖。所选分区方案只在同一个 job 和工作区内使用多个进程。
 
-**提高单个插桩进程内的 Vitest worker 数。** 不予采用,因为已完成的 Windows 高扇出试验暴露了 worker 退出、fixture(测试前置数据)不稳定和 Node 24 CJS lexer 故障。相互独立的单 worker 进程既保留隔离,也能让所选分区并发执行。
+**提高单个插桩进程内的 Vitest worker 数。** 不予采用,因为已完成的 Windows 高扇出试验暴露了 worker 退出、fixture 不稳定和 Node 24 CJS lexer 故障。相互独立的单 worker 进程既保留隔离,也能让所选分区并发执行。
 
 **在每种宿主上使用相同的分区数量。** 先前不予采用,因为 Linux 的 4 进程运行与 Windows 的 8 进程运行具有不同的启动成本与资源上限。本次变更在 Windows 高并发运行暴露 8 分片 worker 启动失败后,将两者统一为 4 分区。
 

+ 6 - 0
.agents/notes/implemented/process/2026-09-08-comment-only-review-routing.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/process/2026-09-08-comment-only-review-routing.md
+2026-09-08-comment-only-review-routing.md: 050905285b2291b34da9873d19c2f122c088a9e5
+2026-09-08-comment-only-review-routing.zh.md: b98f5d70b4d5c0fd27df1c393238b0802e420e09

+ 41 - 0
.agents/notes/implemented/process/2026-09-08-comment-only-review-routing.md

@@ -0,0 +1,41 @@
+# Agent Note: Exclude documentation and comment-only changes from review routing
+
+Status: implemented
+
+English | [中文](2026-09-08-comment-only-review-routing.zh.md)
+
+## Problem
+
+Directory ownership alone treats documentation and comment edits like executable changes. These edits do not require the automatic code-owner request that protects behavior changes.
+
+GitHub may omit or truncate a file patch. A scanner that assumes every patch is complete can miss executable changes that occur outside the supplied hunks.
+
+## Decision
+
+Review routing classifies every old and new path in this order: test, documentation, comment-only, then reviewable code. Test classification wins when a test path also has a documentation extension. Every filename ending in `.md` or `.yaml`, matched without case sensitivity, is documentation. A `.yml` file is not documentation under this rule.
+
+Comment-only classification applies only to files with `status: modified` and a declared source-comment syntax. The scanner reconstructs the before and after text for each patch hunk, removes comments outside quoted strings, removes empty lines left by comments, and requires the remaining text to be identical.
+
+The scanner counts added and deleted patch lines and compares them with GitHub's file record before accepting a comment-only result. A missing patch, a count mismatch, a rename, an unsupported extension, or a comment form that remains visible to the lexer keeps the file reviewable. This fail-safe result can request an unnecessary review but cannot suppress a known code change.
+
+The supported lexical rules cover C-style line and block comments, hash comments, SQL comments, CSS block comments, and HTML comments for an explicit extension set in the scanner. Comment directives such as JSDoc tags, lint controls, compiler controls, and coverage controls are comments for routing purposes.
+
+## Verification
+
+[Scanner tests](../../../../.github/review-ownership/request-review.test.mjs) cover documentation extensions, supported comment forms, quoted comment markers, executable token changes, incomplete patches, renames, unsupported extensions, exclusion precedence, and the no-request result when every file is excluded.
+
+## Alternatives considered
+
+**Keep every non-test file reviewable.** This requests code owners for documentation and comment maintenance even though the routing policy is intended to identify executable changes.
+
+**Infer arbitrary semantic equivalence.** Proving behavior equivalence across the repository's languages requires language toolchains and still cannot assign one stable meaning to generated files, configuration, or build directives. The scanner performs only lexical comment removal.
+
+**Trust every patch returned by GitHub.** GitHub can omit or truncate patches. Matching the patch's added and deleted line counts to the file record prevents a partial patch from producing a comment-only verdict.
+
+**Fetch and parse every complete file revision.** Per-file content requests multiply API traffic for large pull requests and still require the same language-specific parsing. The changed-file response already carries enough evidence for complete ordinary patches.
+
+## Consequences
+
+Documentation and proven comment-only changes request nobody. The workflow logs them separately from tests so maintainers can audit why owner matching ignored a file.
+
+Unsupported or incomplete inputs remain reviewable. Comment directives do not request owners even when another tool interprets them, because this policy classifies their lexical form rather than downstream tool behavior.

+ 41 - 0
.agents/notes/implemented/process/2026-09-08-comment-only-review-routing.zh.md

@@ -0,0 +1,41 @@
+# Agent Note: 从评审路由中排除文档和纯注释变更
+
+Status: implemented
+
+[English](2026-09-08-comment-only-review-routing.md) | 中文
+
+## 问题
+
+只按目录分配 owner 会把文档和注释编辑视为可执行变更。这些编辑不需要用于保护行为变更的自动代码 owner 请求。
+
+GitHub 可能省略或截断文件 patch。如果扫描器假定每个 patch 都完整,就可能漏掉位于已提供 hunk 之外的可执行变更。
+
+## 决策
+
+评审路由按测试、文档、纯注释、可评审代码的顺序对每个新旧路径分类。当测试路径同时具有文档扩展名时,测试分类优先。所有以 `.md` 或 `.yaml` 结尾的文件均视为文档,扩展名匹配不区分大小写;此规则不把 `.yml` 文件视为文档。
+
+纯注释分类只适用于 `status: modified` 且已声明源码注释语法的文件。扫描器重建每个 patch hunk 的变更前后文本,移除引号字符串外的注释和注释留下的空行,并要求其余文本完全相同。
+
+扫描器会统计 patch 的新增行和删除行,并在接受纯注释结果前与 GitHub 文件记录比较。缺失 patch、计数不符、重命名、不受支持的扩展名,或词法分析器仍能看到的注释形式都会使文件保持可评审状态。该保守结果可能产生不必要的评审请求,但不会隐藏已知代码变更。
+
+受支持的词法规则按扫描器中显式的扩展名集合覆盖 C 风格行注释和块注释、井号注释、SQL 注释、CSS 块注释及 HTML 注释。JSDoc 标签、lint 控制、编译器控制和覆盖率控制等注释指令在评审路由中仍属于注释。
+
+## 验证
+
+[扫描器测试](../../../../.github/review-ownership/request-review.test.mjs)覆盖文档扩展名、受支持的注释形式、引号内的注释标记、可执行 token 变更、不完整 patch、重命名、不受支持的扩展名、排除优先级,以及所有文件均被排除时不发出请求的结果。
+
+## 考虑过的替代方案
+
+**让每个非测试文件都保持可评审。** 这会为文档和注释维护请求代码 owner,但该路由策略的目标是识别可执行变更。
+
+**推断任意语义等价。** 证明仓库中多种语言的行为等价需要各语言工具链,而且仍然无法为生成文件、配置或构建指令提供一种稳定含义。扫描器只执行词法注释移除。
+
+**信任 GitHub 返回的每个 patch。** GitHub 可能省略或截断 patch。将 patch 的新增和删除行数与文件记录匹配,可以防止不完整 patch 产生纯注释结论。
+
+**获取并解析每个文件的完整修订版本。** 对于大型 PR,逐文件内容请求会增加多倍 API 流量,而且仍需相同的语言专用解析。普通完整 patch 所需的证据已包含在变更文件响应中。
+
+## 后果
+
+文档和确认的纯注释变更不会请求任何人。Workflow 会将它们与测试分开记录,以便维护者检查 owner 匹配忽略文件的原因。
+
+不受支持或不完整的输入仍需评审。即使其他工具会解释注释指令,这些指令也不会请求 owner,因为该策略按词法形式分类,而不是按下游工具行为分类。

+ 19 - 11
.agents/notes/implemented/process/2026-09-08-trusted-changed-file-review-routing.md

@@ -2,8 +2,6 @@
 
 Status: implemented
 
-English | [中文](2026-09-08-trusted-changed-file-review-routing.zh.md)
-
 ## Problem
 
 GitHub's native CODEOWNERS behavior requests reviewers whenever a matching path changes. It cannot apply this repository's distinction between reviewable implementation or documentation files and test-only evidence. A native CODEOWNERS file also makes GitHub, rather than an inspected repository program, responsible for the request decision.
@@ -12,21 +10,25 @@ Review routing needs an observable changed-file input, explicit owner rules, com
 
 ## Decision
 
-The repository keeps a CODEOWNERS-compatible map at [`.github/review-ownership/CODEOWNERS`](../../../../.github/review-ownership/CODEOWNERS), outside GitHub's native CODEOWNERS locations. The map accepts only explicit absolute directory patterns and individual GitHub users. It rejects wildcards, hidden-directory patterns, teams, duplicate patterns, and duplicate owners. Later matching patterns replace earlier matches.
+The repository keeps a CODEOWNERS-compatible map at [`.github/review-ownership/CODEOWNERS`](../../../../.github/review-ownership/CODEOWNERS), outside GitHub's native CODEOWNERS locations. The map accepts only explicit absolute directory patterns with one or two individual GitHub users. It rejects wildcards, hidden-directory patterns, teams, more than two owners, duplicate patterns, and duplicate owners. Later matching patterns replace earlier matches.
 
 The policy test counts non-test tracked lines in directories that match an ownership rule. It rejects a map in which `@turtle1999` owns more than one third of that eligible owned codebase.
 
-The [`request-review` workflow](../../../../.github/workflows/request-review.yml) runs on non-draft `pull_request_target` events for opened, synchronized, reopened, and ready-for-review pull requests. Its write-capable job checks out the default branch and executes only the default branch's scanner and ownership map. It does not check out pull-request code or read repository secrets.
+The [`request-review` workflow](../../../../.github/workflows/request-review.yml) runs on `pull_request_target` events for opened, synchronized, reopened, ready-for-review, and converted-to-draft pull requests. Its write-capable job checks out the default branch and executes only the default branch's scanner and ownership map. It does not check out pull-request code or read repository secrets.
 
 The scanner fetches every changed-file record before deciding. It fails if the pull request reports more than GitHub's 3,000-file API limit or if pagination returns an incomplete list. It normalizes repository paths, evaluates old and new paths of a rename independently, and escapes filenames before logging them.
 
-The scanner excludes test-only paths before owner matching. Excluded paths comprise directories named `test`, `tests`, `__tests__`, `__snapshots__`, `benches`, or `stress-tests`; the top-level `benchmarks` and `snapshots` trees; `packages/test-support`; `scripts/fixtures` and `scripts/snapshots`; filenames ending in `.bench.<ext>`, `.corpus.<ext>`, `.e2e.<ext>`, `.perf.<ext>`, `.snapshot.<ext>`, `.spec.<ext>`, `.stress.<ext>`, or `.test.<ext>`; and Python `test_*.py`, `*_test.py`, or `*_tests.py` files. Test infrastructure such as `vitest*.config.ts` and gate implementations remains reviewable because it changes how repository evidence is produced.
+The scanner excludes test-only paths before owner matching. Excluded paths comprise directories named `test`, `tests`, `__tests__`, `__snapshots__`, `benches`, or `stress-tests`; the top-level `benchmarks` and `snapshots` trees; `packages/test-support`; `scripts/fixtures` and `scripts/snapshots`; filenames ending in `.bench.<ext>`, `.corpus.<ext>`, `.e2e.<ext>`, `.perf.<ext>`, `.snapshot.<ext>`, `.spec.<ext>`, `.stress.<ext>`, or `.test.<ext>`; and Python `test_*.py`, `*_test.py`, or `*_tests.py` files. Test infrastructure such as `vitest*.config.ts` and gate implementations remains reviewable because it changes how repository evidence is produced. The [comment-only routing decision](2026-09-08-comment-only-review-routing.md) owns the additional documentation and comment exclusions.
+
+The workflow prints the changed code paths, each exclusion class, per-file owner matches and changed LOC, aggregate owner relevance, approved owners omitted from new requests, current individual requests, the available counted slot after planned cancellations, and final reviewer actions before any review-request mutation. For a non-draft pull request, it fetches the complete chronological review list and reduces each owner's undismissed `APPROVED` and `CHANGES_REQUESTED` reviews to the latest decisive state; `COMMENTED` and `PENDING` reviews leave that state unchanged. It removes the pull-request author, owners with an active approval, and users who remain requested from the matched individual owners. An active approval remains sufficient after later synchronize events, while a later changes-requested review makes the owner eligible again. The review-list operation fails before mutation at 3,000 entries or on an invalid record.
+
+The workflow keeps at most one current individual review request other than `@turtle1999`. An existing request for `@turtle1999` does not consume that slot, but each workflow run adds at most one reviewer. An existing non-turtle request leaves no slot, so the workflow does not add anyone, including `@turtle1999`. Existing individual requests consume the slot even when they do not match the ownership map. An owner's relevance is the sum of GitHub-reported additions and deletions for each reviewable changed-file record whose current or previous path matches that owner. Each record contributes once per owner, including when both paths of a rename match the same owner. Higher changed LOC selects candidates first when the available slot cannot cover the remaining owners; login order resolves equal scores.
 
-The workflow prints the changed non-test paths, excluded test paths, per-file owner matches, and final reviewer list before any review-request mutation. It requests the union of matched individual owners after removing the pull-request author and users who are already requested. A test-only or wholly unmatched change requests nobody.
+When current review requests exist, the workflow reads the complete review-request timeline before mutation. A current reviewer is workflow-authored only when its latest matching `review_requested` event identifies `github-actions[bot]` as `review_requester`; a request without an attributable event is preserved. A non-draft run cancels workflow-authored reviewers that no longer match the current candidates and excess workflow-authored non-turtle reviewers above the counted limit; current relevance order selects which matching workflow reviewer remains. Planned cancellations release capacity before the workflow selects a new reviewer. A draft run cancels every current workflow-authored request. Requests made by people remain unchanged. An attributable event with invalid provenance and timelines above 3,000 events fail before mutation.
 
 ## Verification
 
-[Scanner tests](../../../../.github/review-ownership/request-review.test.mjs) cover admitted ownership syntax, rejected syntax, each test convention, production-name negative controls, renames, last-match behavior, unmatched files, complete pagination, the 3,000-file limit, log-before-request ordering, author and existing-reviewer filtering, test-only changes, drafts, and API failures. [Workflow tests](../../../../scripts/ci-workflow.spec.ts) pin the event set, least permissions, trusted default-branch checkout, absence of pull-request-head references and secrets, and executed command. The gate graph includes both suites in static CI and `check-all`.
+[Scanner tests](../../../../.github/review-ownership/request-review.test.mjs) cover admitted ownership syntax, rejected syntax, each exclusion class, production-name negative controls, renames, last-match behavior, unmatched files, changed-LOC aggregation and ranking, complete pagination, file and review limits, approval-state reduction, approved-owner suppression and next-owner selection, log-before-mutation ordering, author and existing-reviewer filtering, non-draft reconciliation, draft cancellation provenance, and API failures. [Workflow tests](../../../../scripts/ci-workflow.spec.ts) pin the event set, least permissions, trusted default-branch checkout, absence of pull-request-head references and secrets, and executed command. The gate graph includes both suites in static CI and `check-all`.
 
 ## Alternatives considered
 
@@ -36,12 +38,18 @@ The workflow prints the changed non-test paths, excluded test paths, per-file ow
 
 **Execute the pull request's scanner or owner map under `pull_request_target`.** This lets an untrusted pull request choose its own write-capable behavior or owners.
 
-**Infer semantic source changes from patches or language parsers.** GitHub can truncate patches, and the repository spans TypeScript, JavaScript, Python, Rust, YAML, Markdown, and generated evidence. A cross-language semantic classifier would add ambiguous rules without providing a complete input. The scanner therefore uses the complete non-test changed-file list and does not claim to distinguish formatting, comments, or documentation-only edits inside an eligible file.
+**Select capped candidates by login order.** Login order is stable but ignores how much reviewable code changed under each owner's directories. Changed LOC makes the limited requests follow the pull request's strongest ownership relevance while retaining login order for ties.
+
+**Cancel every reviewer that no longer matches.** A person may request a reviewer for reasons outside the ownership map. Only requests attributed to the workflow identity are safe for automated reconciliation.
+
+**Treat an empty current request as an owner who still needs review.** GitHub removes the pending request when the reviewer submits a review. Requesting an owner with an active approval again adds no ownership coverage and creates repeated notifications after later synchronize events.
+
+**Infer arbitrary semantic source changes from patches or language parsers.** GitHub can omit or truncate patches, and the repository spans many languages. The scanner does not try to prove that two programs behave identically. The later [comment-only routing decision](2026-09-08-comment-only-review-routing.md) adds a narrow lexical comparison only when changed-line counts prove that GitHub supplied the complete patch.
 
 ## Consequences
 
-Reviewer requests are reproducible from a trusted policy and the file list printed in the workflow log. Test-only changes do not request owners. Ownership changes become effective only after merge, so the pull request that changes policy cannot apply its untrusted policy to itself.
+Reviewer mutations are reproducible from a trusted policy, the file classifications printed in the workflow log, and review-request provenance in the pull-request timeline. Excluded changes do not request owners, rule and changed-file updates remove obsolete workflow-authored requests on the next run, and draft pull requests do not retain workflow-authored requests. Ownership changes become effective only after merge, so the pull request that changes policy cannot apply its untrusted policy to itself.
 
-The workflow requests every matched owner rather than choosing one owner nondeterministically. Shared ownership on large directories therefore produces multiple requests. GitHub-generated review-request events may not start other workflows that depend on recursively triggered events from `GITHUB_TOKEN`; those workflows must not rely on this request as their only trigger.
+The workflow requests at most one reviewer per run, does not repeat a request while that owner has an active approval, keeps no more than one current individual reviewer other than `@turtle1999`, and prefers owners whose matched reviewable files carry more changed LOC. An existing `@turtle1999` request leaves the counted slot available; an existing non-turtle request prevents every additional request. Shared ownership gives each owner the same file-level relevance without counting one renamed file twice for the same owner. GitHub-generated review-request events may not start other workflows that depend on recursively triggered events from `GITHUB_TOKEN`; those workflows must not rely on this request as their only trigger.
 
-Any non-test change under an owned directory remains eligible, including comment-only or formatting-only edits and documentation changes. Unmatched paths are logged and request nobody. Pull requests above the API file limit fail without requesting a partial owner set.
+Any change that does not match an explicit exclusion remains eligible under an owned directory. Unmatched paths are logged and request nobody. Pull requests above the file, review, or timeline API limit fail without applying a partial reviewer mutation.

+ 0 - 47
.agents/notes/implemented/process/2026-09-08-trusted-changed-file-review-routing.zh.md

@@ -1,47 +0,0 @@
-# Agent Note: 基于受信任的变更文件策略路由评审
-
-Status: implemented
-
-[English](2026-09-08-trusted-changed-file-review-routing.md) | 中文
-
-## 问题
-
-只要匹配路径发生变更,GitHub 原生 CODEOWNERS 就会请求评审者。它无法应用本仓库对需评审的实现或文档文件与纯测试证据的区分。使用原生 CODEOWNERS 文件还会让 GitHub 负责请求决策,而不是由可检查的仓库程序负责。
-
-评审路由需要可观测的变更文件输入、显式 owner 规则、完整的测试排除规则,以及对 fork PR 仍然安全且具备写权限的 workflow。
-
-## 决策
-
-仓库在 GitHub 原生 CODEOWNERS 路径之外的 [`.github/review-ownership/CODEOWNERS`](../../../../.github/review-ownership/CODEOWNERS) 中保存兼容 CODEOWNERS 格式的映射。该映射只接受显式绝对目录模式和 GitHub 个人用户。通配符、隐藏目录模式、团队、重复模式和重复 owner 都会被拒绝。靠后的匹配模式会替换靠前的匹配结果。
-
-策略测试会统计匹配所有权规则的目录中的非测试跟踪文件行数。如果 `@turtle1999` 拥有的有效代码库超过三分之一,测试就会拒绝该映射。
-
-[`request-review` workflow](../../../../.github/workflows/request-review.yml) 在非草稿 PR 的 `pull_request_target` 事件上运行,订阅创建、同步、重新打开和标记为可评审操作。具备写权限的 job 检出默认分支,只执行默认分支上的扫描器和所有权映射。它不会检出 PR 代码,也不会读取仓库 secret。
-
-扫描器在决策之前获取所有变更文件记录。如果 PR 报告的文件数超过 GitHub API 的 3,000 个文件上限,或者分页只返回了部分列表,扫描器就会失败。它会规范化仓库路径,分别检查重命名前后的路径,并在记录文件名之前进行转义。
-
-扫描器会在匹配 owner 之前排除纯测试路径。排除范围包括名为 `test`、`tests`、`__tests__`、`__snapshots__`、`benches` 或 `stress-tests` 的目录,顶层 `benchmarks` 和 `snapshots` 目录树,`packages/test-support`、`scripts/fixtures` 和 `scripts/snapshots`,以 `.bench.<ext>`、`.corpus.<ext>`、`.e2e.<ext>`、`.perf.<ext>`、`.snapshot.<ext>`、`.spec.<ext>`、`.stress.<ext>` 或 `.test.<ext>` 结尾的文件名,以及 Python 的 `test_*.py`、`*_test.py` 或 `*_tests.py` 文件。`vitest*.config.ts` 和门禁实现等测试基础设施仍需评审,因为它们会改变仓库证据的生成方式。
-
-Workflow 会在发出任何评审请求变更之前,依次打印变更的非测试路径、排除的测试路径、逐文件 owner 匹配结果和最终评审者列表。它合并匹配到的个人 owner,并排除 PR 作者和已经收到评审请求的用户。纯测试变更或全部未匹配的变更不会请求任何人。
-
-## 验证
-
-[扫描器测试](../../../../.github/review-ownership/request-review.test.mjs)覆盖允许的所有权语法、拒绝的语法、每种测试约定、生产文件名负向对照、重命名、最后匹配规则、未匹配文件、完整分页、3,000 个文件上限、先记录后请求的顺序、作者与现有评审者过滤、纯测试变更、草稿和 API 失败。[Workflow 测试](../../../../scripts/ci-workflow.spec.ts)固定事件集合、最小权限、受信任的默认分支检出、不引用 PR head 和 secret,以及执行的命令。门禁图在静态 CI 和 `check-all` 中包含这两组测试。
-
-## 考虑过的替代方案
-
-**使用原生 CODEOWNERS。** 原生路由无法忽略纯测试变更,也无法在请求评审者之前提供由仓库控制的决策日志。
-
-**在 `pull_request` 下运行并检出 PR head。** Fork workflow 无法获得具备写权限的 token,而向不受信任 head 中的代码授予写权限 token 并不安全。
-
-**在 `pull_request_target` 下执行 PR 中的扫描器或 owner 映射。** 这会让不受信任的 PR 选择自己的写权限行为或 owner。
-
-**根据补丁或语言解析器推断语义源码变更。** GitHub 可能截断补丁,而且仓库包含 TypeScript、JavaScript、Python、Rust、YAML、Markdown 和生成的证据。跨语言语义分类器会增加含义不明确的规则,却无法提供完整输入。因此,扫描器使用完整的非测试变更文件列表,并且不会声称能够区分合格文件中的纯格式、注释或仅文档编辑。
-
-## 后果
-
-评审请求可以根据受信任的策略和 workflow 日志中打印的文件列表复现。纯测试变更不会请求 owner。所有权变更只有合并后才会生效,因此修改策略的 PR 无法对自身应用其中不受信任的策略。
-
-Workflow 会请求所有匹配的 owner,不会随机选择一人。因此,大目录上的共享所有权会产生多个请求。GitHub 使用 `GITHUB_TOKEN` 生成的评审请求事件可能不会启动依赖递归触发事件的其他 workflow;这些 workflow 不得把此请求作为唯一触发条件。
-
-所有已分配目录下的非测试变更仍符合请求条件,其中包括纯注释、纯格式调整和文档变更。未匹配的路径会被记录,但不会请求任何人。超过 API 文件上限的 PR 会失败,并且不会请求不完整的 owner 集合。

+ 6 - 0
.agents/notes/implemented/simplification/2026-09-07-file-content-scan.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/simplification/2026-09-07-file-content-scan.md
+2026-09-07-file-content-scan.md: 146f41a26b54e2823b7dfbdab5b6738c7a5041da
+2026-09-07-file-content-scan.zh.md: 07b4a9ecbca5d63eacccb43a9e3a40d0afe2a4cf

+ 32 - 0
.agents/notes/implemented/simplification/2026-09-07-file-content-scan.md

@@ -0,0 +1,32 @@
+# Agent Note: Scan file content without per-array callbacks
+
+Status: implemented
+
+English | [中文](2026-09-07-file-content-scan.zh.md)
+
+## Problem
+
+Every model dispatch checks complete message content for files, including nested tool results. A request-history CPU profile attributes 23.540 ms of self time to `contentHasFile` and 5.584 ms to its callback. This traversal remains necessary even after [loop-owned freeze provenance](2026-09-06-agent-request-freeze-provenance.md) removes repeated request freezing. The hot LLM source is identical at master `bd5917`, master `112a5`, and the measured `f834b002826453e7918eeb558d052b2c24c56a76`; these observations do not establish PR causality.
+
+## Decision
+
+[`contentHasFile`](../../../../packages/llm/llm/src/content.ts) uses direct iteration instead of recursive `Array.some` callbacks. It preserves early exit, nested tool-result traversal, and false results for other block kinds. It stores no identities, validation results, or freeze proofs. Image detection, file projection, request construction, and the 238 ms request-history budget are unchanged.
+
+## Measurement evidence
+
+Apple M4 Pro, Node 24.19.0: nine alternating original/candidate pairs run the unchanged [request-history worker](../../../../benchmarks/agent-continuation/agent-continuation.worker.ts) in fresh plain-Node processes. Each process receives a copy of one native-V3 seed. Only the built LLM entry changes; every sample completes 40 requests, zero live tools, and 13,925 events. All totals below are milliseconds, in pair order.
+
+| Variant | Raw totals | Median |
+|---|---|---:|
+| Original | 61.772167, 66.480917, 63.590042, 62.250333, 63.804875, 62.504208, 61.887958, 61.062959, 67.120125 | 62.504208 |
+| Direct iteration | 53.655375, 56.223416, 57.078125, 54.559250, 54.271459, 55.288917, 55.400416, 56.191916, 54.877041 | 55.288917 |
+
+The median improves 11.54%; all nine pairs improve, by 4.871–12.243 ms. User CPU medians are 82.019/76.150 ms. A separate five-pair scan probe uses the same synthetic history: 5,601 frozen messages, 13,600 blocks, and 8,801 content arrays scanned 40 times. Original totals are 17.188584, 17.944000, 16.869458, 17.437750, 17.459708; direct iteration totals are 8.726333, 8.373000, 8.851750, 8.479167, 9.074083. These local measurements establish an implementation gain, not a hosted-CI pass or a new calibration.
+
+## Alternatives considered
+
+A weak negative-result cache needs proof that every relevant descendant is immutable; a shallow-frozen root is insufficient. Direct iteration provides measured savings without introducing that ownership or invalidation problem. Optimizing image traversal or system-prompt projection lacks evidence from this experiment and is outside this change.
+
+## Consequences
+
+The scan remains linear in visited blocks and rereads mutable nested content on each call. [Content tests](../../../../packages/llm/llm/tests/content.spec.ts) cover empty, frozen, nested, and subsequently mutated arrays; service tests preserve file-handle projection, and request-freeze, reconstruction, and resume tests preserve native-history semantics. No model-visible text or Session format changes. The freeze-provenance and [backend-baseline](../testing/2026-09-06-backend-continuation-performance.md) notes retain independent ownership; neither is superseded.

+ 32 - 0
.agents/notes/implemented/simplification/2026-09-07-file-content-scan.zh.md

@@ -0,0 +1,32 @@
+# Agent Note: 文件内容扫描不为每个数组创建回调
+
+Status: implemented
+
+[English](2026-09-07-file-content-scan.md) | 中文
+
+## Problem
+
+每次模型分发都检查完整消息内容中的文件,包括嵌套工具结果。请求历史 CPU profile 将 23.540 ms 自身时间归于 `contentHasFile`,将 5.584 ms 归于其回调。即使[循环自有冻结来源证明](2026-09-06-agent-request-freeze-provenance.zh.md)消除了重复请求冻结,这次遍历仍然必需。master `bd5917`、master `112a5` 与实测的 `f834b002826453e7918eeb558d052b2c24c56a76` 的 LLM 热点源码完全相同;这些观察不能证明 PR 因果关系。
+
+## Decision
+
+[`contentHasFile`](../../../../packages/llm/llm/src/content.ts) 使用直接迭代,替代递归的 `Array.some` 回调。它保留提前退出、嵌套工具结果遍历,以及其他块类型返回 false 的行为。它不存储身份、校验结果或冻结证明。图片检测、文件投影、请求构建和 238 ms 请求历史预算保持不变。
+
+## Measurement evidence
+
+Apple M4 Pro、Node 24.19.0:九组交替原始/候选配对在全新的普通 Node 进程中运行不变的[请求历史 worker](../../../../benchmarks/agent-continuation/agent-continuation.worker.ts)。每个进程接收同一个原生 V3 种子的副本。只有构建后的 LLM 入口变化;每个样本均完成 40 次请求、零次实时工具调用与 13,925 个事件。以下全部总耗时单位为毫秒,按配对顺序排列。
+
+| 变体 | 原始总耗时 | 中位数 |
+|---|---|---:|
+| 原始 | 61.772167, 66.480917, 63.590042, 62.250333, 63.804875, 62.504208, 61.887958, 61.062959, 67.120125 | 62.504208 |
+| 直接迭代 | 53.655375, 56.223416, 57.078125, 54.559250, 54.271459, 55.288917, 55.400416, 56.191916, 54.877041 | 55.288917 |
+
+中位数改善 11.54%;全部九组均改善,幅度为 4.871–12.243 ms。用户 CPU 中位数为 82.019/76.150 ms。独立的五组配对扫描探针使用相同的合成历史:5,601 条冻结消息、13,600 个块和 8,801 个内容数组,扫描 40 次。原始总耗时为 17.188584, 17.944000, 16.869458, 17.437750, 17.459708;直接迭代总耗时为 8.726333, 8.373000, 8.851750, 8.479167, 9.074083。这些本地测量证明实现改善,不代表托管 CI 通过或新的校准。
+
+## Alternatives considered
+
+弱引用的否定结果缓存需要证明每个相关后代均不可变;浅冻结根对象并不足够。直接迭代提供实测收益,无需引入此类所有权或失效问题。本实验没有提供优化图片遍历或系统提示词投影的证据,因此它们不在此次变更范围内。
+
+## Consequences
+
+扫描仍与访问块数呈线性关系,每次调用都重新读取可变的嵌套内容。[内容测试](../../../../packages/llm/llm/tests/content.spec.ts)覆盖空数组、冻结数组、嵌套数组与后续变更的数组;服务测试保留文件 handle 投影,请求冻结、重建与恢复测试保留原生历史语义。模型可见文本与 Session 格式均不改变。冻结来源证明与[后端基线](../testing/2026-09-06-backend-continuation-performance.zh.md)记录仍各自拥有独立决策;两者均未被取代。

+ 2 - 2
.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.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-08-24-session-log-snapshot-corpus.md
-2026-08-24-session-log-snapshot-corpus.md: bd8941a40bf1c5f0dac15205c5ab0c7d4a3179aa
-2026-08-24-session-log-snapshot-corpus.zh.md: d27a3b90e13c73fd46794992d84e6393f0d030ce
+2026-08-24-session-log-snapshot-corpus.md: ebcd9709f9cd17ad288d787a13ca66efee5fcc42
+2026-08-24-session-log-snapshot-corpus.zh.md: 8d0614e150c927b491784b51a166e752a7718dae

+ 3 - 1
.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.md

@@ -16,7 +16,7 @@ Reserve the top-level `snapshots/` tree and `*.snapshot.ts` suffix for scenarios
 
 This decision supersedes the ACP-specific placement and controller ownership in the [record-once/replay-deterministic decision](2026-06-19-acp-snapshot-tests.md), while that note remains authoritative for session-log replay, exceptional overrides, normalization, and ACP transcript comparison.
 
-The recorded session remains the primary input and expected output. Human-originated messages drive the selected public interface, recorded assistant chunks drive deterministic model replay, and the normalized persisted result must equal the fixture. Parent and child sessions share one typed redaction map. Committed fixtures contain relationship-preserving identity tokens and replace request system prompts and tool schemas with tokens; each distinct header class retains one explicit sidecar owner.
+The recorded session remains the primary input and, for current-generation scenarios, the expected output. Human-originated messages drive the selected public interface, recorded assistant chunks drive deterministic model replay, and the normalized persisted result must equal the fixture. Parent and child sessions share one typed redaction map. Committed fixtures contain relationship-preserving identity tokens and replace request system prompts and tool schemas with tokens; each distinct header class retains one explicit sidecar owner.
 
 Fixture decoding and comparison depend only on the selected JSONL content; filenames identify inventory roles but are not parser inputs. The same strict static catalog validates replay, seed, record, refresh, and normalized comparison paths.
 
@@ -30,6 +30,8 @@ Every existing ACP scenario receives a behavior-preserving destination. Ordinary
 
 Workspace inputs remain scenario-local. A mutating scenario compares a complete expected final workspace that record and refresh never rewrite, so a model or tool self-report cannot satisfy the test. Existing intentional session reuse remains an explicit acyclic owner reference; the corpus adds no workspace inheritance or general fixture-merging mechanism.
 
+Current-writer request-header pins are separate from retained migration inputs: `tool-call-turn` pins the default composition, and `empty-response-retry-current` pins the retry composition. Their readable sidecars remain owned by `text-turn`. The six retained historical inputs stay byte-frozen and selected for replay; their pinned directories contain no canonical V3 sibling that could displace them. Separate `writer.expected.jsonl` and `writer.<ordinal>.expected.jsonl` files pin exact normalized native V3 parent and child output, while retained SDK scenarios pin current notifications in `notifications.current.expected.jsonl`. These output oracles are not replay generations. The [snapshot kit](../../../../packages/test-support/session-snapshot/README.md) owns selection and refresh behavior. Structural migration can preserve request meaning without reproducing native writer event layout, so the official migration has independent correctness tests. Reverse projection into historical headers, stripping structural differences, skipping output equality, or replacing frozen inputs would conceal regressions instead of verifying those separate obligations.
+
 ## Alternatives considered
 
 **Keep ACP as the universal driver.** This preserves the existing harness but continues coupling backend coverage to a low-priority protocol and cannot prove the supported headless, SDK, and Web launch paths.

Некоторые файлы не были показаны из-за большого количества измененных файлов