@deepseek-ai/dsh-llm-replay 0.1.2-alpha.5 → 0.1.3-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/test-support/llm-replay/README.md
5
- README.md: c0643bf6823e96cf3663f8299d0f5645d7974011
6
- README.zh.md: 1c2fe3393691b4988678497aa5948957f678c80b
5
+ README.md: e8a257c2ee9f95f1e7d20197e6db67b2b4c7b45c
6
+ README.zh.md: 5d085959982bcaa090a893089c0295426141bed2
package/README.md CHANGED
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  ## Summary
11
11
 
12
- `dsh-llm-replay` makes snapshot tests run without an API key: it installs a replay LLM adapter that serves model streams reconstructed from a recorded session JSONL fixture, so a test boots the real agent against a fixed transcript. The fixture is a projection of the persisted session log — `assistant/chunk` events group into per-call chunk sequences, and an explicitly marked local compaction call replays as one canonical stream. A `replay.override.json` sidecar covers what a log cannot reconstruct: a throw before any chunk, a cancel/hang, or an injected retry. Live sessions bind to recorded scripts by first-call order, so parent-and-subagent scenarios each get their own script. It is the model source behind the ACP and headless snapshot suites and the Web browser e2e lane.
12
+ `dsh-llm-replay` makes snapshot tests run without an API key: it installs a replay LLM adapter that serves model streams reconstructed from a recorded session JSONL fixture, so a test boots the real agent against a fixed transcript. The fixture is a projection of the persisted session log — each `assistant/message` or `assistant/attempt` embeds one model-call stream, and an explicitly marked local compaction call replays as one canonical stream. A `replay.override.json` sidecar covers what a settlement cannot reconstruct: a throw before any chunk, a cancel/hang, or an injected retry. Live sessions bind to recorded scripts by first-call order, so parent-and-subagent scenarios each get their own script. It is the model source behind the ACP and headless snapshot suites and the Web browser e2e lane.
13
13
 
14
14
  ## Table of Contents
15
15
 
@@ -55,7 +55,7 @@ With `providers` configured, the plugin registers a replay-only adapter whose ca
55
55
 
56
56
  | Field | Default | Meaning |
57
57
  |---|---|---|
58
- | `file` | `$DSH_SNAPSHOT_FILE` | Path to the primary (parent) `session.jsonl` fixture; required (config or env) |
58
+ | `file` | `$DSH_SNAPSHOT_FILE` | Path to the selected primary fixture: `session.jsonl` for v0 or `session.vN.jsonl` for a positive generation; required (config or env) |
59
59
  | `overrideFile` | `$DSH_SNAPSHOT_OVERRIDE` | Optional `ReplayOverrideDoc` sidecar for the primary session |
60
60
  | `childFiles` | `$DSH_SNAPSHOT_CHILD_FILES` | Recorded subagent child-session logs for a nested scenario |
61
61
  | `providers` | — | Optional replay-only provider and model catalog; a model may declare `contextWindow`, text/image modalities, and positive `imageRequestTokens` when image-capable; invalid values fail at load and routes never perform provider I/O |
@@ -65,17 +65,17 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
65
65
 
66
66
  ### How the fixture works
67
67
 
68
- The fixture is a projection of a persisted session log (`<scenario>/session.jsonl`) produced by running the real agent once — this plugin does not record. It keeps the header and every event payload but omits body `seq`/`time` envelopes (`seq0`/`time0` for packed rows); replay restores contiguous synthetic envelopes while parsing, and one file cannot mix projected and complete body rows. Runtime persistence continues to write complete logs. Replay derives each model call's chunk sequence from `assistant/chunk` events, so a recorded fixture replays the same logical stream the live model produced. A fixture may carry its `request/header` content tokenized to `{{system}}`/`{{tools}}`; replay is indifferent because derivation reads only the chunk and summary events plus the line-0 session header.
68
+ The fixture is a projection of one selected persisted Session generation produced by running the real agent once — this plugin does not record. The snapshot harness supplies the numerically highest canonical parent path (`<scenario>/session.jsonl` for v0 or `<scenario>/session.vN.jsonl` for a positive generation), and validates filename/header agreement before replay. The fixture keeps the header and every event payload but omits body `seq`/`time` envelopes (`seq0`/`time0` for historical packed rows). Replay supplies contiguous sequences and deterministic timestamps, restores typed values replaced by snapshot tokens, rejects partial or mixed envelopes, decodes the complete physical artifact through the build-static Session format catalog, and migrates historical input in memory before it exposes events or the inherited cut; current input takes direct restoration. For a projected v0 header only, an absent `delegationDepth` denotes `0`. The parser never rewrites or renames the fixture. Runtime persistence continues to write complete logs. Replay expands the compact stream on each current-view `assistant/message` or `assistant/attempt`, so a recorded fixture replays the same logical stream the live model produced. A fixture may carry its `request/header` content tokenized to `{{system}}`/`{{tools}}`; replay materializes validation-only values, while derivation reads only Assistant settlements, marked summary events, and Session metadata. Every replay and comparison fixture must pass the same content-only catalog validation; replay never repairs a refused artifact.
69
69
 
70
70
  ### Nested agents
71
71
 
72
- A scenario where a parent agent delegates to in-process subagents records one log per session: the parent (`session.jsonl`) plus one per child (`session.1.jsonl`, …). Live session ids are freshly random each run, so replay binds each live session to a recorded script by first-call order: the first live session to make a model call claims the first script, the next new session the next, and so on, with each session advancing its own cursor. More distinct live sessions than recorded scripts fails loud.
72
+ A scenario where a parent agent delegates to in-process subagents records one role per Session: parent `session[.vN].jsonl`, then contiguous children `session.<ordinal>[.vN].jsonl`. The snapshot harness supplies only the highest generation of each role. Live Session ids are freshly random each run, so replay binds each live Session to a recorded script by first-call order: the first live Session to make a model call claims the parent script, the next new Session the next child script, and so on, with each Session advancing its own cursor. More distinct live Sessions than recorded scripts fails loud.
73
73
 
74
74
  ### Failure modes and overrides
75
75
 
76
76
  When replay serves `deepseek-official` with `ctx.deepseekLlmApiExtensions`, it prepares and accepts those fields after selecting a valid script entry and before yielding the first chunk. This mirrors the live adapter's post-2xx commit point, so durable acceptance watermarks and SDK event notifications behave the same in recording and replay. Replay supplies a synthetic `{ messages: [] }` base body: it proves acceptance side effects, not prepared field bytes.
77
77
 
