From 56c4c3e47c195ff5edbfe3d307bdef81f3de348b Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Wed, 9 Sep 2026 16:49:47 +0800 Subject: [PATCH] docs(session): centralize format status and timeless guidance --- .../2026-06-14-session-persistence.i18n.yaml | 4 +- .../2026-06-14-session-persistence.md | 4 +- .../2026-06-14-session-persistence.zh.md | 4 +- ...handle-based-session-persistence.i18n.yaml | 4 +- ...-08-27-handle-based-session-persistence.md | 2 +- ...-27-handle-based-session-persistence.zh.md | 2 +- ...historical-unknown-event-refusal.i18n.yaml | 4 +- ...-alpha-historical-unknown-event-refusal.md | 2 +- ...pha-historical-unknown-event-refusal.zh.md | 2 +- ...leased-session-format-migrations.i18n.yaml | 4 +- ...8-31-released-session-format-migrations.md | 2 +- ...1-released-session-format-migrations.zh.md | 2 +- ...02-system-prompt-as-surface-node.i18n.yaml | 4 +- ...026-09-02-system-prompt-as-surface-node.md | 2 +- ...-09-02-system-prompt-as-surface-node.zh.md | 2 +- ...2026-08-10-npm-release-sequences.i18n.yaml | 4 +- .../2026-08-10-npm-release-sequences.md | 2 +- .../2026-08-10-npm-release-sequences.zh.md | 2 +- ...8-24-session-log-snapshot-corpus.i18n.yaml | 4 +- .../2026-08-24-session-log-snapshot-corpus.md | 4 +- ...26-08-24-session-log-snapshot-corpus.zh.md | 4 +- AGENTS.md | 2 +- docs/AGENTS.md | 2 +- .../adding-a-session-format-version.i18n.yaml | 4 +- .../adding-a-session-format-version.md | 18 +-- .../adding-a-session-format-version.zh.md | 18 +-- ...deepseek-llm-api-wire-extensions.i18n.yaml | 4 +- docs/deepseek-llm-api-wire-extensions.md | 4 +- docs/deepseek-llm-api-wire-extensions.zh.md | 4 +- docs/session-format-status.i18n.yaml | 6 + docs/session-format-status.md | 47 ++++++ docs/session-format-status.zh.md | 47 ++++++ docs/subsystems/persistence.i18n.yaml | 4 +- docs/subsystems/persistence.md | 2 +- docs/subsystems/persistence.zh.md | 2 +- docs/subsystems/session.i18n.yaml | 4 +- docs/subsystems/session.md | 2 +- docs/subsystems/session.zh.md | 2 +- docs/testing.i18n.yaml | 4 +- docs/testing.md | 2 +- docs/testing.zh.md | 2 +- packages/core/session/README.i18n.yaml | 4 +- packages/core/session/README.md | 2 +- packages/core/session/README.zh.md | 2 +- packages/core/session/src/types.ts | 2 +- .../session-format-catalog/README.i18n.yaml | 4 +- .../session/session-format-catalog/README.md | 2 +- .../session-format-catalog/README.zh.md | 2 +- .../session-format-v2-to-v3/README.i18n.yaml | 4 +- .../session/session-format-v2-to-v3/README.md | 2 +- .../session-format-v2-to-v3/README.zh.md | 2 +- .../README.i18n.yaml | 4 +- .../session-persistence-jsonl/README.md | 2 +- .../session-persistence-jsonl/README.zh.md | 2 +- .../session-persistence-jsonl/src/format.ts | 4 +- .../session-snapshot/README.i18n.yaml | 4 +- .../test-support/session-snapshot/README.md | 6 +- .../session-snapshot/README.zh.md | 6 +- scripts/doc-standard.spec.ts | 147 +++++++++++++++++- 59 files changed, 345 insertions(+), 100 deletions(-) create mode 100644 docs/session-format-status.i18n.yaml create mode 100644 docs/session-format-status.md create mode 100644 docs/session-format-status.zh.md diff --git a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.i18n.yaml index 8771aecc10..f827897a1e 100644 --- a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.i18n.yaml +++ b/.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: afe372c51f5056d02bdf3a17157ef755dd02e3f8 -2026-06-14-session-persistence.zh.md: 1fe84d955fe8878b715b3d23f05ecfc1fe5bbc78 +2026-06-14-session-persistence.md: d79975e1fedb6efcd0e4ea83bc799bb158082e41 +2026-06-14-session-persistence.zh.md: f70b8005a254cc6a9ea8ada31c32e5552ee0c8b2 diff --git a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.md b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.md index afe372c51f..d79975e1fe 100644 --- a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.md +++ b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.md @@ -19,7 +19,7 @@ Persistence is a **capability seam** with an abstract Service Definition ([capab Key durable, contested choices: -- **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. +- **The canonical durable log persists every current `SessionEvent` losslessly.** 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](../../../../packages/session/session-checkpoint-policy/README.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](../../archived/simplification/2026-06-19-drop-mutable-session-summary.md).) @@ -29,7 +29,7 @@ Key durable, contested choices: 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`; 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. +Format versioning: the header carries a `version`; handles expose only the logical format selected by `SESSION_FORMAT_VERSION` ([version authority](../../../../docs/session-format-status.md)). JSONL event-body reads compose the complete static adjacent migration chain before returning a handle; each edge owns its historical transformations. 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 diff --git a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md index 1fe84d955f..f70b8005a2 100644 --- a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md @@ -19,7 +19,7 @@ Status: implemented 长期有效、存在争议的关键选择: -- **规范持久日志无损保留每个当前 `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)会显式执行该改写,而不是过滤规范日志。 +- **规范持久日志无损保留每个当前 `SessionEvent`。** 一个 `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)会显式执行该改写,而不是过滤规范日志。 - **仅追加;崩溃的轮次被关闭,而非截断。** 已刷写的事件永不被重写。[语义检查点策略](../../../../packages/session/session-checkpoint-policy/README.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 后来因属于死状态而被移除——见 [移除可变会话摘要](../../archived/simplification/2026-06-19-drop-mutable-session-summary.md)。) @@ -29,7 +29,7 @@ Status: implemented 上述每个关键选择都在陈述处记录了被否决的替代方案:**过滤 stream 的规范日志**会丢失 attempt 证据,而未通过显式迁移移除事件会破坏连续序号;**截断崩溃的轮次**会静默销毁长时间自主运行中的真实工作;**日志内 `session/meta` 事件作为第 0 行**——元数据不是可回放状态;**有限的非整数 `createdAt` 值**没有生产方,且与整数 Unix 毫秒存储不一致;**将 `sessionPersistence` 硬注入循环**会让非持久化的演示永远挂起。 -格式版本控制: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 必须定义自己的断电与恢复约定。 +格式版本控制:header 携带 `version`;句柄只暴露由 `SESSION_FORMAT_VERSION` 选定的逻辑格式([版本真源](../../../../docs/session-format-status.zh.md))。JSONL 的事件正文读取会在返回句柄前组合完整的静态相邻迁移链;每条迁移边拥有自身的历史转换。V0 保留无后缀的 `session.jsonl[.zstd]`,正版本则使用不可变的小写 `session.vN.jsonl[.zstd]` 名称([已发布 Session 迁移](2026-08-31-released-session-format-migrations.zh.md))。当前 generation 的 append 与 flush 能稳健处理不完整尾部写入;未来 provider 或 WAL 必须定义自己的断电与恢复约定。 ## 后果 diff --git a/.agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.i18n.yaml index 6d86465557..0521855ea8 100644 --- a/.agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.i18n.yaml +++ b/.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: 9b5ff5d46444ac924859c4121abc1cf5d1538485 -2026-08-27-handle-based-session-persistence.zh.md: c9230c899141f4ab9979f93256da42a3aaae0984 +2026-08-27-handle-based-session-persistence.md: e2f07856b7ef8ffd8180ed7ff515210c95dde29b +2026-08-27-handle-based-session-persistence.zh.md: 472a1024887ecca67245de27f52c68ec0b34caa1 diff --git a/.agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.md b/.agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.md index 9b5ff5d464..e2f07856b7 100644 --- a/.agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.md +++ b/.agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.md @@ -32,7 +32,7 @@ The previous persistence seam owned far more than storage. A shared coordinator ## Consequences -Resume, fork, subagent, ACP, webhook, and SDK sessions all persist through one explicit acquisition point, and dispose provably releases write ownership (reopening for write succeeds after teardown). The costs: a backend plugin reload under live sessions invalidates their handles — writes fail loudly until the sessions restart, where adoption previously re-attached silently; `ctx.sessions.create` + `flush` in a test persists nothing without a handle (tests seed through `create`/`append`/`close`); resume re-reads a cold log only when no immediately preceding observation parsed the same artifact — a bounded provider-local memo (session id + stat revision, invalidated by every local mutation) serves the observe-then-promote and authorize-then-resume handoffs without restoring the deleted borrow/reservation lifecycle, and the session-query reader's own prepared cache remains the pin-capable layer above it (a later consolidation may fold one into the other); and an empty created session is invisible to other processes until an explicit flush (ACP forces one for its resumable-empty-session promise). `SESSION_FORMAT_VERSION` stays 0. +Resume, fork, subagent, ACP, webhook, and SDK sessions all persist through one explicit acquisition point, and dispose provably releases write ownership (reopening for write succeeds after teardown). The costs: a backend plugin reload under live sessions invalidates their handles — writes fail loudly until the sessions restart, where adoption previously re-attached silently; `ctx.sessions.create` + `flush` in a test persists nothing without a handle (tests seed through `create`/`append`/`close`); resume re-reads a cold log only when no immediately preceding observation parsed the same artifact — a bounded provider-local memo (session id + stat revision, invalidated by every local mutation) serves the observe-then-promote and authorize-then-resume handoffs without restoring the deleted borrow/reservation lifecycle, and the session-query reader's own prepared cache remains the pin-capable layer above it (a later consolidation may fold one into the other); and an empty created session is invisible to other processes until an explicit flush (ACP forces one for its resumable-empty-session promise). Handle ownership does not change the serialized Session representation. ## Related diff --git a/.agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.zh.md b/.agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.zh.md index c9230c8991..472a102488 100644 --- a/.agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.zh.md @@ -32,7 +32,7 @@ Status: implemented ## 后果 -恢复、fork、subagent、ACP、webhook 与 SDK 会话全部经由一个显式获取点持久化,且 dispose 可证明地释放写所有权(teardown 之后重新以写模式打开可以成功)。代价:在有活跃会话时重载后端插件会使它们的句柄失效——写入会响亮地失败,直到会话重启,而以前接管会静默重连;测试中 `ctx.sessions.create` + `flush` 在没有句柄时什么也不持久化(测试通过 `create`/`append`/`close` 播种);只有当紧邻其前没有观察读解析过同一产物时,恢复才重新读取冷日志——一个有界的 provider 内部 memo(按会话 id + stat 修订号,任何本地修改都使其失效)服务观察后提升与授权后恢复这两类交接,而不恢复已删除的 borrow/reservation 生命周期;session-query reader 自己的已准备缓存仍是其上方具备 pin 能力的一层(后续可考虑二者收敛);空的已创建会话在显式 flush 之前对其他进程不可见(ACP 为其可恢复空会话承诺强制执行一次 flush)。`SESSION_FORMAT_VERSION` 保持为 0。 +恢复、fork、subagent、ACP、webhook 与 SDK 会话全部经由一个显式获取点持久化,且 dispose 可证明地释放写所有权(teardown 之后重新以写模式打开可以成功)。代价:在有活跃会话时重载后端插件会使它们的句柄失效——写入会响亮地失败,直到会话重启,而以前接管会静默重连;测试中 `ctx.sessions.create` + `flush` 在没有句柄时什么也不持久化(测试通过 `create`/`append`/`close` 播种);只有当紧邻其前没有观察读解析过同一产物时,恢复才重新读取冷日志——一个有界的 provider 内部 memo(按会话 id + stat 修订号,任何本地修改都使其失效)服务观察后提升与授权后恢复这两类交接,而不恢复已删除的 borrow/reservation 生命周期;session-query reader 自己的已准备缓存仍是其上方具备 pin 能力的一层(后续可考虑二者收敛);空的已创建会话在显式 flush 之前对其他进程不可见(ACP 为其可恢复空会话承诺强制执行一次 flush)。句柄所有权不改变序列化的 Session 表示。 ## 相关 diff --git a/.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.i18n.yaml index d018fd81f7..c976a60147 100644 --- a/.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-31-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 +2026-08-31-alpha-historical-unknown-event-refusal.md: c63b36f63206e0992416239483d908b0645a98c2 +2026-08-31-alpha-historical-unknown-event-refusal.zh.md: 231c46c11486b05a69a200b857a0ea6df35b6e79 diff --git a/.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.md b/.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.md index 58690e3028..c63b36f632 100644 --- a/.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.md +++ b/.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.md @@ -14,7 +14,7 @@ Silently copying such an event can leave stale numeric references after a later 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. +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. Native current-format 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. diff --git a/.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.zh.md b/.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.zh.md index 73ab2ca47a..231c46c114 100644 --- a/.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.zh.md @@ -14,7 +14,7 @@ Status: implemented 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,但不会自动获得未来格式迁移能力。 +该规则只适用于跨越历史格式迁移边。普通当前格式读取保留既有信封行为:未知必需事件被拒绝,带 `ignorable: true` 的未知事件仍可读取。因此原生当前格式的外部事件继续使用既有同版本扩展 seam,但不会自动获得未来格式迁移能力。 每个第一方源事件类型都在迁移边包中拥有可执行 disposition 与目标 validator。catalog 在构建时静态确定且与 profile 无关,因此 producer 插件是否挂载不会改变旧产物能否迁移。 diff --git a/.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.i18n.yaml index 2d7c9e0a83..759741d3f1 100644 --- a/.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md -2026-08-31-released-session-format-migrations.md: b10de9b20570bf6a9f7407d11c16af9b61a4a62c -2026-08-31-released-session-format-migrations.zh.md: 7707cfffe02f42c685741870e97b6a2f75af540c +2026-08-31-released-session-format-migrations.md: d566459af64f4177ed0135813e75aaf77480823c +2026-08-31-released-session-format-migrations.zh.md: f82b406117691197d808111f1c8aa4c722d4bab2 diff --git a/.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md b/.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md index b10de9b205..d566459af6 100644 --- a/.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md +++ b/.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md @@ -76,7 +76,7 @@ Preset renames cover the creation header and every selection event because the l A source inherited count can be unknown before EOF: V2 derives it from seed markers, and V1→V2 can change cardinality. The chain passes that absence to the next stage instead of fabricating a count. The [V2-to-V3 inheritance rules](../../../../packages/session/session-format-v2-to-v3/README.md#sequence-references) support this case; older stages that require a header-supplied count still refuse when it is absent. This permits seeded multi-hop restoration without retaining an intermediate artifact array. -V3 is a released Session format: [dsh-v0.1.5-alpha.1](https://github.com/deepseek-harness/deepseek-harness/releases/tag/dsh-v0.1.5-alpha.1) ships a [V3 writer](https://github.com/deepseek-harness/deepseek-harness/blob/dsh-v0.1.5-alpha.1/packages/core/session/src/types.ts#L88). An alpha product release still establishes a released persistence format. V0 through V3 retain their released semantics; committed generations remain byte-preserved during migration. A subsequent structural change requires the next adjacent edge under the [versioning rule](2026-08-10-session-log-version-mechanism.md), not an amendment to V2→V3. Ordinary event additions follow that rule’s required-event refusal mechanism rather than automatically allocating a version. An existing V3 file does not rerun its incoming migration; integration tests use isolated disposable homes and unchanged historical inputs. +The [version and release-status reference](../../../../docs/session-format-status.md) owns the published-format record and identifies the code’s writer authority. Released formats retain their semantics; committed generations remain byte-preserved during migration. A subsequent structural change requires the next adjacent edge under the [versioning rule](2026-08-10-session-log-version-mechanism.md), not an amendment to a released conversion. Ordinary event additions follow that rule’s required-event refusal mechanism rather than automatically allocating a version. A current-format file does not rerun its incoming migration; integration tests use isolated disposable homes and unchanged historical inputs. The [committed-corpus inventory](../../../../packages/test-support/llm-replay/tests/session-format-corpus-inventory.ts) identifies deliberately unsupported historical conversions by source path, generation, and exact refusal reason. Retaining those artifacts must not force chronology-changing migration or permit a blanket skip: every listed artifact must still raise the typed migration refusal, and unlisted artifacts must restore. Native current-generation fixtures cannot be classified as unsupported, because they do not traverse an incoming edge. Headerless test-harness protocol examples remain a separate explicit class. The corpus test checks source bytes after both successful and refused restoration; it does not rewrite historical evidence to satisfy the current reader. diff --git a/.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md b/.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md index 7707cfffe0..f82b406117 100644 --- a/.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md @@ -76,7 +76,7 @@ Chain 中不存在 `flatMap`、spread expansion、中间 event array 或 schedul 源继承数量在 EOF 前可能未知:V2 从种子标记推导它,而 V1→V2 可以改变事件数量。迁移链将这种缺失传递给下一个 Stage,而不伪造数量。[V2 到 V3 继承规则](../../../../packages/session/session-format-v2-to-v3/README.zh.md#sequence-references)支持此情况;需要 header 提供数量的旧 Stage 仍在数量缺失时拒绝。这使有种子的多跳恢复无需保留中间产物数组。 -V3 是已发布的 Session 格式:[dsh-v0.1.5-alpha.1](https://github.com/deepseek-harness/deepseek-harness/releases/tag/dsh-v0.1.5-alpha.1) 已交付 [V3 写入器](https://github.com/deepseek-harness/deepseek-harness/blob/dsh-v0.1.5-alpha.1/packages/core/session/src/types.ts#L88)。产品的 alpha 发布同样确立已发布的持久化格式。V0 至 V3 保留各自已发布的语义;迁移期间已提交代际的字节保持不变。后续结构性变更必须按[版本规则](2026-08-10-session-log-version-mechanism.zh.md)添加下一条相邻迁移边,而非修改 V2→V3。普通事件新增遵循该规则的必需事件拒绝机制,而非自动分配版本。已有 V3 文件不会重新执行入边迁移;集成测试使用隔离、可丢弃的 home 和未变更的历史输入。 +[版本与发布状态参考](../../../../docs/session-format-status.zh.md)拥有已发布格式记录,并指明代码中的写入器真源。已发布格式保留其语义;迁移期间已提交代际的字节保持不变。后续结构性变更必须按[版本规则](2026-08-10-session-log-version-mechanism.zh.md)添加下一条相邻迁移边,而非修改已发布转换。普通事件新增遵循该规则的必需事件拒绝机制,而非自动分配版本。当前格式文件不会重新执行入边迁移;集成测试使用隔离、可丢弃的 home 和未变更的历史输入。 [已提交语料清单](../../../../packages/test-support/llm-replay/tests/session-format-corpus-inventory.ts) 按源路径、代际与精确拒绝原因标识有意不支持的历史转换。保留这些产物不能迫使迁移改变时序,也不能允许统一跳过:每个清单中的产物仍必须抛出类型化迁移拒绝,未列入的产物必须还原。原生当前代际 fixture 不经过入边,因此不能被归为不支持。没有版本 header 的测试框架协议示例保持为独立的显式类别。语料测试在还原成功和拒绝后都检查源字节;它不通过改写历史证据来满足当前 reader。 diff --git a/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.i18n.yaml index 12fdfd3b82..174aa2592e 100644 --- a/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.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-system-prompt-as-surface-node.md -2026-09-02-system-prompt-as-surface-node.md: 1e374e4d30fc6c623c9cf1ee573375ee0c1ea1ad -2026-09-02-system-prompt-as-surface-node.zh.md: 8d5173dd0bf2814da323fc9fd7ca60160a9ac1bf +2026-09-02-system-prompt-as-surface-node.md: 1500fd2350f02ab5d8f203c0d62b98832c6c5de5 +2026-09-02-system-prompt-as-surface-node.zh.md: 306f347092caa3e9daf2494fa26d290b1c1ab9a7 diff --git a/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md b/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md index 1e374e4d30..1500fd2350 100644 --- a/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md +++ b/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md @@ -60,7 +60,7 @@ In `packages/core/agent-loop/src/agent.ts`, `preStep` renders the prompt with `r The [V2-to-V3 specification](../../../../packages/session/session-format-v2-to-v3/README.md#system-head) owns system-head conversion and message identities; its [reference rules](../../../../packages/session/session-format-v2-to-v3/README.md#sequence-references) and [source refusal](../../../../packages/session/session-format-v2-to-v3/README.md#source-audit) define preservation and unsupported inputs. The migrated layout is semantically equivalent to native requests, not byte-identical to a native recording. A valid V2 source can lack an order-preserving conversion under the current step invariant; refusing it is preferable to moving history or relaxing ownership. Historical acceptance coordinates must not become acknowledgements of the transformed log. -The [released-format policy](2026-08-31-released-session-format-migrations.md) covers V3 as well as V0, V1, and V2. The V2-to-V3 conversion preserves its released semantics; an existing V3 generation does not rerun that edge. Projection-cache version 4 is independent of the Session format and does not imply Session V4. +The [released-format policy](2026-08-31-released-session-format-migrations.md) preserves each released conversion’s semantics; an existing target-format generation does not rerun its incoming edge. Projection-cache versions are independent of Session format versions. The [canonical-envelope specification](../../../../packages/session/session-format-v2-to-v3/README.md#canonical-envelopes) defines composition with the structural conversion; the [canonical-envelope decision](2026-09-06-v3-canonical-session-envelopes.md) owns the strict-acceptance rationale. diff --git a/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md b/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md index 8d5173dd0b..306f347092 100644 --- a/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md @@ -60,7 +60,7 @@ Status: implemented [V2 到 V3 规范](../../../../packages/session/session-format-v2-to-v3/README.zh.md#system-head)负责系统头节点转换与消息身份;其[引用规则](../../../../packages/session/session-format-v2-to-v3/README.zh.md#sequence-references)和[源拒绝](../../../../packages/session/session-format-v2-to-v3/README.zh.md#source-audit)定义保留内容与不支持的输入。迁移布局与原生请求语义等价,而非与原生录制逐字节相同。有效 V2 源在当前步骤不变量下可能没有保持顺序的转换方式;拒绝它优于移动历史或放宽归属。历史接收坐标不得变为对转换后日志的确认。 -[已发布格式策略](2026-08-31-released-session-format-migrations.zh.md)既覆盖 V0、V1、V2,也覆盖 V3。V2-to-V3 转换保留其已发布的语义;已有 V3 代际不会重跑该迁移边。投影缓存版本 4 独立于 Session 格式,并不意味着 Session V4。 +[已发布格式策略](2026-08-31-released-session-format-migrations.zh.md)保留每条已发布转换的语义;已有目标格式代际不会重跑其入边。投影缓存版本独立于 Session 格式版本。 [规范信封规范](../../../../packages/session/session-format-v2-to-v3/README.zh.md#canonical-envelopes)定义与结构转换的组合;[规范信封决策](2026-09-06-v3-canonical-session-envelopes.zh.md)负责严格准入的依据。 diff --git a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml index 692a6fe140..6c552822ed 100644 --- a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-10-npm-release-sequences.md -2026-08-10-npm-release-sequences.md: c039db2c7463f93f6f8847ae0ae6100df650f2a5 -2026-08-10-npm-release-sequences.zh.md: 1f14c03beb57d3a574305b348379b1542836083c +2026-08-10-npm-release-sequences.md: 19e8d8db8550816f542111a9b831a3694a080de5 +2026-08-10-npm-release-sequences.zh.md: b7a84f7af4203d775555770b8c61b87720bcddb1 diff --git a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md index c039db2c74..19e8d8db85 100644 --- a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md +++ b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md @@ -117,7 +117,7 @@ The dsh family applies the repository's publication payload policy, which reject The `pack` job walks the whole release set once, packing each member into one directory, writes the upload order, and uploads that directory as one artifact; it lives in `release.yml` / `release-vendor.yml`. The release set is one unit — half the packages can never reach the registry while the other half is still building. -`pack` carries no credentials and runs on every pull request and master push, so a pull request proves the release set still packs. Publication lives in a separate `release-publish.yml` / `release-vendor-publish.yml` workflow that is `workflow_dispatch`-only (so it never appears as a PR check): it repacks the current tree and then publishes each entry in order, behind the `npm-publish` environment for human approval. Pack runs are grouped per ref so concurrent pull requests do not displace each other; the `publish` job carries the global `Release-publish` group, because dist-tags are shared registry state. +`pack` carries no credentials and runs on every pull request and master push, so a pull request proves the release set still packs. Publication lives in a separate `release-publish.yml` / `release-vendor-publish.yml` workflow that is `workflow_dispatch`-only (so it never appears as a PR check): it repacks the current tree and then publishes each entry in order, behind the `npm-publish` environment for human approval. Pack runs are grouped per ref so concurrent pull requests do not displace each other; the `publish` job carries the global `Release-publish` group, because dist-tags are shared registry state. After a dsh publication succeeds, the release operator verifies its Session writer against the [release record](../../../../docs/session-format-status.md#updating-the-record) and updates that record when a higher Session format has shipped. A dsh verification installs the vendored family's pack output too. The harness packages declare the vendored framework as a peer, those packages live in another sequence, and the credential-free job cannot fetch them from a private registry — so the dsh `pack` job packs the vendored family for verification while publishing only the dsh set. The publish workflow (`release-publish.yml`) repacks the current tree and publishes only the dsh set. diff --git a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md index 1f14c03beb..b7a84f7af4 100644 --- a/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md +++ b/.agents/notes/implemented/process/2026-08-10-npm-release-sequences.zh.md @@ -117,7 +117,7 @@ dsh 族套用仓库的发布 payload 策略(拒绝源码与声明映射)。v `pack` job 一趟遍历整个发布集,把每个成员打进同一个目录,写出上传顺序,整个目录作为一份 artifact 上传;它位于 `release.yml` / `release-vendor.yml`。发布集是一个整体——绝不会出现一半的包已经上了 registry、另一半还在构建。 -`pack` 无凭据,在每个 pull request 和每次 master push 上跑,所以一个 pull request 就能证明发布集仍能完整打出来。发布则位于独立的 `release-publish.yml` / `release-vendor-publish.yml` 工作流,仅 `workflow_dispatch`(因此不会作为 PR check 出现):它重新打包当前树,再按顺序逐个发布,挂在 `npm-publish` environment 后面等人工审批。pack 的 run 按 ref 分组,并发的 pull request 不会互相顶掉;全局 `Release-publish` 分组落在 `publish` job 上,因为 dist-tag 是共享的 registry 状态。 +`pack` 无凭据,在每个 pull request 和每次 master push 上跑,所以一个 pull request 就能证明发布集仍能完整打出来。发布则位于独立的 `release-publish.yml` / `release-vendor-publish.yml` 工作流,仅 `workflow_dispatch`(因此不会作为 PR check 出现):它重新打包当前树,再按顺序逐个发布,挂在 `npm-publish` environment 后面等人工审批。pack 的 run 按 ref 分组,并发的 pull request 不会互相顶掉;全局 `Release-publish` 分组落在 `publish` job 上,因为 dist-tag 是共享的 registry 状态。 dsh 发布成功后,发布操作者按[发布记录](../../../../docs/session-format-status.zh.md#updating-the-record)核实其 Session 写入器;若交付了更高的 Session 格式,则更新该记录。 dsh 的验证会一并安装 vendored 族的 pack 产物。harness 的包把 vendored 框架声明成 peer,而那些包属于另一条序列,无凭据的 job 无法从私有 registry 取到——所以 dsh 的 `pack` job 为验证而打包 vendored 族,发布的仍只有 dsh 那一份。发布工作流(`release-publish.yml`)重新打包当前树,只发布 dsh 族。 diff --git a/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.i18n.yaml b/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.i18n.yaml index c2a7bcd232..84f01fcd06 100644 --- a/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.md -2026-08-24-session-log-snapshot-corpus.md: ebcd9709f9cd17ad288d787a13ca66efee5fcc42 -2026-08-24-session-log-snapshot-corpus.zh.md: 8d0614e150c927b491784b51a166e752a7718dae +2026-08-24-session-log-snapshot-corpus.md: 593e1c5569107d50cfc85d7df4fd9491107847b8 +2026-08-24-session-log-snapshot-corpus.zh.md: 2ddccef3cc2942449707edd166e75bffa3ab64d8 diff --git a/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.md b/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.md index ebcd9709f9..593e1c5569 100644 --- a/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.md +++ b/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.md @@ -22,7 +22,7 @@ Fixture decoding and comparison depend only on the selected JSONL content; filen Headless stderr reconstruction expands embedded reasoning from both `assistant/message` and log-only `assistant/attempt` settlements, so failed or retried reasoning remains part of the projected process output. -Each parent or child role uses `session[.][.vN].jsonl`, with v0 encoded by an omitted version and every filename matching its header. Replay, record, and refresh select the numerically highest generation per role. Most owners omit `sessionFormat` and track the current writer; a bounded historical owner declares its exact version and closed coverage names. The v2 corpus keeps selected v0 roles for multi-hop, packed-row, retry/failure, and shipped-profile coverage plus selected v1 roles for the adjacent structural edge. Record and refresh never rewrite an explicitly retained historical fixture, rename a committed generation, or delete one through automatic cleanup. A retained Session generation does not freeze its non-Session expected outputs: refresh still writes owned system-prompt and tool-schema sidecars from the current run. Reviewed source-tree curation removes a predecessor only after the same role has a verified current successor. The corpus policy requires current selected roles to remain the majority and caps historical selected roles at ten; lower predecessor generations may remain beside a selected current successor. +Each parent or child role uses `session[.][.vN].jsonl`, with v0 encoded by an omitted version and every filename matching its header. Replay, record, and refresh select the numerically highest generation per role. Most owners omit `sessionFormat` and track the current writer; a bounded historical owner declares its exact version and closed coverage names. The corpus keeps selected v0 roles for multi-hop, packed-row, retry/failure, and shipped-profile coverage plus selected v1 roles for the adjacent structural edge. Record and refresh never rewrite an explicitly retained historical fixture, rename a committed generation, or delete one through automatic cleanup. A retained Session generation does not freeze its non-Session expected outputs: refresh still writes owned system-prompt and tool-schema sidecars from the current run. Reviewed source-tree curation removes a predecessor only after the same role has a verified current successor. The corpus policy requires current selected roles to remain the majority and caps historical selected roles at ten; lower predecessor generations may remain beside a selected current successor. Scenario-owned HTTP fixtures separate the stable authority recorded in the session from their transport listener. Each fixture binds loopback port `0`, lets the operating system allocate and bind the port atomically, and maps the recorded URL or endpoint through the real provider to that listener. Any process-global transport interception matches only the recorded endpoint, is owned by the fixture fiber, and is restored before the listener closes. @@ -30,7 +30,7 @@ Every existing ACP scenario receives a behavior-preserving destination. Ordinary Workspace inputs remain scenario-local. A mutating scenario compares a complete expected final workspace that record and refresh never rewrite, so a model or tool self-report cannot satisfy the test. Existing intentional session reuse remains an explicit acyclic owner reference; the corpus adds no workspace inheritance or general fixture-merging mechanism. -Current-writer request-header pins are separate from retained migration inputs: `tool-call-turn` pins the default composition, and `empty-response-retry-current` pins the retry composition. Their readable sidecars remain owned by `text-turn`. The six retained historical inputs stay byte-frozen and selected for replay; their pinned directories contain no canonical V3 sibling that could displace them. Separate `writer.expected.jsonl` and `writer..expected.jsonl` files pin exact normalized native V3 parent and child output, while retained SDK scenarios pin current notifications in `notifications.current.expected.jsonl`. These output oracles are not replay generations. The [snapshot kit](../../../../packages/test-support/session-snapshot/README.md) owns selection and refresh behavior. Structural migration can preserve request meaning without reproducing native writer event layout, so the official migration has independent correctness tests. Reverse projection into historical headers, stripping structural differences, skipping output equality, or replacing frozen inputs would conceal regressions instead of verifying those separate obligations. +Current-writer request-header pins are separate from retained migration inputs: `tool-call-turn` pins the default composition, and `empty-response-retry-current` pins the retry composition. Their readable sidecars remain owned by `text-turn`. The six retained historical inputs stay byte-frozen and selected for replay; their pinned directories contain no newer canonical sibling that could displace them. Separate `writer.expected.jsonl` and `writer..expected.jsonl` files pin exact normalized native current-format parent and child output, while retained SDK scenarios pin current notifications in `notifications.current.expected.jsonl`. These output oracles are not replay generations. The [snapshot kit](../../../../packages/test-support/session-snapshot/README.md) owns selection and refresh behavior. Structural migration can preserve request meaning without reproducing native writer event layout, so the official migration has independent correctness tests. Reverse projection into historical headers, stripping structural differences, skipping output equality, or replacing frozen inputs would conceal regressions instead of verifying those separate obligations. ## Alternatives considered diff --git a/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.zh.md b/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.zh.md index 8d0614e150..2ddccef3cc 100644 --- a/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.zh.md +++ b/.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.zh.md @@ -22,7 +22,7 @@ Fixture 解码与比较只取决于选定 JSONL 内容;文件名标识 invento Headless stderr 重建会同时展开 `assistant/message` 与仅写入日志的 `assistant/attempt` settlement 中嵌入的 reasoning,因此失败或重试尝试的 reasoning 仍属于进程输出投影。 -每个 parent 或 child 角色都使用 `session[.][.vN].jsonl`;v0 省略版本,且每个文件名都与其 header 一致。回放、录制与刷新按角色选择数值最高的 generation。大多数 owner 省略 `sessionFormat` 并跟随当前 writer;受限的历史 owner 会声明精确版本与封闭 coverage 名称。v2 语料保留选定 v0 角色,覆盖多跳、打包行、重试/失败与随附 profile,并保留选定 v1 角色覆盖相邻结构 edge。录制与刷新绝不改写显式保留的历史 fixture、重命名已提交 generation 或通过自动清理删除 generation。保留 Session generation 不会冻结非 Session 预期输出:refresh 仍会根据当前 run 写入 owner 持有的 system-prompt 与 tool-schema sidecar。受审阅的源树整理只有在同角色存在已验证的当前后继后才移除前代。语料策略要求选定当前角色始终占多数,并将选定历史角色上限设为十个;更低的前代 generation 可以保留在选定当前后继旁。 +每个 parent 或 child 角色都使用 `session[.][.vN].jsonl`;v0 省略版本,且每个文件名都与其 header 一致。回放、录制与刷新按角色选择数值最高的 generation。大多数 owner 省略 `sessionFormat` 并跟随当前 writer;受限的历史 owner 会声明精确版本与封闭 coverage 名称。语料保留选定 v0 角色,覆盖多跳、打包行、重试/失败与随附 profile,并保留选定 v1 角色覆盖相邻结构 edge。录制与刷新绝不改写显式保留的历史 fixture、重命名已提交 generation 或通过自动清理删除 generation。保留 Session generation 不会冻结非 Session 预期输出:refresh 仍会根据当前 run 写入 owner 持有的 system-prompt 与 tool-schema sidecar。受审阅的源树整理只有在同角色存在已验证的当前后继后才移除前代。语料策略要求选定当前角色始终占多数,并将选定历史角色上限设为十个;更低的前代 generation 可以保留在选定当前后继旁。 场景拥有的 HTTP fixture 将会话中录制的稳定 authority 与传输 listener 分离。每个 fixture 在回环地址上绑定端口 `0`,由操作系统以一次原子操作分配并绑定端口,再将录制的 URL 或 endpoint 通过真实 provider 映射到该 listener。任何进程全局传输拦截只匹配录制 endpoint,由 fixture fiber 拥有,并在关闭 listener 前恢复。 @@ -30,7 +30,7 @@ Headless stderr 重建会同时展开 `assistant/message` 与仅写入日志的 Workspace 输入继续归各场景本地所有。变更文件的场景比较完整的预期最终 workspace,record 与 refresh 绝不改写该预期,因此模型或工具的自报结果无法满足测试。现有的有意会话复用继续使用显式、无环的所有者引用;语料不增加 workspace 继承或通用 fixture 合并机制。 -当前 writer 的 request-header pin 与保留的迁移输入分离:`tool-call-turn` 固定 default 组合,`empty-response-retry-current` 固定 retry 组合。可读 sidecar 仍由 `text-turn` 持有。六份保留的历史输入保持字节冻结,并继续被选为回放输入;其固定历史版本的目录不含会取代它们的规范 V3 同角色文件。单独的 `writer.expected.jsonl` 与 `writer..expected.jsonl` 文件固定精确的规范化原生 V3 父子会话输出,保留历史输入的 SDK 场景则通过 `notifications.current.expected.jsonl` 固定当前通知。这些输出比较基准不是 replay 代际。[快照工具包](../../../../packages/test-support/session-snapshot/README.zh.md)负责选择与刷新行为。结构迁移可以保留请求含义而不复现原生 writer 的事件布局,因此正式迁移拥有独立的正确性测试。反向投影为历史 header、剥除结构差异、跳过输出相等断言或替换冻结输入都会掩盖回归,而不是验证这些相互独立的约定。 +当前 writer 的 request-header pin 与保留的迁移输入分离:`tool-call-turn` 固定 default 组合,`empty-response-retry-current` 固定 retry 组合。可读 sidecar 仍由 `text-turn` 持有。六份保留的历史输入保持字节冻结,并继续被选为回放输入;其固定历史版本的目录不含会取代它们的更新的规范同角色文件。单独的 `writer.expected.jsonl` 与 `writer..expected.jsonl` 文件固定精确的规范化原生当前格式的父子会话输出,保留历史输入的 SDK 场景则通过 `notifications.current.expected.jsonl` 固定当前通知。这些输出比较基准不是 replay 代际。[快照工具包](../../../../packages/test-support/session-snapshot/README.zh.md)负责选择与刷新行为。结构迁移可以保留请求含义而不复现原生 writer 的事件布局,因此正式迁移拥有独立的正确性测试。反向投影为历史 header、剥除结构差异、跳过输出相等断言或替换冻结输入都会掩盖回归,而不是验证这些相互独立的约定。 ## Alternatives considered diff --git a/AGENTS.md b/AGENTS.md index a479e76625..83ea425984 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ DeepSeek Harness is an all-plugin Cordis agent harness. Read [docs/architecture. ## Pre-stable APIs and released Session data -Public APIs are pre-stable; update every consumer. Released Session JSONL follows [adjacent migration](.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md): body reads may add a version-named successor but never move, overwrite, or delete committed generations; predecessors imply neither fallback nor downgrade support. SQLite domains use monotonic `SCHEMA_VERSION`. +Public APIs are pre-stable; update every consumer. [Session version/status](docs/session-format-status.md) defines the authorities. [Adjacent migration](.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md) may add a version-named successor but never move, overwrite, or delete committed generations; predecessors imply neither fallback nor downgrade support. SQLite uses monotonic `SCHEMA_VERSION`. **Application launch.** Only `dsh` profiles launch supported Node apps; package bins, demos, and public SDK argv escapes are forbidden ([rule](docs/architecture.md#application-launch)). diff --git a/docs/AGENTS.md b/docs/AGENTS.md index bb248a9042..7c345ecac8 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -35,7 +35,7 @@ Placement: bugs → postmortems; rationale → Agent Notes; procedures → cookb ## Writing rules -- **Document current state, not change history.** Avoid "previously/now/no longer", PRs, commits, and stack positions in durable prose; name the live mechanism. Put change stories in commits, PRs, Agent Notes, or postmortems; the latter two may cite merged PRs and issues as evidence. +- **Document current state, not change history.** Name live mechanisms, not PRs, commits, stack positions, or "previously/now/no longer". Keep history in commits, PRs, Agent Notes, or postmortems. General Session-format prose links [version/status authority](session-format-status.md); retain numbers for version-specific contracts, examples, or evidence. - **Every non-trivial change includes at least one Agent Note in the same PR.** Update the owning note or add one; only mechanical/local edits are exempt ([scope](../.agents/notes/README.md#when-to-write-one)). - **One physical line per paragraph** (`verify-md-wrap`): use editor soft-wrap. Code blocks, tables, and list structure keep their formatting; code comments stay under the linter's column limit. - **Fenced `ts` blocks must compile** (`doc-typecheck`); a pasted type declaration and its original JSDoc use ` ```ts type-equiv `, while a body-stripped public class declaration uses ` ```ts public-api `; register either in the manifest so neither can drift ([mechanics](development.md#documenting-types-verbatim-ts-type-equiv)). diff --git a/docs/cookbook/adding-a-session-format-version.i18n.yaml b/docs/cookbook/adding-a-session-format-version.i18n.yaml index d0020b2d3a..020554516a 100644 --- a/docs/cookbook/adding-a-session-format-version.i18n.yaml +++ b/docs/cookbook/adding-a-session-format-version.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 docs/cookbook/adding-a-session-format-version.md -adding-a-session-format-version.md: c445e53b0cb3e974581401d2bc99f2d180215933 -adding-a-session-format-version.zh.md: 3c3be794303dc7353d62c3e1eb51465942bc966d +adding-a-session-format-version.md: 54af2806bf7193ce9d1f5cd6c75f92b809c4ed95 +adding-a-session-format-version.zh.md: 4f7135b00aba37758343b8c17f2b4aff3146eeab diff --git a/docs/cookbook/adding-a-session-format-version.md b/docs/cookbook/adding-a-session-format-version.md index c445e53b0c..54af2806bf 100644 --- a/docs/cookbook/adding-a-session-format-version.md +++ b/docs/cookbook/adding-a-session-format-version.md @@ -4,7 +4,7 @@ English | [中文](adding-a-session-format-version.zh.md) ## Summary -Use this tutorial to introduce the next structural Session log version without rewriting released data. Let N be the latest released Session format, verified from release evidence and source, and N+1 the target. V3 is released and frozen; the concrete V3→V4 example assumes N=3. Start with a working contributor checkout and read the [package checklist](adding-a-package.md), [format library](../../packages/session/session-format/README.md), and [released-format decision](../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md). +Use this tutorial to introduce the next structural Session log version without rewriting released data. Read the [version and release-status authority](../session-format-status.md) to identify the checkout writer and the latest released format. Let N denote that verified released format and N+1 the target; substitute numeric values for these placeholders in names and metadata. Start with a working contributor checkout and read the [package checklist](adding-a-package.md), [format library](../../packages/session/session-format/README.md), and [released-format decision](../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md). ## Table of Contents @@ -21,20 +21,20 @@ Use this tutorial to introduce the next structural Session log version without r Bump the format for a structural change to headers, event envelopes, core event semantics, or surface reconstruction. Ordinary event additions do not require a bump; follow the [versioning rule](../../.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md). Distinguish the Session format integer from package release versions, SQLite schema versions, projection-unit versions, and protocol-wrapper versions. -For the V3→V4 example, use a shared `release/*` integration base, such as `release/session-log-v4`. The base change adds the V4 writer, codec, catalog wiring, identity migration, and verification. Create each independent child branch from that base and target its PR at the release branch, not another independent child's branch. Each child adds its structural transformation, validators, consumers, and tests to the example's `session-format-v3-to-v4` package. Do not introduce V5 or V6 just to represent review order. Merge reviewed children into the release branch through PRs, then validate the combined result before release. Honor release-branch force-push and deletion protections; do not force-sync it. +Use a shared `release/*` integration base for N+1. The base change adds the writer, codec, catalog wiring, identity migration, and verification. Create each independent child branch from that base and target its PR at the release branch, not another independent child’s branch. Each child adds its structural transformation, validators, consumers, and tests to the same adjacent migration package. Do not allocate extra versions just to represent review order. Merge reviewed children into the release branch through PRs, then validate the combined result before release. Honor release-branch force-push and deletion protections; do not force-sync it. -Released codecs and migration semantics, including V3 and V2→V3, remain frozen. Do not amend V0→V1, V1→V2, or V2→V3 to implement a new structural feature. Only the N→N+1 edge may incorporate coordinated changes before N+1 ships; after release, further structural changes need the next adjacent edge. +Released codecs and migration semantics remain frozen. Do not amend a released edge to implement a new structural feature. Only the N→N+1 edge may incorporate coordinated changes before N+1 ships; after release, further structural changes need the next adjacent edge. Use disposable, isolated Harness homes for unreleased N+1 integration testing. An interim N+1 file already has the target writer version, so a later edit to N→N+1 will not migrate that file again. Re-run from unchanged historical input in a fresh test home; never repair this by rewriting a committed generation or reusing a real user's home. ## 2. Add an identity edge -To implement the example V3→V4 edge, follow the package checklist to create a library, not a mounted plugin. An identity body conversion is only an initial wiring scaffold. The released [V2-to-V3 specification](../../packages/session/session-format-v2-to-v3/README.md#v2-to-v3-specification) is an example of explicit transformations and preservation rules, not an edge to extend or treat as an identity conversion. +Follow the package checklist to create a library for N→N+1, not a mounted plugin. An identity body conversion is only an initial wiring scaffold. The [V2-to-V3 specification](../../packages/session/session-format-v2-to-v3/README.md#v2-to-v3-specification) is a fixed example of explicit transformations and preservation rules, not an edge to extend or treat as an identity conversion. -In the example package manifest, declare `dsh.sessionFormatMigration` with `from: 3`, `to: 4`, an export path, and the exported migration, source codec, target codec, target-header validator, and target restorer. Reuse `releasedV3SessionFormatCodec` from the existing V2→V3 package and depend on that package; do not copy or redefine the released V3 codec. Export the example's V4 codec and validators from the new package. Add the new edge as a direct dependency of the catalog and add the workspace's TypeScript paths and project references. +Declare `dsh.sessionFormatMigration` with numeric `from: N` and `to: N+1`, an export path, and the exported migration, source codec, target codec, target-header validator, and target restorer. Reuse the source codec exported by the preceding edge package and depend on that package; do not copy or redefine a released codec. Export the target codec and validators from the new package. Add the edge as a direct dependency of the catalog and add the workspace’s TypeScript paths and project references. -Set `SESSION_FORMAT_VERSION` in [core Session types](../../packages/core/session/src/types.ts) to N+1 (4 in the example) alongside the new edge declarations, then generate the catalog. The command below generates only the declared chain; it does not implement a new version: +Set `SESSION_FORMAT_VERSION` in [core Session types](../../packages/core/session/src/types.ts) to N+1 alongside the new edge declarations, then generate the catalog. The command below generates only the declared chain; it does not implement a new version: ```sh pnpm run gen-session-format-catalog @@ -49,7 +49,7 @@ Use the [Stage interfaces](../../packages/session/session-format/src/types.ts), Implement `transformEvent(event, context)`, `transformRun(run, context)`, and `finish(context)`. Emit synchronously through `context.emitEvent` or `context.emitRun`; a call can produce zero, one, or many outputs. Let a stage consume codec-owned compact runs directly, or iterate `run.expand()` without materializing an intermediate array. The caller owns scheduling, and the chain finishes upstream stages before downstream stages. -Treat the inherited cut as a logical event count, not a physical row count. Expose `headerInheritedEventCount` only when it is known before EOF; `finish` returns the exact target cut. A preceding cardinality-changing edge can make that count unavailable at construction. Derive it from validated seed markers when required, and test the example's V0→V1→V2→V3→V4, V1→V2→V3→V4, and V2→V3→V4 chains with seeded Sessions, not just direct V3 input. Never substitute zero for an unknown cut. +Treat the inherited cut as a logical event count, not a physical row count. Expose `headerInheritedEventCount` only when it is known before EOF; `finish` returns the exact target cut. A preceding cardinality-changing edge can make that count unavailable at construction. Derive it from validated seed markers when required, and test seeded multi-hop restoration from each supported historical generation through N+1, not just direct N input. Never substitute zero for an unknown cut. Define the new edge's event admission and transformation rules explicitly. The [V2-to-V3 source audit](../../packages/session/session-format-v2-to-v3/README.md#source-audit) and [alpha V0→V1 rule](../../.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.md) own the policies of those released edges, not the new edge. Do not generalize either to every edge. A change to structure or event positions requires classifying source events, payload members, and references, and explicitly deciding whether opaque data can remain valid. [Equal-version retention](../../.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.md) alone does not prove a structural transformation safe. Validate target semantics and give each newly accepted case a rejecting counterexample; never widen older edges to hide an unsupported transformation. @@ -67,7 +67,7 @@ Verify both read and write paths. Header-only listing must not read bodies or pu ## 5. Create snapshot successors -Read [snapshot ownership](../../snapshots/AGENTS.md) and the [snapshot library](../../packages/test-support/session-snapshot/README.md). Select the owning scenario, not an adapter that only references it. After implementing N+1, keep each historical file and generate its successor. In the V3→V4 example, use `session.v4.jsonl` for the parent and `session.1.v4.jsonl`, `session.2.v4.jsonl`, and so on for children. Never rename `session.v3.jsonl` to V4 or change only its header. +Read [snapshot ownership](../../snapshots/AGENTS.md) and the [snapshot library](../../packages/test-support/session-snapshot/README.md). Select the owning scenario, not an adapter that only references it. After implementing N+1, keep each historical file and generate its successor using the target version’s canonical parent and child filenames. Never rename a predecessor to the target filename or change only its header. For unchanged replay input, use keyless refresh on the owner, then replay without write-back. These SDK commands use `text-turn` and the checkout's writer version. Implement and wire N+1 before using them to generate that version, and select the actual affected owner for a feature: @@ -93,7 +93,7 @@ pnpm run test:snapshot scripts/session-snapshot-corpus.corpus.ts After implementing the new edge, add its actual test path to the focused Vitest run. Add the changed JSONL, replay, projection, and SDK tests selected by the actual diff, plus the built publication-Worker smoke when that path changes. Require successful strict migration, identity preservation for the skeleton, malformed and unknown-required-event refusal, deterministic repeated restores, independent concurrent stage state, seeded multi-hop cuts, unchanged predecessors, and no fallback. Report exact commands and failures, not an inferred full-suite result. -Update the [owning Agent Note](../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md) rather than adding a redundant decision record. Audit related active notes for supersession; retain independent rationale and leave archived notes frozen. Update bilingual prose together, re-record each changed pair with the repository tool, then run documentation checks: +Update the [owning Agent Note](../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md) rather than adding a redundant decision record. Keep the [release record](../session-format-status.md#updating-the-record) unchanged until publication; after publication, update it with verified release evidence. Audit related active notes for supersession; retain independent rationale and leave archived notes frozen. Update bilingual prose together, re-record each changed pair with the repository tool, then run documentation checks: ```sh pnpm run verify-translation-pairing --write docs/cookbook/adding-a-session-format-version.md diff --git a/docs/cookbook/adding-a-session-format-version.zh.md b/docs/cookbook/adding-a-session-format-version.zh.md index 3c3be79430..4f7135b00a 100644 --- a/docs/cookbook/adding-a-session-format-version.zh.md +++ b/docs/cookbook/adding-a-session-format-version.zh.md @@ -4,7 +4,7 @@ ## 概述 -本教程介绍如何添加下一个结构性 Session 日志版本,同时不改写已发布数据。令 N 为经发布证据与源码确认的最新已发布 Session 格式,N+1 为目标版本。V3 已发布并冻结;具体的 V3→V4 示例假设 N=3。开始前,请准备可用的贡献者工作区,并阅读[包检查清单](adding-a-package.zh.md)、[格式库](../../packages/session/session-format/README.zh.md)和[已发布格式决策](../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md)。 +本教程介绍如何添加下一个结构性 Session 日志版本,同时不改写已发布数据。阅读[版本与发布状态真源](../session-format-status.zh.md),确定工作区写入器与最新已发布格式。令 N 表示经核实的已发布格式,N+1 表示目标版本;名称与元数据中的这些占位符须替换为数字。开始前,请准备可用的贡献者工作区,并阅读[包检查清单](adding-a-package.zh.md)、[格式库](../../packages/session/session-format/README.zh.md)和[已发布格式决策](../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md)。 ## 目录 @@ -21,20 +21,20 @@ 当 header、事件信封、核心事件语义或表面重建发生结构性变更时,提升格式版本。普通事件新增不需要提升版本;遵循[版本规则](../../.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.zh.md)。区分 Session 格式整数与包发布版本、SQLite schema 版本、投影单元版本及协议包装层版本。 -在 V3→V4 示例中,使用共享的 `release/*` 集成基线,例如 `release/session-log-v4`。基线变更添加 V4 写入器、codec、catalog 接线、恒等迁移与验证。从该基线创建各个独立子分支,并将其 PR(Pull Request)的目标设为发布分支,而非另一个独立子分支。每个子分支在示例的 `session-format-v3-to-v4` 包内添加自身的结构变换、校验器、消费方和测试。不要只为表示评审顺序而引入 V5 或 V6。通过 PR 将评审后的子分支合入发布分支,并在发布前验证组合结果。遵守发布分支的强制推送与删除保护;不要强制同步该分支。 +为 N+1 使用共享的 `release/*` 集成基线。基线变更添加写入器、codec、catalog 接线、恒等迁移与验证。从该基线创建各个独立子分支,并将其 PR(Pull Request)的目标设为发布分支,而非另一个独立子分支。每个子分支在同一个相邻迁移包内添加自身的结构变换、校验器、消费方和测试。不要只为表示评审顺序而分配额外版本。通过 PR 将评审后的子分支合入发布分支,并在发布前验证组合结果。遵守发布分支的强制推送与删除保护;不要强制同步该分支。 -已发布 codec 和迁移语义(包括 V3 与 V2→V3)保持冻结。不要通过修改 V0→V1、V1→V2 或 V2→V3 来实现新的结构性功能。只有 N→N+1 迁移边可在 N+1 发布前纳入协同变更;发布后,进一步的结构性变更需要下一条相邻迁移边。 +已发布 codec 和迁移语义保持冻结。不要通过修改已发布迁移边来实现新的结构性功能。只有 N→N+1 迁移边可在 N+1 发布前纳入协同变更;发布后,进一步的结构性变更需要下一条相邻迁移边。 未发布 N+1 的集成测试应使用可丢弃、相互隔离的 Harness home。中间版本产生的 N+1 文件已标为目标写入器版本,因此后续对 N→N+1 的修改不会再次迁移该文件。请在全新测试 home 中从未变更的历史输入重新运行;绝不通过改写已提交代际或复用真实用户 home 来修复这个问题。 ## 2. 添加恒等迁移边 -若要实现示例中的 V3→V4 迁移边,按照包检查清单创建库,而非挂载插件。恒等正文转换仅是最初的接线骨架。已发布的 [V2 到 V3 规范](../../packages/session/session-format-v2-to-v3/README.zh.md#v2-to-v3-specification)展示如何明确转换与保留规则,而不是可继续扩展或视为恒等转换的迁移边。 +按照包检查清单为 N→N+1 创建库,而非挂载插件。恒等正文转换仅是最初的接线骨架。[V2 到 V3 规范](../../packages/session/session-format-v2-to-v3/README.zh.md#v2-to-v3-specification)是明确转换与保留规则的固定示例,而不是可继续扩展或视为恒等转换的迁移边。 -在示例包的 manifest(元数据清单)中声明 `dsh.sessionFormatMigration`,包含 `from: 3`、`to: 4`、导出路径,以及导出的迁移、源 codec、目标 codec、目标 header 校验器和目标恢复器。复用现有 V2→V3 包的 `releasedV3SessionFormatCodec`,并依赖该包;不要复制或重新定义已发布 V3 codec。从新包导出示例的 V4 codec 和校验器。将新迁移边加入 catalog 的直接依赖,并添加工作区的 TypeScript 路径与项目引用。 +在 manifest(元数据清单)中声明 `dsh.sessionFormatMigration`,包含数值 `from: N` 和 `to: N+1`、导出路径,以及导出的迁移、源 codec、目标 codec、目标 header 校验器和目标恢复器。复用前一条迁移边所属包导出的源 codec,并依赖该包;不要复制或重新定义已发布 codec。从新包导出目标 codec 和校验器。将迁移边加入 catalog 的直接依赖,并添加工作区的 TypeScript 路径与项目引用。 -在添加新迁移边声明的同时,将[核心 Session 类型](../../packages/core/session/src/types.ts)中的 `SESSION_FORMAT_VERSION` 设为 N+1(示例中为 4),然后生成 catalog。下面的命令只生成已声明的迁移链;它不会实现新版本: +在添加新迁移边声明的同时,将[核心 Session 类型](../../packages/core/session/src/types.ts)中的 `SESSION_FORMAT_VERSION` 设为 N+1,然后生成 catalog。下面的命令只生成已声明的迁移链;它不会实现新版本: ```sh pnpm run gen-session-format-catalog @@ -49,7 +49,7 @@ pnpm run gen-session-format-catalog 实现 `transformEvent(event, context)`、`transformRun(run, context)` 和 `finish(context)`。通过 `context.emitEvent` 或 `context.emitRun` 同步输出;一次调用可以产生零个、一个或多个输出。让 Stage 直接消费 codec 所有的紧凑 run,或者迭代 `run.expand()`,而不物化中间数组。调用方负责调度,迁移链先结束上游 Stage,再结束下游 Stage。 -继承截点是逻辑事件数量,不是物理行数。只有在 EOF 前已知时才公开 `headerInheritedEventCount`;`finish` 返回精确的目标截点。前一条改变事件数量的迁移边可能使该数量在构造时不可知。必要时从已校验的种子标记推导它,并用有种子的 Session 测试示例中的 V0→V1→V2→V3→V4、V1→V2→V3→V4 和 V2→V3→V4 迁移链,而非仅测试直接 V3 输入。绝不以零替代未知截点。 +继承截点是逻辑事件数量,不是物理行数。只有在 EOF 前已知时才公开 `headerInheritedEventCount`;`finish` 返回精确的目标截点。前一条改变事件数量的迁移边可能使该数量在构造时不可知。必要时从已校验的种子标记推导它,并测试从每个受支持历史代际到 N+1 的有种子多跳恢复,而非仅测试直接 N 输入。绝不以零替代未知截点。 显式定义新迁移边的事件准入与变换规则。[V2 到 V3 源审计](../../packages/session/session-format-v2-to-v3/README.zh.md#source-audit)和 [Alpha V0→V1 规则](../../.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.zh.md)分别负责对应已发布迁移边的策略,而非新迁移边的策略。不要将任一策略推广到所有迁移边。结构或事件位置变化时,必须分类源事件、载荷成员与引用,并显式判断不透明数据能否保持有效。[同版本保留](../../.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.zh.md)本身不能证明结构变换安全。校验目标语义,并为每个新增可接受案例提供一个被拒绝的反例;绝不放宽旧迁移边来掩盖不受支持的转换。 @@ -67,7 +67,7 @@ pnpm run gen-session-format-catalog ## 5. 创建快照后继代际 -阅读[快照所有权](../../snapshots/AGENTS.md)和[快照库](../../packages/test-support/session-snapshot/README.zh.md)。选择拥有数据的场景,而非仅引用它的适配器。实现 N+1 后,保留每份历史文件,并生成其后继文件。在 V3→V4 示例中,父角色使用 `session.v4.jsonl`,子角色依次使用 `session.1.v4.jsonl`、`session.2.v4.jsonl` 等。绝不将 `session.v3.jsonl` 重命名为 V4,或仅修改其 header。 +阅读[快照所有权](../../snapshots/AGENTS.md)和[快照库](../../packages/test-support/session-snapshot/README.zh.md)。选择拥有数据的场景,而非仅引用它的适配器。实现 N+1 后,保留每份历史文件,并按目标版本的规范父子文件名生成后继文件。绝不将前代重命名为目标文件名,或仅修改其 header。 如果回放输入不变,在所有者上执行无密钥 refresh,再执行不写回的 replay。以下 SDK 命令使用 `text-turn` 和工作区的写入器版本。先实现并接入 N+1,才能用它们生成该版本;功能变更应选择实际受影响的所有者: @@ -93,7 +93,7 @@ pnpm run test:snapshot scripts/session-snapshot-corpus.corpus.ts 实现新迁移边后,将其实际测试路径加入聚焦的 Vitest 命令。根据实际 diff 添加受影响的 JSONL、回放、投影与 SDK 测试;发布 Worker 路径变化时还需构建产物冒烟测试。要求严格迁移成功、骨架保持恒等、拒绝格式错误与未知必需事件、重复恢复确定、并发 Stage 状态独立、有种子的多跳截点正确、前代不变且无回退。报告确切命令与失败,不要推断整个测试套件的结果。 -更新[所属 Agent Note](../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md),而非添加重复决策记录。审计相关活跃记录的取代关系;保留独立理由,并保持归档记录冻结。一起更新双语正文,通过仓库工具重新记录每个变更的配对,然后运行文档检查: +更新[所属 Agent Note](../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md),而非添加重复决策记录。发布前保持[发布记录](../session-format-status.zh.md#updating-the-record)不变;发布后,使用已核实的发布证据更新它。审计相关活跃记录的取代关系;保留独立理由,并保持归档记录冻结。一起更新双语正文,通过仓库工具重新记录每个变更的配对,然后运行文档检查: ```sh pnpm run verify-translation-pairing --write docs/cookbook/adding-a-session-format-version.md diff --git a/docs/deepseek-llm-api-wire-extensions.i18n.yaml b/docs/deepseek-llm-api-wire-extensions.i18n.yaml index 27bcf349da..e0b7be4a17 100644 --- a/docs/deepseek-llm-api-wire-extensions.i18n.yaml +++ b/docs/deepseek-llm-api-wire-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 docs/deepseek-llm-api-wire-extensions.md -deepseek-llm-api-wire-extensions.md: 5b1c4de68949d56f99cbfa70ba4af7ca0b71715d -deepseek-llm-api-wire-extensions.zh.md: a78baace34dcb3e24667295cf06e111ce355aa72 +deepseek-llm-api-wire-extensions.md: a6689c97670260a7b673fc73d455726e357fd022 +deepseek-llm-api-wire-extensions.zh.md: 9f54d78609186055d3ac5e3b9b4f4272c7304e83 diff --git a/docs/deepseek-llm-api-wire-extensions.md b/docs/deepseek-llm-api-wire-extensions.md index 5b1c4de689..a6689c9767 100644 --- a/docs/deepseek-llm-api-wire-extensions.md +++ b/docs/deepseek-llm-api-wire-extensions.md @@ -73,7 +73,7 @@ An enabled inventory with no qualifying entries sends `packages: []`; disabling ## `dsh_session_log` -[`@deepseek-ai/dsh-session-log-deepseek`](../packages/session/session-log-deepseek/README.md) contributes one contiguous suffix of the canonical Session log. The field is disabled by default. When enabled, it applies to a request with a live Session and at least one event; a direct request, a stale Session id, or an empty log omits the field. +[`@deepseek-ai/dsh-session-log-deepseek`](../packages/session/session-log-deepseek/README.md) contributes one contiguous suffix of the canonical Session log. The field is disabled by default. When enabled, it applies to a request with a live Session and at least one event; a direct request, a stale Session id, or an empty log omits the field. The examples below use logical Session format 2 only to illustrate the wire fields; they do not identify the [current writer format](session-format-status.md). ```json { @@ -119,7 +119,7 @@ The `session` member projects `Session.header`, not a complete runtime Session o | Member | Presence | Meaning | |---|---|---| -| `version` | required | Logical Session format version; currently `2` | +| `version` | required | Logical Session format version from `Session.header`; see [format status](session-format-status.md) | | `id` | required | Exact Session id | | `createdAt` | required | Non-negative safe-integer Unix epoch milliseconds | | `cwd` | optional | Absolute working directory recorded at Session creation | diff --git a/docs/deepseek-llm-api-wire-extensions.zh.md b/docs/deepseek-llm-api-wire-extensions.zh.md index a78baace34..9f54d78609 100644 --- a/docs/deepseek-llm-api-wire-extensions.zh.md +++ b/docs/deepseek-llm-api-wire-extensions.zh.md @@ -73,7 +73,7 @@ ## `dsh_session_log` -[`@deepseek-ai/dsh-session-log-deepseek`](../packages/session/session-log-deepseek/README.zh.md) 贡献权威会话日志的一段连续后缀。该字段默认禁用。启用后,它适用于携带存活会话且至少存在一个事件的请求;直接请求、陈旧会话 id 或空日志会省略该字段。 +[`@deepseek-ai/dsh-session-log-deepseek`](../packages/session/session-log-deepseek/README.zh.md) 贡献权威会话日志的一段连续后缀。该字段默认禁用。启用后,它适用于携带存活会话且至少存在一个事件的请求;直接请求、陈旧会话 id 或空日志会省略该字段。下方示例使用逻辑 Session 格式 2 仅为说明协议字段,并不标识[当前写入格式](session-format-status.zh.md)。 ```json { @@ -119,7 +119,7 @@ | 成员 | 出现条件 | 含义 | |---|---|---| -| `version` | 必需 | 逻辑 Session 格式版本;当前为 `2` | +| `version` | 必需 | 来自 `Session.header` 的逻辑 Session 格式版本;见[格式状态](session-format-status.zh.md) | | `id` | 必需 | 确切的会话 id | | `createdAt` | 必需 | 非负安全整数 Unix epoch 毫秒数 | | `cwd` | 可选 | 创建会话时记录的绝对工作目录 | diff --git a/docs/session-format-status.i18n.yaml b/docs/session-format-status.i18n.yaml new file mode 100644 index 0000000000..01391d480e --- /dev/null +++ b/docs/session-format-status.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 docs/session-format-status.md +session-format-status.md: 222afcbd7e9d5baede55e748065ea49363da361b +session-format-status.zh.md: 6c7e97816cb97416b225304e99f23c68fd31a855 diff --git a/docs/session-format-status.md b/docs/session-format-status.md new file mode 100644 index 0000000000..222afcbd7e --- /dev/null +++ b/docs/session-format-status.md @@ -0,0 +1,47 @@ +# Session format version and release status + +English | [中文](session-format-status.zh.md) + +## Summary + +Use this reference to distinguish the checkout’s Session writer version from the latest published Session format. The code constant owns the writer version; the release record below owns the latest released format and its publication evidence. Other documentation links here instead of restating which version is current, next, or unreleased. + +## Table of Contents + +- [Sources of truth](#sources-of-truth) +- [Release record](#release-record) +- [Updating the record](#updating-the-record) +- [Dev Note](#dev-note) + + +## Sources of truth + +- **Checkout writer:** `SESSION_FORMAT_VERSION` in [core Session types](../packages/core/session/src/types.ts) is the only hand-maintained current-writer number in code. The [catalog generator](../scripts/gen-session-format-catalog.ts) derives codec ordering and checks that adjacent migrations reach it. A package version, codec export name, fixture filename, or projection-cache version is not the writer authority. +- **Latest released format:** `latestReleasedVersion` in the following record identifies the published Session format. `evidenceTag` names a published product release whose tagged writer has that value; it need not be the first release carrying the format. The bilingual copy is checked against the same record, not maintained as a separate decision. +- **Release status:** compare the writer constant with the verified release record. Equality means the writer format has shipped. A greater writer version is a development target beyond the recorded release. A lower writer version means this checkout predates that release. No separate released boolean is maintained. Before declaring a greater version unreleased, verify that no published release has advanced the record. + +An alpha, beta, or release-candidate product publication establishes released Session-format obligations. GitHub’s prerelease flag does not make persisted user data disposable. A missing release record is not evidence of non-publication. The [versioning rule](../.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md) owns compatibility decisions; [released-format migration](../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md) owns immutable generations and adjacent conversion. + + +## Release record + +```yaml session-format-release +latestReleasedVersion: 3 +evidenceTag: dsh-v0.1.5-alpha.1 +``` + +Evidence: [published release](https://github.com/deepseek-harness/deepseek-harness/releases/tag/dsh-v0.1.5-alpha.1) and [its tagged writer source](https://github.com/deepseek-harness/deepseek-harness/blob/dsh-v0.1.5-alpha.1/packages/core/session/src/types.ts). + + +## Updating the record + +When a structural writer change is implemented, update the code constant and adjacent catalog together; do not advance this release record before publication. When a product release first publishes a higher Session format, confirm publication and its tagged writer, then advance this record and both evidence links in the same bilingual update. Later product releases carrying the same format do not require changing the record. Never lower it on the development trunk. + +The [documentation-standard test](../scripts/doc-standard.spec.ts) checks record structure, bilingual equality, evidence-link consistency, and that the documented release does not exceed the checkout writer. This keyless check does not query GitHub or prove that the record is up to date; publication verification remains part of the release update. + +Use “current format” and “next adjacent version” for general behavior. Keep explicit numbers for fixed migration inputs and outputs, wire schemas, historical evidence, and tests of those particular versions. The [format-version cookbook](cookbook/adding-a-session-format-version.md) uses N for the verified latest released format and N+1 for its successor. + + +## Dev Note + +None. diff --git a/docs/session-format-status.zh.md b/docs/session-format-status.zh.md new file mode 100644 index 0000000000..6c7e97816c --- /dev/null +++ b/docs/session-format-status.zh.md @@ -0,0 +1,47 @@ +# Session 格式版本与发布状态 + +[English](session-format-status.md) | 中文 + +## 概述 + +本参考区分工作区的 Session 写入器版本与最新已发布的 Session 格式。代码常量拥有写入器版本;下方发布记录拥有最新已发布格式及其发布证据。其他文档链接到这里,而不重复声明哪个版本是当前、下一个或尚未发布的版本。 + +## 目录 + +- [单一真源](#sources-of-truth) +- [发布记录](#release-record) +- [更新记录](#updating-the-record) +- [开发备注](#dev-note) + + +## 单一真源 + +- **工作区写入器:**[核心 Session 类型](../packages/core/session/src/types.ts)中的 `SESSION_FORMAT_VERSION` 是代码中唯一手工维护的当前写入器版本号。[目录生成器](../scripts/gen-session-format-catalog.ts)推导 codec 顺序,并检查相邻迁移是否到达该版本。包版本、codec 导出名称、fixture(测试前置数据)文件名或投影缓存版本都不是写入器版本的权威来源。 +- **最新已发布格式:**下方记录中的 `latestReleasedVersion` 标识已发布的 Session 格式。`evidenceTag` 指定一个已发布的产品版本,其标签对应的写入器具有该值;它不必是首次携带该格式的发布。双语副本按同一记录校验,不作为独立决策维护。 +- **发布状态:**比较写入器常量与已核实的发布记录。相等表示写入器格式已经发布。写入器版本更高表示它是超出记录中发布版本的开发目标。写入器版本更低表示该工作区早于该发布。不另行维护 released 布尔值。在声明更高版本尚未发布前,必须核实是否已有产品发布推进了记录。 + +产品的 alpha、beta 或 release-candidate 发布都会确立已发布 Session 格式的义务。GitHub 的 prerelease 标记不会让持久化用户数据成为可丢弃数据。缺少发布记录不代表尚未发布。[版本规则](../.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.zh.md)拥有兼容性决策;[已发布格式迁移](../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md)拥有不可变代际与相邻转换规则。 + + +## 发布记录 + +```yaml session-format-release +latestReleasedVersion: 3 +evidenceTag: dsh-v0.1.5-alpha.1 +``` + +证据:[已发布产品版本](https://github.com/deepseek-harness/deepseek-harness/releases/tag/dsh-v0.1.5-alpha.1)及[对应标签的写入器源码](https://github.com/deepseek-harness/deepseek-harness/blob/dsh-v0.1.5-alpha.1/packages/core/session/src/types.ts)。 + + +## 更新记录 + +实现结构性写入器变更时,一起更新代码常量与相邻迁移目录;不要在产品发布前推进此发布记录。当产品首次发布更高的 Session 格式时,确认发布事实及对应标签的写入器,然后在同一次双语更新中推进本记录与两个证据链接。后续携带相同格式的产品发布无需改变此记录。开发主干上的记录绝不降低。 + +[文档标准测试](../scripts/doc-standard.spec.ts)检查记录结构、双语一致性、证据链接一致性,以及文档中的已发布版本不高于工作区写入器。这个无密钥检查不会查询 GitHub,也不能证明记录是最新的;核实发布事实仍属于发布更新的一部分。 + +一般行为使用“当前格式”和“下一条相邻版本”等表述。固定迁移的输入与输出、协议 schema、历史证据及针对特定版本的测试保留明确版本号。[格式版本实操手册](cookbook/adding-a-session-format-version.zh.md)用 N 表示已核实的最新发布格式,用 N+1 表示其后继版本。 + + +## 开发备注 + +无。 diff --git a/docs/subsystems/persistence.i18n.yaml b/docs/subsystems/persistence.i18n.yaml index e5a3fe2a7e..1abbf9957d 100644 --- a/docs/subsystems/persistence.i18n.yaml +++ b/docs/subsystems/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 docs/subsystems/persistence.md -persistence.md: b75ed288e1d034dc361457c8124a9974ecadad92 -persistence.zh.md: 1e5fa4269c520f2b196b245e4f2af4e8601da8eb +persistence.md: 7e18e9bbfec8c7aa81525da33ad3cffa4681ea73 +persistence.zh.md: 8250fb93b20dc33c2df705fd2c7092bc8dea155c diff --git a/docs/subsystems/persistence.md b/docs/subsystems/persistence.md index b75ed288e1..7e18e9bbfe 100644 --- a/docs/subsystems/persistence.md +++ b/docs/subsystems/persistence.md @@ -202,7 +202,7 @@ interface CreateSessionOptions { /** Initial replay or fork history supplied at construction. */ readonly seed?: readonly SessionEvent[] /** - * Exact fork-inherited prefix length when `meta.isSeeded` is true. In v2 the + * Exact fork-inherited prefix length when `meta.isSeeded` is true. The * constructor seed is exactly this inherited prefix; the constructor * appends the child-owned tagged marker at the cut. */ diff --git a/docs/subsystems/persistence.zh.md b/docs/subsystems/persistence.zh.md index 1e5fa4269c..8250fb93b2 100644 --- a/docs/subsystems/persistence.zh.md +++ b/docs/subsystems/persistence.zh.md @@ -202,7 +202,7 @@ interface CreateSessionOptions { /** Initial replay or fork history supplied at construction. */ readonly seed?: readonly SessionEvent[] /** - * Exact fork-inherited prefix length when `meta.isSeeded` is true. In v2 the + * Exact fork-inherited prefix length when `meta.isSeeded` is true. The * constructor seed is exactly this inherited prefix; the constructor * appends the child-owned tagged marker at the cut. */ diff --git a/docs/subsystems/session.i18n.yaml b/docs/subsystems/session.i18n.yaml index 883c778ccb..616359889e 100644 --- a/docs/subsystems/session.i18n.yaml +++ b/docs/subsystems/session.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 docs/subsystems/session.md -session.md: 50dbbf6474f1b1e756dedd508bb1d8b0ef349832 -session.zh.md: 9a1b1399e701a1fa2bda2ad59b59c9fbe484ef4a +session.md: 1443f6c3e5795d5a02662f25c3b39525084bb085 +session.zh.md: 8e4ca4b4b5c5b113ff6989a92030e318a888f449 diff --git a/docs/subsystems/session.md b/docs/subsystems/session.md index 50dbbf6474..1443f6c3e5 100644 --- a/docs/subsystems/session.md +++ b/docs/subsystems/session.md @@ -656,7 +656,7 @@ The optional `dsh-session/invariant` companion enforces the relations owned by c A fresh fork constructor requires its seed to equal the inherited prefix and appends `session/end-seed { inherited: true }` at the exact durable cut. A restore retains that tagged marker and appends an ordinary `session/end-seed {}` only when its complete stored seed does not already end in a marker. Both forms are log-only and produce no message; `Session`'s constructor is the only legitimate writer. -For fork lineage, locate the LAST marker whose payload carries `inherited: true`; v2 decoding requires it exactly when `SessionHeader.isSeeded` is true and derives `inheritedEventCount` from its seq. For lifecycle ownership, locate the last `session/end-seed` of either form. Reopening a seed that already ends in any marker does not append another ordinary marker. +For fork lineage, locate the LAST marker whose payload carries `inherited: true`; current-format decoding requires it exactly when `SessionHeader.isSeeded` is true and derives `inheritedEventCount` from its seq. For lifecycle ownership, locate the last `session/end-seed` of either form. Reopening a seed that already ends in any marker does not append another ordinary marker. It exists because seed history and live work are otherwise byte-identical, which defeats any plugin owning a standalone open/close bracket: an unmatched `compaction/start` reads the same whether the writer crashed mid-compaction or is compacting right now. An opening marker before `session/end-seed` came from the constructor seed and belongs to an ended lifecycle, whatever ended it (a crash, a succeeding process, or a fork out of a still-running parent), so its owner may treat it as dead. That covers only brackets *this* session inherited: a concurrently live session holding an open bracket over the same history has its own boundary elsewhere, so tolerating concurrent writers needs a liveness signal beyond the log. Core writes the boundary and reads nothing from it — a bracket's vocabulary stays with its owning plugin, which is why crash repair closes turn/step/tool boundaries and never `compaction/*`. diff --git a/docs/subsystems/session.zh.md b/docs/subsystems/session.zh.md index 9a1b1399e7..8e4ca4b4b5 100644 --- a/docs/subsystems/session.zh.md +++ b/docs/subsystems/session.zh.md @@ -660,7 +660,7 @@ interface TurnEndReasonMap { 新 fork constructor 要求 seed 等于 inherited prefix,并在精确持久 cut 追加 `session/end-seed { inherited: true }`。restore 会保留该 tagged marker,并且只在完整 stored seed 尚未以 marker 结尾时追加普通 `session/end-seed {}`。两种形式都只进入 log 且不产生 message;`Session` constructor 是唯一合法 writer。 -对于 fork lineage,定位 payload 携带 `inherited: true` 的最后一个 marker;v2 decoding 只在 `SessionHeader.isSeeded` 为 true 时要求该 marker,并从其 seq 推导 `inheritedEventCount`。对于 lifecycle ownership,定位任一形式的最后一个 `session/end-seed`。重新打开已经以任一 marker 结尾的 seed 时,不会再追加普通 marker。 +对于 fork lineage,定位 payload 携带 `inherited: true` 的最后一个 marker;当前格式 decoding 只在 `SessionHeader.isSeeded` 为 true 时要求该 marker,并从其 seq 推导 `inheritedEventCount`。对于 lifecycle ownership,定位任一形式的最后一个 `session/end-seed`。重新打开已经以任一 marker 结尾的 seed 时,不会再追加普通 marker。 它之所以必要,是因为种子历史与实时工作在字节层面完全相同,这会让任何拥有独立开/闭括号的插件失效:一个未配对的 `compaction/start`,无论写入方是在压缩中途崩溃、还是此刻正在压缩,读起来都一样。在 `session/end-seed` 之前的开启标记来自构造种子,并且属于一个已结束的生命周期,无论结束原因为何(崩溃、进程接替,或从仍在运行的父会话 fork 出来),因此其所有方可以视之为已死。这只覆盖*本*会话继承的括号:另一个并发存活的会话可能在同一段历史上持有开放括号,而它自己的边界在别处,因此容忍并发写入方还需要日志之外的存活信号。核心写入该边界但不从中读取任何内容——括号的词汇表仍归其所属插件,这也正是崩溃修复只关闭轮次/步骤/工具边界而从不处理 `compaction/*` 的原因。 diff --git a/docs/testing.i18n.yaml b/docs/testing.i18n.yaml index 899424ae54..82fc949fe6 100644 --- a/docs/testing.i18n.yaml +++ b/docs/testing.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 docs/testing.md -testing.md: b9800bd7aa25e2556d2fa97e9397c140fffdb442 -testing.zh.md: 59f1a7ca05f0e50f6a3999498b41670228618514 +testing.md: bbf7db5d788667dc85c34dfdac4784e2b0dccca8 +testing.zh.md: 7d386494bb8fd712b93aeeeb4f8c7b196349a1db diff --git a/docs/testing.md b/docs/testing.md index b9800bd7aa..bbf7db5d78 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -14,7 +14,7 @@ How this repo tests, tier by tier, and the rules that keep a green suite meaning - **Snapshot** (`pnpm run test:snapshot`): a top-level scenario's highest recorded parent generation supplies user input and model replay, then serves as the expected persisted result. Parent filenames are `session[.vN].jsonl`; child roles are `session.[.vN].jsonl`; v0 omits `.v0`, positive versions require lowercase `.vN`, and each filename must agree with its header. Process scenarios start through `dsh`: headless owns one-shot behavior, the SDK owns persistent control, ACP owns automation-protocol behavior, and Web retains browser/ARIA evidence beside the same Session. `snapshot.yml` declares the profile, composition/header class, recording policy, exceptional replay or input metadata, and workspace facts. Typed tokens preserve parent/child identity relationships; only header pins own prompt/schema sidecars. A mutating scenario independently compares the complete `workspace.expected/` tree, which record and refresh never rewrite. Use `test:snapshot:record` when a model transcript changes and `test:snapshot:refresh` when replay input remains valid; review every resulting diff. - **Web browser snapshot** (`pnpm run test:web`; required Linux PR gate): Chromium compares session-driven output under `snapshots/web/` and UI-only output under `apps/web/tests/expected/`. CI forces read-only `DSH_SNAPSHOT=replay`, never writing expected outputs; record/refresh stay local and every diff is reviewed ([web e2e lane](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md), [CI gate decision](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md)). `test:web` builds first for plugin CSS. -Session fixtures retain headers and payloads but omit body sequence/time envelopes; replay synthesizes them. Replay, record, and refresh select each parent/child role's highest generation. Current V3 uses `.v3`, one row per event, and embedded compact Assistant streams. Historical fixtures retain their released representation; explicit `sessionFormat` owners preserve migration coverage. Follow the [format-version cookbook](cookbook/adding-a-session-format-version.md#snapshot-successors) to add successors without changing predecessors. +Session fixtures retain headers and payloads but omit body sequence/time envelopes; replay synthesizes them. Replay, record, and refresh select each parent/child role's highest generation. Current fixtures use the [writer format](session-format-status.md) in their filenames and headers, one row per event, and embedded compact Assistant streams. Historical fixtures retain their released representation; explicit `sessionFormat` owners preserve migration coverage. Follow the [format-version cookbook](cookbook/adding-a-session-format-version.md#snapshot-successors) to add successors without changing predecessors. ## How specs execute diff --git a/docs/testing.zh.md b/docs/testing.zh.md index 59f1a7ca05..7d386494bb 100644 --- a/docs/testing.zh.md +++ b/docs/testing.zh.md @@ -14,7 +14,7 @@ - **快照**(`pnpm run test:snapshot`):顶层场景数值最高的已录制 parent generation 同时提供用户输入和模型回放,并作为持久化结果的预期值。parent 文件名是 `session[.vN].jsonl`;child 角色使用 `session.[.vN].jsonl`;v0 省略 `.v0`,正版本必须使用小写 `.vN`,且每个文件名必须与其 header 一致。进程级场景都通过 `dsh` 启动:headless 负责一次性行为,SDK 负责持久控制,ACP 负责自动化协议行为,Web 在同一 Session 旁保留浏览器与 ARIA 证据。`snapshot.yml` 声明 profile、组合与请求头类别、录制策略、例外回放或输入元数据以及 workspace 事实。带类型的 token 保留父子身份关系;只有请求头 pin 拥有 prompt/schema sidecar。变更 workspace 的场景会独立比较完整的 `workspace.expected/` 目录,record 与 refresh 绝不改写该目录。当模型 transcript(文本记录)变化时使用 `test:snapshot:record`,回放输入仍有效时使用 `test:snapshot:refresh`;请审查所有结果差异。 - **Web 浏览器快照**(`pnpm run test:web`;必需的 Linux PR(Pull Request)门禁):Chromium 比较 `snapshots/web/` 下由会话驱动的输出,以及 `apps/web/tests/expected/` 下仅含 UI 的输出。CI 强制只读的 `DSH_SNAPSHOT=replay`,绝不写入预期输出;record/refresh 留在本地,每处 diff 都须评审([web e2e 车道](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md)、[CI 门禁决策](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.zh.md))。`test:web` 会先构建以交付插件 CSS。 -Session fixture 保留 header 与 payload,但省略正文 seq/time envelope;replay 会合成这些 envelope。Replay、record 与 refresh 会选择每个 parent/child 角色的最高 generation。当前 V3 使用 `.v3`、每个事件一行,并嵌入紧凑 Assistant stream。历史 fixture 保留其已发布表示;显式 `sessionFormat` 所有者保留迁移覆盖。按照[格式版本实操手册](cookbook/adding-a-session-format-version.zh.md#snapshot-successors)添加后继代际,不改动前代。 +Session fixture 保留 header 与 payload,但省略正文 seq/time envelope;replay 会合成这些 envelope。Replay、record 与 refresh 会选择每个 parent/child 角色的最高 generation。当前 fixture 在文件名与 header 中使用[写入格式](session-format-status.zh.md),每个事件一行,并嵌入紧凑 Assistant stream。历史 fixture 保留其已发布表示;显式 `sessionFormat` 所有者保留迁移覆盖。按照[格式版本实操手册](cookbook/adding-a-session-format-version.zh.md#snapshot-successors)添加后继代际,不改动前代。 ## spec 如何被执行 diff --git a/packages/core/session/README.i18n.yaml b/packages/core/session/README.i18n.yaml index fd052eb9f9..6b68ca2d74 100644 --- a/packages/core/session/README.i18n.yaml +++ b/packages/core/session/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/core/session/README.md -README.md: a06b22b5e8f48743047c68e883edd496fbc6eb77 -README.zh.md: 755894b4c30f0a3807f472cece32685d102efbeb +README.md: cdcab8ec4d2f6960bb651aff46745b20f91632e2 +README.zh.md: 745615f67084430475e0739b22afa35556b71ee6 diff --git a/packages/core/session/README.md b/packages/core/session/README.md index a06b22b5e8..cdcab8ec4d 100644 --- a/packages/core/session/README.md +++ b/packages/core/session/README.md @@ -181,7 +181,7 @@ Logging causes no invalidation, and exact reconstruction preserves request-prefi These limits define when the session store needs special care. They are current package constraints, not a task backlog. - **`fork()` cuts only at stable boundaries of live sessions** — the selected prefix must end outside an open turn and the source must be in the store; forking a persisted-but-unloaded session is excluded from the fork API. -- **`SESSION_FORMAT_VERSION` names the current V3 logical representation** — the V3 reader rejects retired `header.system` and validates `system/message` payloads and protected-head rewrites. Historical headers and events belong to adjacent format packages; the V2→V3 edge converts supported history before constructing `Session`, and write open publishes only the V3 successor. Equal-version unknown events require the envelope's explicit `ignorable` marker, which does not promise safe structural migration ([mechanism](../../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md)). +- **`SESSION_FORMAT_VERSION` names the [current logical representation](../../../docs/session-format-status.md)** — the current reader rejects retired `header.system` and validates `system/message` payloads and protected-head rewrites. Historical headers and events belong to adjacent format packages; the adjacent migration chain converts supported history before constructing `Session`, and write open publishes only the current-format successor. Equal-version unknown events require the envelope's explicit `ignorable` marker, which does not promise safe structural migration ([mechanism](../../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md)). - **`TurnEndReasonMap` omits the ACP-named `refusal` / `max_turn_requests` variants** — producer-gated: they land when an adapter or the loop first emits them. - **No session tree beyond fork** — a pi-style entry tree over branched sessions is deferred unless a consumer needs more than boundary-based forking. diff --git a/packages/core/session/README.zh.md b/packages/core/session/README.zh.md index 755894b4c3..745615f670 100644 --- a/packages/core/session/README.zh.md +++ b/packages/core/session/README.zh.md @@ -181,7 +181,7 @@ session.deriveMessages() // the derived model history 这些限制说明会话存储何时需要特别留意。它们是当前包约束,不是任务积压。 - **`fork()` 仅在实时会话的稳定边界处切分**:所选前缀结束时不得有开放轮次,且源会话必须位于存储中;fork API 不支持对已持久化但未加载的会话进行 fork。 -- **`SESSION_FORMAT_VERSION` 命名当前 V3 逻辑表示**——V3 读取器拒绝已退役的 `header.system`,并校验 `system/message` 载荷与受保护头节点的重写。历史 header 与事件归相邻格式包所有;V2→V3 迁移边在构造 `Session` 前转换受支持的历史,写打开只发布 V3 后继代际。同版本未知事件要求信封显式带有 `ignorable` 标记,但这不保证结构迁移的安全性([机制](../../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md))。 +- **`SESSION_FORMAT_VERSION` 命名[当前逻辑表示](../../../docs/session-format-status.zh.md)**——当前读取器拒绝已退役的 `header.system`,并校验 `system/message` 载荷与受保护头节点的重写。历史 header 与事件归相邻格式包所有;相邻迁移链在构造 `Session` 前转换受支持的历史,写打开只发布当前格式的后继代际。同版本未知事件要求信封显式带有 `ignorable` 标记,但这不保证结构迁移的安全性([机制](../../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md))。 - **`TurnEndReasonMap` 不含 ACP(Agent Client Protocol)命名的 `refusal`/`max_turn_requests` 变体**:受生产方约束;只有当适配器或循环首次产生这些变体时才加入。 - **fork 之外没有会话树**:基于分支会话的 pi 风格条目树被推迟,除非消费方需要超越基于边界的 forking 的能力。 diff --git a/packages/core/session/src/types.ts b/packages/core/session/src/types.ts index 192ef12502..139fccd5a5 100644 --- a/packages/core/session/src/types.ts +++ b/packages/core/session/src/types.ts @@ -138,7 +138,7 @@ export interface CreateSessionOptions { /** Initial replay or fork history supplied at construction. */ readonly seed?: readonly SessionEvent[] /** - * Exact fork-inherited prefix length when `meta.isSeeded` is true. In v2 the + * Exact fork-inherited prefix length when `meta.isSeeded` is true. The * constructor seed is exactly this inherited prefix; the constructor * appends the child-owned tagged marker at the cut. */ diff --git a/packages/session/session-format-catalog/README.i18n.yaml b/packages/session/session-format-catalog/README.i18n.yaml index 8823bffdc0..1a88983d94 100644 --- a/packages/session/session-format-catalog/README.i18n.yaml +++ b/packages/session/session-format-catalog/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-format-catalog/README.md -README.md: 5550dd90b4f57ac24f34bc05d04a9e075a8dce72 -README.zh.md: b80b3bb6876c00e4dc33ae5bb2aa7eb48fdcb7b5 +README.md: 8cebff66b4d788792075784dfc5c59a6da92a400 +README.zh.md: 949cb7c21f0550c702072217fd567058f440454d diff --git a/packages/session/session-format-catalog/README.md b/packages/session/session-format-catalog/README.md index 5550dd90b4..8cebff66b4 100644 --- a/packages/session/session-format-catalog/README.md +++ b/packages/session/session-format-catalog/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -`dsh-session-format-catalog` gives persistence one deterministic Session format reader without consulting mounted plugins. It assembles codecs and adjacent edges from released v0 through current v3, checks the complete gap-free chain at module initialization, and exposes physical dispatch, header-only classification, single-pass row restoration, and current record encoding through `sessionFormatCatalog`. +`dsh-session-format-catalog` gives persistence one deterministic Session format reader without consulting mounted plugins. It assembles codecs and adjacent edges from the earliest supported format through the [current writer format](../../../docs/session-format-status.md), checks the complete gap-free chain at module initialization, and exposes physical dispatch, header-only classification, single-pass row restoration, and current record encoding through `sessionFormatCatalog`. ## Table of Contents diff --git a/packages/session/session-format-catalog/README.zh.md b/packages/session/session-format-catalog/README.zh.md index b80b3bb687..949cb7c21f 100644 --- a/packages/session/session-format-catalog/README.zh.md +++ b/packages/session/session-format-catalog/README.zh.md @@ -9,7 +9,7 @@ kind: "package-library" ## 概述 -`dsh-session-format-catalog` 为持久化提供一个确定性的 Session 格式读取器,且无需查询已挂载插件。它装配从已发布 v0 到当前 v3 的编解码器与相邻迁移边,在模块初始化时校验完整且无缺口的迁移链,并通过 `sessionFormatCatalog` 暴露物理分派、仅 header 分类、单遍行还原和当前格式逐记录编码。 +`dsh-session-format-catalog` 为持久化提供一个确定性的 Session 格式读取器,且无需查询已挂载插件。它装配从最早受支持格式到[当前写入格式](../../../docs/session-format-status.zh.md)的编解码器与相邻迁移边,在模块初始化时校验完整且无缺口的迁移链,并通过 `sessionFormatCatalog` 暴露物理分派、仅 header 分类、单遍行还原和当前格式逐记录编码。 ## 目录 diff --git a/packages/session/session-format-v2-to-v3/README.i18n.yaml b/packages/session/session-format-v2-to-v3/README.i18n.yaml index 1d01109622..b156fd7dae 100644 --- a/packages/session/session-format-v2-to-v3/README.i18n.yaml +++ b/packages/session/session-format-v2-to-v3/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-format-v2-to-v3/README.md -README.md: d13abe2b6324db5459babc173a6c6b4b7e0069b5 -README.zh.md: 5603d5a4049a0dc69c12fecf5347c9881da945be +README.md: 65e62f6d28f9f4cb0a75127429ae9a7ae610f5b2 +README.zh.md: c13a9fa57489759abf65e6ec1f7df178bdc2cdc8 diff --git a/packages/session/session-format-v2-to-v3/README.md b/packages/session/session-format-v2-to-v3/README.md index d13abe2b63..65e62f6d28 100644 --- a/packages/session/session-format-v2-to-v3/README.md +++ b/packages/session/session-format-v2-to-v3/README.md @@ -194,7 +194,7 @@ The edge preserves historical request meaning and model configuration; it does n - **Historical preset ambiguity** — released `code` references cannot distinguish a custom preset with the legacy built-in id; the [exact rename](#header-and-presets) is host-independent. -- **No file or settings migration** — this package never changes committed generations or `settings.yaml`. Persistence owns publishing the final successor; an existing V3 generation does not rerun its incoming edge. V3 is released; its compatibility obligations follow the [released-format policy](../../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md). +- **No file or settings migration** — this package never changes committed generations or `settings.yaml`. Persistence owns publishing the final successor; an existing V3 generation does not rerun its incoming edge. See [format release status](../../../docs/session-format-status.md) and the compatibility obligations in the [released-format policy](../../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md). ### Dev Note diff --git a/packages/session/session-format-v2-to-v3/README.zh.md b/packages/session/session-format-v2-to-v3/README.zh.md index 5603d5a404..c13a9fa574 100644 --- a/packages/session/session-format-v2-to-v3/README.zh.md +++ b/packages/session/session-format-v2-to-v3/README.zh.md @@ -194,7 +194,7 @@ V2 `session-log-deepseek/delivery-accepted` 若携带 `data.sessionFormatVersion - **历史预设歧义** — 已发布 `code` 引用无法区分与旧内置标识同名的自定义预设;[精确重命名](#header-and-presets)不依赖宿主。 -- **不迁移文件或设置** — 本包绝不修改已提交代或 `settings.yaml`。持久化负责发布最终后继代;已有 V3 代不重新运行其入边。V3 已发布;其兼容性义务遵循[已发布格式策略](../../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md)。 +- **不迁移文件或设置** — 本包绝不修改已提交代或 `settings.yaml`。持久化负责发布最终后继代;已有 V3 代不重新运行其入边。格式发布状态见[状态记录](../../../docs/session-format-status.zh.md),兼容性义务见[已发布格式策略](../../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md)。 ### 开发备注 diff --git a/packages/session/session-persistence-jsonl/README.i18n.yaml b/packages/session/session-persistence-jsonl/README.i18n.yaml index 856b68353f..7b0ceb074f 100644 --- a/packages/session/session-persistence-jsonl/README.i18n.yaml +++ b/packages/session/session-persistence-jsonl/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-persistence-jsonl/README.md -README.md: f60a1cb20ada758f0deaf59a16ce662ff66eaa71 -README.zh.md: d56513a05bb8fe04b1a518acf36d78a7b6c542c5 +README.md: 194ea835f87e4c34e0821083246bcf89c0e0a684 +README.zh.md: 4512d67e3439cbb4cf8642ca67be56bf0c7ac5b1 diff --git a/packages/session/session-persistence-jsonl/README.md b/packages/session/session-persistence-jsonl/README.md index f60a1cb20a..194ea835f8 100644 --- a/packages/session/session-persistence-jsonl/README.md +++ b/packages/session/session-persistence-jsonl/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -`dsh-session-persistence-jsonl` stores each session in a current append-only JSONL log and retains immutable historical format generations — checksummed Zstandard frames by default, raw newline-delimited lines when compression is disabled. It serves the current logical `SessionEvent` stream through persistence handles, so format migration, compression, historical decoding, and crash recovery remain storage-internal details. Choose it when consumers need a per-session file on disk; the logs are readable as plain lines when `compression: 'none'` is selected. A root directory is the one required configuration; durability, lazy materialization, released-v0/v1 migration, and torn-tail crash recovery come with the backend. +`dsh-session-persistence-jsonl` stores each session in a current append-only JSONL log and retains immutable historical format generations — checksummed Zstandard frames by default, raw newline-delimited lines when compression is disabled. It serves the current logical `SessionEvent` stream through persistence handles, so format migration, compression, historical decoding, and crash recovery remain storage-internal details. Choose it when consumers need a per-session file on disk; the logs are readable as plain lines when `compression: 'none'` is selected. A root directory is the one required configuration; durability, lazy materialization, [supported historical-format migration](../session-format-catalog/README.md), and torn-tail crash recovery come with the backend. ## Table of Contents diff --git a/packages/session/session-persistence-jsonl/README.zh.md b/packages/session/session-persistence-jsonl/README.zh.md index d56513a05b..4512d67e34 100644 --- a/packages/session/session-persistence-jsonl/README.zh.md +++ b/packages/session/session-persistence-jsonl/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -`dsh-session-persistence-jsonl` 把每个会话存为当前的仅追加 JSONL 日志,并保留不可变的历史格式 generation——默认以带校验和的 Zstandard 帧存储,禁用压缩时以换行分隔的原始文本行存储。它通过持久化句柄提供当前逻辑 `SessionEvent` 流,因此格式迁移、压缩、历史解码与崩溃恢复仍是存储内部细节。当消费方需要按会话的磁盘文件时选择它;选择 `compression: 'none'` 后日志可作为纯文本按行读取。根目录是唯一必填配置;持久性、延迟实体化、已发布 v0/v1 迁移与撕裂尾部崩溃恢复都随后端提供。 +`dsh-session-persistence-jsonl` 把每个会话存为当前的仅追加 JSONL 日志,并保留不可变的历史格式 generation——默认以带校验和的 Zstandard 帧存储,禁用压缩时以换行分隔的原始文本行存储。它通过持久化句柄提供当前逻辑 `SessionEvent` 流,因此格式迁移、压缩、历史解码与崩溃恢复仍是存储内部细节。当消费方需要按会话的磁盘文件时选择它;选择 `compression: 'none'` 后日志可作为纯文本按行读取。根目录是唯一必填配置;持久性、延迟实体化、[受支持的历史格式迁移](../session-format-catalog/README.zh.md)与撕裂尾部崩溃恢复都随后端提供。 ## 目录 diff --git a/packages/session/session-persistence-jsonl/src/format.ts b/packages/session/session-persistence-jsonl/src/format.ts index f8fdb25874..4129207ece 100644 --- a/packages/session/session-persistence-jsonl/src/format.ts +++ b/packages/session/session-persistence-jsonl/src/format.ts @@ -304,7 +304,7 @@ export function logPath( } /** - * Serialize a v2 event batch as JSONL lines (no trailing newline). Compact + * Serialize a current event batch as JSONL lines (no trailing newline). Compact * Assistant streams are nested event data; every event occupies one row. * @param events - the batch to serialize, in log order. * @returns the batch's JSONL text; the writer adds the final newline. @@ -314,7 +314,7 @@ export function eventLines(events: readonly SessionEvent[]): string { } /** - * Serialize one v2 event as one JSONL record without its trailing newline. + * Serialize one current event as one JSONL record without its trailing newline. * @param event - current event to encode. * @returns one physical JSON record. */ diff --git a/packages/test-support/session-snapshot/README.i18n.yaml b/packages/test-support/session-snapshot/README.i18n.yaml index 8cc6afcbb6..648c027c08 100644 --- a/packages/test-support/session-snapshot/README.i18n.yaml +++ b/packages/test-support/session-snapshot/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/test-support/session-snapshot/README.md -README.md: c8d2ecc7c9d187006b711fdf4efd1ca205773cd9 -README.zh.md: 20fea4e4351a857908d5a9d9f78c1f80131c0ada +README.md: a73ea265516f965bec8d11c41d6eb5bb028c7ac6 +README.zh.md: 544694a9fe9678d89cffb1f3293ea4e648e89f5d diff --git a/packages/test-support/session-snapshot/README.md b/packages/test-support/session-snapshot/README.md index c8d2ecc7c9..a73ea26551 100644 --- a/packages/test-support/session-snapshot/README.md +++ b/packages/test-support/session-snapshot/README.md @@ -70,15 +70,15 @@ defineAcpSnapshotSuite({ Each recorded-session directory carries a closed `snapshot.yml` manifest plus canonical parent and contiguous child roles. Parent filenames are `session[.vN].jsonl`; children are `session.[.vN].jsonl`; v0 omits `.v0`, positive versions use lowercase `.vN`, and every filename agrees with its header. A role may retain older generations, but the harness selects the numerically highest one. An owning manifest may declare `sessionFormat.version` plus one or more closed `coverage` names to keep that historical generation as an explicit migration fixture; absence tracks the current writer. The manifest also names the scenario, shipped profile, composition/header class, recording source, and only the replay, platform, permission, environment, workspace, or input facts the completed Session cannot reconstruct. Storage guards check tool results and portable paths in every selected parent and child role. Prompt/schema scrubbing, message identity, and prompt-before-request ordering apply to current generations; retained predecessors keep their historical representation. The adapter registers expected-output, Session-log, and optional `workspace.expected/` comparisons; guards reject orphan directories, missing roles, noncanonical names, absolute paths, malformed manifests, and platform-specific separators. -`normalizeSessionSnapshot` retains the complete Session header and event payloads but omits top-level `seq`/`time` envelopes from committed fixtures after normalizing paths and scrubbing system-prompt text and tool schemas; it also normalizes embedded stream clocks and historical packed-row `seq0`/`time0` envelopes, and catalog child-creation clocks. Event order and source-event references are preserved. Replay synthesizes the top-level envelopes in memory, while runtime persistence continues to write complete logs. Multi-session comparison validates expected and harvested logs through the strict build-static Session format catalog before identity redaction and normalization; source filenames cannot alter format validation. A retained historical replay input is not the native V3 writer-output oracle: structural migration preserves request meaning but can produce a different event layout. Normalization preserves unexpected request-header fields, including `system`, so regressions remain visible. Versionless protocol-adapter unit fixtures remain outside the released Session format corpus. Current v3 fixtures use one row per event; retained v0/v1 fixtures may use canonical packed rows. The [temporary repository migrator](../../../scripts/migrate-packed-session-fixtures.ts) (`pnpm run migrate:packed-session-fixtures`) rewrites older historical layouts, and its [removal proposal](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md) owns its deletion. +`normalizeSessionSnapshot` retains the complete Session header and event payloads but omits top-level `seq`/`time` envelopes from committed fixtures after normalizing paths and scrubbing system-prompt text and tool schemas; it also normalizes embedded stream clocks and historical packed-row `seq0`/`time0` envelopes, and catalog child-creation clocks. Event order and source-event references are preserved. Replay synthesizes the top-level envelopes in memory, while runtime persistence continues to write complete logs. Multi-session comparison validates expected and harvested logs through the strict build-static Session format catalog before identity redaction and normalization; source filenames cannot alter format validation. A retained historical replay input is not the native current-format writer-output oracle: structural migration preserves request meaning but can produce a different event layout. Normalization preserves unexpected request-header fields, including `system`, so regressions remain visible. Versionless protocol-adapter unit fixtures remain outside the released Session format corpus. Fixtures in the [current writer format](../../../docs/session-format-status.md) use one row per event; retained v0/v1 fixtures may use canonical packed rows. The [temporary repository migrator](../../../scripts/migrate-packed-session-fixtures.ts) (`pnpm run migrate:packed-session-fixtures`) rewrites older historical layouts, and its [removal proposal](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md) owns its deletion. Spill scenarios save through the real local provider under a private temporary root. Their fixture adapter exposes fixed-length logical locators and maps only this run’s saved locators back to real files for retrieval, preserving preview budgets without writing into a shared logical path. Known snapshot spill paths normalize to stable locator tokens, including paths quoted inside JSON omission notices with JSON-escaped Windows separators. Refresh extraction preserves the matched serialized path spelling for literal replacement. Normalization changes only the locator: saved byte lengths and omission counts remain comparison evidence. -Retained historical scenarios keep their canonical Session files unchanged and selected for replay, with no canonical V3 sibling in the pinned directory. Their exact normalized native V3 output is recorded separately in `writer.expected.jsonl` for the parent and `writer..expected.jsonl` for children; these are output oracles, not replay generations. Retained SDK scenarios use `notifications.current.expected.jsonl` for current protocol output. Comparisons neither project current events back into a historical format nor strip structural differences. Independent migration tests verify the official transform rather than treating native writer layout as its expected event sequence. +Retained historical scenarios keep their canonical Session files unchanged and selected for replay, with no newer canonical sibling in the pinned directory. Their exact normalized native current-format output is recorded separately in `writer.expected.jsonl` for the parent and `writer..expected.jsonl` for children; these are output oracles, not replay generations. Retained SDK scenarios use `notifications.current.expected.jsonl` for current protocol output. Comparisons neither project current events back into a historical format nor strip structural differences. Independent migration tests verify the official transform rather than treating native writer layout as its expected event sequence. ### Record, replay, and refresh -`pnpm run test:snapshot:record` calls the live LLM and writes the harvested current generation under its canonical versioned filename. Record and refresh never rename or delete a completed generation, including generations of a child role absent from a later run; reviewed source-tree curation removes a predecessor only after the same role has a verified current replacement. Scenarios with an explicit `sessionFormat` remain read-only in record mode. `pnpm run test:snapshot:refresh` stays keyless, runs the selected highest replay input, and writes stdout, owned prompt and tool-schema sidecars, and a fresh current-generation comparable Session output; retained historical scenarios write the separate writer-output oracles instead of a canonical V3 replay generation. Each composition owner keeps its replay patch beside its live patch; top-level `snapshots/` owns Session-driven scenarios, while other expected outputs stay beside their owning package. [`dsh-llm-replay`](../llm-replay/README.md) serves the recorded streams selected through `DSH_SNAPSHOT_*` environment values. +`pnpm run test:snapshot:record` calls the live LLM and writes the harvested current generation under its canonical versioned filename. Record and refresh never rename or delete a completed generation, including generations of a child role absent from a later run; reviewed source-tree curation removes a predecessor only after the same role has a verified current replacement. Scenarios with an explicit `sessionFormat` remain read-only in record mode. `pnpm run test:snapshot:refresh` stays keyless, runs the selected highest replay input, and writes stdout, owned prompt and tool-schema sidecars, and a fresh current-generation comparable Session output; retained historical scenarios write the separate writer-output oracles instead of a canonical current-format replay generation. Each composition owner keeps its replay patch beside its live patch; top-level `snapshots/` owns Session-driven scenarios, while other expected outputs stay beside their owning package. [`dsh-llm-replay`](../llm-replay/README.md) serves the recorded streams selected through `DSH_SNAPSHOT_*` environment values. ### Pinning request headers and system prompts diff --git a/packages/test-support/session-snapshot/README.zh.md b/packages/test-support/session-snapshot/README.zh.md index 20fea4e435..544694a9fe 100644 --- a/packages/test-support/session-snapshot/README.zh.md +++ b/packages/test-support/session-snapshot/README.zh.md @@ -70,15 +70,15 @@ defineAcpSnapshotSuite({ 每个已记录 Session 目录携带封闭的 `snapshot.yml` manifest,以及规范 parent 与连续 child 角色。parent 文件名是 `session[.vN].jsonl`;child 是 `session.[.vN].jsonl`;v0 省略 `.v0`,正版本使用小写 `.vN`,且每个文件名与其 header 一致。一个角色可以保留旧 generation,但 harness 会选择数值最高的一项。拥有 fixture 的 manifest 可以声明 `sessionFormat.version` 与一个或多个封闭 `coverage` 名称,把该历史 generation 保留为显式迁移 fixture;省略此字段时跟随当前 writer。manifest 还会指名场景、随附 profile、组合/header 类别、录制来源,以及已完成 Session 无法重建的 replay、平台、权限、环境、workspace 或输入事实。存储保护检查每个选定 parent 与 child 角色的工具结果和可移植路径。提示词/schema 擦除、消息身份及提示词先于请求的顺序检查适用于当前 generation;保留的前代维持其历史表示。适配器注册预期输出、Session 日志与可选 `workspace.expected/` 比较;保护会拒绝遗留目录、缺失角色、非规范名称、绝对路径、malformed manifest 与平台专用分隔符。 -`normalizeSessionSnapshot` 在规范化路径并擦除系统提示文本与工具 schema 后,会保留完整 Session header 与事件 payload,但从已提交 fixture 中省略顶层 `seq`/`time` envelope;它还会规范化嵌入式 stream clock 与历史 packed-row 的 `seq0`/`time0` envelope 与 catalog child 创建时钟。事件顺序与来源事件引用保持不变。Replay 只在内存中合成顶层 envelope,而运行时持久化仍写入完整日志。多 Session 比较会先通过严格的构建期静态 Session 格式目录校验预期日志与收集日志,再进行身份脱敏与规范化;来源文件名不能改变格式校验。保留的历史 replay 输入不是原生 V3 writer 输出的比较基准:结构迁移保留请求含义,但可以产生不同的事件布局。归一化保留意外的 request-header 字段(包括 `system`),使回归保持可见。无版本的协议适配器单元测试 fixture 不属于已发布 Session 格式语料。当前 v3 fixture 每个事件占一行;保留的 v0/v1 fixture 可以使用规范 packed row。[临时仓库迁移器](../../../scripts/migrate-packed-session-fixtures.ts)(`pnpm run migrate:packed-session-fixtures`)会改写更旧的历史布局,由其[移除提案](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.zh.md)负责删除该迁移器。 +`normalizeSessionSnapshot` 在规范化路径并擦除系统提示文本与工具 schema 后,会保留完整 Session header 与事件 payload,但从已提交 fixture 中省略顶层 `seq`/`time` envelope;它还会规范化嵌入式 stream clock 与历史 packed-row 的 `seq0`/`time0` envelope 与 catalog child 创建时钟。事件顺序与来源事件引用保持不变。Replay 只在内存中合成顶层 envelope,而运行时持久化仍写入完整日志。多 Session 比较会先通过严格的构建期静态 Session 格式目录校验预期日志与收集日志,再进行身份脱敏与规范化;来源文件名不能改变格式校验。保留的历史 replay 输入不是原生当前格式 writer 输出的比较基准:结构迁移保留请求含义,但可以产生不同的事件布局。归一化保留意外的 request-header 字段(包括 `system`),使回归保持可见。无版本的协议适配器单元测试 fixture 不属于已发布 Session 格式语料。[当前写入器格式](../../../docs/session-format-status.zh.md)的 fixture 每个事件占一行;保留的 v0/v1 fixture 可以使用规范 packed row。[临时仓库迁移器](../../../scripts/migrate-packed-session-fixtures.ts)(`pnpm run migrate:packed-session-fixtures`)会改写更旧的历史布局,由其[移除提案](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.zh.md)负责删除该迁移器。 Spill 场景通过真实本地 provider 保存到私有临时根目录。夹具适配器提供固定长度的逻辑定位符,并仅将本次运行已保存的定位符映射回实际文件以供检索,在不写入共享逻辑路径的情况下保留预览预算。已知的快照 spill 路径会规范化为稳定的定位信息 token,包括 JSON 省略通知中带引号、使用 JSON 转义 Windows 分隔符的路径。刷新提取会保留匹配路径的序列化写法,以便进行字面替换。规范化只改变定位信息:保存字节数与省略计数仍作为比较证据。 -保留历史输入的场景保持规范 Session 文件不变,并继续选择它们进行回放;固定历史版本的目录中没有规范 V3 同角色文件。其精确的规范化原生 V3 输出单独记录在父会话的 `writer.expected.jsonl` 和子会话的 `writer..expected.jsonl` 中;这些是输出比较基准,而非 replay 代际。保留历史输入的 SDK 场景使用 `notifications.current.expected.jsonl` 记录当前协议输出。比较既不将当前事件反向投影为历史格式,也不剥除结构差异。独立迁移测试验证正式转换,而不把原生 writer 布局当作其预期事件序列。 +保留历史输入的场景保持规范 Session 文件不变,并继续选择它们进行回放;固定历史版本的目录中没有更新的规范同角色文件。其精确的规范化原生当前格式输出单独记录在父会话的 `writer.expected.jsonl` 和子会话的 `writer..expected.jsonl` 中;这些是输出比较基准,而非 replay 代际。保留历史输入的 SDK 场景使用 `notifications.current.expected.jsonl` 记录当前协议输出。比较既不将当前事件反向投影为历史格式,也不剥除结构差异。独立迁移测试验证正式转换,而不把原生 writer 布局当作其预期事件序列。 ### 录制、回放与刷新 -`pnpm run test:snapshot:record` 调用在线 LLM(大语言模型),并在规范具名版本文件下写入收集到的当前 generation。record 与 refresh 绝不重命名或删除已完成的 generation,即使后续运行不再产生某个 child 角色也一样;受审阅的源树整理只有在同角色存在已验证的当前替代文件后才移除前代。显式声明 `sessionFormat` 的场景在录制模式下保持只读。`pnpm run test:snapshot:refresh` 保持无密钥,运行选定的最高 replay 输入,并写入 stdout、各 pin 自有的 prompt 与工具 schema sidecar,以及新鲜当前 generation 的可比较 Session 输出;保留历史输入的场景写入单独的 writer 输出比较基准,而非规范 V3 replay 代际。每个组合 owner 把 replay patch 放在 live patch 旁;顶层 `snapshots/` 拥有 Session 驱动场景,其他预期输出留在其 package owner 旁。[`dsh-llm-replay`](../llm-replay/README.zh.md) 提供通过 `DSH_SNAPSHOT_*` 环境值选择的已记录流。 +`pnpm run test:snapshot:record` 调用在线 LLM(大语言模型),并在规范具名版本文件下写入收集到的当前 generation。record 与 refresh 绝不重命名或删除已完成的 generation,即使后续运行不再产生某个 child 角色也一样;受审阅的源树整理只有在同角色存在已验证的当前替代文件后才移除前代。显式声明 `sessionFormat` 的场景在录制模式下保持只读。`pnpm run test:snapshot:refresh` 保持无密钥,运行选定的最高 replay 输入,并写入 stdout、各 pin 自有的 prompt 与工具 schema sidecar,以及新鲜当前 generation 的可比较 Session 输出;保留历史输入的场景写入单独的 writer 输出比较基准,而非规范当前格式 replay 代际。每个组合 owner 把 replay patch 放在 live patch 旁;顶层 `snapshots/` 拥有 Session 驱动场景,其他预期输出留在其 package owner 旁。[`dsh-llm-replay`](../llm-replay/README.zh.md) 提供通过 `DSH_SNAPSHOT_*` 环境值选择的已记录流。 ### 固定请求 header 与系统提示 diff --git a/scripts/doc-standard.spec.ts b/scripts/doc-standard.spec.ts index bfb90703e0..9838d3e2c5 100644 --- a/scripts/doc-standard.spec.ts +++ b/scripts/doc-standard.spec.ts @@ -9,8 +9,9 @@ import { existsSync, globSync, readFileSync } from 'node:fs' import { resolve, sep } from 'node:path' -import { load } from 'js-yaml' +import { JSON_SCHEMA, load } from 'js-yaml' import { describe, expect, it } from 'vitest' +import { readCurrentSessionFormatVersion } from './gen-session-format-catalog.ts' const root = resolve(import.meta.dirname, '..') const PACKAGE_README_GLOBS = [ @@ -146,6 +147,150 @@ function packageReadmeStructureErrors(file: string, source: string): string[] { return required.flatMap(([pattern, label]) => pattern.test(source) ? [] : [`missing ${label}`]) } +interface SessionFormatRelease { + latestReleasedVersion: number + evidenceTag: string +} + +function validateSessionFormatRelease(source: string, currentWriterVersion: number): SessionFormatRelease { + const normalized = source.replaceAll('\r\n', '\n') + const openings = [...normalized.matchAll(/^```yaml session-format-release[ \t]*$/gmu)] + if (openings.length !== 1) throw new Error('Expected exactly one session-format-release record') + const block = /^```yaml session-format-release[ \t]*\n([\s\S]*?)^```[ \t]*$/mu.exec(normalized) + if (block === null) throw new Error('Expected a closed session-format-release record') + const metadata: unknown = load(block[1]!, { schema: JSON_SCHEMA }) + if (metadata === null || typeof metadata !== 'object' || Array.isArray(metadata)) { + throw new Error('Session format release record must be a mapping') + } + const fields = Object.keys(metadata).sort() + if (fields.join(',') !== 'evidenceTag,latestReleasedVersion') { + throw new Error('Session format release record requires exactly latestReleasedVersion and evidenceTag') + } + const { latestReleasedVersion, evidenceTag } = metadata as Record + if (typeof latestReleasedVersion !== 'number' || !Number.isSafeInteger(latestReleasedVersion) + || latestReleasedVersion < 0) { + throw new Error('latestReleasedVersion must be a non-negative safe integer') + } + if (latestReleasedVersion > currentWriterVersion) { + throw new Error('latestReleasedVersion must not exceed the current writer version') + } + if (typeof evidenceTag !== 'string' + || !/^dsh-v\d+\.\d+\.\d+(?:-[\dA-Za-z]+(?:[.-][\dA-Za-z]+)*)?(?:\+[\dA-Za-z]+(?:[.-][\dA-Za-z]+)*)?$/u.test(evidenceTag)) { + throw new Error('evidenceTag must be a non-empty dsh-v version tag without URL delimiters') + } + const repository = 'https://github.com/deepseek-harness/deepseek-harness' + for (const link of [ + `${repository}/releases/tag/${evidenceTag}`, + `${repository}/blob/${evidenceTag}/packages/core/session/src/types.ts`, + ]) { + if (!normalized.includes(`](${link})`)) throw new Error(`Missing matching evidence link: ${link}`) + } + return { latestReleasedVersion, evidenceTag } +} + +function sessionFormatReleaseFixture(): { record: SessionFormatRelease; body: string; links: string; source: string } { + const record = validateSessionFormatRelease( + readFileSync(resolve(root, 'docs/session-format-status.md'), 'utf8'), + readCurrentSessionFormatVersion(root), + ) + const body = `latestReleasedVersion: ${record.latestReleasedVersion}\nevidenceTag: ${record.evidenceTag}` + const repository = 'https://github.com/deepseek-harness/deepseek-harness' + const links = `[release](${repository}/releases/tag/${record.evidenceTag})\n` + + `[source](${repository}/blob/${record.evidenceTag}/packages/core/session/src/types.ts)` + return { record, body, links, source: releaseDocument(body, links) } +} + +function releaseDocument(body: string, links: string): string { + return `\`\`\`yaml session-format-release\n${body}\n\`\`\`\n\n${links}\n` +} + +describe('Session format release authority', () => { + it('keeps the bilingual release records equal and consistent with the writer and evidence links', () => { + const records = ['docs/session-format-status.md', 'docs/session-format-status.zh.md'].map(file => + validateSessionFormatRelease(readFileSync(resolve(root, file), 'utf8'), readCurrentSessionFormatVersion(root)), + ) + expect(records[0]).toEqual(records[1]) + }) + + it('accepts a released writer and a newer development writer, including format zero', () => { + const { record, body, links, source } = sessionFormatReleaseFixture() + expect(validateSessionFormatRelease(source, record.latestReleasedVersion)).toEqual(record) + expect(validateSessionFormatRelease(source, record.latestReleasedVersion + 1)).toEqual(record) + const zero = releaseDocument(body.replace(`latestReleasedVersion: ${record.latestReleasedVersion}`, 'latestReleasedVersion: 0'), links) + expect(validateSessionFormatRelease(zero, 0)).toEqual({ ...record, latestReleasedVersion: 0 }) + }) + + it('rejects missing, duplicated, unclosed, and malformed release records', () => { + const { record, body, links, source } = sessionFormatReleaseFixture() + for (const invalid of [ + links, + source + source, + source + '\n```yaml session-format-release\n', + `\`\`\`yaml session-format-release\n${body}`, + releaseDocument('[unterminated', links), + releaseDocument('', links), + releaseDocument('null', links), + releaseDocument('scalar', links), + releaseDocument(`- latestReleasedVersion: ${record.latestReleasedVersion}`, links), + releaseDocument(`${body}\n---\n${body}`, links), + ]) { + expect(() => validateSessionFormatRelease(invalid, record.latestReleasedVersion), invalid).toThrow() + } + }) + + it('rejects missing, duplicate, and extra record fields', () => { + const { record, body, links } = sessionFormatReleaseFixture() + for (const invalid of [ + '{}', + `evidenceTag: ${record.evidenceTag}`, + `latestReleasedVersion: ${record.latestReleasedVersion}`, + `${body}\nlatestReleasedVersion: ${record.latestReleasedVersion}`, + `${body}\nevidenceTag: ${record.evidenceTag}`, + `${body}\nreleased: true`, + ]) { + expect(() => validateSessionFormatRelease(releaseDocument(invalid, links), record.latestReleasedVersion), invalid).toThrow() + } + }) + + it('rejects invalid released versions and releases beyond the current writer', () => { + const { record, links } = sessionFormatReleaseFixture() + for (const value of ['-1', '1.5', String(Number.MAX_SAFE_INTEGER + 1), '.inf', '.nan', 'null', 'true', '"0"']) { + const source = releaseDocument(`latestReleasedVersion: ${value}\nevidenceTag: ${record.evidenceTag}`, links) + expect(() => validateSessionFormatRelease(source, Number.MAX_SAFE_INTEGER), value).toThrow('non-negative safe integer') + } + const writer = readCurrentSessionFormatVersion(root) + const future = releaseDocument(`latestReleasedVersion: ${writer + 1}\nevidenceTag: ${record.evidenceTag}`, links) + expect(() => validateSessionFormatRelease(future, writer)).toThrow('must not exceed the current writer') + }) + + it('rejects empty, malformed, and URL-injecting evidence tags', () => { + const { record, links } = sessionFormatReleaseFixture() + for (const tag of [ + null, true, 1, '', ' ', 'dsh-v', record.evidenceTag.replace('dsh-v', 'v'), + `${record.evidenceTag}/other`, `${record.evidenceTag}?query`, `${record.evidenceTag}#fragment`, + `${record.evidenceTag}%2Fother`, `${record.evidenceTag})`, `${record.evidenceTag}\n`, + ]) { + const source = releaseDocument(`latestReleasedVersion: ${record.latestReleasedVersion}\nevidenceTag: ${JSON.stringify(tag)}`, links) + expect(() => validateSessionFormatRelease(source, record.latestReleasedVersion), String(tag)).toThrow('dsh-v version tag') + } + }) + + it('rejects absent or mismatched release and tagged-source links', () => { + const { record, body, links } = sessionFormatReleaseFixture() + for (const invalid of [ + '', + links.replace(`/releases/tag/${record.evidenceTag}`, `/releases/tag/${record.evidenceTag}-other`), + links.replace(`/blob/${record.evidenceTag}/`, '/blob/main/'), + links.replace('/packages/core/session/src/types.ts', '/packages/core/session/src/other.ts'), + links.replaceAll('github.com', 'example.com'), + links.replace(`${record.evidenceTag})`, `${record.evidenceTag}?query)`), + links.replace('types.ts)', 'types.ts#fragment)'), + ]) { + expect(() => validateSessionFormatRelease(releaseDocument(body, invalid), record.latestReleasedVersion), invalid).toThrow('Missing matching evidence link') + } + }) +}) + describe('dsh-doc skill consolidation', () => { it('carries no prototype-era language', () => { const files = [