فهرست منبع

Merge remote-tracking branch 'origin/master' into worktree/2984-generic-file-upload

# Conflicts:
#	apps/web/tests/scaffold.ts
#	docs/config-catalog.i18n.yaml
#	docs/config-catalog.md
#	docs/config-catalog.zh.md
#	docs/event-producer-consumer.i18n.yaml
#	docs/event-producer-consumer.md
#	docs/event-producer-consumer.zh.md
#	packages/api/session-controller/README.i18n.yaml
#	packages/api/session-controller/README.md
#	packages/api/session-controller/README.zh.md
#	packages/extensions/tool-cordis/src/api-catalog.ts
#	packages/llm/token-meter/src/index.ts
#	packages/session-query/session-log-export/README.i18n.yaml
#	packages/session-query/session-log-export/README.md
#	packages/session-query/session-log-export/README.zh.md
#	packages/session-query/session-log-export/src/archive.ts
#	scripts/package-dependency-policy.ts
creatixchu 1 روز پیش
والد
کامیت
fb743759a7
100فایلهای تغییر یافته به همراه659 افزوده شده و 229 حذف شده
  1. 6 0
      .agents/notes/archived/architecture/2026-07-26-packed-chunk-rows-by-default.i18n.yaml
  2. 1 0
      .agents/notes/archived/architecture/2026-07-26-packed-chunk-rows-by-default.md
  3. 1 0
      .agents/notes/archived/architecture/2026-07-26-packed-chunk-rows-by-default.zh.md
  4. 6 0
      .agents/notes/archived/architecture/2026-08-15-packed-session-history-transport.i18n.yaml
  5. 1 0
      .agents/notes/archived/architecture/2026-08-15-packed-session-history-transport.md
  6. 1 0
      .agents/notes/archived/architecture/2026-08-15-packed-session-history-transport.zh.md
  7. 6 0
      .agents/notes/archived/manifest.json
  8. 2 2
      .agents/notes/implemented/architecture/2026-06-14-session-persistence.i18n.yaml
  9. 6 6
      .agents/notes/implemented/architecture/2026-06-14-session-persistence.md
  10. 6 6
      .agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md
  11. 2 2
      .agents/notes/implemented/architecture/2026-06-18-session-surface.i18n.yaml
  12. 5 5
      .agents/notes/implemented/architecture/2026-06-18-session-surface.md
  13. 5 5
      .agents/notes/implemented/architecture/2026-06-18-session-surface.zh.md
  14. 2 2
      .agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml
  15. 3 3
      .agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md
  16. 3 3
      .agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md
  17. 2 2
      .agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.i18n.yaml
  18. 1 1
      .agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.md
  19. 1 1
      .agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.zh.md
  20. 2 2
      .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml
  21. 1 1
      .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md
  22. 2 2
      .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md
  23. 2 2
      .agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml
  24. 3 3
      .agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md
  25. 3 3
      .agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md
  26. 2 2
      .agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.i18n.yaml
  27. 3 3
      .agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.md
  28. 3 3
      .agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.zh.md
  29. 2 2
      .agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.i18n.yaml
  30. 1 1
      .agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.md
  31. 1 1
      .agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.zh.md
  32. 2 2
      .agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.i18n.yaml
  33. 2 2
      .agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.md
  34. 2 2
      .agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.zh.md
  35. 2 2
      .agents/notes/implemented/architecture/2026-08-05-large-session-jsonl-restore-pipeline.i18n.yaml
  36. 1 1
      .agents/notes/implemented/architecture/2026-08-05-large-session-jsonl-restore-pipeline.md
  37. 1 1
      .agents/notes/implemented/architecture/2026-08-05-large-session-jsonl-restore-pipeline.zh.md
  38. 2 2
      .agents/notes/implemented/architecture/2026-08-08-bounded-session-persistence-write-batching.i18n.yaml
  39. 7 7
      .agents/notes/implemented/architecture/2026-08-08-bounded-session-persistence-write-batching.md
  40. 7 7
      .agents/notes/implemented/architecture/2026-08-08-bounded-session-persistence-write-batching.zh.md
  41. 2 2
      .agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml
  42. 4 4
      .agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md
  43. 4 4
      .agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md
  44. 2 2
      .agents/notes/implemented/architecture/2026-08-10-cancelled-stream-prefix-finalize.i18n.yaml
  45. 7 7
      .agents/notes/implemented/architecture/2026-08-10-cancelled-stream-prefix-finalize.md
  46. 7 7
      .agents/notes/implemented/architecture/2026-08-10-cancelled-stream-prefix-finalize.zh.md
  47. 2 2
      .agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.i18n.yaml
  48. 3 3
      .agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md
  49. 3 3
      .agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.zh.md
  50. 2 2
      .agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.i18n.yaml
  51. 1 1
      .agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.md
  52. 1 1
      .agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.zh.md
  53. 2 2
      .agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.i18n.yaml
  54. 1 1
      .agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md
  55. 1 1
      .agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md
  56. 2 2
      .agents/notes/implemented/architecture/2026-08-25-rename-code-mode-to-ptc.i18n.yaml
  57. 3 3
      .agents/notes/implemented/architecture/2026-08-25-rename-code-mode-to-ptc.md
  58. 3 3
      .agents/notes/implemented/architecture/2026-08-25-rename-code-mode-to-ptc.zh.md
  59. 2 2
      .agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.i18n.yaml
  60. 2 2
      .agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.md
  61. 2 2
      .agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.zh.md
  62. 2 2
      .agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.i18n.yaml
  63. 2 0
      .agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.md
  64. 2 0
      .agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.zh.md
  65. 6 0
      .agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.i18n.yaml
  66. 37 0
      .agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.md
  67. 37 0
      .agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.zh.md
  68. 3 3
      .agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.i18n.yaml
  69. 24 0
      .agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.md
  70. 24 0
      .agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.zh.md
  71. 6 0
      .agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.i18n.yaml
  72. 57 0
      .agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md
  73. 57 0
      .agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md
  74. 3 3
      .agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.i18n.yaml
  75. 74 0
      .agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.md
  76. 74 0
      .agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.zh.md
  77. 2 2
      .agents/notes/implemented/architecture/2026-09-02-projcache-cross-version-read-compat.i18n.yaml
  78. 24 20
      .agents/notes/implemented/architecture/2026-09-02-projcache-cross-version-read-compat.md
  79. 24 20
      .agents/notes/implemented/architecture/2026-09-02-projcache-cross-version-read-compat.zh.md
  80. 2 2
      .agents/notes/implemented/bug-fix/2026-07-20-jsonl-storage-identity.i18n.yaml
  81. 1 1
      .agents/notes/implemented/bug-fix/2026-07-20-jsonl-storage-identity.md
  82. 1 1
      .agents/notes/implemented/bug-fix/2026-07-20-jsonl-storage-identity.zh.md
  83. 2 2
      .agents/notes/implemented/bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.i18n.yaml
  84. 1 1
      .agents/notes/implemented/bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.md
  85. 1 1
      .agents/notes/implemented/bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.zh.md
  86. 2 2
      .agents/notes/implemented/bug-fix/2026-07-31-english-compaction-checkpoints.i18n.yaml
  87. 1 1
      .agents/notes/implemented/bug-fix/2026-07-31-english-compaction-checkpoints.md
  88. 1 1
      .agents/notes/implemented/bug-fix/2026-07-31-english-compaction-checkpoints.zh.md
  89. 2 2
      .agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.i18n.yaml
  90. 2 2
      .agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.md
  91. 2 2
      .agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.zh.md
  92. 2 2
      .agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.i18n.yaml
  93. 1 1
      .agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md
  94. 1 1
      .agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.zh.md
  95. 2 2
      .agents/notes/implemented/bug-fix/2026-08-20-turn-error-survives-same-turn-retry-history.i18n.yaml
  96. 1 1
      .agents/notes/implemented/bug-fix/2026-08-20-turn-error-survives-same-turn-retry-history.md
  97. 1 1
      .agents/notes/implemented/bug-fix/2026-08-20-turn-error-survives-same-turn-retry-history.zh.md
  98. 2 2
      .agents/notes/implemented/feature/2026-07-10-sqlite-session-query-provider.i18n.yaml
  99. 3 3
      .agents/notes/implemented/feature/2026-07-10-sqlite-session-query-provider.md
  100. 3 3
      .agents/notes/implemented/feature/2026-07-10-sqlite-session-query-provider.zh.md

+ 6 - 0
.agents/notes/archived/architecture/2026-07-26-packed-chunk-rows-by-default.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/archived/architecture/2026-07-26-packed-chunk-rows-by-default.md
+2026-07-26-packed-chunk-rows-by-default.md: c230c1f1faf5e597321654ebd01d60fae725f518
+2026-07-26-packed-chunk-rows-by-default.zh.md: e354efdf6bb68c02f30dc17c8d4ba17a495b61bd

+ 1 - 0
.agents/notes/implemented/architecture/2026-07-26-packed-chunk-rows-by-default.md → .agents/notes/archived/architecture/2026-07-26-packed-chunk-rows-by-default.md

@@ -1,6 +1,7 @@
 # Agent Note: Make packed chunk rows the default JSONL layout
 
 Status: implemented
+Archived: 2026-09-01
 
 English | [中文](2026-07-26-packed-chunk-rows-by-default.zh.md)
 

+ 1 - 0
.agents/notes/implemented/architecture/2026-07-26-packed-chunk-rows-by-default.zh.md → .agents/notes/archived/architecture/2026-07-26-packed-chunk-rows-by-default.zh.md

@@ -1,6 +1,7 @@
 # Agent Note: 将打包分片行设为默认 JSONL 布局
 
 Status: implemented
+Archived: 2026-09-01
 
 [English](2026-07-26-packed-chunk-rows-by-default.md) | 中文
 

+ 6 - 0
.agents/notes/archived/architecture/2026-08-15-packed-session-history-transport.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/archived/architecture/2026-08-15-packed-session-history-transport.md
+2026-08-15-packed-session-history-transport.md: 1fe8c78a89a0541475d10fae9ad643203f144c30
+2026-08-15-packed-session-history-transport.zh.md: b2aa5bf0bc0bc4952df29674f0b9ffe836368f73

+ 1 - 0
.agents/notes/implemented/architecture/2026-08-15-packed-session-history-transport.md → .agents/notes/archived/architecture/2026-08-15-packed-session-history-transport.md

@@ -1,6 +1,7 @@
 # Agent Note: Carry packed chunk rows through session history
 
 Status: implemented
+Archived: 2026-09-01
 
 English | [中文](2026-08-15-packed-session-history-transport.zh.md)
 

+ 1 - 0
.agents/notes/implemented/architecture/2026-08-15-packed-session-history-transport.zh.md → .agents/notes/archived/architecture/2026-08-15-packed-session-history-transport.zh.md

@@ -1,6 +1,7 @@
 # Agent Note: 在会话历史中传输打包分片行
 
 Status: implemented
+Archived: 2026-09-01
 
 [English](2026-08-15-packed-session-history-transport.md) | 中文
 

+ 6 - 0
.agents/notes/archived/manifest.json

@@ -40,6 +40,9 @@
     "architecture/2026-07-24-dsh-commander-argument-adapter.i18n.yaml": "sha256:cf99eda0e58b49630d5f95792459d7095666fafbef61f614165d5cdd031b7118",
     "architecture/2026-07-24-dsh-commander-argument-adapter.md": "sha256:705654c8a43bcd199f72c21a77d24ca8bfa02447aff1c7f3e4e820be61dcd562",
     "architecture/2026-07-24-dsh-commander-argument-adapter.zh.md": "sha256:3844f02d7659d18caf5d39e1131ed775c789cbf92dc44b4a446c7d6468aa5d00",
+    "architecture/2026-07-26-packed-chunk-rows-by-default.i18n.yaml": "sha256:41aa86c65f78e125ca2178295d02d5994d5e0f2e9359b1465777db1aba4a105b",
+    "architecture/2026-07-26-packed-chunk-rows-by-default.md": "sha256:2b4e14675d12a1fc07eb373ab8566c3e63c7c15e2b5a0b391e6acbc896c7359d",
+    "architecture/2026-07-26-packed-chunk-rows-by-default.zh.md": "sha256:e0b0e8a4ab529a3461c22f434cbbe1e7b0ea381592871caf7e3fe5157ef844ba",
     "architecture/2026-07-27-tui-chat-channel-module-split.i18n.yaml": "sha256:7b9dbe8b4a340640610abe7e54fb29492d77a187c176996a53d0e1fc7c8e1945",
     "architecture/2026-07-27-tui-chat-channel-module-split.md": "sha256:3e2cd43f306a18b3eaf9bac23e6bdc3a5dbdc7388b7c399ce71e4f71b8f71d2a",
     "architecture/2026-07-27-tui-chat-channel-module-split.zh.md": "sha256:d6b84fdcd91a2693b72cf6884b3a0c39e56e571b2b694f630805d894a6ba292f",
@@ -55,6 +58,9 @@
     "architecture/2026-08-11-plugin-settings-tabs.i18n.yaml": "sha256:0365da2b317fc5f94dd190064198565f4c624afc91d2e62161ab9170f79d11bc",
     "architecture/2026-08-11-plugin-settings-tabs.md": "sha256:fdd92cfe55b6c4cd31b3f768dd46a2ecf129a04c9818249cbdd33857cf722bbf",
     "architecture/2026-08-11-plugin-settings-tabs.zh.md": "sha256:8993df1a0178aba1ea35c460ee67c522900344a4b386287bba9dfac2bfb87efa",
+    "architecture/2026-08-15-packed-session-history-transport.i18n.yaml": "sha256:547b89497b009593db5acfae2a3b989f17b8392f5df73ef631b38f8f68f629f1",
+    "architecture/2026-08-15-packed-session-history-transport.md": "sha256:ec7f84d59eea95668a8cb6c92ae433a2b7e7b76446a27e57dd5dba05856f563f",
+    "architecture/2026-08-15-packed-session-history-transport.zh.md": "sha256:0d8eb5444557a18f76c68ce5ae9f0779c580eaddb921ee8651464baea34e0e53",
     "architecture/2026-08-18-sqlite-physical-chunk-row-compression.i18n.yaml": "sha256:42bce930799cb511e9fb245dec5e26efd78bdab4c9b75f7393e37b40fbee4d10",
     "architecture/2026-08-18-sqlite-physical-chunk-row-compression.md": "sha256:4fe241f1b272278d9f3ca1a4431971220e1fa54411df043826ef6f59225bf949",
     "architecture/2026-08-18-sqlite-physical-chunk-row-compression.zh.md": "sha256:73178c9ec5abf571680d8facfb145cbadc1efbb2e67e3f039747c2f9cf4bb730",

+ 2 - 2
.agents/notes/implemented/architecture/2026-06-14-session-persistence.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-14-session-persistence.md
-2026-06-14-session-persistence.md: a7e06af78c4a372be7a68f3e0f6dc18e38cbead1
-2026-06-14-session-persistence.zh.md: 6d458d4f4c31793212d674bb406204c3882a25ed
+2026-06-14-session-persistence.md: 8f6c8753007e1998f9b072974e154c5a76ef116b
+2026-06-14-session-persistence.zh.md: b6d8e16377e0c294f19c69334c0770a66add756e

+ 6 - 6
.agents/notes/implemented/architecture/2026-06-14-session-persistence.md

@@ -14,12 +14,12 @@ The [event-sourced model](2026-06-11-event-sourced-sessions.md) makes the append
 
 Persistence is a **capability seam** with an abstract Service Definition ([capability seams](2026-06-13-capability-seams.md), the `dsh-shell` template), not loop or core logic:
 
-1. **Interface** (`dsh-session-persistence`, `ctx.sessionPersistence`) — an abstract `SessionPersistence` service: `create`/`open`/`stat`/`list`/`export`, with `create`/`open` returning per-session `SessionHandle`s that carry `read`/`append`/`flush`/`close` ([handle-based seam](2026-08-27-handle-based-session-persistence.md)). Its persisted unit IS the existing `SessionEvent` (`{ type, seq, time, data }`), reused verbatim — no conversion type.
-2. **Implementation** (`dsh-session-persistence-jsonl`) — an append-only logical JSONL log per session: a `SessionHeader` line followed by storage records that losslessly represent the contiguous `SessionEvent` stream. Eligible `assistant/chunk` delta runs use packed rows by default; [checksummed Zstandard frames](2026-07-19-zstandard-jsonl-session-logs.md) are the default physical encoding, with raw lines configurable.
+1. **Interface** (`dsh-session-persistence`, `ctx.sessionPersistence`) — an abstract `SessionPersistence` service: `create`/`open`/`stat`/`list`/`flush`, with `create`/`open` returning per-session `SessionHandle`s that carry `read`/`append`/`flush`/`close` ([handle-based seam](2026-08-27-handle-based-session-persistence.md)). Its persisted unit IS the existing `SessionEvent` (`{ type, seq, time, data }`), reused verbatim — no conversion type.
+2. **Implementation** (`dsh-session-persistence-jsonl`) — an append-only logical JSONL log per session: a `SessionHeader` line followed by storage records that losslessly represent the contiguous `SessionEvent` stream. Current v2 writes one event per row; frozen v0 and v1 readers retain their historical packed-delta representation. [Checksummed Zstandard frames](2026-07-19-zstandard-jsonl-session-logs.md) are the default physical encoding, with raw lines configurable.
 
 Key durable, contested choices:
 