78
- Two failure modes are not reconstructable from `assistant/chunk` alone — a pure throw before any chunk (for example an HTTP 401, where the log holds only a `turn/end {error}`) and a cancel/hang. A scenario that needs those supplies an optional sidecar (`<scenario>/replay.override.json`) that either replaces the derived script with a bare `ReplayEntry[]` or augments it with `{ patches: [{ at, entry }] }`, which keeps every derived call and swaps the named 0-based call indexes; `at` equal to the derived length appends the retry attempt after an injected transient throw. A `throw` entry accepts DeepSeek request extensions when it has prefix chunks; a zero-chunk throw defaults to pre-2xx non-acceptance and may set `accepted: true` for a post-2xx failure. A `hang` entry may name `readyFile`, which replay writes before waiting for cancellation so an external driver can cancel deterministically.
78
+ Two failure modes are not reconstructable from a durable Assistant settlement alone: a pure throw before any chunk has no exception-bearing stream member, and a cancel/hang requires nontermination rather than a replayed finite prefix. A scenario that needs those supplies an optional sidecar (`<scenario>/replay.override.json`) that either replaces the derived script with a bare `ReplayEntry[]` or augments it with `{ patches: [{ at, entry }] }`, which keeps every derived call and swaps the named 0-based call indexes; `at` equal to the derived length appends the retry attempt after an injected transient throw. A `throw` entry accepts DeepSeek request extensions when it has prefix chunks; a zero-chunk throw defaults to pre-2xx non-acceptance and may set `accepted: true` for a post-2xx failure. A `hang` entry may name `readyFile`, which replay writes before waiting for cancellation so an external driver can cancel deterministically.
79
79
 
80
80
  ### What can go wrong
81
81
 
@@ -95,13 +95,16 @@ This section explains the design of the replay plugin; the observable behavior i
95
95
 
96
96
  ### Design
97
97
 
98
- Replay is built on one idea: the projected session log is the fixture. `deriveReplayScript` parses the JSONL header (for `id`/`createdAt` ordering facts) and splits `assistant/chunk` events at every `finish` chunk, keyed by `(turn, step)`, so each recorded `stream()` call becomes one `chunks` entry; an assistant group without a `finish` chunk is the fingerprint of a thrown `stream()` and must be expressed through an override sidecar. A `compaction/summary` carrying `llmStreamCall: true` and a complete `rawOutput` replays as one canonical successful stream at that event's position. Scripted strings may embed `{{fromRequest:<regex>}}`; at stream time each placeholder resolves against the live request's string leaves, taking the pattern's last match and its first capture group (or the whole match) in place.
98
+ Replay treats the selected projected Session generation as the fixture. One parser completes projected envelopes, validates and migrates the whole artifact through `sessionFormatCatalog`, and returns the current header, inherited cut, and event list as one result. `deriveReplayScript` expands each `assistant/message` or `assistant/attempt` stream in log order, so each durable settlement becomes one `chunks` entry; a non-empty stream without a `finish` chunk is the fingerprint of a thrown `stream()` and must be expressed through an override sidecar. A `compaction/summary` carrying `llmStreamCall: true` and a complete `rawOutput` replays as one canonical successful stream at that event's position. Scripted strings may embed `{{fromRequest:<regex>}}`; at stream time each placeholder resolves against the live request's string leaves, taking the pattern's last match and its first capture group (or the whole match) in place.
99
+
100
+ The committed-corpus test discovers every versioned `session*.jsonl` under `snapshots/`, `packages/`, and `scripts/snapshots/python-sdk-single-exe/`. Every artifact must restore to the current view through the real catalog. Any refusal fails with the artifact path, so the fixture owner must correct the invalid relationship before replay or comparison.
99
101
 
100
102
  ### Source map
101
103
 
102
104
  | File | Role |
103
105
  |---|---|
104
106
  | [`src/index.ts`](src/index.ts) | Types, fixture derivation, override validation, placeholder resolution, session binding, `installLlmReplay`, and the plugin export |
107
+ | [`tests/session-format-corpus.spec.ts`](tests/session-format-corpus.spec.ts) | Complete committed-generation restoration burn-in; every refusal is a failure |
105
108
  | — | No runtime invariant companion is published; this test-only adapter consumes a fixed replay script; its stream grammar is checked by the LLM companion and fixture derivation tests. |
106
109
 
107
110
  ### Binding and stream flow
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@ kind: "package-reference"
9
9
 
10
10
  ## 概述
11
11
 
12
- `dsh-llm-replay` 让快照测试无需 API 密钥即可运行:它安装一个回放 LLM(大语言模型)适配器,从已记录的会话 JSONL fixture(测试前置数据)重建模型流,使测试针对固定 transcript(文本记录)启动真实 agent(智能体)。fixture 是持久化会话日志的投影——`assistant/chunk` 事件按调用分组为分片序列,显式标记的本地压缩(compaction)调用回放为一条规范流。`replay.override.json` 伴随文件覆盖日志无法重建的情况:任何分片之前就抛出、取消/挂起,或注入重试。实时会话按首次调用顺序绑定到已记录脚本,因此父会话与 subagent 场景各自获得自己的脚本。它是 ACP 与 headless 快照套件以及 Web 浏览器 e2e 流水线的模型来源。
12
+ `dsh-llm-replay` 让快照测试无需 API 密钥即可运行:它安装一个回放 LLM(大语言模型)适配器,从已记录的会话 JSONL fixture(测试前置数据)重建模型流,使测试针对固定 transcript(文本记录)启动真实 agent(智能体)。fixture 是持久化会话日志的投影——每个 `assistant/message` 或 `assistant/attempt` 都嵌入一次模型调用的 stream,显式标记的本地压缩(compaction)调用则回放为一条规范流。`replay.override.json` 伴随文件覆盖 settlement 无法重建的情况:任何分片之前就抛出、取消/挂起,或注入重试。实时会话按首次调用顺序绑定到已记录脚本,因此父会话与 subagent 场景各自获得自己的脚本。它是 ACP 与 headless 快照套件以及 Web 浏览器 e2e 流水线的模型来源。
13
13
 
14
14
  ## 目录
15
15
 
@@ -55,7 +55,7 @@ kind: "package-reference"
55
55
 
56
56
  | 字段 | 默认值 | 含义 |
57
57
  |---|---|---|
58
- | `file` | `$DSH_SNAPSHOT_FILE` | 主(父)`session.jsonl` fixture 的路径;必需(配置或 env) |
58
+ | `file` | `$DSH_SNAPSHOT_FILE` | 选定 primary fixture 路径:v0 为 `session.jsonl`,正 generation 为 `session.vN.jsonl`;必需(config 或 env) |
59
59
  | `overrideFile` | `$DSH_SNAPSHOT_OVERRIDE` | 主会话的可选 `ReplayOverrideDoc` 伴随文件 |
60
60
  | `childFiles` | `$DSH_SNAPSHOT_CHILD_FILES` | 嵌套场景中已记录的 subagent 子会话日志 |
61
61
  | `providers` | 无 | 可选的仅回放提供方与模型目录;模型可声明 `contextWindow`、文本/图片模态,以及图片模型使用的正整数 `imageRequestTokens`;非法值会在加载时失败,路由绝不执行提供方 I/O |
@@ -65,17 +65,17 @@ kind: "package-reference"
65
65
 
66
66
  ### fixture 的工作方式
67
67
 
68
- fixture 是运行一次真实 agent 所产生的持久化会话日志(`<scenario>/session.jsonl`)的投影——本插件不录制。它保留 header 与每个事件 payload,但省略正文的 `seq`/`time` envelope(打包行使用 `seq0`/`time0`);回放在解析时恢复连续的 synthetic envelope,且同一文件不能混用投影正文行与完整正文行。运行时持久化仍写入完整日志。回放从 `assistant/chunk` 事件派生每次模型调用的分片序列,因此已记录 fixture 会回放与在线模型产生的相同逻辑流。fixture 的 `request/header` 内容可能被标记化为 `{{system}}`/`{{tools}}`;回放不受影响,因为派生只读取分片与摘要事件以及第 0 行的会话 header。
68
+ fixture 是运行一次真实 agent 所产生的一份选定持久化 Session generation 投影,本插件不录制。snapshot harness 会提供数值最高的规范 parent 路径(v0 为 `<scenario>/session.jsonl`,正 generation 为 `<scenario>/session.vN.jsonl`),并在 replay 前校验文件名与 header 一致。fixture 保留 header 与每个事件 payload,但省略正文的 `seq`/`time` envelope(历史 packed row 使用 `seq0`/`time0`)。replay 补充连续序号与确定性 timestamp,恢复被 snapshot token 替换的类型化值,拒绝不完整或混合 envelope,通过构建期静态 Session 格式 catalog 解码完整物理产物,并在公开事件或继承 cut 前于内存中迁移历史输入;当前输入直接 restore。仅对投影 v0 header,缺失的 `delegationDepth` 表示 `0`。parser 从不重写或重命名 fixture。runtime persistence 继续写入完整日志。replay 会展开当前视图中每个 `assistant/message` 或 `assistant/attempt` 的紧凑 stream,因此已记录 fixture 会 replay 与在线模型产生的相同逻辑流。fixture 的 `request/header` 内容可能 token 化为 `{{system}}`/`{{tools}}`;replay 会物化仅用于校验的值,而派生只读取 Assistant settlement、带标记的 summary 事件与 Session metadata。每个 replay 与 comparison fixture 都必须通过同一个只基于内容的 catalog 校验;replay 绝不修复被拒绝的产物。
69
69
 
70
70
  ### 嵌套 agent
71
71
 
72
- 父 agent 委托给进程内 subagent 的场景会按会话记录日志:父会话使用 `session.jsonl`,每个子会话各使用一个(`session.1.jsonl` 等)。实时会话 id 每次运行都会重新随机生成,因此回放按首次调用顺序把每个实时会话绑定到已记录脚本:第一个发起模型调用的实时会话取得第一个脚本,下一个新会话取得下一个,依此类推,每个会话分别推进自己的游标。不同实时会话数量超过已记录脚本数时会明确报错。
72
+ parent agent 委托给进程内 subagent 的场景会为每个 Session 记录一个角色:parent 为 `session[.vN].jsonl`,随后是连续 child `session.<ordinal>[.vN].jsonl`。snapshot harness 只提供每个角色的最高 generation。live Session id 每次运行都会重新随机生成,因此 replay 按首次调用顺序把每个 live Session 绑定到已记录脚本:第一个发起模型调用的 live Session 取得 parent 脚本,下一个新 Session 取得下一条 child 脚本,依此类推,每个 Session 分别推进自己的 cursor。不同 live Session 数量超过已记录脚本数时会明确报错。
73
73
 
74
74
  ### 失败模式与覆盖
75
75
 
76
76
  当回放在带有 `ctx.deepseekLlmApiExtensions` 的组合中服务 `deepseek-official` 时,它会在选择有效脚本条目后、产生首个分片前准备并接受这些字段。这与实时适配器的 2xx 后提交点一致,因此持久接受水位与 SDK 事件通知在录制和回放中行为相同。回放提供合成 `{ messages: [] }` 基础 body:它证明接受副作用,而非准备后的字段字节。
77
77
 
78
- 有两种失败模式无法仅根据 `assistant/chunk` 重建:在产生任何分片前直接抛出(例如 HTTP 401,此时日志只有 `turn/end {error}`),以及取消或挂起。需要这些行为的场景可提供可选伴随文件(`<scenario>/replay.override.json`):它用裸 `ReplayEntry[]` 替换派生脚本,或用 `{ patches: [{ at, entry }] }` 增补——保留所有派生调用,只替换指定的从 0 开始计数的调用索引;当 `at` 等于派生长度时,则在注入瞬态异常后的重试位置追加。有前缀分片的 `throw` 条目会接受 DeepSeek 请求扩展;零分片 throw 默认表示 2xx 前未接受,也可设 `accepted: true` 表示 2xx 后无分片失败。`hang` 条目可以指定 `readyFile`,回放在等待取消前写入它,使外部 driver 可以确定性取消。
78
+ 有两种失败模式无法仅根据持久 Assistant settlement 重建:任何 chunk 之前的纯 throw 没有携带异常的 stream member,而 cancel/hang 需要的是不终止语义,不能用有限前缀回放。需要这些行为的场景可提供可选伴随文件(`<scenario>/replay.override.json`):它用裸 `ReplayEntry[]` 替换派生脚本,或用 `{ patches: [{ at, entry }] }` 增补——保留所有派生调用,只替换指定的从 0 开始计数的调用索引;当 `at` 等于派生长度时,则在注入瞬态异常后的重试位置追加。有前缀分片的 `throw` 条目会接受 DeepSeek 请求扩展;零分片 throw 默认表示 2xx 前未接受,也可设 `accepted: true` 表示 2xx 后无分片失败。`hang` 条目可以指定 `readyFile`,回放在等待取消前写入它,使外部 driver 可以确定性取消。
79
79
 
80
80
  ### 可能出什么问题
81
81
 