-- **The canonical durable log persists every `SessionEvent` losslessly, including `assistant/chunk`.** JSONL storage may encode a consecutive delta run as one packed row, but logical readers reconstruct the exact event boundaries, sequence numbers, and timestamps. `deriveMessages()` skips chunks, and a chunk-filtered rollout (Codex's `policy.rs`) is tempting — but `seq = log.length` and validation of `events[i].seq === i` require a *contiguous* logical log; filtering chunks out would leave holes and break both the contract and resume. A chunk-filtered projection is possible later as a derived view with its own renumbering, but it is NOT the canonical log.
+- **The canonical durable log persists every current `SessionEvent` losslessly.** In v2, one `assistant/message` or `assistant/attempt` embeds the exact timed provider stream for an attempt; `deriveMessages()` projects only the surface message. Dropping embedded stream members is tempting, but it loses replay, timing, usage, partial-failure, and diagnostic facts. Removing a complete event likewise requires dense renumbering because `seq = log.length` and `events[i].seq === i`; the [v1-to-v2 migration](2026-09-01-v2-embedded-assistant-streams.md) performs that rewrite explicitly rather than filtering the canonical log.
 - **Append-only; a crashed turn is closed, never truncated.** Flushed events are never rewritten. The [semantic checkpoint policy](../bug-fix/2026-07-21-semantic-session-checkpoints.md) drains the request before model dispatch, a recorded top-level call before tool dispatch, and the complete response/result batch after a step; the loop drains the final turn boundary. Because one interrupted turn may contain substantial valid work, persistence returns its contiguous, parseable events unmodified; the reader owns balancing — resume computes risk-classified error results for unanswered assistant calls, a missing `step/end`, and `turn/end` with `{ kind: 'interrupted' }` (`interruptedTurnClosers`) and appends them through its write handle, while read-only observers add the same closers in memory. The synthetic results keep resumed provider transcripts valid. Only the incomplete fragment of a torn final append is discarded — complete records recovered from it are durably rewritten by the write path before its first new append; a parse error or sequence gap in the committed prefix is corruption and makes the session unloadable.
 - **The file backend is canonical while the service remains extensible.** `dsh-session-persistence-jsonl` is the sole first-party provider and passes `runPersistenceContract`; the abstract service remains available to out-of-tree providers. The [JSONL-only persistence decision](../simplification/2026-08-30-jsonl-only-session-persistence.md) owns removal of the first-party database provider and its deliberate compatibility cut.
 - **Metadata is out-of-log.** Format version, cwd, and lineage are storage concerns, not replayable conversation state, so they live in a `SessionHeader` owned by `dsh-session` and attached to a `Session` via a new readonly `session.header` — never in `SessionEventMap`, never reaching `deriveMessages()`. `createdAt` is non-negative safe-integer Unix epoch milliseconds: live creation and persistence registration reject fractional values, and JSONL validates the decoded header. The alternative (a merge-extensible `session/meta` event as log line 0) was rejected: an in-log event would ride along with a seeded/forked session for free, but metadata is not replayable state, so the explicit out-of-log header boundary is the cleaner cost. (The header was originally split into an immutable `SessionHeader` plus a mutable `SessionSummary` whose union was `SessionMeta`; the mutable summary was later removed as dead state — see [Drop the mutable session summary](../simplification/2026-06-19-drop-mutable-session-summary.md).)
@@ -27,10 +27,10 @@ Key durable, contested choices:
 
 ## Alternatives considered
 
-Each key choice above records its rejected alternative where the choice is stated: a **chunk-filtered canonical log** (Codex's `policy.rs` shape) — breaks the contiguous-seq contract; **truncating a crashed turn** — silently destroys a long autonomous run's real work; an **in-log `session/meta` event as log line 0** — metadata is not replayable state; **finite fractional `createdAt` values** — have no producer and diverge from integer Unix-millisecond storage; **hard-injecting `sessionPersistence` into the loop** — would pend non-persistent demos forever.
+Each key choice above records its rejected alternative where the choice is stated: a **stream-filtered canonical log** — loses attempt evidence, while removing events without an explicit migration breaks contiguous sequence numbers; **truncating a crashed turn** — silently destroys a long autonomous run's real work; an **in-log `session/meta` event as log line 0** — metadata is not replayable state; **finite fractional `createdAt` values** — have no producer and diverge from integer Unix-millisecond storage; **hard-injecting `sessionPersistence` into the loop** — would pend non-persistent demos forever.
 
-Format versioning: the header carries a `version`; cold reads reject any non-current version. The pre-release session format stays pinned at `SESSION_FORMAT_VERSION = 0` and carries no compatibility promise: reads validate current v0 records only, and retired same-version shapes refuse fail-closed ([export and pre-release trims](../simplification/2026-08-27-persistence-export-and-pre-release-trims.md)). Append-only + flush is robust to partial trailing writes (tolerated during cold preparation) but not to fsync-less power loss mid-line; a DB/WAL backend is the stronger option there.
+Format versioning: the header carries a `version`; handles expose only `SESSION_FORMAT_VERSION = 2`. JSONL event-body reads compose the static v0-to-v1 and v1-to-v2 adjacent migration chain before returning a handle; the first edge owns bounded legacy normalization, while the second owns Assistant stream embedding and dense reference remapping. V0 remains at suffixless `session.jsonl[.zstd]`, while positive versions use immutable lowercase `session.vN.jsonl[.zstd]` names ([released Session migration](2026-08-31-released-session-format-migrations.md)). Current-generation append and flush are robust to partial trailing writes; a future provider or write-ahead log needs its own power-loss and recovery contract.
 
 ## Consequences
 
-The Service Definition, JSONL provider, and metadata contract in `dsh-session` (`session.header`, the `create(id?, options?)` signature) buy durable resume/fork, a read/replay path, crash tolerance, and host-side session access over the existing event-sourced log. The reusable `runPersistenceContract` suite holds the provider and future implementations to the same append-only, contiguous-seq, lazy-materialization, logical-recovery, integer-metadata, and serializability semantics. Persisting the full logical log also settles event fidelity: every `assistant/chunk` survives exactly even when JSONL packs several into one storage row.
+The Service Definition, JSONL provider, and metadata contract in `dsh-session` (`session.header`, the `create(header, options?)` signature) buy durable resume/fork, a read/replay path, crash tolerance, and host-side session access over the existing event-sourced log. The reusable `runPersistenceContract` suite holds the provider and future implementations to the same append-only, contiguous-seq, lazy-materialization, logical-recovery, integer-metadata, and serializability semantics. Persisting the full logical log also settles event fidelity: every Assistant attempt retains its exact compact timed stream in one durable settlement.

+ 6 - 6
.agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md

@@ -14,12 +14,12 @@ Status: implemented
 
 持久化是一个具有抽象 Service Definition 的**能力 seam**([能力 seam](2026-06-13-capability-seams.zh.md),`dsh-shell` 模板),而非循环或核心逻辑:
 
-1. **接口**(`dsh-session-persistence`,`ctx.sessionPersistence`):一个抽象的 `SessionPersistence` 服务,提供 `create`/`open`/`stat`/`list`/`export`,其中 `create`/`open` 返回逐会话的 `SessionHandle`,句柄承载 `read`/`append`/`flush`/`close`([基于句柄的 seam](2026-08-27-handle-based-session-persistence.zh.md))。其持久化单元就是现有的 `SessionEvent`(`{ type, seq, time, data }`),原样复用,无转换类型。
-2. **实现**(`dsh-session-persistence-jsonl`):每个会话一个仅追加的逻辑 JSONL 日志:先是一行 `SessionHeader`,随后是无损表示连续 `SessionEvent` 流的存储记录。符合条件的 `assistant/chunk` 增量连续段默认使用打包行;[带校验和的 Zstandard 帧](2026-07-19-zstandard-jsonl-session-logs.zh.md)是默认物理编码,也可通过配置使用原始行。
+1. **接口**(`dsh-session-persistence`,`ctx.sessionPersistence`):一个抽象的 `SessionPersistence` 服务,提供 `create`/`open`/`stat`/`list`/`flush`,其中 `create`/`open` 返回逐会话的 `SessionHandle`,句柄承载 `read`/`append`/`flush`/`close`([基于句柄的 seam](2026-08-27-handle-based-session-persistence.zh.md))。其持久化单元就是现有的 `SessionEvent`(`{ type, seq, time, data }`),原样复用,无转换类型。
+2. **实现**(`dsh-session-persistence-jsonl`):每个会话一个仅追加的逻辑 JSONL 日志:先是一行 `SessionHeader`,随后是无损表示连续 `SessionEvent` 流的存储记录。当前 v2 每个事件写一行;冻结的 v0 与 v1 reader 保留其历史 packed-delta 表示。[带校验和的 Zstandard 帧](2026-07-19-zstandard-jsonl-session-logs.zh.md)是默认物理编码,也可通过配置使用原始行。
 
 长期有效、存在争议的关键选择:
 
-- **规范的持久日志无损保留每个 `SessionEvent`,包括 `assistant/chunk`。** JSONL 存储可以将一段连续的增量事件编码为一条打包行,但逻辑读取方会重建精确的事件边界、序号与时间戳。`deriveMessages()` 跳过分片,而过滤分片的方案(Codex 的 `policy.rs`)很有吸引力,但 `seq = log.length` 以及 `events[i].seq === i` 验证要求*连续*的逻辑日志;过滤掉分片会留下空洞,同时破坏约定和恢复功能。基于分片过滤的投影可以作为派生视图在后续实现(带有自己的重新编号),但它不是规范日志。
+- **规范持久日志无损保留每个当前 `SessionEvent`。** 在 v2 中,一个 `assistant/message` 或 `assistant/attempt` 会嵌入该 attempt 的精确带时间 provider stream;`deriveMessages()` 只投影 surface message。删除嵌入 stream 成员看似诱人,但会丢失 replay、timing、usage、部分失败与诊断事实。移除完整事件同样需要密集重新编号,因为 `seq = log.length` 且 `events[i].seq === i`;[v1 到 v2 迁移](2026-09-01-v2-embedded-assistant-streams.zh.md)会显式执行该改写,而不是过滤规范日志。
 - **仅追加;崩溃的轮次被关闭,而非截断。** 已刷写的事件永不被重写。[语义检查点策略](../bug-fix/2026-07-21-semantic-session-checkpoints.zh.md)会在调用模型前排空请求、在调用工具前排空已记录的顶层调用,并在步骤结束后排空完整的响应/结果批次;循环则排空最终轮次边界。由于一个被中断的轮次可能包含大量有效工作,持久化会原样返回其连续、可解析的事件;配平是读方的职责——resume 会为未应答的 assistant 调用计算按风险分类的错误结果、补一个缺失的 `step/end`,以及带 `{ kind: 'interrupted' }` 的 `turn/end`(`interruptedTurnClosers`),并通过其写句柄追加它们,而只读观察方仅在内存中添加同样的收尾事件。合成结果保证恢复后的提供方 transcript(文本记录)仍然有效。只有撕裂的最终 append 中不完整的碎片会被丢弃——从中恢复的完整记录由写路径在第一次新 append 之前持久重写;已提交前缀中的解析错误或序号间隙,属于数据损坏,会使该会话不可加载。
 - **文件后端为规范实现,服务保持可扩展。** `dsh-session-persistence-jsonl` 是唯一 first-party provider,并通过 `runPersistenceContract`;抽象服务继续供仓库外 provider 使用。[JSONL-only 持久化决策](../simplification/2026-08-30-jsonl-only-session-persistence.zh.md)负责 first-party 数据库 provider 的删除及其明确 compatibility cut。
 - **元数据在日志之外。** 格式版本、cwd 和谱系是存储关注点,不是可回放的对话状态,因此它们存放在 `dsh-session` 拥有的 `SessionHeader` 中,并通过新的只读属性 `session.header` 附加到 `Session` 上——永远不进入 `SessionEventMap`,永远不到达 `deriveMessages()`。`createdAt` 是以 Unix epoch 毫秒表示的非负安全整数:运行时创建和持久化注册会拒绝小数值,JSONL 会验证解码后的 header。替代方案(一个可合并扩展的 `session/meta` 事件作为日志第 0 行)被否决:日志内事件会自然随 seed/fork 的会话携带,但元数据不是可回放状态,因此显式的日志外 header 边界是更清晰的取舍。(header 最初被拆分为不可变的 `SessionHeader` 加可变的 `SessionSummary`,二者的联合类型为 `SessionMeta`;可变 summary 后来因属于死状态而被移除——见 [移除可变会话摘要](../simplification/2026-06-19-drop-mutable-session-summary.zh.md)。)
@@ -27,10 +27,10 @@ Status: implemented
 
 ## 曾考虑的替代方案
 
-上述每个关键选择都在陈述处记录了被否决的替代方案:**过滤分片的规范日志**(Codex 的 `policy.rs` 形式)破坏连续 seq 约定;**截断崩溃的轮次**会静默销毁长时间自主运行中的真实工作;**日志内 `session/meta` 事件作为第 0 行**——元数据不是可回放状态;**有限的非整数 `createdAt` 值**没有生产方,且与整数 Unix 毫秒存储不一致;**将 `sessionPersistence` 硬注入循环**会让非持久化的演示永远挂起。
+上述每个关键选择都在陈述处记录了被否决的替代方案:**过滤 stream 的规范日志**会丢失 attempt 证据,而未通过显式迁移移除事件会破坏连续序号;**截断崩溃的轮次**会静默销毁长时间自主运行中的真实工作;**日志内 `session/meta` 事件作为第 0 行**——元数据不是可回放状态;**有限的非整数 `createdAt` 值**没有生产方,且与整数 Unix 毫秒存储不一致;**将 `sessionPersistence` 硬注入循环**会让非持久化的演示永远挂起。
 
-格式版本控制:header 携带一个 `version`;冷读取拒绝任何非当前版本。预发布阶段的会话格式仍固定为 `SESSION_FORMAT_VERSION = 0`,不作兼容承诺:读取只校验当前 v0 记录,已废弃的同版本形态会以 fail-closed 方式拒绝([导出与预发布精简](../simplification/2026-08-27-persistence-export-and-pre-release-trims.zh.md))。仅追加 + 刷写对尾部的不完整写入具有健壮性(冷准备时可容忍),但无法抵御未使用 fsync 时在行写入中途断电;数据库/WAL 后端是该场景下更强的选项
+格式版本控制:header 携带 `version`;句柄只暴露 `SESSION_FORMAT_VERSION = 2`。JSONL 的事件正文读取会在返回句柄前组合静态 v0-to-v1 与 v1-to-v2 相邻迁移链;第一条边负责有界 legacy normalization,第二条边负责 Assistant stream 嵌入与密集引用重映射。V0 保留无后缀的 `session.jsonl[.zstd]`,正版本则使用不可变的小写 `session.vN.jsonl[.zstd]` 名称([已发布 Session 迁移](2026-08-31-released-session-format-migrations.zh.md))。当前 generation 的 append 与 flush 能稳健处理不完整尾部写入;未来 provider 或 WAL 必须定义自己的断电与恢复约定
 
 ## 后果
 
-Service Definition、JSONL provider 与 `dsh-session` 中的元数据约定(`session.header`,`create(id?, options?)` 签名)带来持久恢复/fork、读取/回放路径、崩溃容忍,以及基于现有事件溯源日志的宿主侧会话访问。可复用的 `runPersistenceContract` 测试套件以相同的仅追加、连续 seq、惰性物化、逻辑恢复、整数元数据与可序列化语义约束该 provider 与未来实现。持久化完整的逻辑日志还确定了事件保真度:即使 JSONL 将多个 `assistant/chunk` 打包到一条存储行中,每个事件也会精确保留
+Service Definition、JSONL provider 与 `dsh-session` 中的元数据约定(`session.header`,`create(header, options?)` 签名)带来持久恢复/fork、读取/replay 路径、崩溃容忍,以及基于现有事件溯源日志的宿主侧 Session 访问。可复用 `runPersistenceContract` 测试套件以相同的仅追加、连续 seq、惰性物化、逻辑恢复、整数元数据与可序列化语义约束该 provider 与未来实现。持久化完整逻辑日志也确定了事件保真度:每个 Assistant attempt 都在一个持久 settlement 中保留其精确紧凑带时间 stream

+ 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: 95298da0e4bd16e822cb5960718d23ecda7a1b5c
-2026-06-18-session-surface.zh.md: 7dd05d79f635b193b2c11cb3599264ebf2424d79
+2026-06-18-session-surface.md: 0139cc4beba766e4e8b936594899649304234eaa
+2026-06-18-session-surface.zh.md: 0596d2a0425890924276265dd9cc6c32fcffb974

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

@@ -16,7 +16,7 @@ Add a **surface** — a derived, cached order of event sequences (the subset of
 
 Every `SessionEvent` gains two optional fields (structural metadata, like `seq`/`time`):
 
-- **`sourceEventSeqs?: number[]`** — seq numbers of earlier events cited as sources (e.g., the `assistant/chunk` seqs that built an `assistant/message`, or the surface nodes shadowed by a compaction marker). A present `[]` is valid only on `assistant/message` and records a known empty provider stream; when the field is absent, a legacy or foreign event does not record which earlier events produced the message. Other surface events require a non-empty list when the field is present. Without these cited seqs, replay cannot validate that a replace-range operation names every event it removed.
+- **`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.
 
 ### SurfaceOp: two operations
@@ -27,7 +27,7 @@ export type SurfaceOp =
   | { op: 'replace'; start: number; end: number }  // shadow [start, end] inclusive
 ```
 
-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: every successful `assistant/message` records its complete `assistant/chunk` source set, including `[]`, while `tool/result` records its `tool/call` source.
+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.
 
 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.
 
@@ -41,7 +41,7 @@ Delta processing is O(1) when no new events and O(new events) when new events ar
 
 ### 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. The session format `version` is pinned at `SESSION_FORMAT_VERSION = 0`; the optional surface fields are absorbed without bumping it.
+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.
 
 ### Crash recovery
 
@@ -49,9 +49,9 @@ The `repair.ts` module synthesizes `tool/result` closers for orphaned tool calls
 
 ### Invariants
 
-`Session` validates `sourceEventSeqs` and `surfaceOp` at the always-on seed/append boundary: only `assistant/message` may use an empty source-event list; references are unique, earlier, and known; 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.
+`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 loaded logs. Invalid seeds are rejected rather than upgraded under the pre-release format policy.
+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.
 
 ## Alternatives considered
 

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

@@ -16,7 +16,7 @@ Status: implemented
 
 每个 `SessionEvent` 获得两个可选字段(结构性元数据,与 `seq`/`time` 同级):
 
-- **`sourceEventSeqs?: number[]`**:被引用为数据来源的早期事件 seq 编号(例如构成 `assistant/message` 的各 `assistant/chunk` 的 seq,或被压缩标记遮蔽的 surface 节点)。出现的 `[]` 只在 `assistant/message` 上有效,表示已知为空的提供方流;旧格式或外部事件缺少该字段时,没有记录这条消息由哪些早期事件产生。其他 surface 事件一旦出现此字段,就必须是非空列表。如果没有这些引用的 seq,回放就无法验证 replace-range 操作是否列出了它移除的每个事件。
+- **`sourceEventSeqs?: number[]`**:被引用为数据来源的早期事件 seq 编号,例如 result 引用的 `tool/call`,或被 compaction marker 遮蔽的 surface 节点。出现的列表必须非空、唯一、更早且已知。V2 `assistant/message` 嵌入其 provider stream,不能携带该字段。如果没有这些引用的 seq,回放就无法验证 replace-range 操作是否列出了它移除的每个事件。
 - **`surfaceOp?: SurfaceOp`**:该事件如何进入 surface。非 surface 事件不携带此字段。
 
 ### SurfaceOp:两种操作
@@ -27,7 +27,7 @@ export type SurfaceOp =
   | { op: 'replace'; start: number; end: number }  // shadow [start, end] inclusive
 ```
 
-1. **Append**:在尾部追加新事件的 seq。`user/message`、`assistant/message`、`tool/result`、`context/message` 使用此操作。agent loop(智能体循环)在所有此类追加上传入 `surfaceOp: 'append'`,并在适用时记录 `sourceEventSeqs`:每个成功的 `assistant/message` 都记录完整的 `assistant/chunk` 来源集合(包括 `[]`),而 `tool/result` 记录其 `tool/call` 来源。
+1. **Append**:在尾部追加新事件的 seq。`user/message`、`assistant/message`、`tool/result`、`context/message` 使用此操作。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 上。
 
@@ -41,7 +41,7 @@ export type SurfaceOp =
 
 ### 持久化
 
-新字段作为顶层 JSON 属性序列化。JSONL 存储无需单独列映射:其无损 JSON 边界会保留两个值。会话格式 `version` 固定为 `SESSION_FORMAT_VERSION = 0`;可选 surface 字段被吸收而不递增版本号
+新字段作为顶层 JSON 属性序列化。JSONL 存储无需单独列映射:其无损 JSON 边界会保留两个值。已发布 v0 与 v1 共享该 surface 表示,恒等的 v0-to-v1 边会精确保留它;未来结构性表示变更会递增 `SESSION_FORMAT_VERSION` 并拥有一项相邻迁移
 
 ### 崩溃恢复
 
@@ -49,9 +49,9 @@ export type SurfaceOp =
 
 ### 不变式
 
-`Session` 在始终启用的 seed/append 边界校验 `sourceEventSeqs` 与 `surfaceOp`:只有 `assistant/message` 可以使用空的源事件列表;引用必须唯一、更早且已知;替换端点必须存在于 surface 顺序中;`sourceEventSeqs` 必须覆盖每个被遮蔽的节点。这些是单记录接纳与存储投影规则,不是由可选的不变式服务提供的规则。
+`Session` 在始终启用的 seed/append 边界校验 `sourceEventSeqs` 与 `surfaceOp`:source list 必须非空、唯一、更早且已知;`assistant/message` 不携带 source list;replacement endpoint 必须存在于 surface 顺序中;`sourceEventSeqs` 必须覆盖每个被遮蔽的节点。这些是单记录接纳与存储投影规则,不是由可选 invariant service 提供的规则。
 
-每个可进入 surface 的事件都必须携带 `surfaceOp`,否则它将从派生历史中消失。类型化的 `append` 重载对字面事件类型强制执行此规则;`append` 和种子构造函数中的运行时检查覆盖宽化联合类型和加载的日志。按照预发布格式策略,无效的种子被拒绝而非升级
+每个可进入 surface 的事件都必须携带 `surfaceOp`,否则它将从派生历史中消失。类型化的 `append` 重载对字面事件类型强制执行此规则;`append` 和种子构造函数中的运行时检查覆盖宽化联合类型和当前已加载日志。历史 v0 的校验与规范化属于 v0-to-v1 边,而不属于通用 Session 代码
 
 ## 曾考虑的替代方案
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.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-21-bounded-llm-request-recovery.md
-2026-06-21-bounded-llm-request-recovery.md: 42bf460e52133b2a5471479fa3d7647e70092b48
-2026-06-21-bounded-llm-request-recovery.zh.md: 2a13f0a740348a5f74bd3d90120a148b25f2e870
+2026-06-21-bounded-llm-request-recovery.md: 6ae3915462c00eadcc550c80165fab7247a9170c
+2026-06-21-bounded-llm-request-recovery.zh.md: 82e2e783fffaedcb5bfaabe72f56f7c4176d3784

+ 3 - 3
.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md

@@ -10,7 +10,7 @@ The [per-provider request retry policy](../feature/2026-07-24-provider-retry-pol
 
 Provider adapters can fail by throwing during dispatch or iteration or by ending with `finish { kind: 'error' | 'aborted' }`. The final adapter boundary normalizes thrown values to that terminal finish protocol before `dsh-agent-loop` receives them; middleware and result-processing defects remain thrown. The loop offers a terminal model-request failure to `agent/request-error`. An unhandled failure is terminal; a handling listener repairs policy-owned state, returns `{ kind: 'retry' }`, and stops waterfall delegation. The [retry-action decision](../simplification/2026-07-27-request-error-retry-action.md) owns this return contract.
 
-That boundary is already safe for another request attempt. Raw `assistant/chunk` events carry the failed `turn` and `step`, message derivation ignores them unless a successful `assistant/message` cites them, tool calls are dispatched only after a successful terminal finish and assembly, and a retry reconstructs its next attempt from the durable log. The harness therefore does not need a second response lifecycle or tentative-output protocol to keep two attempts separate.
+That boundary is already safe for another request attempt. Each failed stream commits one log-only `assistant/attempt` with its exact compact stream, message derivation ignores it, tool calls are dispatched only after a successful terminal finish and assembled `assistant/message`, and a retry reconstructs its next attempt from the durable surface. The harness therefore does not need a second response lifecycle or tentative-output protocol to keep two attempts separate.
 
 The prior boundary left three narrower gaps.
 
@@ -38,7 +38,7 @@ interface LlmFailure {
 }
 ```
 
-`code` remains the provider-neutral machine-routing taxonomy established by `HarnessError`; the new fields are observations from the provider boundary. `ProviderRequestId` is owned and constructed by `dsh-llm`, then serializes as its provider-issued string. The payload deliberately has no `retryable`, `failover`, `partialOutput`, provider, model, phase, or route id fields. Retryability belongs to policy, provider/model are already in the durable request header, and partial output is derived from the failed step's `assistant/chunk` events.
+`code` remains the provider-neutral machine-routing taxonomy established by `HarnessError`; the new fields are observations from the provider boundary. `ProviderRequestId` is owned and constructed by `dsh-llm`, then serializes as its provider-issued string. The payload deliberately has no `retryable`, `failover`, `partialOutput`, provider, model, phase, or route id fields. Retryability belongs to policy, provider/model are already in the durable request header, and partial output is preserved by the failed attempt's embedded stream.
 
 `LlmError` carries `failure: LlmFailure` and preserves `failure.code === error.code`. `FinishReasonMap.error` and `FinishReasonMap.aborted` carry the same payload instead of parallel failure shapes. The final adapter boundary detaches those facts from adapter-thrown values and emits the appropriate terminal finish; unknown SDK exceptions receive an `UNKNOWN` payload. Exact thrown-object identity does not cross the LLM stream seam.
 
@@ -82,7 +82,7 @@ Boundary tests prove termination at both actual transports. The hand-written ada
 
 ### Keep attempts separate in the existing log
 
-A failed attempt may leave `assistant/chunk` events in its step, but it never appends `assistant/message` and never dispatches a tool. A retry continues inside the failing turn and step, reconstructs the request from the durable surface, and produces its own chunks; only the final outcome closes the turn. UIs may render live chunks while a step is open, then mark or clear that transient view when `llm/retry` identifies the failed attempt or `turn/end` records failure. Web validates the complete retry payload contract, clears the failed partial at `llm/retry`, projects each producer-correlated `retryId` chain into one stable row updated to the latest attempt, and derives scheduled, started, or cancelled status from `llm/retry-started` and the owning turn and step boundaries' closure. Its countdown anchors the scheduled delay to browser receipt rather than the Host event clock, uses ceiling-rounded seconds with a one-second floor, animates only while unresolved, and keeps exact latest failure details collapsed behind the row. Retry nodes anchor their own trajectory turn even when the failed attempt has no assistant node. Message derivation continues to ignore the failed chunks, and Web applies the same projection during history rebuild so refreshing cannot resurrect discarded partials or duplicate retry rows.
+A failed attempt appends `assistant/attempt` with its embedded stream, but never appends a surface `assistant/message` or dispatches a tool. A retry continues inside the failing turn and step, reconstructs the request from the durable surface, and produces its own settlement; only the final outcome closes the turn. UIs may render transient `assistant/live-chunk` updates while a step is open, then settle the failed attempt when `llm/retry` identifies it or `turn/end` records failure. Web validates the complete retry payload contract, projects each producer-correlated `retryId` chain into one stable row updated to the latest attempt, and derives scheduled, started, or cancelled status from `llm/retry-started` and the owning turn and step boundaries' closure. Its countdown anchors the scheduled delay to browser receipt rather than the Host event clock, uses ceiling-rounded seconds with a one-second floor, animates only while unresolved, and keeps exact latest failure details collapsed behind the row. Retry nodes anchor their own trajectory turn even when the failed attempt has no surface Assistant node. Message derivation ignores `assistant/attempt`, and Web applies the same projection during history rebuild so refreshing cannot promote failed partials into model history or duplicate retry rows.
 
 If recovery is exhausted, the final failure is stored once on `turn/end.reason` with the structured facts. Web derives one `turn-error` node at that sequence position and renders its display-safe message and optional code inline; AUTH projections replace provider copy that may echo credential fragments with `API key is invalid`, while the raw diagnostic remains in the session log. The same fold runs for live events and history replay. While transient recovery continues, `llm/retry` is the durable home for each intermediate failure and delay; the terminal row exists only once `turn/end` records the error, and because exhausted recovery shares the failing turn, the turn's retry history never suppresses that row — the settled retry chain and the terminal error render side by side. No standalone final-error event or response-id vocabulary is added.
 

+ 3 - 3
.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md

@@ -10,7 +10,7 @@ Status: implemented
 
 提供方适配器可能在分发或迭代时抛出异常,也可能以 `finish { kind: 'error' | 'aborted' }` 结束。最终适配器边界会在 `dsh-agent-loop` 接收前把抛出值规范化为该终止 finish 协议;middleware 与结果处理缺陷仍会抛出。loop 会将终止模型请求失败交给 `agent/request-error`。未被处理的失败是终态;处理失败的监听器修复策略自有状态,返回 `{ kind: 'retry' }`,并停止 waterfall(瀑布式事件)委托。[重试动作决策](../simplification/2026-07-27-request-error-retry-action.zh.md)规定这一返回约定。
 
-该边界已能安全地再次发起请求。原始 `assistant/chunk` 事件携带失败的 `turn` 和 `step`;除非某条成功的 `assistant/message` 引用这些事件,否则消息派生会忽略它们。只有终止性 finish 成功且组装完成后,系统才会分发工具调用;重试则会从持久日志重建下一次尝试。因此,harness 无需引入第二套响应生命周期或暂定输出协议,即可分隔两次尝试
+该边界已能安全地再次发起请求。每个失败 stream 会提交一个包含精确紧凑 stream 的仅日志 `assistant/attempt`,message derivation 会忽略它;系统只会在 terminal finish 成功并组装 `assistant/message` 后分派工具调用,重试则从持久 surface 重建下一次 attempt。因此,harness 无需引入第二套响应生命周期或暂定输出协议,即可分隔两次 attempt
 
 此前的边界还留有三个较窄的缺口。
 
@@ -38,7 +38,7 @@ interface LlmFailure {
 }
 ```
 
-`code` 仍是 `HarnessError` 建立的提供方无关机器路由分类体系;新字段是在提供方边界观测到的事实。`ProviderRequestId` 由 `dsh-llm` 拥有并构造,序列化后为提供方发放的字符串。该载荷有意不包含 `retryable`、`failover`、`partialOutput`、提供方、模型、阶段或路由 id 字段。是否可重试属于策略,提供方/模型已位于持久请求头中,部分输出则从失败步骤的 `assistant/chunk` 事件派生
+`code` 仍是 `HarnessError` 建立的 provider-neutral 机器路由分类;新字段是在 provider 边界观测到的事实。`ProviderRequestId` 由 `dsh-llm` 拥有并构造,序列化后是 provider 发放的字符串。该 payload 有意不包含 `retryable`、`failover`、`partialOutput`、provider、model、phase 或 route id。是否可重试属于 policy,provider/model 已位于持久 request header 中,部分输出由失败 attempt 的嵌入式 stream 保留
 
 `LlmError` 携带 `failure: LlmFailure`,并保持 `failure.code === error.code`。`FinishReasonMap.error` 和 `FinishReasonMap.aborted` 携带同一载荷,而不是并行的失败形状。最终适配器边界会从适配器抛出值中分离这些事实,并发出相应的终止 finish;未知 SDK 异常会获得 `UNKNOWN` 载荷。精确的抛出对象身份不会跨越 LLM 流 seam。
 
@@ -82,7 +82,7 @@ agent loop(智能体循环)会将终止 finish 的 `LlmFailure` 传给 `agen
 
 ### 在现有日志中分隔尝试
 
-一次失败尝试可以在其步骤中留下 `assistant/chunk` 事件,但绝不会追加 `assistant/message`,也不会分发工具。重试在失败的轮次与步骤内继续,从持久表层重建请求,并生成自己的分片;只有最终结果才会关闭该轮次。步骤仍处于打开状态时,UI 可以渲染实时分片;当 `llm/retry` 标识失败尝试,或 `turn/end` 记录失败时,UI 再标记或清除这份暂时视图。Web 会验证完整的重试载荷约定,在 `llm/retry` 到达时清除失败的部分输出,将每条生产方关联的 `retryId` 重试链投影为稳定的一行,并用最新一次尝试更新该行,再从 `llm/retry-started` 与所属轮次、步骤边界的关闭派生 scheduled、started 或 cancelled 状态。倒计时以浏览器收到事件的时刻为计划延迟的起点,而不是使用 Host 事件时钟;它按向上取整且不低于 1 秒的秒数显示,仅在重试尚未结束时显示动画,并把最近一次失败的准确详情折叠在该行之后。即使失败尝试没有 assistant 节点,重试节点也会锚定自身的轨迹轮次。消息派生仍会忽略失败分片;Web 在重建历史时也会应用同一投影,因此刷新页面不会让已丢弃的部分输出重新出现,也不会生成重复的重试行
+失败 attempt 会追加带嵌入式 stream 的 `assistant/attempt`,但绝不追加 surface `assistant/message` 或分派工具。重试在失败 turn 与 step 内继续,从持久 surface 重建请求,并产生自己的 settlement;只有最终结果才会关闭 turn。step 打开时,UI 可以渲染瞬态 `assistant/live-chunk` update;当 `llm/retry` 标识失败 attempt 或 `turn/end` 记录失败时,UI 再结算它。Web 会校验完整 retry payload contract,把每条 producer-correlated `retryId` chain 投影为稳定一行并更新到最新 attempt,再从 `llm/retry-started` 与所属 turn、step boundary 的关闭派生 scheduled、started 或 cancelled 状态。倒计时以浏览器收到 event 的时刻为计划延迟起点,而不是 Host event clock;它按向上取整且不低于 1 秒的秒数显示,只在未结算时动画,并把最新失败详情折叠在该行后。即使失败 attempt 没有 surface Assistant node,retry node 也会锚定自己的 trajectory turn。Message derivation 会忽略 `assistant/attempt`,Web 在历史重建时应用同一投影,因此刷新不会把失败 partial 提升进模型历史,也不会生成重复 retry row
 
 如果恢复预算耗尽,最终失败会连同结构化事实在 `turn/end.reason` 中存储一次。Web 会在该序列位置派生一个 `turn-error` 节点,并内联渲染适合展示的消息与可选错误码;AUTH 投影会把可能回显凭据片段的提供方文案替换为 `API key is invalid`,原始诊断仍保留在会话日志中。实时事件和历史回放使用同一套折叠逻辑。暂时性恢复继续期间,`llm/retry` 是每次中间失败与延迟的持久归属位置;终态错误行只在 `turn/end` 记录错误后才存在,而由于耗尽的恢复与失败共享同一轮次,该轮次的重试历史绝不会抑制这一行——定格的重试链与终态错误并列渲染。本决策不增加独立的最终错误事件或响应 id 词汇。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.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-30-event-domain-semantics.md
-2026-06-30-event-domain-semantics.md: 70da718b5471ce309a090c8aade3e7290cc949dc
-2026-06-30-event-domain-semantics.zh.md: c3b12a167da0a41b792914d82a675a98b3a0b860
+2026-06-30-event-domain-semantics.md: f3e4686a1e9e14c9284de5a4bff16501c50aa907
+2026-06-30-event-domain-semantics.zh.md: f4bc92111666b0c30586222e14ee1aeab49ebb3f

+ 1 - 1
.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.md

@@ -21,7 +21,7 @@ This vocabulary is the foundation for interception decisions, the durable `hook/
 **Three domains, one job each, with a single boundary rule.**
 
 - **`session/*` — the durable, replayable FACT log.** Owns `SessionEventMap`; every entry is JSON-only (no live objects). One `session/event` emit per append, plus the `session/flush` parallel durability checkpoint. It is also the live transcript feed: a consumer that wants to render or react to what happened subscribes here, so live rendering and replay projections share one path.
-- **`agent/*` — the LIVE runtime surface.** Always carries the live `Agent`. Interception waterfalls (`agent/pre-step`, `agent/request`, `agent/request-error`) transform, reject, or recover; awaited `agent/turn-stopping` observes the stop boundary; transient emits report lifecycle, status, inbox insertion/claim/discard, and errors. Turn and step BOUNDARIES are NOT here — they are durable session events read off `session/event`, as are the token stream (`assistant/chunk`) and mid-turn steering (a `user/message`).
+- **`agent/*` — the LIVE runtime surface.** Always carries the live `Agent`. Interception waterfalls (`agent/pre-step`, `agent/request`, `agent/request-error`) transform, reject, or recover; awaited `agent/turn-stopping` observes the stop boundary; transient emits report lifecycle, status, inbox insertion/claim/discard, errors, and process-local `agent/assistant-stream` frames. Turn and step BOUNDARIES are NOT here — they are durable session events read off `session/event`; Assistant stream evidence becomes durable only inside one `assistant/message` or `assistant/attempt` settlement, and mid-turn steering is a durable `user/message`.
 - **`tools/*` — the tool registry and execution pipeline.**
 
 **The boundary rule:** a durable, replayable fact is a `SessionEvent`; a live interception or a transient/live-object signal is an `agent`/`tools` Cordis event. A turn or step boundary is a durable fact, so it lives in the session log and is read off the `session/event` feed — it is NOT mirrored as an `agent/*` emit.

+ 1 - 1
.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.zh.md

@@ -21,7 +21,7 @@ harness 通过 Cordis 事件分类体系扩展 agent loop(智能体循环)
 **三个域,各司其职,以一条边界规则统一。**
 
 - **`session/*`——持久的、可回放的事实日志。** 拥有 `SessionEventMap`;每条记录仅含 JSON(无活对象)。每次追加触发一次 `session/event` emit,加上 `session/flush` 并行持久性检查点。它同时也是实时 transcript(文本记录)源:想渲染或响应已发生事件的消费方在此订阅,因此实时渲染与回放投影共享同一路径。
-- **`agent/*`——运行时实时表面。** 始终携带活的 `Agent`。拦截 waterfall(瀑布式事件)(`agent/pre-step`、`agent/request`、`agent/request-error`)负责变换、拒绝或恢复;awaited `agent/turn-stopping` 观察停止边界;瞬态 emit 报告生命周期、状态、inbox 的插入、领取和丢弃,以及错误。轮次和步骤边界不在此处——它们是持久的会话事件,从 `session/event` 读取;token 流(`assistant/chunk`)和轮次中途以 `user/message` 呈现的 steering(中途引导)同理
+- **`agent/*`——运行时实时表面。** 始终携带活的 `Agent`。拦截 waterfall(瀑布式事件)(`agent/pre-step`、`agent/request`、`agent/request-error`)负责变换、拒绝或恢复;awaited `agent/turn-stopping` 观察停止边界;瞬态 emit 报告生命周期、状态、inbox 插入、领取与丢弃、错误,以及进程本地 `agent/assistant-stream` frame。轮次和步骤边界不在此处——它们是从 `session/event` 读取的持久 Session event;Assistant stream 证据只在一个 `assistant/message` 或 `assistant/attempt` settlement 内变为持久事实,轮次中途 steering 则是持久 `user/message`
 - **`tools/*`——工具注册表与执行流水线。**
 
 **边界规则:** 持久的、可回放的事实是 `SessionEvent`;实时拦截或瞬态/活对象信号是 `agent`/`tools` Cordis 事件。轮次或步骤边界是持久事实,因此存在于会话日志中并从 `session/event` 源读取——不会被镜像为 `agent/*` emit。

+ 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: 3786de02d06c0b6c094297ae89ac3f84053e408d
-2026-07-05-reconstructable-requests.zh.md: 851045aca7dababd0da859f3b04b721c65382fc3
+2026-07-05-reconstructable-requests.md: bc8ba640c400b18598f18aa303f2bd1b5c5b9cdc
+2026-07-05-reconstructable-requests.zh.md: de1802aac83f1e0980172d2d541a093f2d729e4a

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

@@ -53,5 +53,5 @@ Like MiniCode, the conversation advances append-only and resets only when model-
 - `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.
 - 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. `SESSION_FORMAT_VERSION` stays `0`; legacy delta events are rejected rather than migrated.
+- 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.
 - 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.

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

@@ -53,5 +53,5 @@ Status: implemented
 - `agent/pre-step` 是当前请求的消息通道;直接修改 inbox 则是最终进入后续请求的通道。
 - 工具结果裁剪无需新机制:一个已记录的单条目 surface replace(`start === end`),携带同一 `callId` 下裁剪后的 `tool/result`——属压缩家族,回放正确,缓存失效由相同的压力逻辑批量处理。
 - 无法读取的被引用附件对象仍会让模型请求失败;[附件自动隔离](../../proposed/bug-fix/2026-08-20-attachment-read-quarantine.zh.md)记录了不削弱字节精确重建的拟议恢复方案。
-- 会话日志会为每个循环实例、真实变更和后续模型消息序列增加一个 `request/header` 快照。重复完整系统提示词与工具目录比 delta 编解码器更大,但相对分片密集型日志仍然很小,并保留一种自包含的回放表示。`SESSION_FORMAT_VERSION` 保持 `0`;旧的 delta 事件被拒绝而非迁移
-- 快照 fixture 包含每个重复的 series header。无密钥 refresh 负责这些确定性日志变化;快照 harness 只为 initial 与真实 change 修订固定提示词和工具 sidecar,并让 `series` 快照复用当前修订。写入文件系统的 fixture 继续以规范化的撰写形式存储,工具参数使用 cwd 相对路径,因为回放只对 cwd 无关的参数路径做往返。
+- 会话日志会为每个循环实例、真实变更和后续模型消息序列增加一个 `request/header` 快照。重复完整系统提示词与工具目录比 delta 编解码器更大,但相对分片密集型日志仍然很小,并保留一种自包含的回放表示。当前 v1 保留这一种表示;冻结的 v0-to-v1 迁移边会在构造当前 Session 前显式拒绝旧版 delta 事件
+- 快照 fixture 包含每个重复的 series header。无密钥 refresh 负责这些确定性日志变化;快照 harness 只为 initial 与真实 change 修订固定提示词和工具 sidecar,并让 `series` 快照复用当前修订。写入文件系统的 fixture 继续以规范化的撰写形式存储,工具参数使用 cwd 相对路径,因为回放只对 cwd 无关的参数路径做往返。

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.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-14-provider-routed-llm-adapters.md
-2026-07-14-provider-routed-llm-adapters.md: 78c8d6788006c503b532ff2bbddd30342415f0a4
-2026-07-14-provider-routed-llm-adapters.zh.md: af8bc4fe27a50d47d7b49b51eada67afe889fc13
+2026-07-14-provider-routed-llm-adapters.md: ab4c485b79f37d9360da9a9a32f38063cae99755
+2026-07-14-provider-routed-llm-adapters.zh.md: c431d710af95352a2ecfc7b76b6393428ebedfec

+ 3 - 3
.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md

@@ -54,7 +54,7 @@ Compaction configuration gains `summarizationProvider` beside `summarizationMode
 
 The JSON-RPC runtime receives provider and model explicitly. Its convenience fallback mounts `dsh-llm-deepseek` only for provider `deepseek` when that provider has no registered owner; other missing providers fail without guessing an adapter.
 
-The on-disk session format remains the pre-release pinned version `0`, with no compatibility promise. Seed/load validation rejects request headers and assistant messages that omit required provider/model fields instead of accepting an old shape that can no longer reconstruct the request.
+Current v1 seed/load validation rejects request headers and assistant messages that omit required provider/model fields. The frozen v0-to-v1 edge requires the same reconstructable routing identity before migration; it never guesses a missing provider or model, and malformed shapes refuse before publication.
 
 ## Alternatives considered
 
@@ -78,7 +78,7 @@ The on-disk session format remains the pre-release pinned version `0`, with no c
 - pi-ai credentials, transport knobs, SDK timeouts, and the five-minute-default `streamIdleTimeoutMs` watchdog are scoped per provider profile. Hidden provider retries are disabled; bounded retries belong to the separately composed agent recovery policy.
 - `dsh-llm-pi-ai` rejects stop sequences because pi-ai's common stream API cannot express them; the native DeepSeek adapter retains its stop support.
 - Replay state is portable only within the adapter instance that owns both the historical and target providers. Cross-provider and cross-model restoration is an adapter responsibility, and another adapter receives provider-neutral history without the opaque state.
-- Current pre-release session JSONL requires provider/model on request headers and assistant messages. Older shapes remain version `0` but are rejected rather than migrated.
+- Current v1 Session JSONL requires provider/model on request headers and assistant messages. The v0 edge migrates only frozen shapes that already carry reconstructable request identity.
 
 ## Testing
 
@@ -88,4 +88,4 @@ The on-disk session format remains the pre-release pinned version `0`, with no c
 
 ## Risks
 
-This is a repo-wide pre-release API break: model-only request construction, adapter registration, app protocols, fixtures, and persisted version-0 event shapes all change together, with no compatibility aliases. The provider exclusivity rule deliberately prevents two implementations of the same upstream from coexisting in one context. A pi-ai dependency update can change the accepted provider/model catalog, so the lockfile and adapter e2e matrix define the tested set. Custom `baseURL` endpoints inherit the chosen catalog model's protocol assumptions and cannot repair an incompatible proxy. Catalog-external model descriptors and multimodal content remain unsupported. pi-ai replay state may contain opaque encrypted reasoning signatures; it is persisted because the provider requires it for continuity, but it is never rendered or logged outside the existing session record.
+This was a repo-wide API break when introduced: model-only request construction, adapter registration, app protocols, fixtures, and persisted v0 event shapes changed together, with no compatibility aliases. Released historical recovery now belongs to the adjacent Session-format edge. The provider exclusivity rule deliberately prevents two implementations of the same upstream from coexisting in one context. A pi-ai dependency update can change the accepted provider/model catalog, so the lockfile and adapter e2e matrix define the tested set. Custom `baseURL` endpoints inherit the chosen catalog model's protocol assumptions and cannot repair an incompatible proxy. Catalog-external model descriptors and multimodal content remain unsupported. pi-ai replay state may contain opaque encrypted reasoning signatures; it is persisted because the provider requires it for continuity, but it is never rendered or logged outside the existing session record.

+ 3 - 3
.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md

@@ -54,7 +54,7 @@ pi-ai 回放状态用其成功 `AssistantMessage` 的带版本最小投影填充
 
 JSON-RPC 运行时显式接收提供方与模型。仅当 `deepseek` 提供方没有注册所有者时,其便利回退才会挂载 `dsh-llm-deepseek`;其他缺失的提供方会直接失败,不会猜测适配器。
 
-磁盘会话格式仍使用预发布阶段固定的版本 `0`,且不承诺兼容性。seed/load 验证会拒绝省略必需提供方/模型字段的请求头和助手消息,不会接受已无法重建请求的旧格式
+当前 v1 的 seed/load 验证会拒绝省略必需提供方/模型字段的请求头和助手消息。冻结的 v0-to-v1 迁移边要求迁移前已具备同一套可重建路由身份;它绝不会猜测缺失的提供方或模型,畸形结构会在发布前被拒绝
 
 ## 考虑过的替代方案
 
@@ -78,7 +78,7 @@ JSON-RPC 运行时显式接收提供方与模型。仅当 `deepseek` 提供方
 - pi-ai 凭据、传输选项、SDK 超时,以及默认五分钟的 `streamIdleTimeoutMs` 空闲超时机制均按提供方配置隔离。系统禁用隐藏的提供方重试;有界重试由单独组合的 agent 恢复策略负责。
 - pi-ai 的通用流 API 无法表达停止序列,因此 `dsh-llm-pi-ai` 会拒绝停止序列;原生 DeepSeek 适配器仍支持停止序列。
 - 仅当历史提供方与目标提供方归同一个适配器实例所有时,回放状态才可移植。适配器负责跨提供方和跨模型恢复;其他适配器只接收不含不透明状态的提供方无关历史。
-- 当前预发布会话 JSONL 要求请求头和助手消息都包含提供方/模型。旧格式仍使用版本 `0`,但会被拒绝,不执行迁移
+- 当前 v1 Session JSONL 要求请求头和助手消息都包含提供方/模型。v0 边只迁移已经携带可重建请求身份的冻结结构
 
 ## 测试
 
@@ -88,4 +88,4 @@ JSON-RPC 运行时显式接收提供方与模型。仅当 `deepseek` 提供方
 
 ## 风险
 
-这是一次覆盖全仓库的预发布 API 破坏性变更:仅模型的请求构造、适配器注册、应用协议、fixture,以及持久化版本 0 事件格式会同时变化,不提供兼容别名。提供方排他规则有意禁止同一上游的两个实现共存于同一上下文。pi-ai 依赖升级可能改变可接受的提供方/模型目录,因此锁文件与适配器 e2e 矩阵定义已验证集合。自定义 `baseURL` 端点会继承所选目录模型的协议假设,无法修复不兼容的代理。目录外模型描述符与多模态内容仍不受支持。pi-ai 回放状态可能包含不透明的加密推理签名;提供方需要该信息维持连续性,因此系统会持久化该状态,但不会在现有会话记录之外渲染或记录它。
+这项变更在引入时是覆盖全仓库的 API 破坏性变更:仅模型的请求构造、适配器注册、应用协议、fixture,以及持久化 v0 事件结构同时变化,不提供兼容别名。已发布历史恢复现在属于相邻 Session 格式边。提供方排他规则有意禁止同一上游的两个实现共存于同一上下文。pi-ai 依赖升级可能改变可接受的提供方模型目录,因此锁文件与适配器 e2e 矩阵定义已验证集合。自定义 `baseURL` 端点会继承所选目录模型的协议假设,无法修复不兼容的代理。目录外模型描述符与多模态内容仍不受支持。pi-ai 回放状态可能包含不透明的加密推理签名;提供方需要该信息维持连续性,因此系统会持久化该状态,但不会在现有会话记录之外渲染或记录它。

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.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-19-zstandard-jsonl-session-logs.md
-2026-07-19-zstandard-jsonl-session-logs.md: 33486251a8b018cda61a2845a55218f13c38072b
-2026-07-19-zstandard-jsonl-session-logs.zh.md: 5e6b4a1ed7cf5869e1700c2884898b9899452a30
+2026-07-19-zstandard-jsonl-session-logs.md: a893a79f063842dfa93e57cb3972ea151421b05d
+2026-07-19-zstandard-jsonl-session-logs.zh.md: bdaeab701359acbd02f47a2a3d83fe4913d607e7

+ 3 - 3
.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.md

@@ -6,7 +6,7 @@ English | [中文](2026-07-19-zstandard-jsonl-session-logs.zh.md)
 
 ## Problem
 
-The JSONL persistence backend keeps every `SessionEvent` verbatim, including high-volume `assistant/chunk` records. Raw text makes logs inspectable but spends storage and I/O on repeated JSON keys and model text. Compression must retain the existing append/fsync commit boundary, collision-safe first materialization, crash repair, and metadata-only listing; rewriting a whole compressed file after every turn would discard those properties.
+The JSONL persistence backend keeps every `SessionEvent` verbatim, including Assistant settlements with embedded model streams. Raw text makes logs inspectable but spends storage and I/O on repeated JSON keys and model text. Compression must retain the existing append/fsync commit boundary, collision-safe first materialization, crash repair, and metadata-only listing; rewriting a whole compressed file after every turn would discard those properties.
 
 The encoding also has to remain explicit at the deployment boundary. Snapshot fixtures and external line readers require raw JSONL, while a backend cannot safely guess between compressed and raw artifacts in one root or silently migrate pre-release session data.
 
@@ -14,9 +14,9 @@ The encoding also has to remain explicit at the deployment boundary. Snapshot fi
 
 ### Configuration and suffix ownership
 
-`dsh-session-persistence-jsonl` accepts `compression?: 'zstd' | 'none'` and explicitly resolves omission to `'zstd'`. Zstandard artifacts end in `.jsonl.zstd`; `'none'` retains the original newline-delimited UTF-8 `.jsonl` representation. `SessionLocation.kind` remains `'jsonl'`, because both encodings carry the same logical record format, and `SESSION_FORMAT_VERSION` remains `0` under the repository's pre-release reject-without-migration policy.
+`dsh-session-persistence-jsonl` accepts `compression?: 'zstd' | 'none'` and explicitly resolves omission to `'zstd'`. Zstandard artifacts end in `.jsonl.zstd`; `'none'` retains the newline-delimited UTF-8 `.jsonl` representation. Within either configured suffix, v0 uses suffixless `session.jsonl[.zstd]` and every positive format generation uses lowercase `session.vN.jsonl[.zstd]`. `SessionLocation.kind` remains `'jsonl'`, because both encodings carry the same logical record format. Session-format migration uses the configured full suffix and one shared logical chain, so compression does not branch generation selection or publication.
 
-Each persistence root belongs to one encoding. A one-time discovery preflight rejects any opposite suffix, and targeted load, live-adoption, listing, and materialization paths repeat the relevant suffix check after an initially empty preflight. The error names the incompatible artifact and directs the deployment to the matching configuration or a separate root. There is no migration, dual read, dual write, or extension-based fallback.
+Each persistence root belongs to one encoding. A one-time discovery preflight rejects any opposite suffix, and targeted load, live-adoption, listing, and materialization paths repeat the relevant suffix check after an initially empty preflight. The error names the incompatible artifact and directs the deployment to the matching configuration or a separate root. There is no compression conversion, dual read, dual write, or extension-based fallback; logical version migration stays within the configured suffix, preserves the source generation, and exclusively publishes the final version-named successor.
 
 ### Frame and write path
 

+ 3 - 3
.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.zh.md

@@ -6,7 +6,7 @@ Status: implemented
 
 ## 问题
 
-JSONL 持久化后端会逐字保留每个 `SessionEvent`,其中包括数量庞大的 `assistant/chunk` 记录。原始文本便于检查,但重复的 JSON 键和模型文本会增加存储与 I/O 开销。压缩编码必须保留既有 append/fsync 提交边界、首次物化时的无冲突发布、崩溃修复以及仅元数据列举;如果每轮都重写整个压缩文件,就会失去这些属性。
+JSONL 持久化后端会逐字保留每个 `SessionEvent`,包括嵌入模型 stream 的 Assistant settlement。原始文本便于检查,但重复的 JSON key 和模型文本会增加存储与 I/O 开销。压缩编码必须保留既有 append/fsync 提交边界、首次物化时的无冲突发布、崩溃修复仅元数据列举;如果每轮都重写整个压缩文件,就会失去这些属性。
 
 编码还必须在部署边界上保持显式。快照 fixture(测试前置数据)与外部逐行读取器需要原始 JSONL,而后端无法在同一根目录中安全猜测压缩产物与原始产物,也不能静默迁移预发布会话数据。
 
@@ -14,9 +14,9 @@ JSONL 持久化后端会逐字保留每个 `SessionEvent`,其中包括数量
 
 ### 配置与后缀归属
 
-`dsh-session-persistence-jsonl` 接受 `compression?: 'zstd' | 'none'`,并将省略值显式解析为 `'zstd'`。Zstandard 产物使用 `.jsonl.zstd` 后缀;`'none'` 保留原有的换行分隔 UTF-8 `.jsonl` 表示。`SessionLocation.kind` 仍为 `'jsonl'`,因为两种编码承载同一逻辑记录格式;按照仓库的预发布拒绝且不迁移策略,`SESSION_FORMAT_VERSION` 仍为 `0`
+`dsh-session-persistence-jsonl` 接受 `compression?: 'zstd' | 'none'`,并将省略值显式解析为 `'zstd'`。Zstandard 产物使用 `.jsonl.zstd` 后缀;`'none'` 保留换行分隔 UTF-8 `.jsonl` 表示。在任一已配置后缀内,v0 使用无版本后缀 `session.jsonl[.zstd]`,每个正格式 generation 使用小写 `session.vN.jsonl[.zstd]`。`SessionLocation.kind` 仍为 `'jsonl'`,因为两种编码承载同一逻辑记录格式。Session 格式迁移使用配置后的完整后缀和同一条逻辑链,因此压缩不会分叉 generation 选择或发布
 
-每个持久化根目录只归属于一种编码。一次性的发现预检会拒绝任何相反后缀,而针对性的加载、活跃采用、列举与物化路径会在最初空目录预检之后再次执行对应后缀检查。错误会指出不兼容产物,并要求部署选择匹配配置或单独根目录。系统不提供迁移、双重读取、双重写入或基于扩展名的兜底
+每个持久化根目录只归属于一种编码。一次性的发现预检会拒绝任何相反后缀,而针对性的加载、活跃采用、列举与物化路径会在最初空目录预检之后再次执行对应后缀检查。错误会指出不兼容产物,并要求部署选择匹配配置或单独根目录。系统不提供压缩转换、双重读取、双重写入或基于扩展名的 fallback;逻辑版本迁移始终留在配置后缀内,保留源 generation,并排他发布最终具名版本后继
 
 ### 帧与写入路径
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.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-29-projected-token-usage-and-request-context.md
-2026-07-29-projected-token-usage-and-request-context.md: 7c984012b7a4b387f1ff24279567fe15aa34cf77
-2026-07-29-projected-token-usage-and-request-context.zh.md: b34fb7702898f4a51524a24ad27927c48220bfad
+2026-07-29-projected-token-usage-and-request-context.md: d62f7dccd544a342da64fc35c24d32d53bf56231
+2026-07-29-projected-token-usage-and-request-context.zh.md: 6d2bb624ac11dbcdac30695913d3c16513bfdf9e

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.md

@@ -14,7 +14,7 @@ Context occupancy needs a numerator and a denominator that no existing surface c
 
 Both values are ordinary durable session-projection state. `@deepseek-ai/dsh-token-meter` registers two units when `ctx.sessionProjections` is present.
 
-`tokenUsage` folds the complete durable log into uncached input, output, cache-read, and cache-write buckets. An `assistant/chunk` usage sample survives a later failed request; an `assistant/message` usage value replaces the earlier sample from the same model attempt instead of double-counting it. A matching `llm/retry-started` boundary ends that replacement scope, so a retry with the same `(turn, step)` contributes a new attempt. Reasoning stays an output subdivision. Compaction and surface replacement do not erase earlier billing.
+`tokenUsage` folds the complete durable log into uncached input, output, cache-read, and cache-write buckets. It expands each `assistant/message` or `assistant/attempt` stream and takes the last usage sample; a message's top-level usage takes precedence over its embedded sample instead of double-counting it. `assistant/attempt` therefore preserves usage from failed requests. A matching `llm/retry-started` boundary opens a new attempt, so a retry with the same `(turn, step)` contributes separately. Reasoning stays an output subdivision. Compaction and surface replacement do not erase earlier billing.
 
 Token-meter also owns the shared pure attempt/Turn fold over durable events. It applies the same retry boundary while adding the stricter completeness and exact-total checks required by an exact per-Turn disclosure. A presentation consumer may select a complete Turn window and invoke that fold, but does not own or duplicate the accounting semantics.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.zh.md

@@ -14,7 +14,7 @@ Web 统计行原先从当前已加载的会话节点推导 token 总量。该窗
 
 这两个值都是普通的持久会话投影状态。当 `ctx.sessionProjections` 存在时,`@deepseek-ai/dsh-token-meter` 会注册两个单元。
 
-`tokenUsage` 将完整持久日志归并为未缓存输入、输出、缓存读取和缓存写入四类计数项。即使后续请求失败,`assistant/chunk` 用量样本仍会保留;`assistant/message` 用量值会替换同一次模型 attempt 的先前样本,不会重复计数。匹配的 `llm/retry-started` 边界会结束该替换作用域,因此复用同一 `(turn, step)` 的重试会贡献一次新的 attempt。推理(reasoning)仍是输出的细分项。压缩和表层替换不会抹除先前的计费用量
+`tokenUsage` 将完整持久日志归并为未缓存输入、输出、缓存读取和缓存写入四类计数项。它会展开每个 `assistant/message` 或 `assistant/attempt` stream 并采用最后一个 usage sample;message 顶层 usage 优先于其嵌入式 sample,因此不会重复计数。`assistant/attempt` 由此保留失败请求的 usage。匹配的 `llm/retry-started` 边界会打开新 attempt,因此复用同一 `(turn, step)` 的重试会单独贡献用量。推理(reasoning)仍是输出的细分项。compaction 和 surface replacement 不会抹除先前计费
 
 token-meter 还拥有在持久事件上运行的共享纯 attempt/Turn fold。它采用相同的重试边界,并增加精确单轮次 disclosure 所需的更严格完整性与精确总量检查。展示消费方可以选择完整 Turn 窗口并调用该 fold,但不拥有或复制记账语义。
 

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

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.md
-2026-07-30-session-end-seed-log-boundary.md: c6ed3911a797480804d064273922d85412664c79
-2026-07-30-session-end-seed-log-boundary.zh.md: 1e9517f9a5aed819fdaff6194ab952c322c85b82
+2026-07-30-session-end-seed-log-boundary.md: aeec2a36d0b1e498591ef509e2e9164f586ed60c
+2026-07-30-session-end-seed-log-boundary.zh.md: ceba46a474c402230dbf215a2d53a41d3c027fc2

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

@@ -48,8 +48,8 @@ The predicate holds for a bracket *this* session inherited, not as a liveness si
 
 Bought: one boundary, written in one place, correct for all six seeded-start paths — including the fork gap the persistence-layer version could not reach. The persistence packages keep a pure read path. `firstLiveSeq` gains a durable twin rather than a second, competing notion of the same boundary.
 
-Cost: a seeded session's log is one event longer, including an empty resumed log. Seq expectations move with that boundary. Two updates are load-bearing rather than mechanical: telemetry's adoption tests assert the boundary IS exported, because it is this lifecycle's own write, and the property suite's replay invariant is "seed reproduced verbatim, plus one log-only boundary" with idempotence as its own property.
+Cost: a seeded session's log is one event longer, including an empty resumed log. Seq expectations move with that boundary. Two updates are load-bearing rather than mechanical: telemetry's adoption tests assert that capture begins with the current lifecycle's newly appended boundary and excludes the constructor seed, and the property suite's replay invariant is "seed reproduced verbatim, plus one log-only boundary" with idempotence as its own property.
 
-`session/end-seed` joins the on-disk vocabulary. Under the pre-release stance (`SESSION_FORMAT_VERSION` pinned at `0`, no compatibility promise) older logs simply lack it, and a log without a boundary correctly classifies nothing as constructor-seed history.
+`session/end-seed` joins the on-disk vocabulary. Current v1 requires the validated marker semantics owned by Session; the frozen v0 codec and migration edge own which historical v0 seed layouts remain admissible. The exact inherited cut stays separate from the logical header and is available after a body read.
 
 The [queued manual compaction decision](../feature/2026-07-30-queued-manual-compaction.md) now supplies the first consumer. Its tail scan independently finds the unmatched `compaction/start` and newest end-seed, treats only a start after that boundary as live, and clears the invariant trace on the same replay transition. The predicate remains in the compaction package rather than becoming a generic core helper.

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

@@ -48,8 +48,8 @@ Status: implemented
 
 买到的:一条边界,在一处写入,对全部六条带种子启动路径都正确——包括持久化层方案触及不到的 fork 缺口。持久化各包保留纯读取路径。`firstLiveSeq` 获得一个持久孪生体,而不是关于同一边界的第二套彼此竞争的概念。
 
-代价:带种子会话的日志长了一个事件,空日志恢复也包括在内。seq 期望会随这条边界移动。两处更新是承重的而非机械的:telemetry 的接管测试断言该边界*会*被导出,因为它是本生命周期的自有写入;属性测试套件的回放不变式则是「种子逐字节复现,外加一个仅日志边界」,并把幂等性作为独立属性。
+代价:带种子会话的日志长了一个事件,空日志恢复也包括在内。seq 期望会随这条边界移动。两处更新是承重的而非机械的:telemetry 的接管测试断言捕获从当前生命周期新追加的边界开始,并排除 constructor seed;属性测试套件的回放不变式则是「种子逐字节复现,外加一个仅日志边界」,并把幂等性作为独立属性。
 
-`session/end-seed` 加入了落盘词汇表。在预发布立场下(`SESSION_FORMAT_VERSION` 固定为 `0`,不作兼容承诺),更旧的日志只是没有它,而没有边界的日志会正确地判定没有任何内容属于构造种子历史
+`session/end-seed` 加入了落盘词汇表。当前 v1 要求由 Session 拥有的已校验 marker 语义;冻结的 v0 codec 与迁移边负责哪些历史 v0 seed 布局仍可接受。精确继承 cut 与逻辑 header 分离,并在读取正文后可用
 
 [排队手动压缩决策](../feature/2026-07-30-queued-manual-compaction.zh.md)如今提供了第一个消费方。其尾部扫描会分别查找未匹配的 `compaction/start` 与最新 end-seed,只把位于该边界之后的 start 视为存活,并在同一个回放转换上清除不变量追踪状态。该谓词仍位于压缩功能所在的包中,不会成为通用核心辅助函数。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-05-large-session-jsonl-restore-pipeline.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-05-large-session-jsonl-restore-pipeline.md
-2026-08-05-large-session-jsonl-restore-pipeline.md: 309d9dc6bdb5c3160f3e6e76a8318915df58fe59
-2026-08-05-large-session-jsonl-restore-pipeline.zh.md: 28acd3ebe804dca22a0619c257ff3ad9c09500a9
+2026-08-05-large-session-jsonl-restore-pipeline.md: e87777cc407e50a0e4808b77c3a7d554659d62e7
+2026-08-05-large-session-jsonl-restore-pipeline.zh.md: 32762bd19914423ef38c6472f0a8087e3e7e45fa

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-05-large-session-jsonl-restore-pipeline.md

@@ -30,7 +30,7 @@ The scanner stops retaining events at the first unparsable row or sequence gap b
 
 ### Restore admission
 
-Persistence transfers freshly materialized JSON values to `Session.fromRestore`. These values are detached, acyclic trees, and packed chunk rows expand into newly allocated events, so the restore-only path validates the fixed event envelope with one `for...in` and `switch`, dispatches current-shape checks by event discriminant, and iteratively freezes the owned graph with an explicit `pending` array and no cycle-tracking set. Surface validation records one transition plan and commits that plan when the exact candidate enters the log instead of planning the same event twice.
+Persistence transfers freshly materialized current JSON values to `Session.fromRestore`. These values are detached, acyclic trees; historical packed rows and adjacent migrations have already produced newly allocated v2 settlements. The restore-only path validates the fixed event envelope with one `for...in` and `switch`, dispatches current-shape checks by event discriminant, and iteratively freezes the owned graph with an explicit `pending` array and no cycle-tracking set. Surface validation records one transition plan and commits that plan when the exact candidate enters the log instead of planning the same event twice.
 
 Borrowed seeds used by ordinary creation and fork paths still take a JSON snapshot and use the generic cycle-safe deep freeze. The specialization therefore changes only durable restoration; it does not weaken acceptance for caller-owned values.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-05-large-session-jsonl-restore-pipeline.zh.md

@@ -30,7 +30,7 @@ Zstandard 结构扫描器会在解码前识别完整帧范围。系统单独解
 
 ### 恢复准入
 
-持久化把刚物化的 JSON 值转移给 `Session.fromRestore`。这些值是已分离且无环的树,打包的分片行也会展开成新分配的事件。因此,恢复专用路径使用一次 `for...in` 与 `switch` 校验固定事件信封,按事件判别字段执行当前数据形状检查,并通过显式 `pending` 数组迭代冻结所拥有的对象图,不使用循环跟踪集合。`surface` 校验会记录一次转换计划;当同一个候选事件进入日志时,系统直接提交该计划,不再对同一事件规划两次
+持久化把刚物化的当前 JSON 值转移给 `Session.fromRestore`。这些值是已分离且无环的 tree;历史 packed row 与相邻 migration 已经生成新分配的 v2 settlement。restore-only path 使用一次 `for...in` 与 `switch` 校验固定 event envelope,按 event discriminant 执行当前表示检查,并通过显式 `pending` array 迭代冻结 owned object graph,不使用 cycle-tracking set。`surface` 校验记录一次 transition plan,并在同一个 candidate event 进入 log 时提交该 plan
 
 普通创建与 fork 路径使用的借用 `seed` 仍会创建 JSON 快照,并使用支持循环检测的通用深度冻结。因此,这项特化仅改变持久恢复,不会放宽调用方所有值的准入要求。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-08-bounded-session-persistence-write-batching.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-08-bounded-session-persistence-write-batching.md
-2026-08-08-bounded-session-persistence-write-batching.md: 6fb44e494fc17bde08eb3132afe42ce73b5a4e47
-2026-08-08-bounded-session-persistence-write-batching.zh.md: 576764583dd8c465ae45866f62ef15773735961e
+2026-08-08-bounded-session-persistence-write-batching.md: 350a4dd7a4af64156b6d68939fbc10ea8f06b7b1
+2026-08-08-bounded-session-persistence-write-batching.zh.md: 495d7a7824fb2c622e37b5e233e4cb8fe45755de

+ 7 - 7
.agents/notes/implemented/architecture/2026-08-08-bounded-session-persistence-write-batching.md

@@ -6,13 +6,13 @@ English | [中文](2026-08-08-bounded-session-persistence-write-batching.zh.md)
 
 ## Problem
 
-Streaming responses can emit many `assistant/chunk` events in a short interval. The persistence coordinator previously scheduled a provider append as soon as an idle queue received one event. Events arriving while that append was active shared a follow-up batch, but a fast provider could still produce many small durable appends. Each JSONL append creates and syncs a Zstandard frame or raw suffix.
+One agent step can emit several durable events in a short interval: request metadata, one Assistant settlement, tool lifecycles, plugin facts, and execution boundaries. Scheduling a provider append as soon as an idle queue receives one event can therefore produce many small durable appends. Each JSONL append creates and syncs a Zstandard frame or raw suffix.
 
-Dropping chunk events or replacing them with assembled messages would reduce logical storage, but it would also change the event log, replay, sequence numbers, timestamps, and the chunk seqs cited by assistant messages. The write-amplification problem does not require that larger semantic change.
+Assistant stream embedding reduces one high-volume event family, but write cadence remains a provider-neutral lifecycle concern for every other burst and for historical generations. The batching decision does not change event semantics or storage encoding.
 
 ### Quantified baseline
 
-Repository fixtures make the logical volume concrete. Decoding the current packed rows in [`goal-multi-turn-actions`](../../../../snapshots/web/goal-multi-turn-actions/session.jsonl) yields 2,098 events: 2,017 chunks (96.1%). Their unpacked JSONL lines occupy 332,647 of 379,225 event bytes (87.7%), while chunk packing reduces the committed file to 89,176 bytes and 182 storage rows, including 23 packed chunk rows. [`permission-policy-context`](../../../../snapshots/web/permission-policy-context/session.jsonl) yields 813 events: 746 chunks (91.8%) and 118,935 of 184,821 unpacked event bytes (64.4%); its packed file is 84,917 bytes and 123 storage rows, including 14 packed rows. These are tracked deterministic fixtures, not a production workload distribution, but they demonstrate why deleting chunks would reduce logical volume and why the existing packed-row layout already removes much of their JSON envelope cost.
+Released-v1 repository fixtures established the original logical volume. Decoding the packed `goal-multi-turn-actions` generation yielded 2,098 events, including 2,017 chunks (96.1%); unpacked chunk lines occupied 332,647 of 379,225 event bytes, while the packed file used 89,176 bytes and 182 rows. The packed `permission-policy-context` generation yielded 813 events, including 746 chunks (91.8%); unpacked chunk lines occupied 118,935 of 184,821 event bytes, while the packed file used 84,917 bytes and 123 rows. These deterministic historical measurements explain why v2 embeds streams, but they are not a production workload distribution or a current-format size claim.
 
 JSONL writes one Zstandard frame and fsync per durable append batch. Runtime files do not record former append boundaries, so fixture row counts cannot honestly be presented as fsync counts.
 
@@ -28,7 +28,7 @@ The window bounds only the controller's intentional batching wait. Event-loop sc
 
 `session/flush` cancels any remaining wait and becomes a shared quiescence barrier. It drains the active attempt and every event admitted while the barrier is running before it resolves. Session retirement (`session/disposed`), the handle's close, and backend teardown's close sweep use that same barrier, so lifecycle teardown never waits for the batching timer. The checkpoint policy continues to place mandatory barriers before model requests and top-level tool side effects.
 
-Every event remains durable in its original order and shape. The controller copies each event on admission; no `assistant/chunk`, `seq`, `time`, surface metadata, or storage record is removed or rewritten. JSONL can therefore encode more events in one append frame without changing its on-disk format.
+Every admitted event remains durable in its original order and representation. The controller copies each event on admission; batching removes or rewrites no sequence, timestamp, surface metadata, embedded Assistant stream, or storage record. JSONL can therefore encode more events in one append frame without changing the Session format.
 
 A failed background drain retains its complete batch in order ahead of newer pending events, reports the failure once, and pauses the automatic timer. The next explicit drain — a `session/flush` barrier, service-level `flush()`, or close — retries immediately and surfaces a repeated failure to its caller. This avoids a timer-driven failure loop while preserving the existing recoverable flush boundary.
 
@@ -36,7 +36,7 @@ This decision supersedes only the immediate scheduling cadence in [Collapse live
 
 ## Alternatives considered
 
-**Do not persist streaming chunk events.** Rejected here: it changes the event-sourced authority and recovery semantics rather than only physical write cadence. The existing [assembled-message rejection](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md) remains the guardrail until a no-information-loss replacement defines replay, fork, cited source-event links, sequence, and crash behavior independently. The [packed-row decision](2026-07-26-packed-chunk-rows-by-default.md) remains the complementary JSONL storage-size optimization.
+**Use one settlement per Assistant attempt instead of batching writes.** The [v2 Assistant stream decision](2026-09-01-v2-embedded-assistant-streams.md) provides that no-information-loss event model and reduces Assistant event cardinality. It does not replace bounded batching for other adjacent events, historical-generation publication, or providers with the same append interface.
 
 **Write only at semantic checkpoints.** Rejected: it maximizes batching but makes the ordinary crash-loss window depend on a separately mounted policy. Bounded background writes preserve progress between checkpoints while mandatory flushes keep their stronger ordering contract.
 
@@ -50,10 +50,10 @@ The shared live-write contract suite (`runLiveWritePathContract`) uses a fake cl
 
 ## Consequences
 
-High-frequency event bursts normally produce fewer durable append operations while preserving the exact logical event count. The reduction depends on arrival rate and backend latency: a burst inside one 200 ms window becomes one batch, while mandatory flushes and sparse events can still produce small batches.
+High-frequency event bursts normally produce fewer durable append operations while preserving the exact admitted event sequence. The reduction depends on arrival rate and backend latency: a burst inside one 200 ms window becomes one batch, while mandatory flushes and sparse events can still produce small batches.
 
 This decision does not cap pending event count or bytes behind a slow provider, and it does not reduce the decoded logical log. A demonstrated memory bound or logical-retention policy would require its own failure and replay contract rather than another hidden timer rule.
 
 An admitted event can remain only in memory during the fixed window, and then while scheduling or backend work is outstanding. Explicit durability boundaries remain unchanged and bypass the wait.
 
-The handle gives the timer, active drain, pending prefix, retry pause, and barrier one owner; the backend's listeners own routing and lifecycle-driven drains. `SESSION_FORMAT_VERSION` remains unchanged.
+The handle gives the timer, active drain, pending prefix, retry pause, and barrier one owner; the backend's listeners own routing and lifecycle-driven drains. Batching itself never changes `SESSION_FORMAT_VERSION`.

+ 7 - 7
.agents/notes/implemented/architecture/2026-08-08-bounded-session-persistence-write-batching.zh.md

@@ -6,13 +6,13 @@ Status: implemented
 
 ## 问题
 
-流式响应可能会在短时间内发出大量 `assistant/chunk` 事件。此前,只要空闲队列收到一个事件,持久化协调器就会立即调度一次 provider 追加。该追加仍在进行时到达的事件会共用一个后续批次,但如果 provider 速度很快,仍可能产生大量小规模的持久化追加。每次 JSONL 追加都会创建并同步一个 Zstandard 帧或原始格式后缀。
+一个 agent 步骤可以在短时间内发出多个持久事件:请求元数据、一个 Assistant settlement、工具生命周期、插件事实与执行边界。只要空闲队列收到一个事件就立即调度 provider 追加,仍可能产生大量小规模的持久化追加。每次 JSONL 追加都会创建并同步一个 Zstandard 帧或原始格式后缀。
 
-丢弃分片事件或用组装后的消息替代它们可以减少逻辑存储量,但也会改变事件日志、回放、序列号、时间戳,以及助手消息引用的分片 seq。写放大问题不要求采取这项语义变化更大的方案
+Assistant stream 嵌入会减少一个高频事件 family,但对于其他事件突发与历史 generation,写入节奏仍是 provider-neutral 生命周期问题。批处理决策不会改变事件语义或存储编码
 
 ### 量化基线
 
-仓库 fixture(测试前置数据)让逻辑数据量有了具体依据。对当前 [`goal-multi-turn-actions`](../../../../snapshots/web/goal-multi-turn-actions/session.jsonl) 中的打包行进行解码,可得到 2,098 个事件,其中 2,017 个是分片(96.1%)。这些分片解包后的 JSONL 行共 332,647 字节,占全部事件 379,225 字节的 87.7%;分片打包则把仓库中的已提交文件缩小到 89,176 字节和 182 个存储行,其中包括 23 个打包分片行。[`permission-policy-context`](../../../../snapshots/web/permission-policy-context/session.jsonl) 可得到 813 个事件,其中 746 个是分片(91.8%);这些分片解包后的 JSONL 行共 118,935 字节,占全部事件 184,821 字节的 64.4%。其打包文件为 84,917 字节,共 123 个存储行,其中包括 14 个打包行。这些是纳入版本控制的确定性 fixture,不代表生产工作负载分布;但它们说明了删除分片为何会降低逻辑数据量,也说明现有打包行布局已经消除了大量 JSON 包装开销
+已发布 v1 仓库 fixture 建立了原始逻辑数据量。解码 packed `goal-multi-turn-actions` generation 得到 2,098 个事件,其中 2,017 个是 chunk(96.1%);解包的 chunk 行占 379,225 个事件字节中的 332,647 字节,而 packed 文件使用 89,176 字节与 182 行。packed `permission-policy-context` generation 得到 813 个事件,其中 746 个是 chunk(91.8%);解包的 chunk 行占 184,821 个事件字节中的 118,935 字节,而 packed 文件使用 84,917 字节与 123 行。这些确定性历史测量解释了 v2 为何嵌入 stream,但不代表生产工作负载分布或当前格式大小
 
 JSONL 每个持久化追加批次会写入一个 Zstandard 帧并执行一次 fsync。运行时文件不记录原有追加边界,因此不能把 fixture 的存储行数当作 fsync 次数。
 
@@ -28,7 +28,7 @@ JSONL 每个持久化追加批次会写入一个 Zstandard 帧并执行一次 fs
 
 `session/flush` 会取消剩余等待,并充当共享的完全停稳屏障。它会在完成前等待活跃写入尝试,并排空屏障运行期间接纳的每个事件。会话退役(`session/disposed`)、句柄的 close 与后端 teardown 的关闭清扫共用该屏障,因此生命周期 teardown 绝不会等待批处理计时器。检查点策略仍会在模型请求与顶层工具副作用之前设置强制屏障。
 
-每个事件仍会按原有顺序和形态持久化。控制器会在接纳时复制每个事件;任何 `assistant/chunk`、`seq`、`time`、surface 元数据或存储记录都不会被删除或重写。因此,JSONL 可以在一个追加帧中编码更多事件,而无需改变其磁盘格式。
+每个已接纳事件仍会按原有顺序和表示持久化。控制器会在接纳时复制每个事件;批处理不会删除或改写任何序号、时间戳、surface 元数据、嵌入式 Assistant stream 或存储记录。因此,JSONL 可以在一个追加 frame 中编码更多事件,而无需改变 Session 格式。
 
 后台排空失败后,其完整批次会按顺序保留在所有较新的待处理事件之前,该失败被报告一次,自动计时器随之暂停。下一次显式排空——`session/flush` 屏障、服务级 `flush()` 或 close——会立即重试,如果故障再次发生,则会向调用方暴露该故障。这可以避免计时器驱动的失败循环,同时保留现有可恢复的 flush 边界。
 
@@ -36,7 +36,7 @@ JSONL 每个持久化追加批次会写入一个 Zstandard 帧并执行一次 fs
 
 ## 备选方案
 
-**不持久化流式分片事件。** 这里不采纳:这会改变事件溯源的权威地位及恢复语义,而不只是改变物理写入节奏。在无信息损失的替代方案独立定义回放、fork、引用源事件的关联、序列和崩溃行为之前,现有的[拒绝仅保留组装消息的决策](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.zh.md)仍是防护规则。[打包行决策](2026-07-26-packed-chunk-rows-by-default.zh.md)仍是配套的 JSONL 存储体积优化
+**使用每个 Assistant attempt 一个 settlement 代替批处理写入。** [v2 Assistant stream 决策](2026-09-01-v2-embedded-assistant-streams.zh.md)提供该无信息损失事件模型,并减少 Assistant 事件基数。它不能替代其他相邻事件、历史 generation 发布或使用同一 append 接口的 provider 所需的有界批处理
 
 **仅在语义检查点写入。** 不采纳:此方案会最大化批处理,却让普通的崩溃丢失窗口取决于另行挂载的策略。有界后台写入会在检查点之间持久化进度,而强制 flush 继续提供更强的顺序约定。
 
@@ -50,10 +50,10 @@ JSONL 每个持久化追加批次会写入一个 Zstandard 帧并执行一次 fs
 
 ## 后果
 
-高频事件突发通常会减少持久化追加操作,同时保持逻辑事件数量完全不变。减少幅度取决于事件到达速率和后端延迟:位于同一 200 ms 窗口内的突发事件会成为一个批次,而强制 flush 与稀疏事件仍可能产生小批次。
+高频事件突发通常会减少持久化追加操作,同时保持已接纳事件序列完全不变。减少幅度取决于事件到达速率和后端延迟:位于同一 200 ms 窗口内的突发事件会成为一个批次,而强制 flush 与稀疏事件仍可能产生小批次。
 
 本决策不会限制因 provider 缓慢而积压的待处理事件数量或字节数,也不会减少解码后的逻辑日志。若要建立经过验证的内存上界或逻辑保留策略,就必须为其另行定义失败与回放约定,而不是再引入一条隐式计时器规则。
 
 接纳后的事件在固定窗口内可能只存在于内存中,此后在等待调度或后端工作完成期间也可能如此。显式持久性边界保持不变,并会绕过等待。
 
-句柄统一负责计时器、活跃排空、待处理前缀、重试暂停和屏障;后端的监听器负责路由和生命周期驱动的排空。`SESSION_FORMAT_VERSION` 保持不变
+句柄统一负责计时器、活跃排空、待处理前缀、重试暂停和屏障;后端的监听器负责路由和生命周期驱动的排空。批处理本身绝不改变 `SESSION_FORMAT_VERSION`。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.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-09-client-conversation-node-assembly.md
-2026-08-09-client-conversation-node-assembly.md: 0aac5056e2cbe22359f8064b1bf4aa0b015a35c8
-2026-08-09-client-conversation-node-assembly.zh.md: b06a92113f91e6297da986866dce097b11bab45f
+2026-08-09-client-conversation-node-assembly.md: 425975944f7b23575c4f716dee8754dc6b3ab54c
+2026-08-09-client-conversation-node-assembly.zh.md: 96439fa951cdc1d6abe397ee37661291c0f954a7

+ 4 - 4
.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md

@@ -51,7 +51,7 @@ Each `(kind, id)` has at most one start Match. A second start fails immediately;
 
 #### `match(event)`
 
-`match(event)` reads only the current `SessionEventLike` and returns `{ id, role: 'start' | 'update' }` or `null`. It cannot access a Context, history, a Reader, a Location, or the view envelope. A `chunkrow/*` event can only be an update; the Assembler rejects it as a start, and `start()` receives a `ConversationStartMatch` containing a standard `SessionEvent`.
+`match(event)` reads only the current `SessionEventLike` and returns `{ id, role: 'start' | 'update' }` or `null`. It cannot access a Context, history, a Reader, a Location, or the view envelope. A Client-only `assistant/live-chunk` event can only be an update; the Assembler rejects every transient start, and `start()` receives a `ConversationStartMatch` containing a durable `SessionEvent`.
 
 This restriction makes one scalar event or packed run's routing cost depend only on the number of registered Definitions. The Assembler never scans a Definition's historical Contexts to decide which one owns an update.
 
@@ -110,7 +110,7 @@ Dependencies point strictly from earlier starts to later starts, so transitive r
 
 #### `update(context, match)`
 
-`update()` handles a post-start scalar or packed Match that `match()` has already routed exactly to the current `(kind, id)`. It does not decide which Context owns the input. A Definition that consumes Assistant deltas folds each matching `chunkrow/*` value as one batch without constructing member events.
+`update()` handles a post-start durable or transient Match that `match()` has already routed exactly to the current `(kind, id)`. It does not decide which Context owns the input. An Assistant Definition folds each `assistant/live-chunk` update directly and expands an embedded `assistant/message` or `assistant/attempt` stream during history replay.
 
 The Assembler invokes `update()` in ascending `seq` order. A live tail update can apply incrementally; any non-tail insertion, newly loaded start, or invalidated dependency causes a complete replay from `start()`.
 
@@ -262,7 +262,7 @@ Page size, record packing, the number of history loads, and RAF coalescing affec
 | Next-step Inbox / `inbox-next-step` | Splice Event seq | Each `agent/inbox/spliced` targeting next-step | None | Append message IDs to persistent splice state; materialize once per claim and expose the shared current claimed batch to Message |
 | Message / `input-message` | Message ID | Append-surface `user/message` | None | Use source for a context message, or read the nearest next-step Inbox to distinguish user from steering |
 | Request Prompt / `request-prompt` | Header Event seq | Each `request/header` | None | Read the preceding Request Prompt through Reader, retain the full prompt state, and classify system/tool changes |
-| Assistant / `assistant-step` | `turn:step` | `step/start` | Scalar or packed `assistant/chunk`, final `assistant/message`, and same-step Retry | Aggregate blocks, usage, first-token time, final evidence, and retry-hidden state, then publish same-key Step data |
+| Assistant / `assistant-step` | `turn:step` | `step/start` | Live `assistant/live-chunk`, durable `assistant/message` or `assistant/attempt`, and same-step Retry | Aggregate blocks, usage, first-token time, settlement evidence, and retry-hidden state, then publish same-key Step data |
 | Tool / `tool-call` | Root call ID | Root `tool/call` | Root result and Code Dispatch start/result | Aggregate the root, children, and parent Map; Dispatch Events route exactly through `rootCallId` |
 | Command / `command` | Command ID | `command/run` | `command/done` and compact lifecycle/checkpoint Events carrying a source command ID | Aggregate command outcome and manual-compaction evidence |
 | Automatic Compaction / `compaction` | Compaction ID | `compaction/start` without a source command ID | Summary, end, and replacement checkpoint | Aggregate summary/checkpoint; sufficient checkpoint evidence supports fallback without a start |
@@ -382,7 +382,7 @@ History-path tests cover complete replace, non-overlapping prepend, complete-ran
 
 **Define a reverse State fold for backward history scanning.** Rejected: every business would maintain two inverse algorithms, and deletion, non-invertible aggregation, and cross-Context dependencies would be difficult to keep equivalent. Ordered Matches followed by forward replay from start preserve one business meaning.
 
-**Add a separate chunk-run matcher and update lifecycle.** Rejected: a second Definition path would duplicate dispatch, replay, publication, and Context types. `ChunkRowEvent` uses the existing `match(event)` and `update(context, match)` lifecycle while making packed handling explicit through its `chunkrow/*` discriminant.
+**Add a separate live-stream matcher and update lifecycle.** Rejected: a second Definition path would duplicate dispatch, replay, publication, and Context types. Client-only `assistant/live-chunk` and durable settlements use the existing `match(event)` and `update(context, match)` lifecycle; only the event discriminator and stream expansion differ.
 
 **Make Inbox a first-class engine concept or one window-wide Context.** Rejected: Inbox is ordinary business State and does not belong in the generic engine. Per-splice instantaneous State plus a strictly backward Reader supports prepend, append, and Message lookup together.
 

+ 4 - 4
.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md

@@ -51,7 +51,7 @@ Assembler 使用 `conversationContextKey(kind, id)` 组合无碰撞 key;不同
 
 #### `match(event)`
 
-`match(event)` 只读取当前 `SessionEventLike`,返回 `{ id, role: 'start' | 'update' }` 或 `null`。它拿不到 Context、历史、Reader、Location 或 view envelope。`chunkrow/*` event 只能作为 update;Assembler 会拒绝 packed start,`start()` 接收的 `ConversationStartMatch` 只包含标准 `SessionEvent`。
+`match(event)` 只读取当前 `SessionEventLike`,返回 `{ id, role: 'start' | 'update' }` 或 `null`。它拿不到 Context、history、Reader、Location 或 view envelope。Client-only `assistant/live-chunk` event 只能作为 update;Assembler 会拒绝每个 transient start,`start()` 接收的 `ConversationStartMatch` 只包含持久 `SessionEvent`。
 
 这项限制使单条 scalar event 或 packed run 的路由成本只随已注册 Definition 数量增长。Assembler 不会为了判断一条 update 属于谁而遍历该 Definition 的历史 Context。
 
@@ -110,7 +110,7 @@ Reader 每次查询都记录 `{ key, revision, windowGap }` 依赖。命中前
 
 #### `update(context, match)`
 
-`update()` 只处理已由 `match()` 精确路由到当前 `(kind, id)` 的 post-start scalar 或 packed Match。它不判断 input 属于哪个 Context。消费 Assistant delta 的 Definition 会把每个匹配的 `chunkrow/*` 值作为一个 batch fold,而不构造成员 event
+`update()` 只处理已由 `match()` 精确路由到当前 `(kind, id)` 的 post-start durable 或 transient Match。它不判断 input 属于哪个 Context。Assistant Definition 会直接 fold 每个 `assistant/live-chunk` update,并在 history replay 期间展开嵌入式 `assistant/message` 或 `assistant/attempt` stream
 
 Assembler 按 `seq` 升序调用 `update()`。实时尾部 update 可以直接增量应用;任何非尾部证据插入、start 补齐或依赖失效都会从 `start()` 完整 replay。
 
@@ -262,7 +262,7 @@ Chat `order` 的结构性变化仍可能重排当前可见 key;纯 data 更新
 | Next-step Inbox / `inbox-next-step` | splice Event seq | 每条目标为 next-step 的 `agent/inbox/spliced` | 无 | 把消息 ID 追加到持久 splice state;每次 claim 只 materialize 一次,并向 Message 暴露共享的当前 claimed batch |
 | Message / `input-message` | message ID | append-surface `user/message` | 无 | 根据 source 生成 context message,或读取最近 next-step Inbox 判断 user/steering |
 | Request Prompt / `request-prompt` | header Event seq | 每条 `request/header` | 无 | 通过 Reader 读取前一条 Request Prompt,保留完整 prompt 状态,并判定 system/tool 变化 |
-| Assistant / `assistant-step` | `turn:step` | `step/start` | scalar 或 packed `assistant/chunk`、final `assistant/message`、同 step Retry | 聚合 blocks、usage、首 token 时间、final 和 retry 隐藏状态,并发布同 key Step data |
+| Assistant / `assistant-step` | `turn:step` | `step/start` | Live `assistant/live-chunk`、持久 `assistant/message` 或 `assistant/attempt`、同 step Retry | 聚合 block、usage、首 token 时间、settlement 证据与 retry-hidden state,再发布同 key Step data |
 | Tool / `tool-call` | root call ID | root `tool/call` | root result、Code Dispatch start/result | 聚合 root、children 和 parent Map;Dispatch Event 用 `rootCallId` 精确路由 |
 | Command / `command` | command ID | `command/run` | `command/done`、带 source command ID 的 compact lifecycle/checkpoint | 聚合 command outcome 和手动压缩证据 |
 | Automatic Compaction / `compaction` | compaction ID | 无 source command ID 的 `compaction/start` | summary、end、replacement checkpoint | 聚合 summary/checkpoint;checkpoint 足够时可在缺 start 下 fallback |
@@ -382,7 +382,7 @@ Assembled Web snapshot、GUI 和浏览器场景覆盖真实 plugin graph。浏
 
 **为历史反扫定义逆向 State fold。** 拒绝:每个业务都要维护互为逆运算的两套逻辑,删除、非可逆聚合和跨 Context 依赖很难保持一致。统一 Matches 后从 start 正序 replay 只有一套业务语义。
 
-**增加独立的 chunk-run matcher 与 update lifecycle。** 拒绝:第二条 Definition 路径会重复 dispatch、replay、publication 与 Context 类型。`ChunkRowEvent` 使用既有 `match(event)` 与 `update(context, match)` lifecycle,并通过 `chunkrow/*` discriminator 明确标记 packed 处理
+**增加独立 live-stream matcher 与 update lifecycle。** 拒绝:第二条 Definition path 会重复 dispatch、replay、publication 与 Context type。Client-only `assistant/live-chunk` 与持久 settlement 使用既有 `match(event)` 和 `update(context, match)` lifecycle;只有 event discriminator 与 stream expansion 不同
 
 **把 Inbox 做成引擎一级公民或一个窗口级 Context。** 拒绝:Inbox 是普通业务状态,不应污染通用引擎;逐 splice 瞬间态加严格前序 Reader 同时支持 prepend、append 和 Message 查询。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-10-cancelled-stream-prefix-finalize.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-10-cancelled-stream-prefix-finalize.md
-2026-08-10-cancelled-stream-prefix-finalize.md: fd397a02663908f5984b4e1798d1b1759b140c79
-2026-08-10-cancelled-stream-prefix-finalize.zh.md: 44adb2ff4163cd1904a9a93895c99519bae2f234
+2026-08-10-cancelled-stream-prefix-finalize.md: 1e6fe59bcc1b323941b69d0e9232f0bcf9e56f53
+2026-08-10-cancelled-stream-prefix-finalize.zh.md: 7aa81f179b80925ef47a8820f8a80c1ef37ec5ea

+ 7 - 7
.agents/notes/implemented/architecture/2026-08-10-cancelled-stream-prefix-finalize.md

@@ -6,23 +6,23 @@ English | [中文](2026-08-10-cancelled-stream-prefix-finalize.zh.md)
 
 ## Problem
 
-A cancelled stream can leave `assistant/chunk` events that clients continue rendering while `deriveMessages()` excludes them because no `assistant/message` records the delivered prefix. A follow-up such as "expand on your second point" then lacks text the user read, and a fork at the cancelled turn inherits the same gap.
+A cancelled stream can leave transient chunks that clients have rendered while `deriveMessages()` excludes them because no `assistant/message` records the delivered prefix. A follow-up such as "expand on your second point" then lacks text the user read, and a fork at the cancelled turn inherits the same gap.
 
 The model history must contain assistant content that remains visible to the user after cancellation.
 
 ## Decision
 
-`ReactLoopAgent.step()` catches cancellation while consuming a model stream, when its `BlockAssembler`, logged chunk seqs, and provider route identify the delivered prefix. It appends that prefix as the step's `assistant/message` with `interrupted: true`, `surfaceOp: 'append'`, and `sourceEventSeqs` containing exactly the logged chunks. The append precedes `step/end` and the aborted `turn/end`.
+`ReactLoopAgent.step()` catches cancellation while consuming a model stream, when its `BlockAssembler`, compact stream accumulator, and provider route identify the delivered prefix. It appends that prefix as the step's `assistant/message` with `interrupted: true`, `surfaceOp: 'append'`, and the exact embedded timed stream. The append precedes the committed `agent/assistant-stream` end frame, `step/end`, and the aborted `turn/end`.
 
-`BlockAssembler.interruptedBlocks()` returns closed and open `text` and `reasoning` blocks with non-whitespace content in stream order. It omits tool calls because interruption precedes dispatch and no real result exists; it also omits empty blocks and open unknown block types. An empty result appends no assistant message. Provider `error` and `aborted` finishes leave the stream-consumption scope before `agent/request-error`, so provider failures and cancellation during recovery commit no content from the failed request.
+`BlockAssembler.interruptedBlocks()` returns closed and open `text` and `reasoning` blocks with non-whitespace content in stream order. It omits tool calls because interruption precedes dispatch and no real result exists; it also omits empty blocks and open unknown block types. An empty result appends `assistant/attempt` instead of a surface message. Provider `error` and `aborted` finishes also commit `assistant/attempt` before `agent/request-error`, so their streams remain durable without contributing failed-request content to model history.
 
-Chat and Trajectory Conversation Definitions read `interrupted` from the durable message. Chat renders the Stopped marker, while Trajectory keeps the provider request in the error lifecycle after `step/end` and retains the durable result seq and provenance. Cancellation during tool execution follows the tool scheduler contract because the assistant message has already committed: started calls produce real results, and undispatched calls receive `ABORTED_BEFORE_DISPATCH` results.
+Chat and Trajectory Conversation Definitions read `interrupted` from the durable message. Chat renders the Stopped marker, while Trajectory keeps the provider request in the error lifecycle after `step/end` and retains the durable result seq and provider information. Cancellation during tool execution follows the tool scheduler contract because the assistant message has already committed: started calls produce real results, and undispatched calls receive `ABORTED_BEFORE_DISPATCH` results.
 
 ## Alternatives considered
 
 **Always discard the prefix.** This avoids a new durable marker but makes every cancel-then-follow-up and fork omit assistant content that remains visible to the user.
 
-**Assemble the prefix from chunks during projection.** `deriveMessages()` and client Conversation Definitions would each need interruption assembly rules, and the log would have no authoritative assistant message for the prefix. This also expands model history beyond the three `SurfaceEventType` events.
+**Assemble the prefix from the embedded attempt during projection.** `deriveMessages()` and Client Conversation Definitions would each need interruption assembly rules, and the log would have no authoritative surface message for the prefix. This also expands model history beyond the three `SurfaceEventType` events.
 
 **Retain complete tool calls with synthetic aborted results.** These calls never dispatched, so synthetic results would claim an execution outcome that did not occur and add content the user did not receive as a tool result.
 
@@ -32,8 +32,8 @@ Chat and Trajectory Conversation Definitions read `interrupted` from the durable
 
 Post-cancel follow-ups and forks include the delivered prefix. The ACP bridge drains ordered assistant output before settling the prompt, so the final `agent_message_chunk` update precedes the cancelled stop reason.
 
-Terminal provider errors still discard their streamed prefix. That asymmetry remains because an error turn ends without the user's cancellation decision and requires its own retention policy.
+Terminal provider errors retain their stream in `assistant/attempt` but keep its content out of model history. Only the user's cancellation decision turns visible delivered text into an interrupted surface message.
 
 ## Testing
 
-`packages/core/agent-loop/tests/cancel.spec.ts` covers content, cited seqs, event order, next-request parity, reasoning-only output, tool-call omission, recovery cancellation, and the empty-prefix case. `packages/llm/llm/tests/assembler.spec.ts` covers `interruptedBlocks()`. `packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts` and `packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts` cover both client projections. The keyless `cancel` ACP snapshot and `goal-round-driver` goal snapshot cover assembled applications.
+`packages/core/agent-loop/tests/cancel.spec.ts` covers content, embedded streams, event order, next-request parity, reasoning-only output, tool-call omission, recovery cancellation, and the empty-prefix attempt. `packages/llm/llm/tests/assembler.spec.ts` covers `interruptedBlocks()`. `packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts` and `packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts` cover both Client projections. The keyless `cancel` ACP snapshot and `goal-round-driver` goal snapshot cover assembled applications.

+ 7 - 7
.agents/notes/implemented/architecture/2026-08-10-cancelled-stream-prefix-finalize.zh.md

@@ -6,23 +6,23 @@ Status: implemented
 
 ## Problem
 
-被取消的流可能留下客户端继续渲染的 `assistant/chunk` 事件,但如果没有 `assistant/message` 记录已送达前缀,`deriveMessages()` 就会排除这部分内容。后续的「第二点展开讲讲」之类追问会缺少用户已读到的文本,在该轮次上创建的分支也会继承这个缺口。
+被取消的流可能留下 Client 已经渲染的瞬态 chunk,但如果没有 `assistant/message` 记录已送达前缀,`deriveMessages()` 就会排除这部分内容。后续的「第二点展开讲讲」之类追问会缺少用户已读到的文本,在该轮次上创建的分支也会继承这个缺口。
 
 模型历史必须包含取消后仍对用户可见的 assistant 内容。
 
 ## Decision
 
-`ReactLoopAgent.step()` 在消费模型流期间捕捉取消,此时 `BlockAssembler`、已记录的分片 seq 和提供方路由可以确定已送达前缀。循环把该前缀追加为 step 的 `assistant/message`,并设置 `interrupted: true`、`surfaceOp: 'append'` 以及恰好包含已记录分片的 `sourceEventSeqs`。该追加先于 `step/end` 和记录 aborted 的 `turn/end`。
+`ReactLoopAgent.step()` 在消费模型 stream 期间捕捉取消,此时 `BlockAssembler`、紧凑 stream accumulator 与 provider route 可以确定已送达前缀。loop 把该前缀追加为 step 的 `assistant/message`,并设置 `interrupted: true`、`surfaceOp: 'append'` 与精确嵌入式带时间 stream。该追加先于 committed `agent/assistant-stream` end frame、`step/end` 和记录 aborted 的 `turn/end`。
 
-`BlockAssembler.interruptedBlocks()` 按流顺序返回内容非空白的已闭合和未闭合 `text` 与 `reasoning` 块。打断先于分派,没有真实工具结果,因此它会省略工具调用,也会省略空块和未闭合的未知块类型。返回结果为空时不追加 assistant 消息。提供方的 `error` 和 `aborted` finish 会在 `agent/request-error` 前离开流消费范围,因此提供方故障和恢复期间的取消都不会提交失败请求的内容
+`BlockAssembler.interruptedBlocks()` 按 stream 顺序返回内容非空白的已闭合和未闭合 `text` 与 `reasoning` block。打断先于分派,没有真实工具结果,因此它会省略工具调用,也会省略空 block 和未闭合的未知 block 类型。返回结果为空时追加 `assistant/attempt`,而不是 surface message。Provider `error` 与 `aborted` finish 也会在 `agent/request-error` 前提交 `assistant/attempt`,因此其 stream 保持持久,但失败请求内容不会进入模型历史
 
-Chat 和 Trajectory Conversation Definition 从持久消息读取 `interrupted`。Chat 渲染 Stopped 标记,Trajectory 则在 `step/end` 后把提供方请求保持在 error 生命周期,并保留持久结果 seq 和提供方信息。工具执行期间的取消遵循工具调度器约定,因为 assistant 消息已提交:已启动的调用生成真实结果,未分派的调用获得 `ABORTED_BEFORE_DISPATCH` 结果。
+Chat 和 Trajectory Conversation Definition 从持久 message 读取 `interrupted`。Chat 渲染 Stopped marker,Trajectory 则在 `step/end` 后把 provider request 保持在 error 生命周期,并保留持久 result seq 与 provider 信息。工具执行期间的取消遵循工具调度器约定,因为 assistant message 已提交:已启动的调用生成真实结果,未分派的调用获得 `ABORTED_BEFORE_DISPATCH` 结果。
 
 ## Alternatives considered
 
 **始终丢弃前缀。** 这能避免新增持久标记,但每次取消后的追问和分支都会缺少仍对用户可见的 assistant 内容。
 
-**在投影时从分片组装前缀。** `deriveMessages()` 和客户端 Conversation Definition 都需要实现打断组装规则,日志中也没有该前缀的权威 assistant 消息。这还会让模型历史超出三类 `SurfaceEventType` 事件。
+**在投影时从嵌入式 attempt 组装前缀。** `deriveMessages()` 与 Client Conversation Definition 都需要实现打断组装规则,日志中也没有该前缀的权威 surface message。这还会让模型历史超出三类 `SurfaceEventType` 事件。
 
 **保留完整工具调用并合成 aborted 结果。** 这些调用从未分派,合成结果会声称一个并未发生的执行结果,还会增加用户未收到的工具结果内容。
 
@@ -32,8 +32,8 @@ Chat 和 Trajectory Conversation Definition 从持久消息读取 `interrupted`
 
 取消后的追问和分支会包含已送达前缀。ACP 桥会在结算 prompt 前排空按序传送的 assistant 输出,因此最后一条 `agent_message_chunk` 更新先于 cancelled stop reason。
 
-终局提供方错误仍会丢弃已流出前缀。该不对称保留,因为 error 轮次的结束不来自用户的取消决定,需要独立的保留策略
+终局 provider error 会在 `assistant/attempt` 中保留其 stream,但不让内容进入模型历史。只有用户的取消决策会把可见的已送达文本变成 interrupted surface message
 
 ## Testing
 
-`packages/core/agent-loop/tests/cancel.spec.ts` 覆盖内容、引用的 seq、事件顺序、下一请求的一致性、仅 reasoning 的输出、工具调用省略、恢复期间的取消和空前缀情形。`packages/llm/llm/tests/assembler.spec.ts` 覆盖 `interruptedBlocks()`。`packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts` 和 `packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts` 覆盖两种客户端投影。keyless 的 `cancel` ACP 快照和 `goal-round-driver` goal 快照覆盖完整应用。
+`packages/core/agent-loop/tests/cancel.spec.ts` 覆盖 content、嵌入式 stream、事件顺序、下一请求的一致性、仅 reasoning 的输出、工具调用省略、恢复期间的取消和空前缀 attempt。`packages/llm/llm/tests/assembler.spec.ts` 覆盖 `interruptedBlocks()`。`packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts` 与 `packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts` 覆盖两种 Client 投影。keyless `cancel` ACP snapshot 与 `goal-round-driver` goal snapshot 覆盖组装应用。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md
-2026-08-10-session-log-version-mechanism.md: 0d4c9e73acc6abd4a67123e3d7b0e4f94e0b5a23
-2026-08-10-session-log-version-mechanism.zh.md: 6c57da618a09c1d423323940ea36dbd4caccde02
+2026-08-10-session-log-version-mechanism.md: 98eb220e49457d3a2783edefce13d053c9b94025
+2026-08-10-session-log-version-mechanism.zh.md: 8853e6ed9a1a1ed43193cfe0949fffdbb9b4a26f

+ 3 - 3
.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md

@@ -14,17 +14,17 @@ Session logs must be upgradable after release, and the runtime that ships first
 
 **The writer decides bumps, not the reader.** A bump is required exactly when an old runtime could no longer handle a new log with full semantic correctness. "Parses without error" is not the bar: silently skipping content that shapes reconstruction is a wrong read. Only structural changes qualify — header shape, event envelope, core event semantics, the surface mechanism (`SurfaceEventType` set, `SurfaceOp` variants). When unsure, bump: a near-identity upgrader is almost free, a missed bump silently corrupts old readers.
 
-**Read rules by direction.** Equal version: read normally. Newer than the reader: refuse, name the direction ("written by a newer harness — upgrade"), and point at the raw log artifact so the user can still see the text (`SessionFormatUnsupportedError`, distinct from `SessionPersistenceCorruptionError` because nothing is damaged). Older than the reader: convert in memory through the chain of n→n+1 upgraders for viewing; persist the converted log only when the session is actually continued (atomic temp-file replace, original kept as backup). A step whose upgrader cannot be written is left empty, which cuts off every version at or below it — those degrade to raw-text viewing.
+**Read rules by direction.** Equal version: read normally. Newer than the reader: refuse, name the direction ("written by a newer harness — upgrade"), and point at the raw log artifact so the user can still see the text (`SessionFormatUnsupportedError`, distinct from `SessionPersistenceCorruptionError` because nothing is damaged). Older than the reader: every event-body operation first runs the complete adjacent chain in memory, leaves the source path, bytes, and inode unchanged, exclusively publishes only the final current generation under its canonical versioned filename, and reopens it before current restoration. Header-only listing remains non-mutating and reports the numerically highest canonical generation. Catalog generation and module initialization reject a missing adjacent step, so a published first-party build never exposes a partial historical chain. Retained lower generations are not automatic fallback or a downgrade compatibility promise.
 
 **A per-event `ignorable` marker covers vocabulary growth, so ordinary event additions never bump the version.** The event vocabulary is decided by which plugins are mounted, which a single version integer cannot describe. A reader meeting an unrecognized event type refuses to interpret the log unless the event carries `ignorable: true` in its envelope. The default is *required*: forgetting the marker over-refuses a resumable session (an inconvenience), while a default of ignorable would make the same mistake silently resume a gutted one (a safety failure). The architecture makes this sound: model-visible content flows only through the three `surfaceOp`-marked surface event types plus the `request/header`/`request/context` folds, so the dangerous unknowns are exactly the non-surface events that change how the rest of the log is read (`session/end-seed` is the existing example).
 
 ## Consequences
 
-What shipped in v0 (release 0812): direction-aware refusal with the raw-log path; the unknown-event guard against a generated known-vocabulary list (`KNOWN_SESSION_EVENT_TYPES`, emitted by `gen-persistence-catalog` from every `SessionEventMap` merge and kept fresh by `verify-persistence-catalog`); the `ignorable` envelope field accepted by seed validation, JSONL, and the BFF wire schema. The upgrader chain itself is deferred until the first real v0→v1 step exists to test it against. First-party writers do not set `ignorable` through `Session.append`, while a repository-external plugin is a current consumer; its retention and replacement condition lives in the [external-plugin retention decision](2026-08-30-retain-ignorable-external-session-events.md). An external informational event carrying the marker remains reloadable, while an unknown required event refuses resume. The unknown-type guard is read-side only: `appendCore` keeps rejecting retired legacy shapes but does not vocabulary-check new types, because an append-time refusal would stall a live session's durability mid-flight, which costs more than a loud refusal at the log's next load. The JSONL provider refuses a foreign version from the raw header line before validating this format version's header or decoding any event row, so a structurally different future format still reports the upgrade direction instead of "corrupt".
+What shipped in v0 (release 0812): direction-aware refusal with the raw-log path; the unknown-event guard against a generated known-vocabulary list (`KNOWN_SESSION_EVENT_TYPES`, emitted by `gen-persistence-catalog` from every `SessionEventMap` merge and kept fresh by `verify-persistence-catalog`); the `ignorable` envelope field accepted by seed validation, JSONL, and the BFF wire schema. V1 adds the static adjacent catalog, the identity v0-to-v1 edge, header-only descriptors, exact-generation JSONL publication, and current-only restoration described in [Released Session formats](2026-08-31-released-session-format-migrations.md). V2 keeps the physical codec neutral to ordinary event vocabulary and payload additions: the adjacent edge freezes its released source and target inventories, while equal-version restoration applies the installed known-event set and current payload semantics. First-party writers do not set `ignorable` through `Session.append`, while a repository-external plugin is a current consumer; equal-version retention lives in the [external-plugin retention decision](2026-08-30-retain-ignorable-external-session-events.md), and the stricter historical rule lives in the [alpha migration refusal decision](2026-08-31-alpha-historical-unknown-event-refusal.md). The unknown-type guard remains read-side because append-time vocabulary refusal would stall a live session's durability. JSONL classifies foreign versions from the minimal raw header before current-header or event parsing, so a structurally different future format reports the upgrade direction instead of "corrupt".
 
 ## Alternatives considered
 
 - **Major/minor versioning** — the "is it convertible" bit lives on each step's upgrader, and pre-committing it into a number shape invites wrong promises.
 - **Default-ignorable unknown events** — inverts the failure mode of a forgotten marker from visible over-refusal into silent corruption.
-- **Auto-migrating on view** — rewriting the artifact on open turns a read into a destructive write: a converter bug corrupts logs at browse time, and a same-directory older runtime loses access because a newer one merely looked.
+- **Migrating during header-only listing** — makes cheap inventory mutate storage and requires event bodies to compute facts that a header cannot prove. Listing returns descriptors; event-body reads own publication.
 - **Per-plugin runtime registration of known event types** — rejected because it would make the known set composition-dependent and register event names without classifying whether omission is safe. The persisted `ignorable` marker keeps that classification with each record; the [external-plugin retention decision](2026-08-30-retain-ignorable-external-session-events.md) owns the current consumer constraint.

+ 3 - 3
.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.zh.md

@@ -14,17 +14,17 @@ Session log 在发布后必须能升级格式,而最先发布的运行时决
 
 **升不升版本由写入方决定,与读取方能力无关。**当且仅当老运行时无法在语义上完全正确地处理新日志时才必须升版本。"解析不报错"不是标准:静默跳过影响重建的内容就是读错。只有结构性变更够得上这条线:header 形状、事件信封、核心事件语义、surface 机制(`SurfaceEventType` 集合、`SurfaceOp` 变体)。拿不准就升:近似恒等的升级器几乎没有成本,漏升一次会让老读取器静默读坏。
 
-**读取规则按方向区分。**版本相等:正常读。比读取器新:拒绝,说明方向("由更新的 harness 写入,请升级"),并给出原始日志文件的路径,用户仍能看到文本(`SessionFormatUnsupportedError`,与 `SessionPersistenceCorruptionError` 区分,因为数据没有损坏)。比读取器旧:查看时经 n→n+1 升级器链在内存中逐级转换;只有会话真正被继续时才把转换落盘(临时文件原子替换,原文件留备份)。写不出升级器的那一步留空,这会切断该步及更早所有版本的升级路径,它们降级为只能看原文
+**读取规则按方向区分。**版本相等:正常读。比读取器新:拒绝,说明方向("由更新的 harness 写入,请升级"),并给出原始日志文件的路径,用户仍能看到文本(`SessionFormatUnsupportedError`,与 `SessionPersistenceCorruptionError` 区分,因为数据没有损坏)。比读取器旧:每个事件正文操作先在内存中运行完整相邻链,保持源路径、字节与 inode 不变,只在规范具名版本文件下排他发布最终当前 generation,再在当前恢复前重新打开。仅 header 的列表保持不变更,并报告数值最高的规范 generation。catalog 生成与模块初始化会拒绝缺失的相邻步骤,因此已发布第一方 build 绝不会暴露不完整历史链。保留的低 generation 不是自动 fallback,也不构成 downgrade compatibility 承诺
 
 **逐事件的 `ignorable` 标记吸收词汇表增长,普通的新增事件永远不用升版本。**事件词汇表由挂载了哪些插件决定,单个版本整数描述不了它。读取器遇到不认识的事件类型时拒绝解读日志,除非该事件的信封带 `ignorable: true`。默认为必需:忘写标记的后果是把一个本可恢复的会话拒绝过头(体验问题),而默认可忽略会让同样的疏忽静默恢复出残缺会话(安全事故)。架构保证了这条规则成立:模型可见内容只经三种带 `surfaceOp` 标记的 surface 事件加 `request/header`、`request/context` 折叠进入重建,危险的未知事件恰好是那些不进 surface 但改变日志其余部分解读方式的事件(`session/end-seed` 是现存例子)。
 
 ## 影响
 
-v0(0812 发布)交付的内容:分方向的拒绝并带原始日志路径;基于生成的已知词汇清单(`KNOWN_SESSION_EVENT_TYPES`,由 `gen-persistence-catalog` 从所有 `SessionEventMap` 声明合并生成,`verify-persistence-catalog` 保证新鲜)的未知事件守卫;`ignorable` 信封字段被种子校验、JSONL 和 BFF 线上 schema 接受。升级器链本身推迟到第一个真实的 v0→v1 变更出现、有真实对象可测时再建。第一方写入方不通过 `Session.append` 设置 `ignorable`,但当前有一个仓库外插件依赖该字段;其保留条件与替代机制要求由[外部插件保留决策](2026-08-30-retain-ignorable-external-session-events.zh.md)定义。带该标记的外部信息性事件可以继续重新加载,未知必需事件则会拒绝恢复。未知类型守卫只在读取侧生效:`appendCore` 继续拒绝已淘汰的 legacy 形状,但不对新类型做词汇检查,因为写入时拒绝会让活跃会话的持久化中途停摆,代价大于下次加载时的显式拒绝。JSONL provider 会在校验本格式版本的 header、解码任何事件行之前,直接从原始 header 行拒绝外来版本,因此结构完全不同的未来格式会报告升级方向而不是"损坏"。
+v0(0812 发布)交付的内容:分方向的拒绝并带原始日志路径;基于生成的已知词汇清单(`KNOWN_SESSION_EVENT_TYPES`,由 `gen-persistence-catalog` 从所有 `SessionEventMap` 声明合并生成,`verify-persistence-catalog` 保证新鲜)的未知事件守卫;`ignorable` 信封字段被种子校验、JSONL 和 BFF 线上 schema 接受。V1 添加静态相邻 catalog、恒等 v0-to-v1 迁移边、仅 header descriptor、精确代际 JSONL 发布与[已发布 Session 格式](2026-08-31-released-session-format-migrations.zh.md)定义的当前专用恢复。V2 让物理 codec 对普通事件词汇与 payload 新增项保持中立:相邻迁移边冻结 released source 与 target 清单,同版本恢复则应用已安装的 known-event set 与当前 payload 语义。第一方 writer 不通过 `Session.append` 设置 `ignorable`,而一个仓库外插件仍依赖该字段;同版本保留由[外部插件保留决策](2026-08-30-retain-ignorable-external-session-events.zh.md)定义,更严格的历史规则由 [alpha 迁移拒绝决策](2026-08-31-alpha-historical-unknown-event-refusal.zh.md)定义。未知类型守卫仍只在读取侧生效,因为 append 时的词汇拒绝会中断活跃 Session 的持久化。JSONL 会在当前 header 或事件解析前从最小原始 header 分类外来版本,因此结构完全不同的未来格式会报告升级方向而不是"损坏"。
 
 ## 曾考虑的替代方案
 
 - **大小两级版本号**:能否转换这一位信息属于每一步的升级器,把它预先固化进编号形状会做出错误承诺。
 - **未知事件默认可忽略**:把忘写标记的后果从可见的过度拒绝反转成静默损坏。
-- **查看时自动迁移落盘**:打开即改写把读操作变成破坏性写操作,转换器的 bug 会在浏览时损坏日志,同目录的旧版本运行时也会因为新版本只是看了一眼就失去访问能力
+- **在仅 header 列表期间迁移**:让便宜清单改变存储,而且需要读取事件正文才能计算 header 无法证明的事实。列表返回 descriptor,事件正文读取负责发布
 - **插件运行时注册已知事件类型**:不予采用,因为该方案会让已知集依赖插件组合,而且只注册事件名称,无法判定省略事件是否安全。持久化的 `ignorable` 标记把该分类保留在每条记录中;[外部插件保留决策](2026-08-30-retain-ignorable-external-session-events.zh.md)定义当前消费方约束。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.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-19-projection-cache-per-session-files.md
-2026-08-19-projection-cache-per-session-files.md: c0ead55557e45905cfd368c70d826bf0c3eaade3
-2026-08-19-projection-cache-per-session-files.zh.md: e5582d14d68c1aed76ef8aab08a8d575e8a1842d
+2026-08-19-projection-cache-per-session-files.md: 0792ed6cac23d1ae09767f608c74f82430db68e9
+2026-08-19-projection-cache-per-session-files.zh.md: 201146ce8568b66eebf6793fcdc489929a3e4cdb

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.md

@@ -21,7 +21,7 @@ Reads and writes share ONE coherent state: every read (`cachedSnapshot`) is a sy
 - ACP, headless, SDK, and Web sessions publish cache rows for later consumers. The log-leading durability barrier may flush a covered prefix at the cache cadence and split otherwise coalesced physical JSONL runs; recorded profile snapshots re-pack the logical event stream so cache timing does not define fixture layout.
 - The per-record contract scopes failure: a malformed or stale-version document reads as an absent record at open, so one bad file never bricks the cache, and a checkpoint schema bump discards stale sessions per record instead of rejecting the whole domain.
 - The json backend bootstraps the per-record tree from the legacy whole-unit cache only when enumeration finds no new-layout document path, the legacy unit name matches, and its version is current or declared compatible. A version outside that accepted set remains untouched and the new domain opens empty; storage never relabels a version the domain owner did not approve. Any new document path, including an unreadable or stale file, suppresses the bootstrap for the whole unit; missing session rows refold from the log. The [cross-version read-compatibility decision](2026-09-02-projcache-cross-version-read-compat.md) owns the version policy.
-- The `session_projcache` domain uses version 6 and declares versions 3, 4, and 5 compatible. Vouched-for records retain their listing projections across upgrades, absent lineage fields normalize to an unseeded identity, and a seeded caller rejects that identity and refolds cold. A record that still fails schema validation is backed up and skipped; every subsequent write stamps version 6.
+- The `session_projcache` domain uses version 7 and declares versions 3 through 6 structurally compatible. Predecessor identities lack the Session `formatVersion`, so they cannot seed current projections and refold cold; a current checkpoint rewrites them with the complete identity. Once the format matches, absent lineage fields normalize to an unseeded identity, and a seeded caller rejects that identity and refolds cold. A record that still fails schema validation is backed up and skipped; every subsequent write stamps version 7.
 - The cache record is bound to the same log lifecycle as before: the stored `{createdAt, cwd, isSeeded, inheritedEventCount}` identity guards against a recreated id or a mismatched inherited prefix.
 
 ## Alternatives considered

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-19-projection-cache-per-session-files.zh.md

@@ -21,7 +21,7 @@ Status: implemented
 - ACP、headless、SDK 与 Web 会话都会发布缓存行,供后续消费方使用。确保日志领先的持久性屏障可能按缓存节奏 flush 已覆盖的前缀,并拆分原本会合并的物理 JSONL 行;各 profile 的录制快照会重新 pack 逻辑事件流,因此缓存时序不会决定 fixture 布局。
 - per-record 契约把故障范围缩小到单记录:畸形或过期版本的文档在打开时读作"无此记录",单个坏文件不会拖垮整个缓存;检查点 schema 升级按会话丢弃过期行,而不是拒绝整个域。
 - json 后端仅在枚举时没有发现任何新布局文档路径、旧单元名称匹配,且其版本为当前版本或已声明兼容版本时,才从旧整单元缓存引导 per-record 目录树。接受集合之外的版本保持不变,新域为空;存储绝不把域 owner 未批准的版本改标为当前版本。只要存在任意新文档路径,即使文件不可读或版本陈旧,也会对整个单元禁用引导;缺失的会话行从日志重折叠。[跨版本读兼容决策](2026-09-02-projcache-cross-version-read-compat.zh.md)是版本策略的权威说明。
-- `session_projcache` 域使用版本 6,并声明版本 3、4、5 兼容。经背书的记录在升级后保留列表投影;缺失的 lineage 字段归一化为 unseeded 身份,seeded 调用方会拒绝该身份并回落冷折叠。仍然通不过 schema 校验的记录会被备份并跳过;后续每次写入都使用版本 6
+- `session_projcache` 域使用版本 7,并声明版本 3 至 6 在结构上兼容。前代 identity 缺少 Session `formatVersion`,因此不能播种当前投影并会回落冷折叠;当前检查点会用完整 identity 重写它们。格式匹配后,缺失的 lineage 字段归一化为 unseeded 身份,seeded 调用方会拒绝该身份并回落冷折叠。仍然通不过 schema 校验的记录会被备份并跳过;后续每次写入都使用版本 7
 - 缓存记录仍绑定同一日志生命周期:存储的 `{createdAt, cwd, isSeeded, inheritedEventCount}` 身份防止被重建的 id 或不匹配的继承前缀误导。
 
 ## Alternatives considered

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.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-21-deepseek-llm-api-request-extensions.md
-2026-08-21-deepseek-llm-api-request-extensions.md: 018b93115f5376affd86a4da3c76f0f367ba9ed0
-2026-08-21-deepseek-llm-api-request-extensions.zh.md: 4bc0f0c992445c5897069b68efa47fdba46dfdb4
+2026-08-21-deepseek-llm-api-request-extensions.md: eadbe2a5f17de446c345120f9a6aeeeb531c43b4
+2026-08-21-deepseek-llm-api-request-extensions.zh.md: f45210adbc075c30484754ce7f8c2b65b515be6c

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md

@@ -73,7 +73,7 @@ The receiver would also need to traverse the tagged tree, resolve paths into the
 
 ### Why not omit assistant chunks or overlapping event data?
 
-About 98% of the measured real-session events were `assistant/chunk`. Omitting chunks after reference encoding reduced the complete identity JSON by another 84.79% for late enable and 6.49% for steady state, but it prevents lossless canonical-log reconstruction and leaves `assistant/message.sourceEventSeqs` pointing to absent events. Fuzzy or normalized substitutions have the same reconstruction defect.
+About 98% of the measured v1 real-session events were `assistant/chunk`. Omitting them after reference encoding reduced the complete identity JSON by another 84.79% for late enable and 6.49% for steady state, but prevented lossless reconstruction and left message provenance dangling. V2 embeds compact streams in attempt settlements; `dsh_session_log` still sends every current canonical event whole and does not omit those embedded records. Fuzzy or normalized substitutions have the same reconstruction defect.
 
 **Keep the upload cursor only in memory.** Rejected because a normal process restart would resend the entire Session. A canonical acceptance event makes restart recovery best-effort durable without another storage backend; the remaining crash window produces allowed duplicates.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md

@@ -73,7 +73,7 @@ Status: implemented
 
 ### 为什么不省略 assistant 分片或重叠事件数据?
 
-实测真实会话事件中约 98% 为 `assistant/chunk`。在引用编码后省略分片,会让完整未压缩 JSON 在延迟启用场景进一步减少 84.79%,在稳态场景进一步减少 6.49%,但这会阻止权威日志的无损重建,并让 `assistant/message.sourceEventSeqs` 指向缺失事件。模糊替换或规范化替换也存在同一重建缺陷。
+实测 v1 真实 Session event 中约 98% 为 `assistant/chunk`。在引用编码后省略它们,会让完整 identity JSON 在延迟启用场景进一步减少 84.79%,在稳态场景进一步减少 6.49%,但会阻止无损重建并让 message provenance 悬空。V2 把紧凑 stream 嵌入 attempt settlement;`dsh_session_log` 仍会完整发送每个当前规范 event,且不会省略这些嵌入式 record。模糊或规范化替换也有相同重建缺陷。
 
 **只在内存中保留上传游标。** 已否决,因为普通进程重启会重发完整会话。权威接受事件让重启恢复获得尽力而为的持久性,无需另一存储后端;剩余崩溃窗口只会产生允许的重复。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-25-rename-code-mode-to-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/architecture/2026-08-25-rename-code-mode-to-ptc.md
-2026-08-25-rename-code-mode-to-ptc.md: 618167516aefc54445d37cb1ce3939419e707bf5
-2026-08-25-rename-code-mode-to-ptc.zh.md: d6cf5cdea1154bd2b8cb424653b76315bb20b05d
+2026-08-25-rename-code-mode-to-ptc.md: 10c81fdca8acc67ab0e5e3b29c785ab4aec1d925
+2026-08-25-rename-code-mode-to-ptc.zh.md: 881ea86030ae09f198f12ac769d5ccd07159dba2

+ 3 - 3
.agents/notes/implemented/architecture/2026-08-25-rename-code-mode-to-ptc.md

@@ -21,7 +21,7 @@ Renamed in this PR:
 - prompt rule `tools:code-only` → `tools:ptc-only`
 - prose "Code Mode" → "PTC mode" / "PTC 模式" in docs, READMEs, and the eight implemented Agent Notes whose topic names the feature (those files were renamed in place)
 
-Deferred to the stacked persistence PR: the session-persistent vocabulary — the durable event types `tool/code-dispatch` / `tool/code-dispatch-start`, the logged plugin name `tools-code-mode`, and the sub-call id segment `:code:`. That PR is blocked until the `SESSION_FORMAT_VERSION` v0→v1 migration lands with it.
+The session-persistent vocabulary remains deferred: the durable event types `tool/code-dispatch` / `tool/code-dispatch-start`, the logged plugin name `tools-code-mode`, and the sub-call id segment `:code:`. Renaming those values is a structural Session-format change and requires its own adjacent edge after the identity v0-to-v1 foundation.
 
 Kept unchanged: `run_code` and its `code` parameter (they name the program payload, not the mode), `CodeSdkLanguage`, `CodeRunFailedError`, the `dsh-code-runtime*` package family, the third-party `codex-code-mode-host` binary name, and every frozen archived note.
 
@@ -30,8 +30,8 @@ Kept unchanged: `run_code` and its `code` parameter (they name the program paylo
 - **`ptc-mode` identifiers** — rejected: PTC is a tool-presentation transport, not a mode in the plan-mode sense, and the identifier should not claim that kinship.
 - **Surface-only rename** — rejected: the pre-release stance updates every reference together.
 - **Renaming `run_code` too** — rejected: the tool name describes running a program, not the mode, and is model-facing API surface.
-- **Renaming the durable event vocabulary in this PR** — rejected: renaming `tool/code-dispatch*` without a format bump would make pre-rename session logs unreadable; that rename belongs to the stacked persistence PR that lands together with the v0→v1 migration.
+- **Renaming the durable event vocabulary without an adjacent edge** — rejected: renaming `tool/code-dispatch*` in place would make pre-rename Session logs unreadable; that rename requires a later structural format version and explicit migration.
 
 ## Consequences
 
-Configs with `mode: code` and preset ids `code` are unsupported on this build. The session-persistent vocabulary still says `tool/code-dispatch*`, `tools-code-mode`, and `:code:`, so existing session logs load unchanged and no `SESSION_FORMAT_VERSION` bump is needed yet. The stacked persistence PR renames that vocabulary and is blocked until the v0→v1 migration lands with it (the version mechanics are in the [session-log versioning note](2026-08-10-session-log-version-mechanism.md)). Keyless snapshot refreshes carry this PR's vocabulary; the persistence PR refreshes the dispatch-bearing fixtures. The shipped decision this note renames is [the PTC foundation note](../feature/2026-06-15-ptc.md).
+Configs with `mode: code` and preset ids `code` are unsupported on this build. The session-persistent vocabulary still says `tool/code-dispatch*`, `tools-code-mode`, and `:code:`; the identity v0-to-v1 edge preserves those values, so no structural version change belongs to this rename. A later adjacent edge must rename that vocabulary and refresh the dispatch-bearing fixtures ([version mechanics](2026-08-10-session-log-version-mechanism.md)). The shipped decision this note renames is [the PTC foundation note](../feature/2026-06-15-ptc.md).

+ 3 - 3
.agents/notes/implemented/architecture/2026-08-25-rename-code-mode-to-ptc.zh.md

@@ -21,7 +21,7 @@ Status: implemented
 - 提示词规则 `tools:code-only` → `tools:ptc-only`
 - 文档、README 与八个以该功能命名的 implemented Agent Note 中的文案 "Code Mode" → "PTC mode"/"PTC 模式"(这些 Note 文件一并就地改名)
 
-延后到堆叠的持久化 PR:会话持久词汇——持久事件类型 `tool/code-dispatch`/`tool/code-dispatch-start`、日志中的插件名 `tools-code-mode`、子调用 id 段 `:code:`。该 PR 被阻塞,直到 `SESSION_FORMAT_VERSION` v0→v1 迁移与其一同落地
+会话持久词汇继续延后处理:持久事件类型 `tool/code-dispatch`/`tool/code-dispatch-start`、日志中的插件名 `tools-code-mode`、子调用 id 段 `:code:`。重命名这些值属于结构性 Session 格式变更,必须在恒等 v0-to-v1 基础之后拥有自己的相邻迁移边
 
 保持不变:`run_code` 及其 `code` 参数(它们描述程序载荷,而非模式)、`CodeSdkLanguage`、`CodeRunFailedError`、`dsh-code-runtime*` 包族、第三方二进制名 `codex-code-mode-host`,以及所有冻结的 archived Note。
 
@@ -30,8 +30,8 @@ Status: implemented
 - **使用 `ptc-mode` 标识符**——否决:PTC 是工具呈现传输层,不是 plan-mode 意义上的模式,标识符不应宣示这种亲缘关系。
 - **仅重命名表面**——否决:预发布立场要求一次性更新所有引用。
 - **连 `run_code` 一起改名**——否决:该工具名描述的是运行程序,不是模式,而且是对模型可见的 API 表面。
-- **在本 PR 中一并重命名持久事件词汇**——否决:在没有格式版本提升的情况下重命名 `tool/code-dispatch*` 会让更名前的会话日志无法读取;该重命名属于与 v0→v1 迁移一同落地的堆叠持久化 PR
+- **不提供相邻迁移边就重命名持久事件词汇**——否决:就地重命名 `tool/code-dispatch*` 会让更名前的 Session 日志无法读取;该重命名需要后续结构格式版本与显式迁移
 
 ## 后果
 
-配置中写 `mode: code`、预设 id 为 `code`,在本构建上不再受支持。会话持久词汇仍为 `tool/code-dispatch*`、`tools-code-mode` 与 `:code:`,因此既有会话日志照常读取,无需 `SESSION_FORMAT_VERSION` 提升。堆叠的持久化 PR 负责重命名该词汇,并被阻塞到 v0→v1 迁移与其一同落地(版本机制见 [Session log 版本 Note](2026-08-10-session-log-version-mechanism.zh.md))。无密钥的 snapshot refresh 携带本 PR 的词汇;持久化 PR 刷新包含分发的夹具。本 Note 所更名的已发布决策是 [PTC 基础 Note](../feature/2026-06-15-ptc.zh.md)。
+配置中写 `mode: code`、预设 id 为 `code`,在本构建上不再受支持。会话持久词汇仍为 `tool/code-dispatch*`、`tools-code-mode` 与 `:code:`;恒等 v0-to-v1 迁移边会保留这些值,因此本次更名不包含结构版本变更。后续相邻迁移边必须重命名该词汇并刷新包含分发的 fixture(参见[版本机制](2026-08-10-session-log-version-mechanism.zh.md))。本 Note 所更名的已发布决策是 [PTC 基础 Note](../feature/2026-06-15-ptc.zh.md)。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.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-27-handle-based-session-persistence.md
-2026-08-27-handle-based-session-persistence.md: ced7a78178d3036fd5fa9a09ca49f8c751c3a169
-2026-08-27-handle-based-session-persistence.zh.md: e13b5b0e9e7286411b6d38f3b9dc0e86fa742a3d
+2026-08-27-handle-based-session-persistence.md: d3fbbe8137c8ac4a1b6897914db26c010036faa8
+2026-08-27-handle-based-session-persistence.zh.md: f8846f4b54cd6b8bb654cb29bbf49d0ee99ccfa8

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.md

@@ -18,7 +18,7 @@ The previous persistence seam owned far more than storage. A shared coordinator
 
 **Visibility and freshness are explicit.** A created session is observable in-process from `create`; physical materialization may be deferred (a pure optimization) until the first append or flush, other processes see only materialized sessions, and a crash before materialization means the session never existed. Once an append or flush resolves, reads started afterwards on the same backend instance observe at least that prefix — the guarantee `message-feedback`'s durable-target check rides on.
 
-**Revision simplifies to a per-instance change token.** Equal tokens may be treated as an unchanged log; ownership churn never changes one. JSONL derives a best-effort token and `sizeBytes` from one `fs.stat`; a backend whose medium can count events cheaply may supply the `eventCount` hint instead. The session-list cold blank probe returns on this metadata (`coldBlankProbeMaxEvents`/`coldBlankProbeMaxBytes`), restoring the capability removed with the path query.
+**Revision simplifies to a per-instance change token.** Equal tokens may be treated as an unchanged log; ownership churn never changes one. JSONL derives a best-effort token and `sizeBytes` from one `fs.stat`; a backend whose medium can count events cheaply may supply the `eventCount` hint instead. Session listing remains a zero-body-read metadata/cache operation: it trusts a cached nonblank projection, keeps a cached blank value only when the current cache identity matches, and otherwise reports `blank: false` as the visible unknown fallback. No cold blank probe or probe-limit configuration remains.
 
 ## Alternatives considered
 
@@ -37,6 +37,6 @@ Resume, fork, subagent, ACP, webhook, and SDK sessions all persist through one e
 ## Related
 
 - [Session persistence as an abstract service](2026-06-14-session-persistence.md) — the seam this reshapes; its interface list reflects the handle API.
-- [Persistence export() and pre-release trims](../simplification/2026-08-27-persistence-export-and-pre-release-trims.md) — the preparatory removals, including the blank probe this note's metadata restores.
+- [Persistence export() and pre-release trims](../simplification/2026-08-27-persistence-export-and-pre-release-trims.md) — the preparatory removals, including the retired cold blank probe.
 - [Retain ignorable external session events](2026-08-30-retain-ignorable-external-session-events.md) — the read-side refusal contract, now shared through `storage-contract` helpers.
 - [Bounded session-persistence write batching](2026-08-08-bounded-session-persistence-write-batching.md) — the batching semantics the routed write path preserves as internal scheduling policy.

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.zh.md

@@ -18,7 +18,7 @@ Status: implemented
 
 **可见性与新鲜度是显式的。**已创建的会话自 `create` 起即可在进程内被观察到;物理实体化(纯粹的优化)可以推迟到第一次 append 或 flush,其他进程只能看到已实体化的会话,实体化之前崩溃意味着该会话从未存在。一旦某次 append 或 flush 完成,其后在同一后端实例上开始的读取至少能观察到该前缀——这正是 `message-feedback` 持久目标检查所依赖的保证。
 
-**revision 简化为逐实例变更令牌。**令牌相等可视为日志未变;所有权变动绝不会改变令牌。JSONL 通过一次 `fs.stat` 派生尽力而为的令牌与 `sizeBytes`;存储介质能够廉价统计事件数的后端可以改为提供 `eventCount` 提示。会话列表的冷空白探测回归到这些元数据之上(`coldBlankProbeMaxEvents`/`coldBlankProbeMaxBytes`),恢复了随路径查询一起移除的能力
+**revision 简化为逐实例变更令牌。**令牌相等可视为日志未变;所有权变动绝不会改变令牌。JSONL 通过一次 `fs.stat` 派生尽力而为的令牌与 `sizeBytes`;存储介质能够廉价统计事件数的后端可以改为提供 `eventCount` 提示。Session listing 保持零正文读取的 metadata/cache 操作:它信任缓存的非空 projection,只在当前 cache identity 匹配时保留缓存的空值,否则用 `blank: false` 表示可见的未知回退。冷空白探测及其探测限制配置均不再存在
 
 ## 考虑过的替代方案
 
@@ -37,6 +37,6 @@ Status: implemented
 ## 相关
 
 - [作为抽象服务的会话持久化](2026-06-14-session-persistence.zh.md)——本 Note 重塑的 seam;其接口列表已反映句柄 API。
-- [持久化 export() 与预发布读取路径精简](../simplification/2026-08-27-persistence-export-and-pre-release-trims.zh.md)——预备性的移除,包括本 Note 的元数据所恢复的空白探测。
+- [持久化 export() 与预发布读取路径精简](../simplification/2026-08-27-persistence-export-and-pre-release-trims.zh.md)——预备性的移除,包括已退役的冷空白探测。
 - [保留可忽略的外部会话事件](2026-08-30-retain-ignorable-external-session-events.zh.md)——读取侧的拒绝约定,现经由 `storage-contract` 辅助函数共享。
 - [为会话持久化写入批处理设定上界](2026-08-08-bounded-session-persistence-write-batching.zh.md)——被路由写路径作为内部调度策略保留的批处理语义。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.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-30-retain-ignorable-external-session-events.md
-2026-08-30-retain-ignorable-external-session-events.md: e79713cf16c627706939f33f6748eaa4821de870
-2026-08-30-retain-ignorable-external-session-events.zh.md: 8fddbfdd7e60716b0a78ef6b33de09935e9bd4d7
+2026-08-30-retain-ignorable-external-session-events.md: 1fe3a6d99a16daa6ad88f6717baa18f18e8c7355
+2026-08-30-retain-ignorable-external-session-events.zh.md: 632b7b418252c2b299162f3e00a4d41169169509

+ 2 - 0
.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.md

@@ -16,6 +16,8 @@ The canonical `SessionEvent` envelope retains `ignorable?: true`, and every repr
 
 The field is removable only after a replacement supports the current third-party plugin across event production, persistence, reload, and transport, with an explicit cutover for sessions already containing the marker. The [session log versioning decision](2026-08-10-session-log-version-mechanism.md) continues to own the default-required safety rule and format-version policy.
 
+Historical format migration is deliberately stricter in the alpha implementation. The v0-to-v1 edge refuses every unknown v0 type, including an ignorable one, because an opaque payload may contain references that a format edge cannot validate. The [alpha historical-event decision](2026-08-31-alpha-historical-unknown-event-refusal.md) owns that bounded exception; equal-version append and reload continue to follow this note.
+
 ## Alternatives considered
 
 **Require every unknown event on read.** Rejected because the current third-party plugin emits an informational event outside the repository-generated vocabulary. A first-party reload would reject that session even though omitting the event is safe.

+ 2 - 0
.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.zh.md

@@ -16,6 +16,8 @@ Status: implemented
 
 只有替代机制在事件生产、持久化、重新加载与传输中都支持当前第三方插件,并为已包含该标记的会话提供显式切换方案后,才能删除此字段。[Session log 版本决策](2026-08-10-session-log-version-mechanism.zh.md)继续定义默认读取必需的安全规则与格式版本策略。
 
+Alpha 实现中的历史格式迁移有意更严格。v0-to-v1 迁移边会拒绝每个未知 v0 类型,包括 ignorable 类型,因为不透明 payload 可能包含格式迁移边无法校验的引用。[Alpha 历史事件决策](2026-08-31-alpha-historical-unknown-event-refusal.zh.md)定义该有限例外;同版本 append 与 reload 继续遵循本记录。
+
 ## 曾考虑的替代方案
 
 **要求读取所有未知事件。** 不予采用,因为当前第三方插件会发出仓库生成词汇之外的信息性事件。即使省略该事件是安全的,第一方重新加载仍会拒绝该会话。

+ 6 - 0
.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.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-alpha-historical-unknown-event-refusal.md
+2026-08-31-alpha-historical-unknown-event-refusal.md: 58690e30281c1f5e10f85726c1f1e50fd4664fe9
+2026-08-31-alpha-historical-unknown-event-refusal.zh.md: 73ab2ca47ab3f68b11e71ffec09287253215aa53

+ 37 - 0
.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.md

@@ -0,0 +1,37 @@
+# Agent Note: Alpha Session migration refuses every unknown historical event
+
+Status: implemented
+
+English | [中文](2026-08-31-alpha-historical-unknown-event-refusal.zh.md)
+
+## Problem
+
+Equal-version Session reading can safely skip an unknown event only when its producer marked the envelope `ignorable: true`. A cardinality-preserving migration has a stricter obligation: it must prove that every preserved payload remains semantically valid in the target generation. An unknown JSON payload may contain Session sequence numbers, lifecycle facts, or model-visible state that compile-time brands cannot discover.
+
+Silently copying such an event can leave stale numeric references after a later edge changes event positions. Silently omitting it loses durable data. Retaining the exact immutable v0 generation does not make either transformed v1 result lossless.
+
+## Decision
+
+The alpha v0-to-v1 edge owns a frozen complete released-v0 event and payload inventory. It refuses every unknown historical event type before target staging, including an event marked `ignorable: true`, and refuses unexpected members of known payloads except fields explicitly classified as owner-opaque JSON. Merge-extensible nested discriminants remain part of that explicit policy: unknown content-block types, message-source kinds, assistant finish-reason kinds, and turn-ending reason kinds are preserved as owner-opaque JSON, while known arms receive structural validation. The diagnostic names the event type, its sequence number, and the unchanged source generation.
+
+The rule applies only while crossing a historical format edge. Ordinary current-format reading retains the established envelope behavior: an unknown required event refuses, while an unknown event carrying `ignorable: true` remains readable. New v1 external events therefore keep the existing equal-version extension seam, but they do not become implicitly migratable by a future format edge.
+
+Every first-party source event type has an executable disposition and target validator in the edge package. The catalog is build-static and profile-independent, so mounting or omitting the producer plugin cannot change whether an old artifact migrates.
+
+## Consequences
+
+Some v0 Sessions produced by repository-external informational plugins may refuse alpha migration even though the v0 codec can decode them. Refusal publishes no successor, so the suffixless v0 path, bytes, and inode remain authoritative and unchanged. Operators can identify the blocking type from the diagnostic and retain full access to its raw text.
+
+Community feedback will determine the next policy. A later release may add an explicit external-owner migration interface, permit omission of explicitly ignorable historical events while retaining the exact source generation, or keep strict refusal. No option is implied by the alpha marker.
+
+`SessionSeq` and `SessionLogOffset` make known first-party numeric fields auditable, but they cannot classify numbers inside an unknown runtime object. The migration rule therefore cannot infer safety from the absence of a recognized branded field.
+
+This note supersedes [Retain ignorable external Session events](2026-08-30-retain-ignorable-external-session-events.md) only for historical format migration. That decision remains current for equal-version append and reload.
+
+## Alternatives considered
+
+- **Copy unknown ignorable events verbatim** — preserves bytes but cannot prove that opaque numeric or lifecycle facts remain valid after structural edges.
+- **Drop unknown ignorable events** — keeps migration available but is not lossless and makes the marker authorize data deletion.
+- **Search unknown JSON for number-like field names** — heuristics cannot establish semantic identity and create false confidence.
+- **Dynamically ask mounted plugins** — makes migration availability depend on one deployment composition and fails before an absent producer can mount.
+- **Refuse only when the first structural edge ships** — would let v1 contain historical values whose safe interpretation was never established; the identity rehearsal is the point where the policy must become executable.

+ 37 - 0
.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.zh.md

@@ -0,0 +1,37 @@
+# Agent Note: Alpha Session 迁移拒绝所有未知历史事件
+
+Status: implemented
+
+[English](2026-08-31-alpha-historical-unknown-event-refusal.md) | 中文
+
+## 问题
+
+同版本 Session 读取只有在 producer 把信封标记为 `ignorable: true` 时,才可以安全跳过未知事件。保持基数的迁移有更严格义务:它必须证明每个保留 payload 在目标代际中仍具备正确语义。未知 JSON payload 可能包含编译期品牌无法发现的 Session 序号、生命周期事实或模型可见状态。
+
+静默复制这类事件,会在后续迁移边改变事件位置时留下陈旧数字引用。静默省略它会丢失持久数据。保留精确且不可变的 v0 generation 不能让任何一种转换后的 v1 结果变得无损。
+
+## 决策
+
+Alpha v0-to-v1 迁移边拥有冻结且完整的已发布 v0 事件与 payload 清单。它在目标 staging 前拒绝每个未知历史事件类型,包括标记了 `ignorable: true` 的事件;除明确分类为 owner 不透明 JSON 的字段外,它也拒绝已知 payload 的意外成员。可合并扩展的嵌套判别字段同样属于这项显式策略:未知 content-block type、message-source kind、assistant finish-reason kind 与 turn-ending reason kind 会作为 owner 不透明 JSON 保留,已知分支则接受结构校验。诊断会点名事件类型、序号和保持不变的源 generation。
+
+该规则只适用于跨越历史格式迁移边。普通当前格式读取保留既有信封行为:未知必需事件被拒绝,带 `ignorable: true` 的未知事件仍可读取。因此新的 v1 外部事件继续使用既有同版本扩展 seam,但不会自动获得未来格式迁移能力。
+
+每个第一方源事件类型都在迁移边包中拥有可执行 disposition 与目标 validator。catalog 在构建时静态确定且与 profile 无关,因此 producer 插件是否挂载不会改变旧产物能否迁移。
+
+## 后果
+
+某些由仓库外信息型插件产生的 v0 Session 可能拒绝 alpha 迁移,即使 v0 codec 能解码它们。拒绝不会发布后继,因此无后缀 v0 路径、字节与 inode 仍然权威且不变。操作者可以从诊断识别阻塞类型,并完整访问其原始文本。
+
+社区反馈将决定下一步策略。后续版本可以添加显式外部 owner 迁移接口、在保留精确源代际时允许省略明确 ignorable 的历史事件,或继续严格拒绝。Alpha 标记不预先承诺任何选项。
+
+`SessionSeq` 与 `SessionLogOffset` 让已知第一方数字字段可审计,但无法分类未知 runtime 对象中的数字。因此迁移规则不能根据没有识别到品牌字段来推断安全。
+
+本记录仅在历史格式迁移方面取代 [保留可忽略外部 Session 事件](2026-08-30-retain-ignorable-external-session-events.zh.md)。原决定对同版本 append 与 reload 仍然有效。
+
+## 考虑过的替代方案
+
+- **逐字复制未知 ignorable 事件**——保留字节,但不能证明不透明数字或生命周期事实在结构迁移后仍有效。
+- **丢弃未知 ignorable 事件**——让迁移保持可用,但不再无损,并让该标记授权删除数据。
+- **在未知 JSON 中搜索类似数字字段的名称**——启发式无法建立语义身份,还会制造虚假信心。
+- **动态询问已挂载插件**——让迁移可用性取决于某个部署组合,并在缺席 producer 能挂载前失败。
+- **等到第一条结构迁移边再拒绝**——会让 v1 包含从未建立安全解释的历史值;恒等演练正是把策略变成可执行规则的时点。

+ 3 - 3
.agents/notes/implemented/architecture/2026-07-26-packed-chunk-rows-by-default.i18n.yaml → .agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.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/architecture/2026-07-26-packed-chunk-rows-by-default.md
-2026-07-26-packed-chunk-rows-by-default.md: bd4b3b9f773afbf6aa7e88d51b6e842d6634c222
-2026-07-26-packed-chunk-rows-by-default.zh.md: eafe6632150aadd74395f4d0f09d064fb703a03d
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.md
+2026-08-31-live-assistant-stream-frames.md: 25d86d152714baeca1df18c353f40bc0052bdeee
+2026-08-31-live-assistant-stream-frames.zh.md: 924649932b29520ad3effa88f18267a3ae20ac02

+ 24 - 0
.agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.md

@@ -0,0 +1,24 @@
+# Agent Note: Live assistant stream frames remain separate from the session log
+
+Status: implemented
+
+English | [中文](2026-08-31-live-assistant-stream-frames.zh.md)
+
+## Problem
+
+The v2 session log keeps one `assistant/message` or `assistant/attempt` settlement with the complete compact timed stream, so replay, cold reads, telemetry, and request reconstruction observe one durable history. A live consumer also needs prompt frame-by-frame presentation while a request runs. Treating a transient presentation update as another durable event would restore token-level event cardinality and make a process-lifetime concern survive restart.
+
+## Decision
+
+`dsh-agent-loop` emits scoped `agent/assistant-stream` frames for each model attempt. `start`, `chunk`, and `end` carry a branded `LlmAttemptId` unique within one Agent lifecycle; every frame advances one revision local to that lifecycle. The start frame names the attempt's turn and step, chunk indexes are dense from zero, chunk timestamps are reused by the compact stream, and `end.index` equals the next chunk position. Stream acquisition and its final cancellation check occur before `start`; a failure there emits no frame. Every started attempt emits one terminal end: the loop appends the final `assistant/message` or `assistant/attempt` before a committed end names that event and seq, while assembly or settlement failure emits an abandoned end without a durable target. Authenticated Session-follow accepts an explicit Web opt-in, opens with a cached active-attempt compact baseline, and carries durable events and cursorless frames in one FIFO. Each follower captures a local arrival ordinal with the opening baseline and drops buffered frames at or before that cut; frame revisions can restart at one with a replacement Agent, so they do not define the opening cut. A settlement arriving after an active opening is owned by that attempt only when its seq follows `startedAfterSeq` and its Turn and Step match; it remains staged until the matching end index, type, and seq arrive, while an earlier retry at the same Turn and Step remains visible. Revision, dense-index, or settlement gaps for a known attempt reopen follow and replace the baseline; unknown-attempt frames fall back to the durable settlement. The TypeScript and Python SDK protocols do not expose these frames. Durable settlements remain the source of replay and model history; the [v2 stream decision](2026-09-01-v2-embedded-assistant-streams.md) owns their representation.
+
+## Alternatives considered
+
+- **Keep only the live stream** — rejected because cold reads, replay, telemetry, usage accounting, and failed-attempt diagnostics require the durable embedded stream.
+- **Persist each live frame as its own event** — rejected because process-local attempt ids, revisions, and reconnect presentation do not survive restart or affect model reconstruction; one settlement owns the durable stream.
+- **Use an unbranded request string as the attempt key** — rejected because consumers need an opaque identity that cannot be confused with provider request IDs or durable Session IDs.
+- **Let UI Chat subscribe to a second live source** — rejected because the Session object owns stream reconciliation and UI Conversation is the sole event-source subscriber; a second source would make settlement order target-dependent.
+
+## Consequences
+
+The Web client renders in-memory chunks before the attempt settles while retaining one durable v2 history. A process restart has no active Assistant frames; reconnect can restore only the baseline held by the current process, while cold replay expands durable settlements. Cursorless notifications never advance the journal cursor, and notifications observed during durable gap repair wait for the replacement page. That page has no Assistant baseline, so the Client clears transient attempts and lets the held notification reopen follow once for a paired page and baseline. The frame declaration remains agent-scoped, so a listener observes only its owning Agent unless it explicitly registers globally.

+ 24 - 0
.agents/notes/implemented/architecture/2026-08-31-live-assistant-stream-frames.zh.md

@@ -0,0 +1,24 @@
+# Agent Note: 实时 assistant 流帧与 Session log 保持分离
+
+Status: implemented
+
+[English](2026-08-31-live-assistant-stream-frames.md) | 中文
+
+## 问题
+
+v2 Session log 通过一个 `assistant/message` 或 `assistant/attempt` settlement 保留完整紧凑带时间 stream,因此 replay、冷读、遥测与请求重建都能观察同一份持久历史。实时消费方还需要在请求运行时逐帧呈现。把瞬态呈现 update 当作另一种持久事件,会恢复 token 粒度事件基数,并让只属于进程生命周期的事实跨重启保留。
+
+## 决定
+
+`dsh-agent-loop` 为每次模型 attempt 发出作用域内的 `agent/assistant-stream` frame。`start`、`chunk` 和 `end` 带有在单个 Agent lifecycle 内唯一的 branded `LlmAttemptId`;每个 frame 都会推进一次该 lifecycle 本地 revision。start frame 给出该 attempt 的 turn 与 step,chunk index 从零开始密集递增,chunk 时间戳会被紧凑 stream 复用,`end.index` 等于下一个 chunk 位置。loop 会先取得 stream 并执行最终取消检查,再发出 `start`;这些步骤失败时不发出任何 frame。每个已开始 attempt 都会发出一个终态 end:loop 会在 committed end 命名事件与 seq 前追加最终 `assistant/message` 或 `assistant/attempt`,而 assembly 或 settlement failure 会发出不命名持久目标的 abandoned end。已认证 Session-follow 接受显式 Web opt-in,以缓存的活跃 attempt 紧凑 baseline 打开,并在一个 FIFO 中携带持久事件和无 cursor frame。每个 follower 会随 opening baseline 捕获本地到达序号,并丢弃该 cut 及之前的 buffered frame;replacement Agent 的 frame revision 可以从一重新开始,因此 revision 不定义 opening cut。活跃 opening 之后到达的 settlement 只有在其 seq 晚于 `startedAfterSeq` 且 Turn 与 Step 匹配时才属于该 attempt;它会保持暂存,直到匹配的 end index、type 与 seq 到达,而同一 Turn 和 Step 中更早的 retry 仍保持可见。已知 attempt 的 revision、密集 index 或 settlement 缺口会重新打开 follow 并替换 baseline;unknown-attempt frame 回退到持久 settlement。TypeScript 和 Python SDK 协议不公开这些 frame。持久 settlement 仍是 replay 与模型历史的真源;其表示由 [v2 stream 决策](2026-09-01-v2-embedded-assistant-streams.zh.md)负责。
+
+## 曾考虑的替代方案
+
+- **只保留 live stream**:不采用,因为冷读、replay、遥测、usage 记账与失败 attempt 诊断需要持久嵌入式 stream。
+- **把每个 live frame 作为独立事件持久化**:不采用,因为进程本地 attempt id、revision 与重连呈现不会跨重启保留或影响模型重建;一个 settlement 拥有持久 stream。
+- **用未加品牌的请求字符串作为尝试键**:不采用,因为消费方需要一个不透明身份,不能把它与 provider request ID 或持久 Session ID 混淆。
+- **让 UI Chat 订阅第二个实时 source**:不采用,因为 Session 对象拥有 stream 对账,UI Conversation 是唯一的 event-source 订阅方;第二个 source 会使结算顺序依赖 target。
+
+## 影响
+
+Web client 可以在 attempt settlement 前渲染内存 chunk,同时保留一份持久 v2 历史。进程重启后没有活跃 Assistant frame;重连只能恢复当前进程持有的 baseline,冷 replay 则展开持久 settlement。无 cursor 通知绝不推进 journal cursor,在持久缺口修复期间观察到的通知会等待 replacement page。该 page 不携带 Assistant baseline,因此 Client 会清空瞬态 attempt,并让 held notification 重新打开 follow 一次,以取得配对的 page 与 baseline。frame 声明保持 agent 作用域,因此监听器只观察所属 Agent,除非它显式全局注册。

+ 6 - 0
.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.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-released-session-format-migrations.md
+2026-08-31-released-session-format-migrations.md: 2c75d0b57a0b513c218b6a67b8c2b31c7cae4d0f
+2026-08-31-released-session-format-migrations.zh.md: d88c643cbfaf7f3d4f52ca6e5fa244a26917eb99

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

@@ -0,0 +1,57 @@
+# Agent Note: Released Session formats migrate on body read through adjacent pure edges
+
+Status: implemented
+
+English | [中文](2026-08-31-released-session-format-migrations.zh.md)
+
+## Problem
+
+Session format v0 shipped in an alpha release, so a structural writer change can no longer treat existing JSONL as disposable pre-release state. Stored event bodies reach consumers through read or write `SessionHandle` instances used by resume, query, export, fork, and continuation paths. Migrating only one consumer would let callers observe different logical generations or fail only when a later writer reaches the old file.
+
+Migration must retain the exact source path, bytes, and inode, including a torn physical tail, while giving every published format one unambiguous canonical filename. Plain JSONL and Zstandard are encoding choices for the same logical format and must not create parallel migration implementations.
+
+## Decision
+
+`SESSION_FORMAT_VERSION` is a monotonic current-writer integer. One profile-independent pure package owns each adjacent `vN -> vN+1` conversion. `@deepseek-ai/dsh-session-format` supplies only lossless snapshots, unique gap-free planning, header-only conversion, and whole-artifact composition; `@deepseek-ai/dsh-session-format-catalog` statically imports the complete chain independently of mounted Cordis plugins. Historical codecs and normalizers live in the named edge package, while current Session and persistence code accept only the latest logical types.
+
+Each edge freezes strict source and target semantics, while its target physical codec remains vocabulary-neutral so ordinary event growth can stay within one format version. The catalog restores the final generation through the installed peer `@deepseek-ai/dsh-session` and its current `KNOWN_SESSION_EVENT_TYPES`, preventing a frozen historical edge from becoming the current vocabulary owner.
+
+The JSONL provider completes ensure-current work before `open` returns a handle for a stored Session. It selects the highest canonical generation, migrates a supported historical body, and decodes the current result from one physical snapshot; the public `SessionPersistence` and `SessionHandle` interfaces contain no migration operations. Header-only `stat` and `list` rescan Session directories, translate supported historical headers in memory, and never publish a successor. `create` checks canonical filenames independently of header readability, so every existing generation reserves its Session id.
+
+Cancellation belongs to the `open`, `stat`, or `list` call that supplied it. Discovery, stable reads, decoding, and pre-publication checks observe that signal; once an immutable successor is published and its directory entry is synced, later cancellation does not delete the committed generation.
+
+The configured JSONL encoding owns one full suffix, `.jsonl` or `.jsonl.zstd`. Migration reads a stable exact source, decodes the recoverable logical prefix, composes every required edge in memory, validates and syncs a same-directory temporary stage for only the final target, rechecks the source fingerprint, publishes that previously absent target without overwrite, syncs the namespace, and reopens it through current validation before returning a handle. The source never moves or changes; only disposable temporary stages may be moved, linked, or removed. Migration does not synthesize interrupted-turn events: agent-loop appends those repairs through the write handle, while read-only query paths balance them in memory.
+
+Canonical filenames encode the physical format generation: v0 is `session.jsonl` or `session.jsonl.zstd`; every positive generation is lowercase `session.vN.jsonl` or `session.vN.jsonl.zstd`. `dsh-session-format` owns the raw basename rule (`sessionFormatLogFilename`, `parseSessionFormatLogFilename`); the JSONL provider, the session-log export archive, and recorded-session fixtures append only the compression suffix. Publication never renames, replaces, or deletes a committed generation path. If the target already exists, it is accepted only as a regular current-format file with exactly the expected bytes; any other target refuses. Lower generations remain for operator inspection or explicit copying, but normal runtime operations select the numerically highest canonical name and never use retained predecessors as automatic fallback, restore, or downgrade support.
+
+The current-format fast path classifies the header from one stable source snapshot, invokes no historical converter or generation write, and passes that snapshot to current decoding without another file read. The decoded log enters the existing bounded revision-keyed memo for an immediate observe-to-resume handoff, while `stat` and `list` deliberately rescan. Multiple edges leave the original generation unchanged and publish only the final target; intermediate versions exist only in memory. A source fingerprint recheck restarts migration when content changes, and exclusive target publication accepts a racing winner only when its bytes match exactly. Cross-process append fencing remains outside this guarantee.
+
+The first edge, `@deepseek-ai/dsh-session-format-v0-to-v1`, is intentionally identity-shaped: aside from the version and bounded historical normalizations already accepted by v0, it preserves logical headers, events, sequence numbers, references, timestamps, payloads, and the configured compression choice. The exact `session.jsonl[.zstd]` source remains byte- and inode-identical, while the current writer encodes the new `session.v1.jsonl[.zstd]` successor. This exercises the complete publication lifecycle before a cardinality-changing format needs it.
+
+Projection-cache records bind their fold to the Session header's `formatVersion`. The `session_projcache` v7 reader may load predecessor domain records structurally, but a record without the format generation cannot seed a current Session; the authoritative log refolds it and the next checkpoint writes the complete current identity. This prevents a cache row produced before a bounded normalizer or cardinality-changing edge from bypassing that migration.
+
+## Consequences
+
+Reading event bodies with a newer build may durably add a higher generation. The exact old generation remains available, but the runtime thereafter selects the highest canonical filename; retention does not promise that an older build can safely downgrade or that the newer build will fall back when the successor is corrupt. A read-only filesystem reports an actionable migration failure instead of returning an in-memory current view that differs from disk.
+
+JSONL publication uses POSIX hard-link creation plus directory sync, and Windows uses no-overwrite `MoveFileExW` with write-through. A competing writer that wins target creation is accepted only when the committed bytes exactly match. One process-local writer per Session is the supported concurrency model. A future per-Session cross-process lock can close the remaining source-check-to-publication race without changing the format edge interface.
+
+Retained generations are not a live-stream write-ahead log. A future optional WAL sidecar may preserve unfinished assistant streams across a hard crash. Explicit generation inspection or copying, retention tooling, compression conversion, and streamed whole-artifact transformation are separate features; automatic fallback and downgrade compatibility are not implied future work.
+
+This note supersedes the continue-only persistence rule and the deferred-chain status in [Session log versioning](2026-08-10-session-log-version-mechanism.md). That note remains the authority for when to bump the version and for ordinary equal-version `ignorable` event behavior.
+
+## Verification
+
+Release verification runs the committed Session-format corpus gate over every versioned persisted-or-projected `session*.jsonl` fixture under `snapshots/`, `packages/`, and `scripts/snapshots/python-sdk-single-exe/`. Fixture-only omitted envelopes and request-header tokens are materialized before the real static catalog; every fixture reaches the current v1 view through current restoration or historical migration. Released-v0 replay inputs remain suffixless, while fresh v1 writer outputs use `session.v1.jsonl` for a parent and `session.<ordinal>.v1.jsonl` for children. Record and refresh preserve every completed generation, including generations of a child role absent from a later run. Malformed historical fixtures are repaired at their source rather than admitted through path-dependent replay policy. The continuing gate discovers the corpus dynamically and fails every restoration refusal; separate assembled JSONL tests own exact physical-byte migration.
+
+Handle-integration verification runs the pure format, catalog, persistence-seam, and JSONL provider suites together: 420 tests cover both encodings, immutable publication races, header-only observation, read and write handles, migration refusal, append after migration, cancellation, and crash-tail behavior with per-file 100% statement, branch, function, and line coverage. Repository typecheck and lint, 113 keyless recorded-session replays with two declared skips, and 28 owner-local expected-output cases also pass on the merged master checkpoint.
+
+The assembled headless profile test stages `session.jsonl`, resumes it through the shipped composition, observes v1 before Session construction, verifies that the exact v0 bytes and inode remain while `session.v1.jsonl` appears, and proves the next append targets v1. JSONL contract tests exercise raw and Zstandard exclusive publication, torn-tail preservation, source changes, target collisions, future-highest refusal, revision-keyed parsed-log reuse, listing rescans, temporary cleanup, committed reopen, and current-format bypass.
+
+## Alternatives considered
+
+- **Migrate only on continuation** — leaves query, export, fork, and suffix consumers on old generations and duplicates restoration policy.
+- **Return a migrated in-memory view without persisting** — lets one process observe state that does not match the highest committed generation and postpones failure until a later writer.
+- **Persist every intermediate version** — consumes space and creates recovery states with no runtime consumer; only the source and final generation are durable.
+- **Let mounted event-owner plugins register migrations** — makes historical readability deployment-dependent; the static catalog must work before feature plugins mount.
+- **Reuse one filename for every current format and relocate its predecessor** — rejected because migration would move or overwrite committed evidence, require collision and retention rules, and make the filename disagree with the stored format. Canonical immutable generation names let discovery select the highest version directly.

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

@@ -0,0 +1,57 @@
+# Agent Note: 已发布 Session 格式在读取正文时通过相邻纯迁移边升级
+
+Status: implemented
+
+[English](2026-08-31-released-session-format-migrations.md) | 中文
+
+## 问题
+
+Session 格式 v0 已随 alpha 版本发布,因此结构化 writer 变更不能再把已有 JSONL 当作可丢弃的预发布状态。已存储事件正文通过读或写 `SessionHandle` 到达恢复、查询、导出、分叉与继续路径。只迁移一个消费方会让调用方看到不同的逻辑 generation,或只在后续 writer 到达旧文件时失败。
+
+迁移必须保留精确源路径、字节与 inode,包括撕裂的物理尾部,同时为每个已发布格式提供一个无歧义的规范文件名。普通 JSONL 与 Zstandard 是同一逻辑格式的编码选择,不能产生两套并行迁移实现。
+
+## 决策
+
+`SESSION_FORMAT_VERSION` 是单调递增的当前 writer 整数。每个相邻 `vN -> vN+1` 转换由一个与 profile 无关的纯包负责。`@deepseek-ai/dsh-session-format` 只提供无损快照、唯一且无缺口的规划、仅 header 转换与整产物组合;`@deepseek-ai/dsh-session-format-catalog` 静态导入完整链,不依赖已挂载的 Cordis 插件。历史 codec 和归一化器位于具名迁移边包中,而当前 Session 与持久化代码只接纳最新逻辑类型。
+
+每条迁移边都会冻结严格的源与目标语义,其目标物理 codec 则保持词汇中立,使普通事件增长可以留在同一格式版本内。目录通过已安装的 peer `@deepseek-ai/dsh-session` 及其当前 `KNOWN_SESSION_EVENT_TYPES` 还原最终代,避免冻结的历史迁移边反过来成为当前词汇 owner。
+
+JSONL provider 在 `open` 为已存储 Session 返回句柄前完成 ensure-current 工作。它选择最高规范 generation、迁移受支持的历史正文,并从同一物理快照解码当前结果;公开 `SessionPersistence` 与 `SessionHandle` 接口不包含迁移操作。仅 header 的 `stat` 与 `list` 会重新扫描 Session 目录,在内存中转换受支持的历史 header,且绝不发布后继。`create` 独立于 header 可读性检查规范文件名,因此每个现有 generation 都会占用其 Session id。
+
+取消属于提供信号的 `open`、`stat` 或 `list` 调用。发现、稳定读取、解码与发布前检查都会观察该信号;不可变后继一旦发布且其目录项已经同步,后续取消不会删除已提交 generation。
+
+配置的 JSONL 编码拥有一个完整后缀:`.jsonl` 或 `.jsonl.zstd`。迁移读取稳定的精确源,解码可恢复逻辑前缀,在内存中组合全部必需迁移边,只为最终目标校验并同步同目录临时 stage,重新检查源 fingerprint,以不覆盖方式发布此前不存在的目标,同步 namespace,并在返回句柄前通过当前格式校验重新打开。源永不移动或改变;只有可丢弃临时 stage 可以被移动、链接或移除。迁移不会合成中断轮次事件:agent-loop 通过写句柄追加这些修复,而只读查询路径在内存中补齐它们。
+
+规范文件名编码物理格式 generation:v0 是 `session.jsonl` 或 `session.jsonl.zstd`;每个正 generation 都是小写 `session.vN.jsonl` 或 `session.vN.jsonl.zstd`。`dsh-session-format` 拥有原始 basename 规则(`sessionFormatLogFilename`、`parseSessionFormatLogFilename`);JSONL provider、session-log 导出归档与 recorded-session fixture 只追加压缩后缀。发布绝不重命名、替换或删除已提交 generation 路径。目标已经存在时,只有它是普通当前格式文件且字节与预期完全相同时才接受;其他目标都会拒绝。低 generation 为 operator 检查或显式复制而保留,但普通 runtime 操作选择数值最高的规范名称,绝不把保留的前任当作自动 fallback、restore 或 downgrade 支持。
+
+当前格式快速路径从一个稳定源快照分类 header,不调用历史 converter,不写 generation,并把该快照交给当前格式解码,而不再次读取文件。解码日志进入现有按 revision 为键的有界 memo,供紧接的观察到恢复交接复用,而 `stat` 与 `list` 会有意重新扫描。多条迁移边保持原 generation 不变,并只发布最终目标;中间版本只存在于内存。源 fingerprint 重新检查会在内容变化时重启迁移,排他目标发布只在竞争胜者字节完全相同时接受它。跨进程 append 隔离不在此保证内。
+
+第一条迁移边 `@deepseek-ai/dsh-session-format-v0-to-v1` 有意保持恒等形态:除版本和 v0 已接纳的有限历史归一化外,它保留逻辑 header、事件、序号、引用、时间戳、payload 与已配置的压缩选择。精确的 `session.jsonl[.zstd]` 源保持字节与 inode 相同,当前 writer 则编码新的 `session.v1.jsonl[.zstd]` 后继。这样可在出现改变基数的格式前先验证完整发布生命周期。
+
+投影缓存记录把自己的折叠结果绑定到 Session header 的 `formatVersion`。`session_projcache` v7 reader 可以在结构上载入前代 domain 记录,但缺少格式代的记录不能播种当前 Session;权威日志会重新折叠它,下一次检查点写入完整的当前 identity。这样,任何在有界规范化或基数变化边之前产生的缓存行都不能绕过该迁移。
+
+## 后果
+
+较新 build 读取事件正文时可能持久增加一个更高 generation。精确旧 generation 仍然可用,但 runtime 此后选择最高规范文件名;保留不承诺旧 build 能安全 downgrade,也不保证新 build 在后继损坏时 fallback。只读文件系统会报告可操作的迁移失败,而不会返回与磁盘不一致的内存当前视图。
+
+JSONL 发布在 POSIX 上使用硬链接创建与目录同步,在 Windows 上使用 write-through 且不覆盖的 `MoveFileExW`。竞争 writer 已先创建目标时,只有已提交字节完全匹配才接受。每个 Session 只支持一个进程内 writer。未来逐 Session 跨进程锁可以关闭剩余的源检查到发布竞态,而无需改变格式迁移边接口。
+
+保留的 generation 不是实时流 WAL。未来可选 WAL sidecar 可以在硬崩溃间保留未完成 assistant 流。显式 generation 检查或复制、保留策略工具、压缩转换与流式整产物转换都是独立功能;自动 fallback 与 downgrade compatibility 并非隐含 future work。
+
+本记录取代 [Session 日志版本机制](2026-08-10-session-log-version-mechanism.zh.md) 中仅在继续时持久化和迁移链仍推迟的规则。原记录继续负责何时递增版本,以及普通同版本 `ignorable` 事件行为。
+
+## 验证
+
+发布验证针对 `snapshots/`、`packages/` 与 `scripts/snapshots/python-sdk-single-exe/` 下每个带版本、来自持久化或投影的 `session*.jsonl` fixture 运行已提交 Session 格式语料门禁。fixture 专用的缺失信封与 request-header token 会先被实体化,再进入真实静态 catalog;每个 fixture 都会通过当前格式 restore 或历史迁移得到当前 v1 视图。Released-v0 replay 输入保持无后缀,而新鲜 v1 writer 输出对 parent 使用 `session.v1.jsonl`、对 child 使用 `session.<ordinal>.v1.jsonl`。Record 与 refresh 会保留每个已完成 generation,包括后续运行不再产生的 child role generation。Malformed 历史 fixture 在来源处修复,不通过依赖路径的 replay 策略准入。持续运行的门禁会动态发现语料,并拒绝每个 restore failure;独立组装式 JSONL 测试负责精确物理字节迁移。
+
+句柄集成验证会一起运行纯格式、catalog、持久化 seam 与 JSONL provider 测试套件:420 个测试覆盖两种编码、不可变发布竞态、仅 header 观察、读写句柄、迁移拒绝、迁移后 append、取消与崩溃尾部行为,并达到逐文件 100% statement、branch、function 与 line coverage。仓库 typecheck 与 lint、含两个已声明 skip 的 113 个无密钥 recorded-session replay,以及 28 个 owner-local expected-output case 也都在合并 master 的 checkpoint 上通过。
+
+组装后的 headless profile 测试会暂存 `session.jsonl`,通过随附组合恢复它,在构造 Session 前观察到 v1,验证精确 v0 字节与 inode 保持不变而 `session.v1.jsonl` 出现,并证明下一次 append 以 v1 为目标。JSONL 约定测试覆盖 raw 与 Zstandard 排他发布、撕裂尾部保留、源变化、目标冲突、最高未来版本拒绝、按 revision 复用已解析日志、列表重新扫描、临时文件清理、已提交重开与当前格式直通。
+
+## 考虑过的替代方案
+
+- **只在继续时迁移**——让查询、导出、分叉与后缀消费者停留在旧代际,并重复恢复策略。
+- **返回迁移后的内存视图但不持久化**——让进程观察到与最高已提交 generation 不一致的状态,并把失败推迟到后续 writer。
+- **持久化每个中间版本**——消耗空间并产生没有 runtime 消费者的恢复状态;只有源与最终代际应持久。
+- **让已挂载事件 owner 插件注册迁移**——使历史可读性依赖部署;静态 catalog 必须在功能插件挂载前工作。
+- **让每个当前格式复用同一个文件名并迁走前任**——不予采用,因为迁移会移动或覆盖已提交证据,需要冲突与保留规则,并让文件名与存储格式不一致。规范不可变 generation 名让发现流程直接选择最高版本。

+ 3 - 3
.agents/notes/implemented/architecture/2026-08-15-packed-session-history-transport.i18n.yaml → .agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.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/architecture/2026-08-15-packed-session-history-transport.md
-2026-08-15-packed-session-history-transport.md: 01e36509b7ad2c878ae4ea04c3a10f029e1b8f3d
-2026-08-15-packed-session-history-transport.zh.md: 6ef847a14da1b4ec1bd59e5aaad9093162b42d84
+#   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: c9d7a66481d4258de8f9f7abdaa12d25f5217510
+2026-09-01-v2-embedded-assistant-streams.zh.md: ff4caceb68c34e470d2dd81fc9cf214555ad676e

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

@@ -0,0 +1,74 @@
+# Agent Note: Embed Assistant streams in v2 attempt settlements
+
+Status: implemented
+
+English | [中文](2026-09-01-v2-embedded-assistant-streams.zh.md)
+
+## Problem
+
+Token-sized `assistant/chunk` events preserve exact stream order, timing, usage, terminal state, replay metadata, and partial failed output, but making each chunk a top-level Session event repeats envelopes throughout persistence, telemetry, history transport, indexing, and client assembly. Physical packed rows reduce JSONL bytes without reducing logical event count or the work of consumers that receive the canonical stream.
+
+Storing only assembled successful messages would remove that overhead but lose failed and abandoned output, token boundaries, timestamps, and deterministic provider replay. The durable record needs one unit per model attempt without reducing the evidence that replay, diagnostics, cancellation recovery, usage accounting, snapshots, and UI history rely on.
+
+Changing event cardinality also changes Session sequence numbers. A released migration must preserve the relative order of unrelated events, rewrite every declared same-Session reference, retain the exact fork cut, and refuse any relationship it cannot preserve semantically.
+
+## Decision
+
+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.
+- `assistant/attempt` is log-only. It preserves the stream for a failed, retried, cancelled, or stream-error attempt that reaches settlement without a surface message, so diagnostics and accounting do not fabricate model-visible history.
+
+`AssistantStreamAccumulator` snapshots each chunk once. Consecutive text, reasoning, or tool-argument deltas for the same block become one compact run with its first timestamp, exact timestamp gaps, and one array member per original delta. Every other chunk remains a timestamped raw record. `expandAssistantStream()` strictly validates and reconstructs the exact timed sequence; compaction never joins delta boundaries.
+
+The current v2 validator requires the embedded stream to reproduce a non-empty `assistant/message`'s content, usage, and replay state. An empty stream remains valid for a migrated legacy message that had no source chunks. `assistant/message` cannot carry obsolete chunk `sourceEventSeqs`; ordinary user and tool surface provenance remains available.
+
+### Live presentation and durable replay
+
+`agent/assistant-stream` publishes process-local start, transient chunk, and end frames. The loop appends the complete `assistant/message` or `assistant/attempt` before a committed end frame names its type and sequence. An abandoned end has no settlement.
+
+The Web follow adapter opts into these process-local frames and adds the last durable sequence observed at each start. It presents chunks as Client-only `assistant/live-chunk` updates between durable cursors, stages only a later matching settlement until the committed end, and reopens follow on a revision gap. A committed end publishes a named settlement delta that removes the attempt's transient matches, adds the durable entry, and replays only affected Conversation Contexts; an abandoned end publishes the same delta without an entry. A reconnect baseline carries the active attempt's durable start cursor and compact prefix. Paged history, replay, telemetry, token accounting, and cold UI assembly read the durable embedded stream rather than the live frames.
+
+### Released v1 to v2 migration
+
+The adjacent migration validates the complete frozen v1 artifact, groups chunks by turn, step, terminal boundary, and exact message provenance, and then substitutes one settlement per attempt. A successful group's chunks move into its message. An unclaimed group becomes `assistant/attempt` at the last consumed chunk's position. Unrelated interleaved events retain their relative order, and survivors receive dense v2 sequence numbers. The edge compacts, expands, and re-assembles embedded streams through the runtime `AssistantStreamAccumulator`, `expandAssistantStream`, and `BlockAssembler` from `dsh-llm` instead of frozen copies, because that package owns the v2 stream encoding. Target validation re-checks agreement between each migrated `assistant/message` and its embedded stream itself, so a disagreeing v1 log is refused as an unsupported migration with its source artifact retained instead of surfacing as corruption from the installed Session restoration. A later format that changes the stream encoding must freeze copies of these helpers into this edge.
+
+The edge remaps the finite declared reference inventory: envelope provenance, surface replacement endpoints, command source events, compaction ranges and shadowed lists, and title message lists. The model-visible text of a validated `session/title-llm-request` remains byte-identical in the source sequence namespace while its `messageSeqs` field moves to the v2 namespace; target validation therefore does not reconstruct that text from remapped sequences. A reference to a consumed chunk refuses migration; it is never redirected to a settlement with different meaning. The edge also refuses an inherited cut that splits an attempt.
+
+The v2 physical header requires `isSeeded` and stores no numeric cut. A seeded artifact marks its exact cut with `session/end-seed { inherited: true }`; decoding derives the cut from the last tagged marker. The v2 codec writes one durable event per physical row, range-encodes only `sourceEventSeqs`, and validates physical envelopes without freezing ordinary event vocabulary or payload additions. The v1-to-v2 target validator separately freezes the released-v2 inventory, while current restoration uses the installed Session vocabulary. Frozen v0 and v1 codecs retain packed-row decoding for their immutable historical generations.
+
+A fresh subagent child's constructor seed is exactly the inherited parent prefix. `Session` appends the tagged cut marker, then subagent setup appends the child-owned descriptor and delegated policies. The former descriptor-seed helper is removed, so a descriptor is never counted as inherited and cold resume replays the persisted child-owned setup. Historical snapshot fixtures that placed an untagged marker after the descriptor are corrected at their source; current comparison keeps marker count and sequence references visible.
+
+The `dsh_session_log` request extension keeps its own outer schema at version 1: its Session header projection still derives `seedLength` from the logical inherited cut, and only its `sessionFormatVersion` member identifies the embedded logical Session generation. Projection units likewise keep their `stateVersion`; the projection cache binds every checkpoint to the Session format generation, so a generation change never needs a unit version bump.
+
+Generation selection and publication follow the [released Session migration decision](2026-08-31-released-session-format-migrations.md): the source path, bytes, and inode remain unchanged, only the final version-named successor is published, and retained predecessors provide neither fallback nor downgrade support.
+
+## Verification
+
+The compact-stream tests pin exact accumulation and expansion for text, reasoning, tool arguments, raw chunks, timestamp gaps, malformed records, and detached snapshots. The v1-to-v2 tests cover successful and failed attempts, interleaving, dense sequence and reference remapping, source-sequence title framing, seed-cut insertion and split refusal, strict source and target validation, one-row v2 encoding, backend-compatible provenance ranges, raw and Zstandard publication, and no-write current reads.
+
+The pre-merge performance acceptance measured static catalog-routing overhead against direct released-v2 restoration of the same already parsed physical rows across three runs, 100 warmup pairs, and 600 measured pairs; it did not compare v1 with v2 or time backend I/O. Every pooled median and p95 regression stayed within the 5% budget, with a worst p95 regression of 3.150%.
+
+Agent-loop tests pin durable-before-end ordering, interrupted visible prefixes, failed and retry attempts, abandonment, usage, and replay metadata. Session Controller and Conversation tests pin live transient display, reconnect baselines, committed settlement release, history replay, Chat and Trajectory parity, while TypeScript and Python SDK snapshots pin the external event representation.
+
+## Alternatives considered
+
+**Persist only assembled successful messages.** This loses partial failed output, timing, token boundaries, usage from attempts without a message, and exact deterministic replay. `assistant/attempt` and the embedded compact stream preserve those facts without adding them to model history.
+
+**Keep top-level chunks and pack only physical rows.** This preserves the v1 logical representation but leaves sequence density, telemetry volume, wire envelopes, Client entries, and consumer dispatch proportional to token count. Historical codecs still decode that representation; it is not the current event model.
+
+**Carry packed chunk rows through the history API.** This reduces wire and Client work for v1 but gives the Client a second event vocabulary and keeps transport coupled to token-row cardinality. The current API carries scalar durable settlements plus a separate live transient stream.
+
+**Store the stream in a sidecar or replay-only fixture.** This splits one attempt's message and evidence across durability owners and cannot give ordinary resumed sessions the same failed-output and timing facts. The settlement is the atomic owner.
+
+**Redirect references from consumed chunks to their settlement.** A chunk and an attempt settlement are not interchangeable facts. Refusal prevents a migration from silently changing the meaning of plugin-owned references.
+
+## Consequences
+
+Current logs, telemetry, history pages, and cold Client assembly scale by model attempts rather than token chunks while retaining exact stream evidence inside each settlement. Live presentation remains incremental and intentionally process-local.
+
+Unlike v1 top-level chunks, which the buffered persistence writer could flush before an attempt ended, v2 has no durable attempt evidence until settlement. A hard process or host loss before settlement discards the complete in-flight stream; `agent/assistant-stream` is not a write-ahead log. This tradeoff avoids a second durability owner for live output.
+
+One settlement can be large, and v1-to-v2 migration materializes the whole artifact plus its sequence map. The closed alpha inventory refuses unknown v1 events and undeclared references instead of guessing. Consumers that need individual chunks call `expandAssistantStream()` and must not infer durability from `agent/assistant-stream`.
+
+Migration changes sequence numbers after consumed v1 chunks, so every same-Session reference belongs to an explicit rewrite rule. This constraint makes future cardinality-changing migrations expensive by design and keeps silent semantic redirection out of the format chain.

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

@@ -0,0 +1,74 @@
+# Agent Note: 在 v2 attempt settlement 中嵌入 Assistant stream
+
+Status: implemented
+
+[English](2026-09-01-v2-embedded-assistant-streams.md) | 中文
+
+## 问题
+
+Token 粒度的 `assistant/chunk` 事件会保留精确的 stream 顺序、时间、usage、terminal state、replay metadata 与失败时的部分输出,但让每个 chunk 成为顶层 Session event 会在持久化、遥测、历史传输、索引和 Client 组装中重复信封。物理 packed row 可以减少 JSONL 字节,却不会减少逻辑事件数,也不会减少接收规范 stream 的消费方工作量。
+
+只存储组装后的成功 message 可以消除这些开销,但会丢失失败与放弃的输出、token 边界、时间戳和确定性 provider replay。持久记录需要让每个模型 attempt 只占一个单位,同时不减少 replay、诊断、取消恢复、usage 记账、snapshot 与 UI 历史依赖的证据。
+
+改变事件基数也会改变 Session 序号。已发布迁移必须保留无关事件的相对顺序、改写每个已声明的同 Session 引用、保留精确 fork 切点,并拒绝任何无法保持语义的关系。
+
+## 决策
+
+Session format v2 没有顶层 `assistant/chunk` 事件。每个模型 attempt 提交一个包含 `stream: AssistantStreamRecord[]` 的持久 settlement:
+
+- `assistant/message` 是成功响应或具有可见组装内容的已取消响应所对应的 surface settlement。它在组装 message 旁嵌入精确的紧凑带时间 stream、可选 usage 与可选 `interrupted: true` marker。
+- `assistant/attempt` 只进入日志。它保留已到达 settlement、但没有 surface message 的失败、重试、取消或 stream error attempt,因此诊断与记账不会虚构模型可见历史。
+
+`AssistantStreamAccumulator` 对每个 chunk 只快照一次。同一 block 的连续 text、reasoning 或 tool argument delta 会变成一个紧凑 run,包含首个时间戳、精确时间戳间隔和每个原始 delta 对应的一个数组成员。其他 chunk 保留为带时间戳的 raw record。`expandAssistantStream()` 会严格校验并重建精确的带时间序列;压缩绝不会合并 delta 边界。
+
+当前 v2 校验器要求嵌入式 stream 能复现非空 `assistant/message` 的 content、usage 与 replay state。对于没有源 chunk 的已迁移旧 message,空 stream 仍然有效。`assistant/message` 不能携带已停用的 chunk `sourceEventSeqs`;普通 user 与 tool surface provenance 保持可用。
+
+### 实时呈现与持久回放
+
+`agent/assistant-stream` 发布进程本地 start、瞬态 chunk 与 end frame。loop 会在 committed end frame 命名其类型和序号前追加完整的 `assistant/message` 或 `assistant/attempt`。abandoned end 没有 settlement。
+
+Web follow adapter 显式选择接收这些进程本地 frame,并为每个 start 补充当时观察到的最后一个持久序号。它把 chunk 呈现为持久 cursor 之间的 Client-only `assistant/live-chunk` update,只暂存 start 之后匹配的 settlement,并在 revision 缺口时重新打开 follow。committed end 会发布具名 settlement delta,删除该 attempt 的 transient match、加入持久 entry,并只重放受影响的 Conversation Context;abandoned end 会发布不含 entry 的同类 delta。重连 baseline 携带活跃 attempt 的持久起始 cursor 与紧凑前缀。分页历史、replay、遥测、token 记账与冷 UI 组装读取持久嵌入式 stream,而不是 live frame。
+
+### 已发布 v1 到 v2 迁移
+
+相邻迁移会校验完整的冻结 v1 产物,按 turn、step、terminal boundary 与精确 message provenance 对 chunk 分组,再为每个 attempt 替换一个 settlement。成功分组的 chunk 移入其 message。未被认领的分组会在最后一个被消费 chunk 的位置变成 `assistant/attempt`。无关的交错事件保持相对顺序,存活事件获得密集 v2 序号。该迁移边通过 `dsh-llm` 运行时的 `AssistantStreamAccumulator`、`expandAssistantStream` 与 `BlockAssembler` 压缩、展开并重组嵌入 stream,而不持有冻结副本,因为该包拥有 v2 stream 编码。目标校验会自行复核每个迁移后的 `assistant/message` 与其嵌入 stream 是否一致,因此不一致的 v1 日志会作为 unsupported migration 被拒绝并保留源产物,而不是由 installed Session restoration 报告为损坏。日后若某个格式改变 stream 编码,必须把这些 helper 的冻结副本纳入本迁移边。
+
+该迁移边会重映射有限的已声明引用清单:信封 provenance、surface replacement 端点、command source event、compaction range 与 shadowed list,以及 title message list。经过校验的 `session/title-llm-request` 模型可见文本会在源序号命名空间中保持逐字节不变,而它的 `messageSeqs` 字段会迁移到 v2 命名空间;因此目标校验不会根据重映射后的序号重建该文本。指向被消费 chunk 的引用会使迁移失败;它绝不会被重定向到含义不同的 settlement。该迁移边也会拒绝切开 attempt 的继承切点。
+
+v2 物理 header 要求 `isSeeded`,且不存储数值切点。带 seed 的产物用 `session/end-seed { inherited: true }` 标记其精确切点;解码从最后一个 tagged marker 推导切点。v2 编解码器为每个持久事件写一条物理行,只对 `sourceEventSeqs` 做范围编码,并在不冻结普通事件词汇或 payload 新增项的前提下校验物理 envelope。v1-to-v2 target validator 会另行冻结 released-v2 清单,current restoration 则使用 installed Session 词汇。冻结的 v0 与 v1 编解码器继续为不可变历史 generation 解码 packed row。
+
+新建 subagent 子项的 constructor seed 与继承的父项前缀完全相同。`Session` 会追加 tagged cut marker,随后 subagent setup 再追加子项持有的 descriptor 与 delegated policy。原 descriptor-seed helper 会被删除,因此 descriptor 绝不会计入继承内容,cold resume 则重放已经持久化的子项 setup。曾把 untagged marker 放在 descriptor 后面的历史 snapshot fixture 会在源处修正;当前比较仍会暴露 marker 数量与序号引用。
+
+`dsh_session_log` request extension 的外层 schema 保持版本 1:它的 Session header 投影仍从逻辑 inherited cut 推导 `seedLength`,只有其中的 `sessionFormatVersion` 成员标识嵌入的逻辑 Session generation。projection unit 同样保持各自的 `stateVersion`;projection cache 把每个 checkpoint 绑定到 Session format generation,因此 generation 变化不需要提升 unit 版本。
+
+Generation 选择与发布遵循[已发布 Session 迁移决策](2026-08-31-released-session-format-migrations.zh.md):源路径、字节与 inode 保持不变,只发布最终具名版本 successor;保留 predecessor 不提供 fallback 或 downgrade 支持。
+
+## 验证
+
+紧凑 stream 测试固定 text、reasoning、tool argument、raw chunk、时间戳间隔、格式错误 record 与分离 snapshot 的精确累积和展开。v1 到 v2 测试覆盖成功与失败 attempt、交错、密集序号与引用重映射、源序号 title framing、seed 切点插入与切分拒绝、严格源与目标校验、每行一个事件的 v2 编码、与 backend 兼容的 provenance range、原始与 Zstandard 发布,以及无写入的当前读取。
+
+合并前的 performance acceptance 在三轮、100 组 warmup pair 与 600 组 measured pair 下,针对同一批已经解析的物理 row,把静态 catalog routing 与直接 released-v2 restoration 比较;它不比较 v1 与 v2,也不计入 backend I/O。每个 pooled median 与 p95 regression 都保持在 5% 预算以内,最差 p95 regression 为 3.150%。
+
+Agent-loop 测试固定先持久后 end 的顺序、中断的可见前缀、失败与重试 attempt、abandonment、usage 与 replay metadata。Session Controller 与 Conversation 测试固定实时瞬态显示、重连 baseline、committed settlement 发布、历史回放以及 Chat 与 Trajectory 一致性;TypeScript 与 Python SDK snapshot 固定外部事件表示。
+
+## 备选方案
+
+**只持久化组装后的成功 message。** 这会丢失部分失败输出、时间、token 边界、没有 message 的 attempt usage,以及精确确定性 replay。`assistant/attempt` 与嵌入式紧凑 stream 会保留这些事实,且不把它们加入模型历史。
+
+**保留顶层 chunk,只打包物理行。** 这会保留 v1 逻辑表示,却让序号密度、遥测量、wire 信封、Client entry 与消费方 dispatch 继续与 token 数成正比。历史编解码器仍然解码该表示;它不是当前事件模型。
+
+**通过历史 API 传递 packed chunk row。** 这会减少 v1 的 wire 与 Client 工作,却让 Client 拥有第二套事件词汇,并让传输继续与 token-row 基数耦合。当前 API 携带标量持久 settlement,并使用独立的实时瞬态 stream。
+
+**把 stream 存在 sidecar 或 replay-only fixture 中。** 这会把一个 attempt 的 message 与证据拆给不同持久性 owner,也无法让普通恢复 Session 获得相同的失败输出与时间事实。settlement 是原子 owner。
+
+**把被消费 chunk 的引用重定向到其 settlement。** Chunk 与 attempt settlement 不是可互换事实。拒绝可以防止迁移悄然改变插件自有引用的含义。
+
+## 后果
+
+当前日志、遥测、历史页与冷 Client 组装按模型 attempt 而非 token chunk 扩展,同时在每个 settlement 内保留精确 stream 证据。实时呈现保持增量,并且有意仅存在于进程内。
+
+v1 的顶层 chunk 可能在 attempt 结束前由带缓冲的持久化 writer 刷盘;与之不同,v2 在 settlement 之前没有持久 attempt 证据。如果进程或主机在 settlement 前硬中断,完整的 in-flight stream 都会丢失;`agent/assistant-stream` 不是 write-ahead log。这项取舍避免为实时输出增加第二个持久性 owner。
+
+一个 settlement 可能很大,v1 到 v2 迁移会物化完整产物及其序号映射。封闭的 Alpha 清单会拒绝未知 v1 事件与未声明引用,而不会猜测。需要单独 chunk 的消费方调用 `expandAssistantStream()`,并且绝不能从 `agent/assistant-stream` 推断持久性。
+
+迁移会改变被消费 v1 chunk 之后的序号,因此每个同 Session 引用都必须属于显式改写规则。该约束有意让未来的基数变化迁移保持昂贵,并防止格式链执行无声的语义重定向。

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-02-projcache-cross-version-read-compat.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-02-projcache-cross-version-read-compat.md
-2026-09-02-projcache-cross-version-read-compat.md: 1d157d90b04fe92e86d858a8e5424a5e517721f6
-2026-09-02-projcache-cross-version-read-compat.zh.md: 3948126271a84810abf4fe1c1e6e874b279a4f5e
+2026-09-02-projcache-cross-version-read-compat.md: b08b651e7c826cd1c22b8e5c563ab594331e3c98
+2026-09-02-projcache-cross-version-read-compat.zh.md: 637bca397e054f3b62160fc10c5809e449ef40e2

+ 24 - 20
.agents/notes/implemented/architecture/2026-09-02-projcache-cross-version-read-compat.md

@@ -1,4 +1,4 @@
-# Agent Note: Projection-cache cross-version read compatibility (session_projcache v3/v4/v5 → v6)
+# Agent Note: Projection-cache predecessor recovery and Session-format binding (session_projcache v3-v6 → v7)
 
 Status: implemented
 
@@ -6,22 +6,25 @@ English | [中文](2026-09-02-projcache-cross-version-read-compat.zh.md)
 
 ## Problem
 
-The `session_projcache` storage domain evolved through three on-disk generations across published releases. An upgraded DSH_HOME failed in two ways:
+The `session_projcache` storage domain evolved through several on-disk generations. An upgraded DSH_HOME exposed three risks:
 
 - **A v3 single-file home bricked startup after the upgrade**: the per-record layout's legacy bootstrap migrated the old whole-unit file without checking its `unit.version`, stamping the old records with the current version into the new tree; the domain layer's per-record zod validation at open then hit the missing now-required fields → `invalid-record` → the whole domain refused to open → the plugin tree failed to load. And because the bootstrap writes before validation runs, **the first boot permanently wrote the bad documents into the new tree** ("poisoning") — every later boot saw a non-empty tree, never took the legacy path again, and the home stayed unusable.
 - **A v4 per-record home lost its listing titles after the upgrade**: v4 documents were silently discarded by the version-stamp check (the per-record contract), and SessionList is a zero-I/O cache-only read, so a miss served the row without projections; titles only returned as each session was individually reopened.
+- **A Session-format bump could reuse a fold produced under older event semantics**: versions 3 through 6 did not record the Session format generation. Treating an absent generation as current would let a cache row bypass bounded historical normalization or a cardinality-changing migration.
 
 The cache domain's own contract is "a stale or unreadable cache costs a longer tail replay, never a wrong value, never a refused load" — the hard failure and the wholesale discard each broke the first half of that contract or the product expectation.
 
-## The three on-disk generations
+## The on-disk generations
 
 | domain version | shipped in | layout | on-disk form | identity fields | row fields |
 |---|---|---|---|---|---|
 | 3 | 0.1.1-rc.2 | single | one file `storages/session_projcache.json` (`{unit:{name,version}, global, tables}`) | `createdAt`, `cwd?` | `ver`, `seq`, `val` |
 | 4 | 0.1.2-alpha.3 | per-record | one file per session `storages/session_projcache/sessions/<sessionId>.json` (`{version, record}`) | `createdAt`, `cwd?` | same |
 | 5 | 0.1.2-alpha.4 | per-record | same as v4 | + `isSeeded` (shipped required; now optional), `inheritedEventCount` (same) | same (`seq` numbers mean the same as v4; only type brands were added) |
+| 6 | pre-v1 mainline | per-record | same as v5 | same as v5 | same |
+| 7 | current | per-record | same as v5 | + `formatVersion`; current writes also require both lineage fields | same |
 
-The only substantive v4→v5 difference is the two new lineage identity fields; the `ver/seq/val` row shape is identical across all three generations, and `seq` numbering did not change ([the 2026-08-31 seq/offset brands note](2026-08-31-session-sequence-and-log-offset-brands.md) pins the on-disk numbers as unchanged). v3→v4 was a layout migration with identical record content.
+The only substantive v4→v5 difference is the two lineage identity fields; v6 changed only the write stamp. The `ver/seq/val` row representation is identical across these predecessor generations, and `seq` numbering did not change ([the 2026-08-31 seq/offset brands note](2026-08-31-session-sequence-and-log-offset-brands.md) pins the on-disk numbers as unchanged). v3→v4 was a layout migration with identical record content. Version 7 adds the Session format generation to the cache identity because row semantics cannot be inferred from the domain stamp.
 
 One derived shape also exists: a v3 home that ran the v5 build once (the poisoned state) — its new tree holds documents **stamped 5 whose content is a v3 record** (no lineage fields).
 
@@ -32,43 +35,44 @@ Declared read compatibility — reads tolerate vouched-for older versions, write
 1. **`DomainSpec.compatibleVersions` (new, optional)**: the domain owner declares "records stored under these older versions are also readable under the current record schemas" (typically by declaring the fields old records lack as optional). `defineDomain` validates each entry as a non-negative integer below the current version; `descriptorOf` projects the set onto the backend `KvUnitDescriptor`.
 2. **json backend per-record reads** accept version stamps in "current ∪ compatibleVersions"; anything outside the set is still discarded as foreign. **The write path always stamps the current version** (the first checkpoint after reading an old record naturally advances it). The `single` layout stays exact-version.
 3. **Legacy-bootstrap version gate (the actual bug fix)**: the old whole-unit file's `unit.version` must fall inside the accepted set to be migrated; otherwise the file is left alone and the unit reads empty — stamping records the owner never vouched for turns a discardable stale cache into hard schema failures at the domain layer.
-4. **The projcache domain declares `version: 6, compatibleVersions: [3, 4, 5]`**, and the two lineage fields become `.optional()`. The single reader of stored identities, `identityMatches`, normalizes absence to the unseeded lineage (`?? false` / `?? 0`): exact for unforked sessions, while a forked session's expectation is seeded → natural mismatch → discard and cold rebuild, so the lineage binding loses none of its protection.
-5. **The poisoned state self-heals**: documents stamped 5 without lineage fields are declared compatible and parse under the optional schema (their content is the real pre-upgrade cache data), so the home boots again and titles serve immediately.
+4. **The projcache domain declares `version: 7, compatibleVersions: [3, 4, 5, 6]`**, and the format and lineage identity fields are optional in the stored schema so vouched-for predecessor records can open. Current writes always include all three fields.
+5. **Identity matching is stricter than structural admission**: an absent `formatVersion` never matches a current Session, so predecessor rows cannot seed a projection and refold from the authoritative log. Once the format matches, `identityMatches` normalizes absent lineage to unseeded (`?? false` / `?? 0`): exact for an unseeded session, while a seeded expectation fails the match. Poisoned v5 homes therefore boot safely, but their unbound rows are not exposed as current values.
 6. **Schema-validation backstop: `invalidRecords: 'backup-and-skip'` (declared by this domain only)**. A stored record that still fails to parse beyond read compatibility no longer refuses the whole domain: the domain layer calls the backend's `KvUnit.backupRecord` (json per-record implementation = rename the document to `<key>.json.bak.<YYYYMMDDHHmm>`, bytes kept, never read again), prints the concrete failure with `logger.error` (domain, table, key, destination, zod cause), and continues the open with the record absent; the next cold read rebuilds and rewrites that session's cache. **The policy is an explicit per-domain declaration and the default stays fail-loud** — other domains still refuse the whole load on invalid stored data, and a backend without `backupRecord` (single layout, row stores) also falls back to fail-loud. Naming history: quarantine → backup-and-skip (user ruling: the word must carry both "back up" and "skip", sharing its root with the `.bak` suffix; skip-backup was rejected because the CLI `--skip-X` convention reads it as "do not back up"). For this domain it supersedes the reset/destroy recovery path of the [2026-07-28 storage recovery proposal](../../proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.md), which stays live for authoritative and whole-medium damage.
 
-### v5 → v6 compatibility
+7. **A predecessor title is a listing hint, not a fold shortcut**: Session-list startup remains metadata/cache-only and never opens cold log bodies. The log header is authoritative; a lifecycle-matching checkpoint is a durable-prefix witness that may lag but cannot lead the log. `cachedPredecessorTitle` therefore exposes only a predecessor `title` row that still passes the current title unit's `stateVersion` and schema. Both adjacent Session-format edges preserve title text. The hint uses `asOfSeq: -1` rather than the stored row sequence because a cardinality-changing log migration can remap that coordinate. Other predecessor rows remain hidden, and `hydratePrepared`/`coldSnapshot` retain strict format identity, because normalizers can change values such as `blank` or `lastPromptAt` even when storage is physically consistent.
 
-Version 6 changes only the current write stamp and keeps the v5 record schema. `compatibleVersions: [3, 4, 5]` therefore admits both healthy v5 records and v5-stamped lineage-less records produced by the faulty bootstrap. The current schema accepts absent lineage; `identityMatches` interprets it as unseeded and rejects the record for a seeded session. The next successful checkpoint rewrites an accepted v5 record with a v6 stamp and complete lineage. No separate v5→v6 rewrite runs at startup: an unaccepted version reads as absent, while a schema-invalid accepted record follows `backup-and-skip`.
+### v3-v6 → v7 disposition
+
+Versions 3 through 6 remain structurally readable because their record and row representations are valid inputs to the current schema. Their identities lack `formatVersion`, so they are deliberately unusable as current fold shortcuts. An unseeded, lifecycle-matching record may still provide its version-compatible title to zero-I/O listing; it cannot provide any authoritative seed. A cold read or live checkpoint rebuilds the values from the migrated Session log and writes a v7 record with the complete format and lineage identity. No eager value migration runs at startup; a schema-invalid accepted record follows `backup-and-skip`.
 
 ### Upgrade matrix
 
 | home shape | behavior after the fix |
 |---|---|
-| v3 single-file (not poisoned) | bootstrap migrates (3 ∈ accepted set) → titles serve immediately |
-| v3 + poisoned new tree | new-tree documents read directly (optional tolerance) → boot restored, titles serve immediately |
-| v4 per-record | documents read directly (4 ∈ accepted set) → titles serve immediately |
-| v5 healthy | documents read directly (5 ∈ accepted set) → titles serve immediately |
-| v6 current | unaffected |
-| old records of forked (seeded) sessions | identity mismatch → discarded, cold rebuild when the session opens (safe side) |
+| v3 single-file (not poisoned) | bootstrap migrates (3 ∈ accepted set) → boot succeeds; compatible title is list-visible, unbound fold waits for cold rebuild |
+| v3 + poisoned new tree | new-tree documents parse under optional fields → boot restored; compatible title is list-visible, unbound fold waits for cold rebuild |
+| v4/v5/v6 per-record | documents read structurally → missing format generation rejects the fold shortcut; compatible title is list-visible; current checkpoint rewrites v7 |
+| v7 current with matching identity | cached values serve normally |
+| matching-format record without lineage | unseeded caller may use it; seeded caller rejects it and refolds cold |
 
 ## Alternatives considered
 
-- **Discard-and-rebuild only** (bootstrap gate without compatible versions): fixes the boot, but every SessionList title is lost after the upgrade until each session is reopened — fails the upgrade-and-go product requirement.
+- **Reject predecessor stamps in the storage layer**: safe for projections, but prevents the guarded legacy bootstrap and loses the ability to retain a structurally sound record until an authoritative refold replaces it. Structural admission plus semantic identity rejection keeps the boot recoverable without serving an unproven value.
+- **Treat a missing format generation as current for every use**: preserves cached values, but lets a pre-migration fold bypass the Session-format edge. Rejected because v0→v1 includes bounded historical normalizers and later edges may change event cardinality. The title-only listing hint is narrower: it does not seed a fold, and title text is invariant across the installed edges.
 - **Schema `.default()` fills**: behaviorally equivalent to optional + reader normalization, but bakes the "absent = unseeded" interpretation into the durable schema's output type; ruled for optional — the schema honestly describes every accepted on-disk shape and the interpretation lives at the consumer (user ruling, 2026-09-02).
 - **Roll the domain version back to 4**: a small diff, but breaks version monotonicity, depends on the "bootstrap skips no versions" bug itself, and drops every poisoned and healthy v5 home's cache.
 
 ## Consequences
 
 - A deployment routing this domain to the sqlite backend gets none of the tolerance: sqlite implements neither `compatibleVersions` nor `backupRecord`, so behavior degrades to the old strict-version semantics (a whole-unit version mismatch still refuses with `version-mismatch`; nothing loosens, nothing serves wrong values). Shipped compositions route this domain to json, so this stays a deployment-configuration risk only.
-- The optional lineage fields let accepted records omit lineage: a lineage-less record decodes as unseeded. The identity match still refuses it for seeded callers, and the per-row `ver` guard still screens every value, so the residual exposure is an unseeded caller reading an unseeded-shaped record — the same trust extended to genuine pre-lineage records.
+- The optional format field lets predecessor records pass structural validation, but absence always fails current identity matching. Optional lineage is normalized only after the format matches; a seeded caller still refuses a lineage-less record. The per-row `ver` guard continues to screen every served value.
 - `backupRecord` overwrites a same-minute backup of the same key (the newer bytes win); distinct minutes and distinct keys never collide.
 
 ## Testing
 
 - `storage-json` unit tests: compat-stamped reads / out-of-set discards / writes stamping current; legacy bootstrap migrating only accepted versions (including the migrated-documents-stamp-current assertion); `backupRecord` move / absent read / rewrite / closed guard.
 - `storage-domain` unit tests: `compatibleVersions` / `invalidRecords` declaration validation; backup-and-skip falling back to fail-loud when the backend has no `backupRecord`.
-- `session-projection-cache` unit tests: records without lineage fields serve unseeded sessions verbatim and are discarded for seeded ones.
-- **Archived-fixture recovery tests** (`tests/fixtures.spec.ts` + `tests/fixtures/`): four media archives produced by the real released builds — `v3-single-unit.json` (the 0.1.1-rc.2 whole-unit file), `v4-session-doc.json` (0.1.2-alpha.3), `v5-session-doc.json` (0.1.2-alpha.4), `v5-lineageless-doc.json` (the unguarded bootstrap's poisoned shape, synthesized from the v3 record) — each opened through the real storage stack, asserting the listing serves the archived title and that a live write rewrites the document to the current version (v6 stamp + lineage fields + fresh value); plus the backup-and-skip behavior for a schema-failing record (boot survives, `.bak` lands, log is concrete, neighbor records unharmed).
-- End-to-end acceptance, executed against the real release artifacts: the published 0.1.1-rc.2 and 0.1.2-alpha.3 npm builds seeded homes through their own web apps (model turns plus a rename RPC), the published 0.1.2-alpha.4 build reproduced both failures (including the poisoned tree), and the fixed build served every home shape — pristine v3, poisoned v3, v4, and fresh — with the SessionList RPC returning the recorded titles verbatim.
+- `session-projection-cache` unit tests: a matching format with absent lineage serves only an unseeded Session; a predecessor record without a format generation cannot seed a fold but may serve only its compatible title hint.
+- **Archived-fixture recovery tests** (`tests/fixtures.spec.ts` + `tests/fixtures/`): four media archives produced by the real released builds — `v3-single-unit.json` (the 0.1.1-rc.2 whole-unit file), `v4-session-doc.json` (0.1.2-alpha.3), `v5-session-doc.json` (0.1.2-alpha.4), `v5-lineageless-doc.json` (the unguarded bootstrap's poisoned form, synthesized from the v3 record) — each opens through the real storage stack, serves only its compatible predecessor title, never serves its unbound fold, then accepts a live write that replaces it with a v7 record carrying the complete identity and fresh value. The same suite proves backup-and-skip for a schema-failing record: boot survives, `.bak` lands, diagnostics name the failure, and a neighboring predecessor record remains rewritable.
 
-Future bump procedure: when a new version's shape can tolerate old records through "optional fields + reader normalization", add the old version to `compatibleVersions`; otherwise bump normally (discard and rebuild) and remove the no-longer-compatible versions from the set. Either way, the package README requires the bump to land with archived fixtures and tests proving the chosen disposition.
+Future bump procedure: add an older domain version to `compatibleVersions` only when the current stored schema can parse it, and let the owning reader decide whether its semantic identity is sufficient. A Session-format change never inherits an absent format generation. The package README requires every bump to land with archived fixtures and tests proving structural admission, semantic use or rejection, and current rewrite.

+ 24 - 20
.agents/notes/implemented/architecture/2026-09-02-projcache-cross-version-read-compat.zh.md

@@ -1,4 +1,4 @@
-# Agent Note: 投影缓存跨版本读兼容(session_projcache v3/v4/v5 → v6
+# Agent Note: 投影缓存前代恢复与 Session 格式绑定(session_projcache v3-v6 → v7
 
 Status: implemented
 
@@ -6,22 +6,25 @@ Status: implemented
 
 ## 问题
 
-`session_projcache` 存储域在已发布版本间演进了三代磁盘结构。升级后的 DSH_HOME 出现两类故障
+`session_projcache` 存储域演进过多代磁盘结构。升级后的 DSH_HOME 暴露了三类风险
 
 - **v3 单文件 home 升级后启动硬失败**:per-record 布局的 legacy bootstrap 迁移旧单文件时不检查其 `unit.version`,把旧记录原样打上当前版本戳写入新树;domain 层开域时逐条 zod 校验,旧记录缺新增必填字段 → `invalid-record` → 整个域拒开 → 插件树加载失败。且 bootstrap 先写盘后校验,**首次启动即把坏文档永久写入新树**("投毒")——此后每次启动新树非空、连 legacy 路径都不再走,home 持续不可用。
 - **v4 per-record home 升级后列表丢标题**:v4 文档被版本戳检查静默丢弃(per-record 契约),SessionList 是零 I/O 纯缓存读,miss 后整行不带投影;标题要等每个会话被逐个重新打开后才恢复。
+- **Session 格式递增可能复用按旧事件语义折叠的结果**:版本 3 至 6 不记录 Session 格式代。若把缺失代视为当前代,缓存行就能绕过有界历史规范化或改变基数的迁移。
 
 缓存域自身的契约是"过期或不可读的缓存只付出更长的尾部重放,绝不给出错值、绝不拒载"——硬失败与整体丢弃都违背该契约的前半句或产品预期。
 
-## 三代磁盘结构差异
+## 磁盘结构代际
 
 | domain version | 携带发布 | 布局 | 磁盘形态 | identity 字段 | 行字段 |
 |---|---|---|---|---|---|
 | 3 | 0.1.1-rc.2 | single | 单文件 `storages/session_projcache.json`(`{unit:{name,version}, global, tables}`) | `createdAt`, `cwd?` | `ver`, `seq`, `val` |
 | 4 | 0.1.2-alpha.3 | per-record | 每会话一份 `storages/session_projcache/sessions/<sessionId>.json`(`{version, record}`) | `createdAt`, `cwd?` | 同上 |
 | 5 | 0.1.2-alpha.4 | per-record | 同 v4 | + `isSeeded`(v5 首发必填;现为 optional)、`inheritedEventCount`(同前) | 同上(`seq` 数值语义与 v4 相同,仅类型加 brand) |
+| 6 | v1 之前的 mainline | per-record | 同 v5 | 同 v5 | 同上 |
+| 7 | 当前版本 | per-record | 同 v5 | + `formatVersion`;当前写入也要求两个 lineage 字段 | 同上 |
 
-v4→v5 的唯一实质差异是 identity 新增两个 lineage 字段;行内 `ver/seq/val` 三代一致,`seq` 的数值含义未变([2026-08-31 seq/offset brands note](2026-08-31-session-sequence-and-log-offset-brands.zh.md) 明确 on-disk 数值不变)。v3→v4 是布局迁移,记录内容结构一致。
+v4→v5 的唯一实质差异是 identity 新增两个 lineage 字段;v6 只改变写入版本戳。这些前代的行内 `ver/seq/val` 表示一致,`seq` 的数值含义未变([2026-08-31 seq/offset brands note](2026-08-31-session-sequence-and-log-offset-brands.zh.md) 明确 on-disk 数值不变)。v3→v4 是布局迁移,记录内容结构一致。v7 把 Session 格式代加入缓存 identity,因为无法从 domain 版本戳推导行语义。
 
 另有一种衍生形态:跑过一次 v5 版本的 v3 home(投毒态)——新树里存在**版本戳为 5 但内容是 v3 记录**(缺 lineage 字段)的文档。
 
@@ -32,43 +35,44 @@ v4→v5 的唯一实质差异是 identity 新增两个 lineage 字段;行内 `
 1. **`DomainSpec.compatibleVersions`(新增,可选)**:域 owner 声明"这些旧版本的存量记录在当前记录 schema 下也可读"(典型手段:新增字段标 optional)。`defineDomain` 校验各项为小于当前 version 的非负整数;`descriptorOf` 透传到后端 `KvUnitDescriptor`。
 2. **json 后端 per-record 读**:接受"当前版本 ∪ compatibleVersions"内的版本戳,集合外照旧视为 foreign 丢弃;**写路径永远戳当前版本**(读到旧记录后的下一次 checkpoint 自然把它推进到当前版本)。single 布局维持 exact-version 不变。
 3. **legacy bootstrap 版本把关(bug 修复本体)**:旧单文件的 `unit.version` 必须落在接受集合内才迁移,否则视为空 unit 留在原地——为 owner 未背书的记录打当前版本戳,会把"可丢弃的过期缓存"变成 domain 层的 schema 硬失败。
-4. **projcache 域声明 `version: 6, compatibleVersions: [3, 4, 5]`**;两个 lineage 字段改为 `.optional()`。唯一消费 stored identity 的读点 `identityMatches` 把缺失归一化为 unseeded lineage(`?? false` / `?? 0`):对非 fork 会话这是精确值;fork 会话的 expected 是 seeded → 天然 mismatch → 丢弃冷读重建,lineage 绑定的防护不放松
-5. **投毒态自愈**:v5 戳缺 lineage 字段的文档被声明为兼容,并由 optional schema 接受(内容本就是升级前的真实缓存数据),home 恢复可启动且标题立即可服务
+4. **projcache 域声明 `version: 7, compatibleVersions: [3, 4, 5, 6]`**;存储 schema 中的格式与 lineage identity 字段均为 optional,使 owner 背书的前代记录可以打开。当前写入始终包含这三个字段
+5. **identity 匹配比结构准入更严格**:缺失 `formatVersion` 的记录绝不匹配当前 Session,因此前代行不能播种投影,而会从权威日志重新折叠。格式匹配后,`identityMatches` 才把缺失 lineage 归一化为 unseeded(`?? false` / `?? 0`):对 unseeded 会话精确,对 seeded 期望则匹配失败。v5 投毒 home 因而可以安全启动,但其未绑定行不会作为当前值暴露
 6. **schema 校验兜底:`invalidRecords: 'backup-and-skip'`(仅本域声明)**。读兼容之外仍然解析失败的存量记录不再让整个域拒开:domain 层调用后端的 `KvUnit.backupRecord`(json per-record 实现=把文档改名为 `<key>.json.bak.<YYYYMMDDHHmm>`,字节留档、不再被读取),用 `logger.error` 打印具体失败信息(域名、表、键、移动去向、zod 失败原因),随后当该记录不存在继续启动;下一次冷读会重建并重写该会话的缓存。**该策略是域级显式声明,缺省仍为 fail-loud**——其他业务域的存量数据校验失败照旧整域拒载;后端没有 `backupRecord` 能力(single 布局、行存储)时也回退 fail-loud。命名沿革:quarantine → backup-and-skip(用户裁决:词要同时含"备份"与"跳过"两义,且与 `.bak` 后缀同源;skip-backup 因 CLI `--skip-X` 惯例存在"不备份"反读而弃用)。对本域而言,该策略取代了 [2026-07-28 存储恢复提案](../../proposed/architecture/2026-07-28-storage-root-and-derived-medium-recovery.zh.md)中 reset/destroy 的恢复途径;该提案对权威介质与整介质损坏仍然有效。
 
-### v5 → v6 兼容方式
+7. **predecessor title 是列表 hint,而不是 fold shortcut**:Session list 启动保持 metadata/cache-only,绝不打开冷 log body。log header 是权威来源;生命周期匹配的 checkpoint 是 durable prefix witness,可以落后但不能领先日志。因此 `cachedPredecessorTitle` 只公开仍通过当前 title unit `stateVersion` 与 schema 的 predecessor `title` row。两条相邻 Session format edge 都保留 title 文本。该 hint 使用 `asOfSeq: -1`,而不使用存储 row 的序号,因为改变日志事件数量的迁移会重新映射该坐标。其他 predecessor row 继续隐藏,`hydratePrepared`/`coldSnapshot` 仍要求严格格式 identity,因为即使物理存储一致,normalizer 仍可能改变 `blank` 或 `lastPromptAt` 等值。
 
-版本 6 只改变当前写入的版本戳,沿用 v5 记录 schema。`compatibleVersions: [3, 4, 5]` 因此同时接受健康的 v5 记录,以及错误 bootstrap 生成的 v5 戳、缺 lineage 记录。当前 schema 允许 lineage 缺失;`identityMatches` 将其解释为 unseeded,并对 seeded 会话拒绝该记录。下一次成功的 checkpoint 会用 v6 戳和完整 lineage 重写已接受的 v5 记录。启动时不单独运行 v5→v6 重写:未接受的版本读作不存在,schema 校验失败的已接受记录则执行 `backup-and-skip`。
+### v3-v6 → v7 处置
+
+版本 3 至 6 仍可结构化读取,因为它们的记录与行表示是当前 schema 的有效输入。其 identity 缺少 `formatVersion`,因此有意不能作为当前折叠捷径。unseeded 且生命周期匹配的记录仍可向零 I/O 列表提供版本兼容的 title,但不能提供任何权威 seed。冷读或实时检查点会从迁移后的 Session 日志重建值,并写入带完整格式与 lineage identity 的 v7 记录。启动时不运行 eager 值迁移;schema 校验失败的已接受记录执行 `backup-and-skip`。
 
 ### 升级矩阵
 
 | home 形态 | 修复后行为 |
 |---|---|
-| v3 单文件(未投毒) | bootstrap 迁移(3 ∈ 接受集)→ 标题立即可服务 |
-| v3 + 投毒新树 | 新树文档直接读入(optional 容忍)→ 启动恢复、标题立即可服务 |
-| v4 per-record | 文档直接读入(4 ∈ 接受集)→ 标题立即可服务 |
-| v5 正常 | 文档直接读入(5 ∈ 接受集)→ 标题立即可服务 |
-| v6 当前版本 | 不受影响 |
-| fork(seeded)会话的旧记录 | identity mismatch → 丢弃,打开会话时冷读重建(安全侧) |
+| v3 单文件(未投毒) | bootstrap 迁移(3 ∈ 接受集)→ 启动成功;兼容 title 在列表可见,未绑定折叠等待冷重建 |
+| v3 + 投毒新树 | optional 字段让新树文档可解析 → 启动恢复;兼容 title 在列表可见,未绑定折叠等待冷重建 |
+| v4/v5/v6 per-record | 文档结构化读入 → 缺格式代而拒绝 fold shortcut;兼容 title 在列表可见;当前检查点重写 v7 |
+| identity 匹配的 v7 当前记录 | 正常服务缓存值 |
+| 格式匹配但缺 lineage 的记录 | unseeded 调用方可以使用;seeded 调用方拒绝并回落冷折叠 |
 
 ## 备选方案
 
-- **只丢弃重建**(bootstrap 把关但不声明兼容版本):启动可修,但升级后 SessionList 标题全丢、要逐会话打开才恢复——不满足升级即用的产品要求。
+- **在存储层拒绝前代版本戳**:对投影安全,但会阻止受保护的 legacy bootstrap,也无法保留结构完好的记录直到权威重折叠替换它。结构准入加语义 identity 拒绝既让启动可恢复,也不服务未经证明的值。
+- **在所有用途中把缺失格式代视为当前代**:可以保留缓存值,但让迁移前折叠绕过 Session 格式边。不采用,因为 v0→v1 含有界历史规范化,后续边还可能改变事件基数。title-only 列表 hint 更窄:它不播种 fold,而且 title 文本在已安装 edge 之间保持不变。
 - **schema `.default()` 填缺省**:行为与 optional+读点归一化等价,但把"缺失=unseeded"的解释固化进 durable schema 的输出类型;拍板为 optional——schema 如实描述介质上所有被接受的形态,解释权在消费点(2026-09-02 用户裁决)。
 - **域版本回退到 4**:改动很小,但破坏版本单调性、依赖"bootstrap 不查版本"这个 bug 本身、且投毒态与正常 v5 home 的缓存全被丢弃。
 
 ## 影响
 
 - 部署方若把本域路由到 sqlite 后端,得不到任何容忍能力:sqlite 既未实现 `compatibleVersions` 也没有 `backupRecord`,行为退化为原有的严格版本语义(整 unit 版本不匹配仍 `version-mismatch` 拒开;不放松、不出错值)。shipped 组合固定路由 json,此风险仅存在于部署配置层面。
-- optional lineage 字段允许被接受的记录缺少 lineage:无 lineage 的记录会解码为 unseeded。身份比对仍会对 seeded 调用方拒收,逐行 `ver` 守卫仍筛查每个值,残余暴露面只是 unseeded 调用方读到 unseeded 形态的记录——与真实 pre-lineage 记录享有的信任完全相同
+- optional 格式字段允许前代记录通过结构校验,但缺失格式始终无法通过当前 identity 匹配。只有格式匹配后才归一化 optional lineage;seeded 调用方仍拒绝缺 lineage 的记录。逐行 `ver` 守卫继续筛查每个实际服务的值
 - `backupRecord` 对同一键的同一分钟内重复备份会覆盖前一份(新字节胜出);不同分钟、不同键永不冲突。
 
 ## 测试
 
 - `storage-json` 单测:compat 版本戳读入/集合外丢弃/写恒当前版本;legacy bootstrap 仅在版本被接受时迁移(含迁移后文档戳当前版本断言);`backupRecord` 移档/读缺席/重写/封闭守卫。
 - `storage-domain` 单测:`compatibleVersions`/`invalidRecords` 声明校验;后端无 `backupRecord` 时 backup-and-skip 回退 fail-loud。
-- `session-projection-cache` 单测:缺 lineage 字段的记录对 unseeded 会话按原值服务、对 seeded 会话丢弃。
-- **归档 fixtures 独立恢复测试**(`tests/fixtures.spec.ts` + `tests/fixtures/`):真实发布物产出的四份介质存档——`v3-single-unit.json`(0.1.1-rc.2 整域单文件)、`v4-session-doc.json`(0.1.2-alpha.3)、`v5-session-doc.json`(0.1.2-alpha.4)、`v5-lineageless-doc.json`(无守卫 bootstrap 的投毒形态,由 v3 记录合成)——逐一走真实存储栈开域,断言列表读出归档标题、且 live 写把文档重写为当前版本(v6 戳 + lineage 字段 + 新值);外加 schema 失败记录的 backup-and-skip 行为(启动不失败、`.bak` 落盘、日志具体、邻居记录不受累)。
-- 端到端验收,以真实发布物执行:已发布的 0.1.1-rc.2 与 0.1.2-alpha.3 npm 包经各自 web app 造数(真实模型对话 + rename RPC),已发布的 0.1.2-alpha.4 包复现两类故障(含投毒树),修复后构建对纯净 v3、投毒 v3、v4、全新四种 home 形态经 SessionList RPC 原样返回记录在案的标题。
+- `session-projection-cache` 单测:格式匹配且缺 lineage 的记录只服务 unseeded Session;缺格式代的前代记录不能播种 fold,但只能提供其兼容 title hint。
+- **归档 fixtures 独立恢复测试**(`tests/fixtures.spec.ts` + `tests/fixtures/`):真实发布物产出的四份介质存档——`v3-single-unit.json`(0.1.1-rc.2 整域单文件)、`v4-session-doc.json`(0.1.2-alpha.3)、`v5-session-doc.json`(0.1.2-alpha.4)、`v5-lineageless-doc.json`(无守卫 bootstrap 的投毒形态,由 v3 记录合成)——逐一走真实存储栈开域、只服务其兼容 predecessor title、绝不服务其未绑定折叠,随后由实时写入替换成带完整 identity 与新值的 v7 记录。同一套件还证明 schema 失败记录的 backup-and-skip:启动不失败、`.bak` 落盘、诊断点名失败,且邻近前代记录仍可重写。
 
-未来 bump 流程:新版本结构若可用"optional 字段 + 读点归一化"容忍旧记录,就把旧版本加入 `compatibleVersions`;否则正常 bump(丢弃重建),并把不再兼容的版本从集合中移除。无论哪条路,包 README 都要求 bump 随附归档 fixture 和论证所选处置方式的测试
+未来 bump 流程:只有当前存储 schema 能解析旧 domain 版本时,才把它加入 `compatibleVersions`,再由 owner reader 判断其语义 identity 是否充分。Session 格式变化绝不继承缺失的格式代。包 README 要求每次 bump 都随附归档 fixture 与测试,证明结构准入、语义使用或拒绝,以及当前重写

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-07-20-jsonl-storage-identity.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-20-jsonl-storage-identity.md
-2026-07-20-jsonl-storage-identity.md: e249640b1cd8900fdb7a136e9ab56abbf474ac86
-2026-07-20-jsonl-storage-identity.zh.md: 4775d6b7aa02abbd58ef89cdfa9377dc4f94b8de
+2026-07-20-jsonl-storage-identity.md: 5224ebf6bebc42b4b72e14243c1af6c02f366372
+2026-07-20-jsonl-storage-identity.zh.md: 05b7150edb99cdf336b9df90bc8e3cc1df9848a3

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-07-20-jsonl-storage-identity.md

@@ -10,7 +10,7 @@ JSONL lookup selects a physical log from the requested session id across project
 
 ## Decision
 
-`loadStored(id)` is the coordinator's single stored-prefix lookup. The JSONL backend scans every project directory, requires at most one matching encoded session directory with a transcript, parses that file, then validates `header.id === id` and that the selected path either equals `logPath(root, header.cwd, header.id)` or filesystem canonicalization resolves both spellings to the same transcript. `list()` applies the same path validation and rejects duplicate ids across project directories.
+`findLog(id)` is the JSONL provider's single physical resolver. It scans every project directory and requires at most one matching encoded session directory with a transcript. For a supported historical snapshot, the generation operation translates its header in memory, then validates `header.id === id` and that the selected path either equals `logPath(root, header.cwd, header.id)` or filesystem canonicalization resolves both spellings to the same transcript before any write. The fused `loadCurrentStored(id)` decodes the resulting current prefix without another file read; an already-current snapshot takes the no-write fast path and applies the same identity check during that decode. The ordinary current `loadStored(id)` hook retains the identity validation for coordinator fallbacks. `list()` applies the same path validation and rejects duplicate ids across project directories.
 
 The coordinator independently asserts the returned id and compares the stored cwd with a live session's cwd before repair, state publication, or suffix persistence. It keeps a detached copy of validated metadata; JSONL append and repair derive their path from that copy. The `PersistenceBackend<TornMarker>` interface therefore needs neither a scope-specific live lookup nor a storage-locator type.
 

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-07-20-jsonl-storage-identity.zh.md

@@ -10,7 +10,7 @@ JSONL 查找会根据请求的会话 id 在各个项目目录中选出物理日
 
 ## 决策
 
-`loadStored(id)` 是协调器唯一的已存前缀查找操作。JSONL 后端扫描所有项目目录,要求名称与该 id 的编码值匹配且其中包含 transcript(文本记录)的会话目录至多有一个,解析其中的 transcript,然后验证 `header.id === id`,并验证选定路径要么等于 `logPath(root, header.cwd, header.id)`,要么经文件系统路径规范化后,两种写法解析为同一份 transcript。`list()` 执行相同的路径验,并拒绝跨项目目录重复的 id。
+`findLog(id)` 是 JSONL provider 唯一的物理解析器。它扫描所有项目目录,并要求名称与该 id 的编码值匹配且其中包含 transcript(文本记录)的会话目录至多有一个。对于受支持的历史快照,generation 操作会在内存中转换其 header,然后验证 `header.id === id`,并验证选定路径要么等于 `logPath(root, header.cwd, header.id)`,要么经文件系统路径规范化后,两种写法解析为同一份 transcript;任何写入都必须在该检查之后。融合的 `loadCurrentStored(id)` 会在不再次读取文件的情况下解码所得当前前缀;已经是当前格式的快照采用无写入快速路径,并在该次解码中执行相同的身份检查。普通当前格式 `loadStored(id)` 钩子为协调器 fallback 保留身份校验。`list()` 执行相同的路径验,并拒绝跨项目目录重复的 id。
 
 协调器会独立断言返回的 id,并在修复、发布状态或持久化后缀之前比较已存 cwd 和活动会话的 cwd。协调器保留一份已验证元数据的独立副本;JSONL 的追加和修复操作根据该副本派生路径。因此,`PersistenceBackend<TornMarker>` 接口既不需要限定范围的活动会话查找,也不需要存储定位器类型。
 

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.md
-2026-07-21-compaction-summary-prefix-cache-reuse.md: 08ceb820ee415cbac9aded7a6b4f55933dc64cf3
-2026-07-21-compaction-summary-prefix-cache-reuse.zh.md: 2d796c8518c703d872ebcec4f548a0e52d91994b
+2026-07-21-compaction-summary-prefix-cache-reuse.md: a1066cd9925b37a05ea06c22b60b81d6107cd336
+2026-07-21-compaction-summary-prefix-cache-reuse.zh.md: 3f487f63250713570ccc7962ba0afab869bc656a

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.md

@@ -29,7 +29,7 @@ Auto-compaction always anchors at the surface head, so the shadowed region is th
 - **Keep the summarizer system prompt but reuse the rest** — rejected: the system slot is the very first token region a provider caches on, so a distinct summarizer system prompt invalidates the whole prefix regardless of what follows. Only moving the directive off the front recovers the cache.
 - **Send only the shadowed region without the `system`/`tools` head** — rejected: a differently-headed sequence still diverges from the cached request at the first token, so it caches no better while losing the framing the summary needs.
 - **Omit `tools` from the summarization request** (the model never calls one) — rejected: tool schemas are part of the cached token sequence; omitting them misaligns every following token and defeats reuse.
-- **A dedicated `assistant/chunk`-emitting summarization sub-session for snapshot replay** — rejected: the durable `compaction/summary` event records the successful local call's position and complete output, while its explicit call marker prevents replay from treating template or remote output as a local stream.
+- **A dedicated Agent-backed summarization sub-session for snapshot replay** — rejected: the durable `compaction/summary` event records the successful local call's position and complete output, while its explicit call marker prevents replay from treating template or remote output as a local stream. Creating a sub-session solely to obtain an Assistant settlement adds an unrelated lifecycle.
 
 ## Consequences
 

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.zh.md

@@ -29,7 +29,7 @@ Status: implemented
 - **保留摘要器系统提示词但复用其余部分**——否决:system 槽位正是提供方最先做缓存的 token 区域,因此一个不同的摘要器系统提示词无论后面跟着什么都会使整个前缀失效。只有把指令移离前端才能恢复缓存。
 - **只发送被遮蔽区域而不带 `system`/`tools` 头部**——否决:头部不同的序列在第一个 token 处仍然与已缓存请求分叉,因此缓存效果并不更好,反而丢失了摘要所需的框架。
 - **从摘要请求中省略 `tools`**(模型从不调用任何工具)——否决:工具 schema 是已缓存 token 序列的一部分;省略它们会让后续每个 token 失去对齐,破坏复用。
-- **为快照回放专门建立一个发出 `assistant/chunk` 的摘要子会话**——否决:持久的 `compaction/summary` 事件会记录成功本地调用的位置和完整输出,而显式调用标记可防止回放把模板或远程输出当作本地流
+- **为 snapshot replay 专门建立 Agent-backed summarization sub-session**——否决:持久 `compaction/summary` event 会记录成功本地调用的位置与完整输出,显式 call marker 可防止 replay 把 template 或 remote output 当作本地 stream。仅为获得 Assistant settlement 而创建 sub-session 会增加无关 lifecycle
 
 ## 后果
 

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-07-31-english-compaction-checkpoints.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-31-english-compaction-checkpoints.md
-2026-07-31-english-compaction-checkpoints.md: dc95ed187a6ba5b86800f06ebefb2776995f9421
-2026-07-31-english-compaction-checkpoints.zh.md: 1dc98b43e5bf2ca66cb3480b8124af6e7a9479b2
+2026-07-31-english-compaction-checkpoints.md: a1123b3d679a8bab2dbd9ecdc7b2079a78ffe28a
+2026-07-31-english-compaction-checkpoints.zh.md: 1543d87493a9cbfb28b18f00afa4ee4d3a074179

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-07-31-english-compaction-checkpoints.md

@@ -25,4 +25,4 @@ The requirement is integrated into the first sentence of the trailing compaction
 
 - New checkpoints normalize narrative context into English while retaining the exact strings that future tool use and code work depend on.
 - Existing checkpoint structure, compaction routing, and cache alignment are unchanged; only the final user instruction is different.
-- The direct summarization call remains outside transcript snapshots because it emits no `assistant/chunk` events. The real-loop regression instead asserts the exact final instruction received by the summarization request.
+- The direct summarization call remains outside transcript snapshots because it emits no Agent-owned Assistant settlement. The real-loop regression instead asserts the exact final instruction received by the summarization request.

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-07-31-english-compaction-checkpoints.zh.md

@@ -25,4 +25,4 @@ Status: implemented
 
 - 新检查点会将叙述性上下文规范化为英语,同时保留未来工具使用和代码工作所依赖的精确字符串。
 - 既有检查点结构、压缩路由和缓存对齐保持不变;只有最后一条 user 指令不同。
-- 直接摘要调用仍不纳入 transcript(文本记录)快照,因为它不会发出 `assistant/chunk` 事件。真实循环回归改为断言摘要请求收到的精确最终指令
+- 直接 summarization call 仍不纳入 transcript snapshot,因为它不会发出 Agent-owned Assistant settlement。真实 loop regression 改为断言 summarization request 收到的精确最终 instruction

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.md
-2026-08-10-subagent-empty-terminal-message-output.md: 24bab01ad844a5b48e0bf6fe0fc54df6403f4bb7
-2026-08-10-subagent-empty-terminal-message-output.zh.md: 28d8d85e316fe4770e55685e9d5641fda846c5c5
+2026-08-10-subagent-empty-terminal-message-output.md: 6ecb0ef254bd9734664aac71fe63c1eb3caf45b7
+2026-08-10-subagent-empty-terminal-message-output.zh.md: bcc6cad60fb5c791c328da1057e836cc1267d11b

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.md

@@ -6,11 +6,11 @@ English | [中文](2026-08-10-subagent-empty-terminal-message-output.zh.md)
 
 ## Problem
 
-The agent loop appends an empty-content `assistant/message` when a `max-tokens` step assembled only tool-call blocks because `BlockAssembler.blocks()` drops truncated tool calls; the message records usage only. Three consumers selected the child's output independently and treated that usage record as output. The in-process driver's `readResult` and the continuable Activation's `subagent/end` capture selected the last `assistant/message` without filtering, while the SDK backend's observer let any `assistant/message` take precedence over accumulated text. In a multi-step turn cut off at max-tokens, the final empty message caused the real partial answer to be omitted from `SubagentResult.output`, the tool result, telemetry, and `subagent/end.lastAssistantMessage`. The in-process driver also lacked a streamed-text fallback, so a cancelled child whose only text existed in `assistant/chunk` events reported `[]`.
+The agent loop appends an empty-content `assistant/message` when a `max-tokens` step assembled only tool-call blocks because `BlockAssembler.blocks()` drops truncated tool calls; the message retains its stream and usage but contributes no output blocks. Three consumers selected the child's output independently and treated that record as output. The in-process driver's `readResult` and the continuable Activation's `subagent/end` capture selected the last `assistant/message` without filtering, while the SDK backend's observer let any `assistant/message` take precedence over accumulated text. In a multi-step turn cut off at max-tokens, the final empty message caused the real partial answer to be omitted from `SubagentResult.output`, the tool result, telemetry, and `subagent/end.lastAssistantMessage`. The in-process driver also lacked a streamed-text fallback, so a cancelled child whose only text existed in an embedded Assistant stream reported `[]`.
 
 ## Decision
 
-`dsh-subagent` owns one canonical selection rule in `src/assistant-output.ts`: select the last non-empty assistant message; without one, select the accumulated `text-delta` stream; ignore empty-content messages. The incremental `AssistantOutputFold` implements the rule through `push(event)` for session-event transports, `pushText(text)` for chunk-only transports, and `collect()` for selection. `finalAssistantOutput(events)` applies it to a complete event suffix for the in-process `readResult` and Activation capture. The SDK backend folds notification events; the ACP backend exposes no complete assistant messages and folds raw chunk text. `SubagentResult.output` defines the result contract, and `subagent/end.lastAssistantMessage` uses the same rule. When a child produces neither form of output, the lifecycle field is absent rather than an empty array for both one-shot and continuable runs. A `max-tokens` or `aborted` result retains its actual stop reason.
+`dsh-subagent` owns one canonical selection rule in `src/assistant-output.ts`: select the last non-empty Assistant message; without one, select accumulated `text-delta` content from embedded `assistant/message` and `assistant/attempt` streams or a chunk-only transport; ignore empty-content messages. The incremental `AssistantOutputFold` implements the rule through `push(event)`, `pushText(text)`, and `collect()`. `finalAssistantOutput(events)` applies it to a complete event suffix for the in-process `readResult` and Activation capture. The SDK backend folds notification events; the ACP backend exposes no complete Assistant messages and folds raw chunk text. `SubagentResult.output` defines the result contract, and `subagent/end.lastAssistantMessage` uses the same rule. When a child produces neither form of output, the lifecycle field is absent rather than an empty array for both one-shot and continuable runs. A `max-tokens` or `aborted` result retains its actual stop reason.
 
 The foreground delegation tool uses the same selection. A non-`completed` result remains an `isError` tool result, but its message presents the optional safe Provider diagnostic owned by the [non-interactive permissions decision](../feature/2026-08-15-product-subagent-noninteractive-permissions.md) after the stop-reason headline and appends the child's partial text afterward. The parent model receives the failure, separate infrastructure detail, and available assistant output without conflating them.
 

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.zh.md

@@ -6,11 +6,11 @@ Status: implemented
 
 ## 问题
 
-当 `max-tokens` 步骤只组装了工具调用块时,agent loop(智能体循环)会追加一条空内容的 `assistant/message`,因为 `BlockAssembler.blocks()` 会丢弃被截断的工具调用;这条消息仅记录 usage。三个消费方独立选取子 agent 的输出,并把这条 usage 记录当成输出。进程内驱动的 `readResult` 与 continuable Activation 的 `subagent/end` capture 不加过滤地选取最后一条 `assistant/message`,SDK 后端的观察器则让任何 `assistant/message` 优先于累积的文本。在被 max-tokens 截断的多步轮次中,最后那条空消息导致 `SubagentResult.output`、工具结果、遥测与 `subagent/end.lastAssistantMessage` 都漏掉真实的部分回答。进程内驱动也没有流式文本兜底,因此被取消的子 agent 若其唯一文本只存在于 `assistant/chunk` 事件中,也会报告 `[]`。
+当 `max-tokens` step 只组装出 tool-call block 时,agent loop 会追加空 content `assistant/message`,因为 `BlockAssembler.blocks()` 会丢弃被截断的 tool call;该 message 保留 stream 与 usage,但不贡献 output block。三个消费方独立选取 child agent 输出,并把该 record 当成输出。进程内 driver 的 `readResult` 与 continuable Activation 的 `subagent/end` capture 不加过滤地选取最后一条 `assistant/message`,SDK backend observer 则让任何 `assistant/message` 优先于累计 text。在被 max-tokens 截断的多 step turn 中,最后的空 message 导致 `SubagentResult.output`、tool result、telemetry 与 `subagent/end.lastAssistantMessage` 漏掉真实 partial answer。进程内 driver 也缺少 streamed-text fallback,因此被取消 child 的唯一 text 若只存在于嵌入式 Assistant stream 中,也会报告 `[]`。
 
 ## 决策
 
-`dsh-subagent` 在 `src/assistant-output.ts` 中拥有唯一的规范选取规则:选取最后一条非空 assistant 消息;没有时选取累积的 `text-delta` 流;忽略空内容消息。增量的 `AssistantOutputFold` 通过 `push(event)` 处理会话事件传输,通过 `pushText(text)` 处理仅分片传输,并通过 `collect()` 完成选取。`finalAssistantOutput(events)` 把规则应用于完整的事件后缀,供进程内 `readResult` 与 Activation capture 使用。SDK 后端折叠通知事件;ACP 后端不暴露完整的 assistant 消息,而是折叠原始分片文本。`SubagentResult.output` 定义结果约定,`subagent/end.lastAssistantMessage` 使用同一规则。子 agent 不产生这两种输出中的任何一种时,一次性与 continuable 运行的生命周期字段都会缺省,而不是空数组。`max-tokens` 或 `aborted` 结果保留实际的终止原因
+`dsh-subagent` 在 `src/assistant-output.ts` 中拥有唯一规范选取规则:选取最后一条非空 Assistant message;没有时,从嵌入式 `assistant/message` 与 `assistant/attempt` stream 或 chunk-only transport 选取累计 `text-delta` content;忽略空 content message。增量 `AssistantOutputFold` 通过 `push(event)`、`pushText(text)` 与 `collect()` 实现该规则。`finalAssistantOutput(events)` 把规则应用于完整 event suffix,供进程内 `readResult` 与 Activation capture 使用。SDK backend 折叠 notification event;ACP backend 不公开完整 Assistant message,并折叠 raw chunk text。`SubagentResult.output` 定义 result contract,`subagent/end.lastAssistantMessage` 使用同一规则。child 不产生任一种输出时,一次性与 continuable run 的 lifecycle field 都缺省,而不是空 array。`max-tokens` 或 `aborted` result 保留实际 stop reason
 
 前台委派工具使用同一选取规则。非 `completed` 的结果仍是 `isError` 工具结果,但其消息会在终止原因标题之后呈现由[非交互权限决策](../feature/2026-08-15-product-subagent-noninteractive-permissions.zh.md)负责的可选安全提供方诊断,再附上子 agent 的部分文本。父模型会同时收到失败、独立的基础设施说明与已有 assistant 输出,而且不会把它们混为一体。
 

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md
-2026-08-13-bounded-cold-blank-verification.md: 8244b93641cfe576cd2f5b0c618ae69fbb215dd5
-2026-08-13-bounded-cold-blank-verification.zh.md: 2c7f392fa8fbd3fed0641fe2b95e717f76cd78fb
+2026-08-13-bounded-cold-blank-verification.md: 9aec2a2b7df17c3a41c1f30c4ccef0b5a7a37950
+2026-08-13-bounded-cold-blank-verification.zh.md: a8ffe326ef5a43ab9e17f70ee1bf440f2239c1b1

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md

@@ -14,7 +14,7 @@ The same cold list used the JSONL artifact mtime for `updatedAt`. Opening a Sess
 
 `dsh-api-session-controller` registers `sessionListMetadata`, a projection containing `blank` and `lastPromptAt`. The attached summary folds the same functions directly over the live log. `blank` changes only from true to false on `turn/start`; `lastPromptAt` changes only on a `user/message` whose source kind is `user`.
 
-A cold summary trusts cached `blank: false`, because a checkpoint prefix containing `turn/start` remains non-blank. Cached `blank: true` and a cache miss do not prove the current log is blank and are served `blank: false`, keeping the Session visible. The earlier physical-size probe — a `locate()` path plus a `coldBlankProbeMaxBytes` eligibility threshold gating an exact `readFrom(id, 0)` fold — is removed with the seam's path query ([export and pre-release trims](../simplification/2026-08-27-persistence-export-and-pre-release-trims.md)); persistence snapshot metadata (`eventCount`/`sizeBytes` on `stat()`/`list()`) is the reintroduction path for exact cold verification.
+A cold summary trusts cached `blank: false`, because a checkpoint prefix containing `turn/start` remains non-blank. A cache miss does not prove the current log is blank and is served `blank: false`, keeping the Session visible. The earlier physical-size probe — a `locate()` path plus a `coldBlankProbeMaxBytes` eligibility threshold gating an exact `readFrom(id, 0)` fold — is removed with the seam's path query ([export and pre-release trims](../simplification/2026-08-27-persistence-export-and-pre-release-trims.md)). Persistence snapshot hints do not reintroduce it: listing remains metadata/cache-only and never opens a cold body.
 
 `updatedAt` is the later of `createdAt` and `lastPromptAt`. A cache miss or stale checkpoint orders the Session too old rather than promoting it from an unrelated file write.
 

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.zh.md

@@ -14,7 +14,7 @@ Web 会话树会隐藏空白 Session,并把当前选中的空白项复用为 N
 
 `dsh-api-session-controller` 注册 `sessionListMetadata` 投影,其中包含 `blank` 与 `lastPromptAt`。已附加摘要直接用同一组函数折叠实时日志。`blank` 只在 `turn/start` 时从 true 单调变为 false;`lastPromptAt` 只在来源 kind 为 `user` 的 `user/message` 上更新。
 
-冷摘要信任缓存的 `blank: false`,因为已包含 `turn/start` 的 checkpoint 前缀会始终保持非空。缓存的 `blank: true` 和 cache miss 无法证明当前日志为空,因而按 `blank: false` 提供,让 Session 保持可见。早先的物理大小探测——`locate()` 路径加上门控一次精确 `readFrom(id, 0)` 折叠的 `coldBlankProbeMaxBytes` 资格阈值——随该 seam 的路径查询一并移除([导出与预发布裁剪](../simplification/2026-08-27-persistence-export-and-pre-release-trims.zh.md));persistence 快照元数据(`stat()`/`list()` 上的 `eventCount`/`sizeBytes`)是重新引入精确冷验证的路径
+冷摘要信任缓存的 `blank: false`,因为已包含 `turn/start` 的 checkpoint 前缀会始终保持非空。cache miss 无法证明当前日志为空,因而按 `blank: false` 提供,让 Session 保持可见。早先的物理大小探测——`locate()` 路径加上门控一次精确 `readFrom(id, 0)` 折叠的 `coldBlankProbeMaxBytes` 资格阈值——随该 seam 的路径查询一并移除([导出与预发布裁剪](../simplification/2026-08-27-persistence-export-and-pre-release-trims.zh.md))。Persistence 快照提示不会重新引入它:listing 保持 metadata/cache-only,绝不打开冷正文
 
 `updatedAt` 取 `createdAt` 与 `lastPromptAt` 中较晚者。cache miss 或陈旧 checkpoint 只会让 Session 排得偏旧,而不会因无关的文件写入被提升。
 

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-20-turn-error-survives-same-turn-retry-history.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-20-turn-error-survives-same-turn-retry-history.md
-2026-08-20-turn-error-survives-same-turn-retry-history.md: 68b09fc32f2d409a9060bb8e53f11837dd365657
-2026-08-20-turn-error-survives-same-turn-retry-history.zh.md: 1aa78aa405113fecc890fb2213f41cf41afec6b4
+2026-08-20-turn-error-survives-same-turn-retry-history.md: 9280d59d020a88d0f0f703d2e2adc54656687b16
+2026-08-20-turn-error-survives-same-turn-retry-history.zh.md: 1856d72e05c6261535df5f94c41cb3ff86c69362

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-08-20-turn-error-survives-same-turn-retry-history.md

@@ -28,4 +28,4 @@ The Definition suite drives the real assembler through a same-turn retry chain e
 
 ## Consequences
 
-Exhausted recovery now leaves durable, replayable feedback: the red terminal row with the display-safe message and code, plus the collapsed retry chain as recovery context. Session logs recorded under the retired new-turn retry model would render one `turn-error` row per failed turn on replay; the pre-release format stance accepts that, and no shipped log producer has emitted that shape since same-turn retries landed.
+Exhausted recovery now leaves durable, replayable feedback: the red terminal row with the display-safe message and code, plus the collapsed retry chain as recovery context. Session logs recorded under the retired new-turn retry model render one `turn-error` row per failed turn on replay; the v0-to-v1 edge preserves those terminal facts, and no shipped log producer has emitted that shape since same-turn retries landed.

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-08-20-turn-error-survives-same-turn-retry-history.zh.md

@@ -28,4 +28,4 @@ Definition 套件驱动真实 assembler 走完同轮次重试链并以 error 原
 
 ## 影响
 
-恢复耗尽后现在留下持久、可回放的反馈:红色终态行带展示安全的消息与错误码,加上折叠的重试链作为恢复上下文。按已退役的"新轮次重试"模型录制的会话日志在回放时会为每个失败轮次各渲染一条 `turn-error` 行;预发布格式立场接受这一点,且自同轮次重试落地以来,没有任何已交付的日志生产方再发出过那种形态
+恢复耗尽后现在留下持久、可回放的反馈:红色终态行带展示安全的消息与错误码,加上折叠的重试链作为恢复上下文。按已退役的“新轮次重试”模型录制的会话日志在回放时会为每个失败轮次各渲染一条 `turn-error` 行;v0-to-v1 迁移边会保留这些终态事实,且自同轮次重试落地以来,没有任何已交付的日志生产方再发出过那种结构

+ 2 - 2
.agents/notes/implemented/feature/2026-07-10-sqlite-session-query-provider.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-10-sqlite-session-query-provider.md
-2026-07-10-sqlite-session-query-provider.md: 76ebbc24e16a9429be63d0e45ec84b14c29d43f6
-2026-07-10-sqlite-session-query-provider.zh.md: 6d8518252d0a79906fd9438876d4a92ba414363e
+2026-07-10-sqlite-session-query-provider.md: 46af02d39ed687a1f4b418109a1da4b071b9e409
+2026-07-10-sqlite-session-query-provider.zh.md: ccb833886a459c5a6888637c6cb8354e9e9f7977

+ 3 - 3
.agents/notes/implemented/feature/2026-07-10-sqlite-session-query-provider.md

@@ -34,13 +34,13 @@ Both persistent and live FTS5 tables use `unicode61`. The implementation experim
 
 The shared extractor includes message text, reasoning, nested tool-call/result content, tool names and arguments, blocked-prompt reasons, todo status/content, and error or terminal status detail. Structural boundaries, stream chunks, request headers, successful completion markers, and unknown declaration-merged event/content variants produce no document. Surface classification reuses `foldSurface()` so search agrees with model-history derivation.
 
-One serialized operation reads the provider-neutral `SessionPersistence` snapshot listing, compares each source-qualified opaque revision with the revision stored beside the indexed session, loads only new or changed logs, reconciles rows in one transaction, and executes the query. It passes the caller's exact abort signal into snapshot listing and non-mutating inspection, directly awaits every started backend operation, and checks cancellation after each await and before starting more work. Cancellation therefore rejects only after active backend work is quiescent, starts no subsequent observation or reconciliation step, and keeps a following search serialized behind cleanup even if a backend ignores the signal. The operation never calls the backend's mutating `load()` for an id currently owned by `ctx.sessions`; the TEMP overlay records persisted availability, and the durable base refreshes after the live owner detaches. A revision identifies its backing persistence store as well as the backend-local log revision, so reopening against the same store reuses indexed rows while switching to an independent store cannot collide on a session id and local counter. Observation repeats when listing changes during a load; this incorporates a mutating load repair's refreshed revision before commit. Repeated queries and unchanged reopen load no full persisted logs. New, changed, and deleted sessions update on the next stable search. A source or extraction failure cannot mark a row current, and a transaction failure rolls back so a later search retries.
+One serialized operation reads the provider-neutral `SessionPersistence` snapshot listing, compares each source-qualified opaque revision with the revision stored beside the indexed session, loads only new or changed logs, reconciles rows in one transaction, and executes the query. It passes the caller's exact abort signal into snapshot listing and inspection, awaits each observer operation, and checks cancellation after each await and before starting more work. Already-current inspection keeps recovery in memory; a supported historical body read may first publish migration and repair. Cancellation settles this query observer without starting another observation or reconciliation step; a shared persistence load or migration already admitted for another observer may continue behind its per-id chain. The operation never calls the backend's mutating `load()` for an id currently owned by `ctx.sessions`; the TEMP overlay records persisted availability, and the durable base refreshes after the live owner detaches. A revision identifies its backing persistence store as well as the backend-local log revision, so reopening against the same store reuses indexed rows while switching to an independent store cannot collide on a session id and local counter. Observation repeats when listing changes during a load; this incorporates migration or mutating load repair's refreshed revision before commit. Repeated queries and unchanged reopen load no full persisted logs. New, changed, and deleted sessions update on the next stable search. A source or extraction failure cannot mark a row current, and a transaction failure rolls back so a later search retries.
 
 Persisted documents survive restarts. Live sessions use connection-local TEMP tables, shadow the persisted base for the same id, and reveal that base on detach. Closing the database drops live rows. Unmounting persistence hides durable rows without treating absence as authoritative deletion; remounting observes and reconciles the backend again. Conflicting immutable live and durable headers fail rather than combining sources.
 
 The derived schema has its own application id and monotonic schema version. Persistent and TEMP session metadata store the integer `SessionHeader.createdAt` contract in strict `INTEGER` columns. A recognized incompatible version resets only this derived database. A database with a foreign application id or unrecognized user tables is refused before journal-mode mutation, which prevents an accidentally configured canonical session database from being changed. On POSIX filesystems, missing directories and database files are created owner-only so new SQLite sidecars inherit that mode; existing modes are preserved. One service in one process exclusively owns a derived-index path; cross-process writers are unsupported because generations and live TEMP shadow state are connection-owned.
 
-Cancellation rejects queued operations promptly. Once asynchronous source observation starts, the caller waits for that backend promise to settle before rejection, without committing an aborted observation or starting more source/index work. Node's synchronous `DatabaseSync` metadata and MATCH calls cannot be interrupted once executing on the JavaScript thread, so the service checks the signal around those calls but does not promise mid-statement preemption.
+Cancellation rejects queued operations promptly. Once source observation starts, cancellation rejects the query observer without committing its result or starting more source/index work; persistence may keep a shared cold load or migration alive for another observer. Node's synchronous `DatabaseSync` metadata and MATCH calls cannot be interrupted once executing on the JavaScript thread, so the service checks the signal around those calls but does not promise mid-statement preemption.
 
 ## Alternatives considered
 
@@ -54,6 +54,6 @@ Cancellation rejects queued operations promptly. Once asynchronous source observ
 
 Search has a small provider-neutral API while its only backend owns every derived-index state transition. The separate database adds configuration and a lightweight snapshot read before queries, but index corruption, reset, and tokenizer changes cannot endanger canonical logs. Durable revisions avoid full-log reads and rewrites for unchanged sessions; TEMP live overlays preserve current-session truth without making uncheckpointed events durable.
 
-The chosen tokenizer supports short tokens with a smaller index but does not promise substring recall. Literal phrases make query syntax safe and predictable at the cost of excluding boolean/full MATCH expressions. Cancellation is prompt while queued and quiescent while awaiting sources; synchronous SQLite execution remains a non-preemptible section bracketed by signal checks.
+The chosen tokenizer supports short tokens with a smaller index but does not promise substring recall. Literal phrases make query syntax safe and predictable at the cost of excluding boolean/full MATCH expressions. Cancellation is prompt while queued and observer-local during shared source work; synchronous SQLite execution remains a non-preemptible section bracketed by signal checks.
 
 Unit coverage pins extraction, filters, both search scopes, all default surfaces, metadata-before-ranking, snippets, literal escaping, deterministic ties, complete pagination, scoped cursor invalidation, dynamic persistence mount/unmount, restart reconciliation, live shadow/reveal/reopen, schema safety, rollback retry, and queued/in-flight source-wait cancellation. A keyless real-Loader-path test combines the derived SQLite index with the real JSONL persistence provider.

+ 3 - 3
.agents/notes/implemented/feature/2026-07-10-sqlite-session-query-provider.zh.md

@@ -34,13 +34,13 @@ Service Definition 包还拥有共享的第一方语义提取与提供方无关
 
 共享提取器会提取消息文本、推理(reasoning)、嵌套的工具调用/结果内容、工具名称与参数、被阻止提示词的原因、待办事项状态与内容,以及错误或结束状态详情。结构性边界、流分片、请求头、成功完成标记,以及通过声明合并扩展的未知事件/内容变体都不会产生文档。surface 分类复用 `foldSurface()`,使搜索与模型历史派生保持一致。
 
-一个串行化操作会读取提供方无关的 `SessionPersistence` 快照清单,将每个包含源身份的不透明修订号与同已索引会话一并存储的修订号比较,只加载新增或变更的日志,在一个事务中对齐各行,然后执行查询。它将调用方传入的原始中止信号传给快照清单查询和不会修改状态的检查,直接等待每个已启动的后端操作,并在每次等待结束后、启动更多工作之前检查取消状态。因此,即使后端忽略该信号,取消也只会在活跃的后端工作完全停稳后才拒绝,不会启动后续观察或对齐步骤;后续搜索仍会串行等待清理完成。它绝不会调用后端会修改状态的 `load()` 来处理当前由 `ctx.sessions` 拥有的 id;TEMP 覆盖层会记录持久化可用性,实时所有者分离后,持久化基础层随之刷新。修订号同时标识其底层持久化存储与后端本地日志修订版本,因此针对同一存储重新打开服务可以复用已索引行,而切换到独立存储时不会因会话 id 与本地计数器相同而发生冲突。如果加载期间清单发生变化,系统会重复观察;因此,会修改状态的加载修复所产生的新修订号会在提交前纳入结果。重复查询与针对未变更存储的重新打开都不会加载完整的持久化日志。新增、变更与删除的会话会在下一次稳定搜索中更新。源读取或提取失败时,不能将相应行标记为最新状态,事务失败则会回滚,使后续搜索能够重试。
+一个串行化操作会读取提供方无关的 `SessionPersistence` 快照清单,将每个包含源身份的不透明修订号与同已索引会话一并存储的修订号比较,只加载新增或变更的日志,在一个事务中对齐各行,然后执行查询。它将调用方传入的原始中止信号传给快照清单查询与检查,等待每项观察操作,并在每次等待结束后、启动更多工作之前检查取消状态。已经是当前格式的检查只在内存中保留恢复;受支持的历史正文读取可能先发布迁移与修复。取消会结算本次查询观察方,且不启动后续观察或对齐步骤;已经为其他观察方接纳的共享持久化加载或迁移仍可能在其逐 id 链后继续。它绝不会调用后端会修改状态的 `load()` 来处理当前由 `ctx.sessions` 拥有的 id;TEMP 覆盖层会记录持久化可用性,实时所有者分离后,持久化基础层随之刷新。修订号同时标识其底层持久化存储与后端本地日志修订版本,因此针对同一存储重新打开服务可以复用已索引行,而切换到独立存储时不会因会话 id 与本地计数器相同而发生冲突。如果加载期间清单发生变化,系统会重复观察;因此,迁移或会修改状态的加载修复所产生的新修订号会在提交前纳入结果。重复查询与针对未变更存储的重新打开都不会加载完整的持久化日志。新增、变更与删除的会话会在下一次稳定搜索中更新。源读取或提取失败时,不能将相应行标记为最新状态,事务失败则会回滚,使后续搜索能够重试。
 
 持久化文档在重启后仍然存在。实时会话使用连接本地的 TEMP 表,遮蔽相同 id 的持久化基础行,并在实时所有者分离时重新显露该基础行。关闭数据库会删除实时行。卸载持久化服务会隐藏持久化行,但不会把缺失视为权威删除;重新挂载后,系统会再次观察并对齐后端。实时会话头与持久化会话头的不可变字段发生冲突时,系统会失败,而不会合并两个来源。
 
 派生 schema 拥有独立的 application id 与单调递增的 schema 版本。持久化与 TEMP 会话元数据均遵循 `SessionHeader.createdAt` 的整数约定,将其存入严格的 `INTEGER` 列。系统识别到不兼容版本时,只会重置该派生数据库。如果数据库具有不属于本应用的 application id 或无法识别的用户表,系统会在修改日志模式前拒绝该数据库,防止意外配置的规范会话数据库遭到修改。在 POSIX 文件系统上,缺失的目录与数据库文件会以仅所有者可访问的权限创建,使新的 SQLite 伴随文件沿用该模式;现有权限模式保持不变。一个进程中的一个服务独占一条派生索引路径;代际与实时 TEMP 遮蔽状态都归连接所有,因此不支持跨进程写入方。
 
-取消会使排队中的操作及时被拒绝。异步源观察一旦开始,调用方必须等待该后端 Promise 结算后才会收到拒绝;系统不会提交已中止的观察结果,也不会启动更多源观察或索引工作。Node 的同步 `DatabaseSync` 元数据与 MATCH 调用一旦开始在 JavaScript 线程上执行就无法中断,因此服务会在这些调用前后检查信号,但不承诺在语句执行期间抢占。
+取消会使排队中的操作及时被拒绝。源观察开始后,取消会拒绝查询观察方,不提交其结果,也不启动更多源观察或索引工作;持久化层仍可能为其他观察方保留共享 cold load 或迁移。Node 的同步 `DatabaseSync` 元数据与 MATCH 调用一旦开始在 JavaScript 线程上执行就无法中断,因此服务会在这些调用前后检查信号,但不承诺在语句执行期间抢占。
 
 ## 曾考虑的替代方案
 
@@ -54,6 +54,6 @@ Service Definition 包还拥有共享的第一方语义提取与提供方无关
 
 搜索只公开精简的提供方无关 API,而唯一后端负责派生索引的全部状态转换。独立数据库增加了配置与查询前的轻量快照读取,但索引损坏、重置与分词器变更都不会危及规范日志。持久化修订号使未变更会话无需读取或重写完整日志;TEMP 实时覆盖层保留当前会话事实,同时不会让尚未经过检查点的事件具有持久性。
 
-选定的分词器以较小的索引体积支持短 token,但不承诺子串召回。字面短语使查询语法安全且可预测,代价是不支持布尔表达式或完整 MATCH 表达式。取消在操作排队期间会及时生效,在等待数据源期间则会等待其完全停稳;同步 SQLite 执行仍是不可抢占区段。
+选定的分词器以较小的索引体积支持短 token,但不承诺子串召回。字面短语使查询语法安全且可预测,代价是不支持布尔表达式或完整 MATCH 表达式。取消在排队期间及时生效,在共享源工作期间只影响当前观察方;同步 SQLite 执行仍是不可抢占区段。
 
 单元测试将以下行为固化为约定:提取、过滤器、两种搜索范围、所有默认 surface、先过滤元数据再排序、摘要片段、字面量转义、确定性平局处理、完整分页、按范围的游标失效、动态挂载/卸载持久化服务、重启对齐、实时遮蔽、显露与重新打开、schema 安全、回滚重试,以及排队中或进行中的数据源等待取消。一个无需密钥的真实 Loader 路径测试会把派生 SQLite 索引与真实 JSONL 持久化 provider 组合使用。

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است