@@ -95,13 +95,16 @@ fixture 是运行一次真实 agent 所产生的持久化会话日志(`<scenar
95
95
 
96
96
  ### 设计
97
97
 
98
- 回放建立在一个想法之上:投影后的会话日志就是 fixture。`deriveReplayScript` 解析 JSONL header(用于 `id`/`createdAt` 排序事实),并按 `(turn, step)` 键在每次 `finish` 分片处切分 `assistant/chunk` 事件,使每次已记录的 `stream()` 调用成为一条 `chunks` 条目;没有 `finish` 分片的 assistant 分组是 `stream()` 抛出异常的指纹,必须通过 override 伴随文件表达。携带 `llmStreamCall: true` 与完整 `rawOutput` 的 `compaction/summary` 会在该事件位置回放为一条规范成功流。脚本字符串可以内嵌 `{{fromRequest:<regex>}}`;流式输出时每个占位符针对实时请求的字符串叶子解析,取该模式的最后一次匹配,用其第一个捕获组(无捕获组时用整个匹配)原位替换。
98
+ replay 把选定的投影 Session generation 视为 fixture。一个 parser 补全投影 envelope,通过 `sessionFormatCatalog` 校验并迁移完整产物,再以一个结果返回当前 header、继承 cut 与事件列表。`deriveReplayScript` 按日志顺序展开每个 `assistant/message` 或 `assistant/attempt` stream,因此每个持久 settlement 都成为一条 `chunks` entry;非空 stream 缺少 `finish` chunk 是 `stream()` 抛出异常的 fingerprint,必须通过 override sidecar 表达。携带 `llmStreamCall: true` 与完整 `rawOutput` 的 `compaction/summary` 会在该事件位置 replay 为一条规范成功 stream。脚本字符串可以内嵌 `{{fromRequest:<regex>}}`;stream 输出时每个 placeholder 针对 live request 的 string leaf 解析,取该 pattern 的最后一次 match,用其第一个 capture group(无 capture group 时用整个 match)原位替换。
99
+
100
+ 已提交语料测试会发现 `snapshots/`、`packages/` 与 `scripts/snapshots/python-sdk-single-exe/` 下每个带版本的 `session*.jsonl`。每个产物都必须通过真实 catalog 还原为当前视图。任何拒绝都会携带产物路径并使测试失败,因此 fixture owner 必须在 replay 或比较前修正无效关系。
99
101
 
100
102
  ### 源码地图
101
103
 
102
104
  | 文件 | 职责 |
103
105
  |---|---|
104
106
  | [`src/index.ts`](src/index.ts) | 类型、fixture 派生、override 校验、占位符解析、会话绑定、`installLlmReplay` 与插件导出 |
107
+ | [`tests/session-format-corpus.spec.ts`](tests/session-format-corpus.spec.ts) | 完整已提交 generation restore burn-in;任何拒绝都是失败 |
105
108
  | — | 不发布运行时不变式伴生入口;流语法由 LLM 伴生插件与派生测试检验。 |
106
109
 
107
110
  ### 绑定与流式流程
package/lib/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { delimiter } from "node:path";
3
- import { SessionLogOffset, decodeSeqRanges, decodeStorageRecord } from "@deepseek-ai/dsh-session";
4
- import { LlmAdapter, LlmError, ReasoningEffortId, requestImageHandleText, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
3
+ import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
4
+ import { SessionFormatUnsupportedMigrationError, sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
5
+ import { LlmAdapter, LlmError, ReasoningEffortId, expandAssistantStream, requestImageHandleText, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
5
6
  import { assertNever } from "@deepseek-ai/dsh-util-values";
6
7
  //#region lib/types/index.js
7
8
  /**
8
9
  * Keyless snapshot-test LLM replay. It derives one model-call script per
9
- * recorded session from `assistant/chunk` events and explicitly marked local
10
+ * recorded session from v2 embedded Assistant streams and explicitly marked local
10
11
  * compaction calls, then binds fresh live sessions to parent/child scripts by
11
12
  * first-call order. Throw and hang cases require an explicit override because
12
13
  * a session log cannot reconstruct them alone.
@@ -17,26 +18,30 @@ const PACKED_CHUNK_ROW_TYPES = new Set([
17
18
  "reasoning-chunks",
18
19
  "tool-call-chunks"
19
20
  ]);
21
+ if (sessionFormatCatalog.currentVersion !== SESSION_FORMAT_VERSION) throw new Error(`llm-replay: format catalog v${sessionFormatCatalog.currentVersion} does not match Session v${SESSION_FORMAT_VERSION}`);
20
22
  /**
21
- * Parse a session `.jsonl` buffer into its event list. Line 0 is the session
22
- * header (a `{type:'session',…}` record), every subsequent non-empty line is a
23
- * {@link SessionEvent} or a packed chunk row. Packed rows expand back into
24
- * events, and JSONL storage-form provenance ranges expand back into
25
- * `number[]`, so physical fixture encodings derive the same script. The
26
- * header is skipped; malformed lines fail loud.
23
+ * Parse a projected session `.jsonl` buffer into current events. The first
24
+ * non-empty line is the physical header. Body rows either all carry complete
25
+ * persistence envelopes or all omit them; projected rows receive deterministic
26
+ * dense sequences and zero timestamps. The build-static format catalog then
27
+ * decodes and migrates the complete artifact before this function returns.
27
28
  * @param text - the raw `.jsonl` file contents.
28
- * @returns every event after the header, in log order.
29
+ * @returns every migrated current event, in log order.
29
30
  */
30
31
  function parseSessionLog(text) {
31
- const events = [];
32
- let nextSeq = SessionLogOffset(0);
33
- let headerSkipped = false;
32
+ return parseSessionFixture(text).events;
33
+ }
34
+ /** Parse, complete, decode, and migrate one projected snapshot artifact without writing its source. */
35
+ function parseSessionFixture(text) {
36
+ let headerLineNumber;
37
+ let sourceHeader;
38
+ let restore;
39
+ const rowLines = [];
40
+ const eventLines = [];
41
+ let bodyKind;
42
+ let nextSeq = 0;
34
43
  for (const [index, line] of text.split(/\r?\n/).entries()) {
35
44
  if (line.trim().length === 0) continue;
36
- if (!headerSkipped) {
37
- headerSkipped = true;
38
- continue;
39
- }
40
45
  let value;
41
46
  try {
42
47
  value = JSON.parse(line);
@@ -44,46 +49,305 @@ function parseSessionLog(text) {
44
49
  throw new Error(`session snapshot line ${index + 1} contains invalid JSON`, { cause: error });
45
50
  }
46
51
  if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`session snapshot line ${index + 1} must be a JSON object`);
47
- const record = value;
52
+ const lineNumber = index + 1;
53
+ const recordValue = value;
54
+ if (restore === void 0) {
55
+ headerLineNumber = lineNumber;
56
+ sourceHeader = recordValue;
57
+ try {
58
+ restore = sessionFormatCatalog.createRestore(normalizeProjectedHeader(recordValue), {
59
+ recovery: "strict",
60
+ validation: "current"
61
+ });
62
+ } catch (error) {
63
+ throw fixtureFormatError(error, lineNumber, [], []);
64
+ }
65
+ continue;
66
+ }
67
+ const record = normalizeProjectedRow(recordValue);
48
68
  const packed = PACKED_CHUNK_ROW_TYPES.has(record.type);
49
69
  const seqKey = packed ? "seq0" : "seq";
50
70
  const timeKey = packed ? "time0" : "time";
51
- if (!Object.hasOwn(record, seqKey)) record[seqKey] = nextSeq;
52
- if (!Object.hasOwn(record, timeKey)) record[timeKey] = 0;
53
- let decoded;
71
+ const hasSeq = Object.hasOwn(record, seqKey);
72
+ if (hasSeq !== Object.hasOwn(record, timeKey)) throw new Error(`session snapshot line ${lineNumber} must contain both ${seqKey} and ${timeKey}, or neither`);
73
+ const currentKind = hasSeq ? "complete" : "projected";
74
+ if (bodyKind !== void 0 && currentKind !== bodyKind) throw new Error(`session snapshot line ${lineNumber} cannot mix projected and complete body rows`);
75
+ bodyKind = currentKind;
76
+ if (currentKind === "projected") {
77
+ record[seqKey] = nextSeq;
78
+ record[timeKey] = 0;
79
+ }
80
+ const cardinality = physicalRowCardinality(record);
81
+ rowLines.push(lineNumber);
82
+ eventLines.push(...Array.from({ length: cardinality }, () => lineNumber));
83
+ nextSeq += cardinality;
54
84
  try {
55
- if (Object.hasOwn(record, "sourceEventSeqs")) record.sourceEventSeqs = decodeSeqRanges(record.sourceEventSeqs);
56
- decoded = decodeStorageRecord(record);
85
+ restore.decodeRow(record);
57
86
  } catch (error) {
58
- /* v8 ignore next -- decodeStorageRecord only throws Error instances; the String arm satisfies unknown narrowing. */
59
- const detail = error instanceof Error ? error.message : String(error);
60
- throw new Error(`session snapshot line ${index + 1}: ${detail}`, { cause: error });
87
+ throw fixtureFormatError(error, headerLineNumber, rowLines, eventLines, rowLines.length - 1);
88
+ }
89
+ }
90
+ if (restore === void 0 || sourceHeader === void 0 || headerLineNumber === void 0) throw new Error("session snapshot must start with a session header");
91
+ try {
92
+ return parsedSessionFixture(restore.finish(), sourceHeader);
93
+ } catch (error) {
94
+ throw fixtureFormatError(error, headerLineNumber, rowLines, eventLines);
95
+ }
96
+ }
97
+ /** Materialize the common replay view from a migrated artifact. */
98
+ function parsedSessionFixture(artifact, sourceHeader) {
99
+ return {
100
+ id: artifact.header.id,
101
+ createdAt: artifact.header.createdAt,
102
+ inheritedEventCount: SessionLogOffset(artifact.inheritedEventCount),
103
+ events: [...artifact.events],
104
+ artifact,
105
+ sourceHeader
106
+ };
107
+ }
108
+ /**
109
+ * Convert one persisted or projected snapshot fixture to the current physical format in memory for expected-output comparison.
110
+ * Projected cwd tokens remain tokens so the ordinary snapshot normalizer can compare them with a fresh run.
111
+ * @param text - one complete Session fixture.
112
+ * @returns current-format JSONL with complete event envelopes; the input string and source file remain unchanged.
113
+ */
114
+ function prepareSessionSnapshotFixtureForComparison(text) {
115
+ return encodeCurrentSessionSnapshotFixture(text, parseSessionFixture(text));
116
+ }
117
+ /** Read one headless or SDK event-notification wrapper. */
118
+ function wrappedSessionEvent(row) {
119
+ if (row["type"] === "session_event" && typeof row["sessionId"] === "string" && row["event"] !== null && typeof row["event"] === "object" && !Array.isArray(row["event"])) return {
120
+ sessionId: row["sessionId"],
121
+ event: row["event"],
122
+ replace: (event) => ({
123
+ ...row,
124
+ event
125
+ })
126
+ };
127
+ const params = row["params"];
128
+ if (row["method"] !== "session.event" || params === null || typeof params !== "object" || Array.isArray(params)) return;
129
+ const record = params;
130
+ if (typeof record["sessionId"] !== "string" || record["event"] === null || typeof record["event"] !== "object" || Array.isArray(record["event"])) return;
131
+ return {
132
+ sessionId: record["sessionId"],
133
+ event: record["event"],
134
+ replace: (event) => ({
135
+ ...row,
136
+ params: {
137
+ ...record,
138
+ event
139
+ }
140
+ })
141
+ };
142
+ }
143
+ /** Append one migrated event to the output assigned to its v1 source row. */
144
+ function assignMigratedEvent(assigned, rowIndex, event) {
145
+ assigned.set(rowIndex, [...assigned.get(rowIndex) ?? [], event]);
146
+ }
147
+ /** Restore fixture tokens materialized only to satisfy released-format validation. */
148
+ function restoreProjectedRequestHeader(target, source) {
149
+ if (target["type"] !== "request/header" || source["type"] !== "request/header") return target;
150
+ const targetData = target["data"];
151
+ const sourceData = source["data"];
152
+ const targetHeader = targetData["header"];
153
+ const sourceTools = sourceData["header"]["tools"];
154
+ if (sourceTools !== "{{tools}}") return target;
155
+ return {
156
+ ...target,
157
+ data: {
158
+ ...targetData,
159
+ header: {
160
+ ...targetHeader,
161
+ tools: sourceTools
162
+ }
61
163
  }
62
- events.push(...decoded);
63
- nextSeq = SessionLogOffset(nextSeq + decoded.length);
164
+ };
165
+ }
166
+ /** Omit delivery cursors whose numeric value depends on the source Session generation. */
167
+ function normalizeWrappedEventProvenance(event) {
168
+ if (event["type"] !== "session-log-deepseek/delivery-accepted") return event;
169
+ const data = event["data"];
170
+ if (data === null || typeof data !== "object" || Array.isArray(data)) return event;
171
+ const normalized = { ...data };
172
+ delete normalized["sessionFormatVersion"];
173
+ delete normalized["throughSeq"];
174
+ return {
175
+ ...event,
176
+ data: normalized
177
+ };
178
+ }
179
+ /** Migrate one session's contiguous v1 notification tail and align its surviving rows. */
180
+ function migrateWrappedEventGroup(entries) {
181
+ const first = entries[0];
182
+ if (!entries.some((entry) => entry.event["type"] === "assistant/chunk")) return /* @__PURE__ */ new Map();
183
+ const firstSeq = first.event["seq"];
184
+ if (!Number.isSafeInteger(firstSeq) || firstSeq < 0) throw new Error("session event comparison requires a non-negative first seq");
185
+ for (const [index, entry] of entries.entries()) if (entry.event["seq"] !== firstSeq + index || typeof entry.event["type"] !== "string") throw new Error(`session event comparison requires a contiguous event tail for ${first.sessionId}`);
186
+ const prefix = Array.from({ length: firstSeq }, (_, seq) => ({
187
+ type: "feedback/record",
188
+ seq,
189
+ time: 0,
190
+ data: { text: `comparison prefix ${String(seq)}` }
191
+ }));
192
+ const restore = sessionFormatCatalog.createRestore({
193
+ type: "session",
194
+ version: 1,
195
+ id: first.sessionId,
196
+ createdAt: 0,
197
+ delegationDepth: 0
198
+ }, {
199
+ recovery: "strict",
200
+ validation: "current"
201
+ });
202
+ for (const event of prefix) restore.decodeRow(event);
203
+ for (const entry of entries) restore.decodeRow(normalizeProjectedRow(entry.event));
204
+ const migrated = restore.finish().events.slice(prefix.length);
205
+ const assigned = /* @__PURE__ */ new Map();
206
+ let migratedIndex = 0;
207
+ let lastChunkRow;
208
+ for (const entry of entries) {
209
+ if (entry.event["type"] === "assistant/chunk") {
210
+ lastChunkRow = entry.rowIndex;
211
+ continue;
212
+ }
213
+ while (migrated[migratedIndex]?.["type"] === "assistant/attempt") {
214
+ assignMigratedEvent(assigned, lastChunkRow, migrated[migratedIndex]);
215
+ migratedIndex += 1;
216
+ }
217
+ const next = migrated[migratedIndex];
218
+ assignMigratedEvent(assigned, entry.rowIndex, restoreProjectedRequestHeader(next, entry.event));
219
+ migratedIndex += 1;
220
+ }
221
+ while (migrated[migratedIndex]?.["type"] === "assistant/attempt") {
222
+ assignMigratedEvent(assigned, lastChunkRow, migrated[migratedIndex]);
223
+ migratedIndex += 1;
224
+ }
225
+ return assigned;
226
+ }
227
+ /**
228
+ * Project v1 session-event notifications to current settlement cardinality in memory.
229
+ * Non-event protocol rows retain their exact positions, and current v2 input passes through.
230
+ * @param text - headless `session_event` or SDK `session.event` JSONL.
231
+ * @returns comparison JSONL using current Session events without modifying its source file.
232
+ */
233
+ function prepareSessionEventNotificationsForComparison(text) {
234
+ const trailingNewline = text.endsWith("\n");
235
+ const rows = text.split("\n").filter((line) => line.trim().length > 0).map((line, index) => {
236
+ const value = JSON.parse(line);
237
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`session event comparison line ${String(index + 1)} must be an object`);
238
+ return value;
239
+ });
240
+ const entries = rows.flatMap((row, rowIndex) => {
241
+ const wrapped = wrappedSessionEvent(row);
242
+ return wrapped === void 0 ? [] : [{
243
+ ...wrapped,
244
+ rowIndex
245
+ }];
246
+ });
247
+ const groups = [];
248
+ for (const entry of entries) {
249
+ const nextSeq = entry.event["seq"];
250
+ const group = groups.findLast((candidate) => {
251
+ const previous = candidate.at(-1);
252
+ const previousSeq = previous?.event["seq"];
253
+ return previous?.sessionId === entry.sessionId && Number.isSafeInteger(previousSeq) && Number.isSafeInteger(nextSeq) && nextSeq === previousSeq + 1;
254
+ });
255
+ if (group === void 0) groups.push([entry]);
256
+ else group.push(entry);
64
257
  }
65
- return events;
258
+ const assigned = /* @__PURE__ */ new Map();
259
+ for (const group of groups) for (const [rowIndex, events] of migrateWrappedEventGroup(group)) assigned.set(rowIndex, events);
260
+ const output = rows.flatMap((row, rowIndex) => {
261
+ const wrapped = wrappedSessionEvent(row);
262
+ if (wrapped === void 0) return [row];
263
+ const migrated = assigned.get(rowIndex);
264
+ if (migrated === void 0) return wrapped.event["type"] === "assistant/chunk" ? [] : [wrapped.replace(normalizeWrappedEventProvenance(wrapped.event))];
265
+ return migrated.map((event) => wrapped.replace(normalizeWrappedEventProvenance(event)));
266
+ }).map((row) => JSON.stringify(row)).join("\n");
267
+ return trailingNewline ? `${output}\n` : output;
268
+ }
269
+ /** Encode one migrated fixture while retaining a projected cwd token. */
270
+ function encodeCurrentSessionSnapshotFixture(text, parsed) {
271
+ const header = { ...sessionFormatCatalog.encodeCurrentHeader(parsed.artifact.header, parsed.artifact.inheritedEventCount) };
272
+ const sourceCwd = parsed.sourceHeader["cwd"];
273
+ if (typeof sourceCwd === "string" && /^\{\{cwd\}\}(?:\/|$)/.test(sourceCwd)) header["cwd"] = sourceCwd;
274
+ const output = [JSON.stringify(header), ...parsed.artifact.events.map((event) => JSON.stringify(sessionFormatCatalog.encodeCurrentEvent(event)))].join("\n");
275
+ return text.endsWith("\n") ? `${output}\n` : output;
276
+ }
277
+ /** Restore typed request-header values replaced by snapshot sidecar tokens. */
278
+ function normalizeProjectedRow(source) {
279
+ const record = { ...source };
280
+ if (record["type"] !== "request/header") return record;
281
+ const data = record["data"];
282
+ if (data === null || typeof data !== "object" || Array.isArray(data)) return record;
283
+ const header = data["header"];
284
+ if (header === null || typeof header !== "object" || Array.isArray(header)) return record;
285
+ const tools = header["tools"];
286
+ let materializedTools;
287
+ if (tools === "{{tools}}") materializedTools = [];
288
+ else if (Array.isArray(tools) && tools.every((tool) => typeof tool === "string" && tool.length > 0)) materializedTools = tools.map((name) => ({
289
+ name,
290
+ description: "",
291
+ parameters: {}
292
+ }));
293
+ else return record;
294
+ record["data"] = {
295
+ ...data,
296
+ header: {
297
+ ...header,
298
+ tools: materializedTools
299
+ }
300
+ };
301
+ return record;
302
+ }
303
+ /** Materialize fixture-only header omissions and tokens before physical validation. */
304
+ function normalizeProjectedHeader(header) {
305
+ const normalized = { ...header };
306
+ if (normalized["version"] === 0 && !Object.hasOwn(header, "delegationDepth")) normalized["delegationDepth"] = 0;
307
+ if (typeof header["cwd"] === "string" && /^\{\{cwd\}\}(?:\/|$)/.test(header["cwd"])) normalized["cwd"] = header["cwd"].replace("{{cwd}}", "/dsh-snapshot-cwd");
308
+ return normalized;
309
+ }
310
+ /** Return how many logical events one physical row contributes for deterministic seq completion. */
311
+ function physicalRowCardinality(row) {
312
+ if (!PACKED_CHUNK_ROW_TYPES.has(row["type"])) return 1;
313
+ const data = row["data"];
314
+ if (data === null || typeof data !== "object" || Array.isArray(data)) return 1;
315
+ const payload = data[row["type"] === "tool-call-chunks" ? "args" : "texts"];
316
+ return Array.isArray(payload) && payload.length > 0 ? payload.length : 1;
317
+ }
318
+ /** Attach the nearest physical source line while preserving unsupported-migration classification. */
319
+ function fixtureFormatError(error, headerLine, rowLines, eventLines, physicalRow) {
320
+ const detail = error instanceof Error ? error.message : String(error);
321
+ const locationDetail = error instanceof Error && error.cause instanceof Error ? error.cause.message : detail;
322
+ const storedRow = /^released Session row (\d+)/.exec(locationDetail);
323
+ const event = /Session event (\d+)/.exec(locationDetail) ?? / at seq (\d+)/.exec(locationDetail) ?? /inherited Session cut (\d+)/.exec(locationDetail);
324
+ let line;
325
+ if (physicalRow !== void 0) line = rowLines[physicalRow];
326
+ else if (storedRow !== null) line = rowLines[Number(storedRow[1])] ?? headerLine;
327
+ else if (event === null) line = headerLine;
328
+ else line = eventLines[Number(event[1])] ?? headerLine;
329
+ const message = `session snapshot line ${line}: ${detail}`;
330
+ if (error instanceof SessionFormatUnsupportedMigrationError) return new SessionFormatUnsupportedMigrationError(message, { cause: error });
331
+ return new Error(message, { cause: error });
66
332
  }
67
333
  /**
68
334
  * Read replay identity, ordering, and fork-seed facts from the JSONL header.
69
335
  *
70
- * @param text - the raw `.jsonl` file contents (only the header line is read).
71
- * @returns the header's `id`, `createdAt`, and inherited-event count, defaulted when absent.
336
+ * @param text - the raw `.jsonl` file contents; the complete artifact is validated and migrated.
337
+ * @returns the migrated header's `id`, `createdAt`, and exact inherited-event count.
72
338
  */
73
339
  function parseSessionHeader(text) {
74
- const firstLine = text.split("\n").find((line) => line.trim().length > 0) ?? "{}";
75
- const parsed = JSON.parse(firstLine);
340
+ const parsed = parseSessionFixture(text);
76
341
  return {
77
- id: typeof parsed.id === "string" ? parsed.id : "",
78
- createdAt: typeof parsed.createdAt === "number" ? parsed.createdAt : 0,
79
- inheritedEventCount: SessionLogOffset(typeof parsed.seedLength === "number" ? parsed.seedLength : 0)
342
+ id: parsed.id,
343
+ createdAt: parsed.createdAt,
344
+ inheritedEventCount: parsed.inheritedEventCount
80
345
  };
81
346
  }
82
347
  /**
83
348
  * Reconstruct the per-`stream()` replay script from a recorded session log.
84
349
  *
85
- * Splits `assistant/chunk` events at every `finish`, using turn and step changes
86
- * to detect an unterminated prior call. A `compaction/summary` explicitly marked
350
+ * Reads one embedded stream from each Assistant settlement. A `compaction/summary` explicitly marked
87
351
  * as one local LLM-stream call becomes a canonical successful stream from its
88
352
  * complete `rawOutput` at the summary's log position. A
89
353
  * missing assistant terminator means the live stream threw, so derivation
@@ -94,8 +358,6 @@ function parseSessionHeader(text) {
94
358
  */
95
359
  function deriveReplayScript(events) {
96
360
  const script = [];
97
- let currentKey;
98
- let current = [];
99
361
  const close = (key, chunks) => {
100
362
  if (chunks.length === 0) return;
101
363
  if (chunks[chunks.length - 1]?.type !== "finish") throw new Error(`llm-replay: model call ${key} ended without a finish chunk (a thrown stream); this scenario needs a replay.override.json sidecar`);
@@ -106,9 +368,6 @@ function deriveReplayScript(events) {
106
368
  };
107
369
  for (const event of events) {
108
370
  if (event.type === "compaction/summary") {
109
- close(currentKey, current);
110
- currentKey = void 0;
111
- current = [];
112
371
  const persisted = event.data;
113
372
  if (persisted.llmStreamCall === true) {
114
373
  if (persisted.rawOutput === void 0) throw new Error("llm-replay: compaction/summary marks an LLM stream call without rawOutput");
@@ -140,19 +399,10 @@ function deriveReplayScript(events) {
140
399
  }
141
400
  continue;
142
401
  }
143
- if (event.type !== "assistant/chunk") continue;
144
- const { turn, step, chunk } = event.data;
145
- const key = `${turn}/${step}`;
146
- if (current.length > 0 && key !== currentKey) close(currentKey, current);
147
- if (current.length === 0) currentKey = key;
148
- current.push(chunk);
149
- if (chunk.type === "finish") {
150
- close(currentKey, current);
151
- currentKey = void 0;
152
- current = [];
153
- }
402
+ if (event.type !== "assistant/message" && event.type !== "assistant/attempt") continue;
403
+ const chunks = expandAssistantStream(event.data.stream).map((member) => member.chunk);
404
+ close(`${String(event.data.turn)}/${String(event.data.step)}`, chunks);
154
405
  }
155
- close(currentKey, current);
156
406
  return script;
157
407
  }
158
408
  const REPLAY_CHUNK_TYPES = new Set([
@@ -346,10 +596,19 @@ function readOverrideDoc(value, file) {
346
596
  * @returns the resolved primary-session script.
347
597
  */
348
598
  function loadReplayScript(config) {
599
+ return resolveReplayScript(config, readPrimaryFixture(config));
600
+ }
601
+ /** Read a primary JSONL unless a whole-script sidecar intentionally occupies the same path. */
602
+ function readPrimaryFixture(config) {
603
+ if (!existsSync(config.file) || config.file === config.overrideFile) return void 0;
604
+ return parseSessionFixture(readFileSync(config.file, "utf8"));
605
+ }
606
+ /** Resolve an override or derive from one already validated and migrated fixture. */
607
+ function resolveReplayScript(config, fixture) {
349
608
  if (config.overrideFile !== void 0 && existsSync(config.overrideFile)) {
350
609
  const doc = readOverrideDoc(JSON.parse(readFileSync(config.overrideFile, "utf8")), config.overrideFile);
351
610
  if (Array.isArray(doc)) return doc;
352
- const script = deriveScriptFromFile(config.file);
611
+ const script = deriveScriptFromFixture(config.file, fixture);
353
612
  const derivedLength = script.length;
354
613
  const seenIndexes = /* @__PURE__ */ new Set();
355
614
  for (const patch of doc.patches) {
@@ -360,12 +619,12 @@ function loadReplayScript(config) {
360
619
  }
361
620
  return script;
362
621
  }
363
- return deriveScriptFromFile(config.file);
622
+ return deriveScriptFromFixture(config.file, fixture);
364
623
  }
365
- /** Derive the primary script from the session JSONL, failing loud on a missing fixture. */
366
- function deriveScriptFromFile(file) {
367
- if (!existsSync(file)) throw new Error(`llm-replay: fixture not found: ${file} — run \`pnpm run test:snapshot:record\` first`);
368
- return deriveReplayScript(parseSessionLog(readFileSync(file, "utf8")));
624
+ /** Derive a script from an already migrated fixture, failing loud when it is absent. */
625
+ function deriveScriptFromFixture(file, fixture) {
626
+ if (fixture === void 0) throw new Error(`llm-replay: fixture not found: ${file} — run \`pnpm run test:snapshot:record\` first`);
627
+ return deriveReplayScript(fixture.events);
369
628
  }
370
629
  /**
371
630
  * Load the primary and child scripts in bind order. Child derivation begins at
@@ -375,8 +634,9 @@ function deriveScriptFromFile(file) {
375
634
  * @returns the primary script first, then the child scripts in bind order.
376
635
  */
377
636
  function loadSessionScripts(config) {
378
- const primaryEntries = loadReplayScript(config);
379
- const primaryHeader = existsSync(config.file) ? parseSessionHeader(readFileSync(config.file, "utf8")) : {
637
+ const primaryFixture = readPrimaryFixture(config);
638
+ const primaryEntries = resolveReplayScript(config, primaryFixture);
639
+ const primaryHeader = primaryFixture ?? {
380
640
  id: "",
381
641
  createdAt: 0
382
642
  };
@@ -389,12 +649,11 @@ function loadSessionScripts(config) {
389
649
  const children = [];
390
650
  for (const childFile of config.childFiles ?? []) {
391
651
  if (!existsSync(childFile)) throw new Error(`llm-replay: child fixture not found: ${childFile} — re-record the scenario`);
392
- const text = readFileSync(childFile, "utf8");
393
- const header = parseSessionHeader(text);
394
- const ownEvents = parseSessionLog(text).slice(header.inheritedEventCount);
652
+ const fixture = parseSessionFixture(readFileSync(childFile, "utf8"));
653
+ const ownEvents = fixture.events.slice(fixture.inheritedEventCount);
395
654
  children.push({
396
- recordedId: header.id,
397
- createdAt: header.createdAt,
655
+ recordedId: fixture.id,
656
+ createdAt: fixture.createdAt,
398
657
  entries: deriveReplayScript(ownEvents),
399
658
  primary: false
400
659
  });
@@ -659,4 +918,4 @@ function apply(ctx, config = {}) {
659
918
  });
660
919
  }
661
920
  //#endregion
662
- export { apply, deriveReplayScript, inject, installLlmReplay, loadReplayScript, loadSessionScripts, name, parseSessionHeader, parseSessionLog, resolveScriptedEntry };
921
+ export { apply, deriveReplayScript, inject, installLlmReplay, loadReplayScript, loadSessionScripts, name, parseSessionHeader, parseSessionLog, prepareSessionEventNotificationsForComparison, prepareSessionSnapshotFixtureForComparison, resolveScriptedEntry };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Keyless snapshot-test LLM replay. It derives one model-call script per
3
- * recorded session from `assistant/chunk` events and explicitly marked local
3
+ * recorded session from v2 embedded Assistant streams and explicitly marked local
4
4
  * compaction calls, then binds fresh live sessions to parent/child scripts by
5
5
  * first-call order. Throw and hang cases require an explicit override because
6
6
  * a session log cannot reconstruct them alone.
@@ -78,16 +78,17 @@ export interface ReplayProviderConfig {
78
78
  /** Resolved plugin configuration. */
79
79
  export interface ReplayConfig {
80
80
  /**
81
- * Path to the PRIMARY (parent) `session.jsonl` fixture. For a single-session
82
- * scenario this is the only log; for a nested-agent scenario it is the parent,
83
- * and the child logs ride in {@link childFiles}.
81
+ * Path to the selected PRIMARY fixture (`session.jsonl` for v0 or
82
+ * `session.vN.jsonl` for vN). For a single-session scenario this is the only
83
+ * log; for a nested-agent scenario it is the parent, and child logs ride in
84
+ * {@link childFiles}.
84
85
  */
85
86
  file: string;
86
87
  /**
87
88
  * Optional sidecar for the PRIMARY session: a bare `ReplayEntry[]` replaces
88
89
  * the derived script; `{ patches }` keeps it and swaps the named call
89
90
  * indexes ({@link ReplayOverrideDoc}). Used by single-session scenarios not
90
- * expressible as `assistant/chunk` (throw-before-chunk, cancel/hang,
91
+ * expressible as a durable embedded stream (throw-before-chunk, cancel/hang,
91
92
  * injected transient failures). Absent for normal and nested scenarios.
92
93
  */
93
94
  overrideFile?: string;
@@ -148,21 +149,34 @@ export interface SessionScript {
148
149
  primary: boolean;
149
150
  }
150
151
  /**
151
- * Parse a session `.jsonl` buffer into its event list. Line 0 is the session
152
- * header (a `{type:'session',…}` record), every subsequent non-empty line is a
153
- * {@link SessionEvent} or a packed chunk row. Packed rows expand back into
154
- * events, and JSONL storage-form provenance ranges expand back into
155
- * `number[]`, so physical fixture encodings derive the same script. The
156
- * header is skipped; malformed lines fail loud.
152
+ * Parse a projected session `.jsonl` buffer into current events. The first
153
+ * non-empty line is the physical header. Body rows either all carry complete
154
+ * persistence envelopes or all omit them; projected rows receive deterministic
155
+ * dense sequences and zero timestamps. The build-static format catalog then
156
+ * decodes and migrates the complete artifact before this function returns.
157
157
  * @param text - the raw `.jsonl` file contents.
158
- * @returns every event after the header, in log order.
158
+ * @returns every migrated current event, in log order.
159
159
  */
160
160
  export declare function parseSessionLog(text: string): SessionEvent[];
161
+ /**
162
+ * Convert one persisted or projected snapshot fixture to the current physical format in memory for expected-output comparison.
163
+ * Projected cwd tokens remain tokens so the ordinary snapshot normalizer can compare them with a fresh run.
164
+ * @param text - one complete Session fixture.
165
+ * @returns current-format JSONL with complete event envelopes; the input string and source file remain unchanged.
166
+ */
167
+ export declare function prepareSessionSnapshotFixtureForComparison(text: string): string;
168
+ /**
169
+ * Project v1 session-event notifications to current settlement cardinality in memory.
170
+ * Non-event protocol rows retain their exact positions, and current v2 input passes through.
171
+ * @param text - headless `session_event` or SDK `session.event` JSONL.
172
+ * @returns comparison JSONL using current Session events without modifying its source file.
173
+ */
174
+ export declare function prepareSessionEventNotificationsForComparison(text: string): string;
161
175
  /**
162
176
  * Read replay identity, ordering, and fork-seed facts from the JSONL header.
163
177
  *
164
- * @param text - the raw `.jsonl` file contents (only the header line is read).
165
- * @returns the header's `id`, `createdAt`, and inherited-event count, defaulted when absent.
178
+ * @param text - the raw `.jsonl` file contents; the complete artifact is validated and migrated.
179
+ * @returns the migrated header's `id`, `createdAt`, and exact inherited-event count.
166
180
  */
167
181
  export declare function parseSessionHeader(text: string): {
168
182
  id: string;
@@ -172,8 +186,7 @@ export declare function parseSessionHeader(text: string): {
172
186
  /**
173
187
  * Reconstruct the per-`stream()` replay script from a recorded session log.
174
188
  *
175
- * Splits `assistant/chunk` events at every `finish`, using turn and step changes
176
- * to detect an unterminated prior call. A `compaction/summary` explicitly marked
189
+ * Reads one embedded stream from each Assistant settlement. A `compaction/summary` explicitly marked
177
190
  * as one local LLM-stream call becomes a canonical successful stream from its
178
191
  * complete `rawOutput` at the summary's log position. A
179
192
  * missing assistant terminator means the live stream threw, so derivation
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-llm-replay",
3
3
  "description": "Replay LLM plugin: short-circuits llm/stream with model chunks reconstructed from a recorded session JSONL (keyless snapshot tests)",
4
- "version": "0.1.2-alpha.5",
4
+ "version": "0.1.3-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -28,10 +28,10 @@
28
28
  "license": "MIT",
29
29
  "peerDependencies": {
30
30
  "@deepseek-ai/cordis": "^4.0.2",
31
- "@deepseek-ai/dsh-deepseek-llm-api-extensions": "^0.1.2-alpha.5",
32
- "@deepseek-ai/dsh-llm": "^0.1.2-alpha.5",
33
- "@deepseek-ai/dsh-session": "^0.1.2-alpha.5",
34
- "@deepseek-ai/dsh-compaction": "^0.1.2-alpha.5"
31
+ "@deepseek-ai/dsh-compaction": "^0.1.3-alpha.2",
32
+ "@deepseek-ai/dsh-deepseek-llm-api-extensions": "^0.1.3-alpha.2",
33
+ "@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
34
+ "@deepseek-ai/dsh-session": "^0.1.3-alpha.2"
35
35
  },
36
36
  "peerDependenciesMeta": {
37
37
  "@deepseek-ai/dsh-deepseek-llm-api-extensions": {
@@ -39,13 +39,14 @@
39
39
  }
40
40
  },
41
41
  "devDependencies": {
42
+ "@deepseek-ai/dsh-compaction": "^0.1.3-alpha.2",
43
+ "@deepseek-ai/dsh-deepseek-llm-api-extensions": "^0.1.3-alpha.2",
44
+ "@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
42
45
  "@deepseek-ai/cordis": "^4.0.2",
43
- "@deepseek-ai/dsh-compaction": "^0.1.2-alpha.5",
44
- "@deepseek-ai/dsh-deepseek-llm-api-extensions": "^0.1.2-alpha.5",
45
- "@deepseek-ai/dsh-llm": "^0.1.2-alpha.5",
46
- "@deepseek-ai/dsh-session": "^0.1.2-alpha.5"
46
+ "@deepseek-ai/dsh-session": "^0.1.3-alpha.2"
47
47
  },
48
48
  "dependencies": {
49
- "@deepseek-ai/dsh-util-values": "^0.1.2-alpha.5"
49
+ "@deepseek-ai/dsh-session-format-catalog": "^0.1.3-alpha.2",
50
+ "@deepseek-ai/dsh-util-values": "^0.1.3-alpha.2"
50
51
  }
51
52
  }