@deepseek-ai/dsh-session-snapshot 0.1.2-rc.1 → 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/session-snapshot/README.md
5
- README.md: 0e099dba28f45031bdaa3c187f3e3ffce5b3b2b9
6
- README.zh.md: f81b3038b14b94716f4f045aee2d70daa27930dc
5
+ README.md: 12acb8d426a2966a852f98d85843d3c246ffeb5f
6
+ README.zh.md: 0b30cbf205f6055ef7b11b791d47adb38f6f704d
package/README.md CHANGED
@@ -68,17 +68,19 @@ defineAcpSnapshotSuite({
68
68
  })
69
69
  ```
70
70
 
71
- Each recorded-session directory carries a closed `snapshot.yml` manifest plus its `session.jsonl` and contiguous `session.<n>.jsonl` child logs. The manifest 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. The adapter registers expected-output, session-log, and optional `workspace.expected/` comparisons; guards reject orphan directories, missing files, absolute paths, malformed manifests, and platform-specific separators.
71
+ 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.<ordinal>[.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. 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.
72
72
 
73
- `normalizeSessionSnapshot` retains the complete session header and event payloads but omits ordinary `seq`/`time` and packed-row `seq0`/`time0` envelopes from committed fixtures after normalizing paths and scrubbing request headers. Replay synthesizes the envelopes in memory, while runtime persistence continues to write complete logs. Fixtures use canonical packed rows; the [temporary repository migrator](../../../scripts/migrate-packed-session-fixtures.ts) (`pnpm run migrate:packed-session-fixtures`) rewrites older layouts, and its [removal proposal](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md) owns its deletion.
73
+ `normalizeSessionSnapshot` retains the complete Session header and event payloads but omits top-level `seq`/`time` envelopes from committed fixtures after normalizing paths and scrubbing request headers; it also normalizes embedded stream clocks and historical packed-row `seq0`/`time0` envelopes. Replay synthesizes the top-level envelopes in memory, while runtime persistence continues to write complete logs. Multi-session comparison restores every selected persisted or projected fixture through the current build-static Session format catalog before identity redaction and normalization, so retained v0/v1 replay input and fresh `session.v2.jsonl` writer output compare as one v2 logical Session without rewriting or renaming historical files. Expected and harvested logs use the same strict restoration path; source filenames cannot alter format validation. Versionless protocol-adapter unit fixtures remain outside the released Session format corpus. Current v2 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.
74
+
75
+ 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.
74
76
 
75
77
  ### Record, replay, and refresh
76
78
 
77
- `pnpm run test:snapshot:record` calls the live LLM and rewrites recorded model fixtures; `pnpm run test:snapshot:refresh` stays keyless, runs the replay overlay, and rewrites stdout, comparable session-log expected outputs, and owned prompt and tool-schema sidecars from committed model scripts. 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.
79
+ `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 unless the manifest retains a historical 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.
78
80
 
79
81
  ### Pinning request headers
80
82
 
81
- A pin owns its generated `system-prompt.expected.md` or `tool-schemas.expected.json` sidecar by default; `systemPromptSource` and `toolSchemasSource` name another pin when the complete corresponding sequence is identical, so each distinct version is committed once. The pin's `session.jsonl` stores `"system":"{{system}}","tools":"{{tools}}"` while retaining config, reason, and any model-visible prefix. A child session whose own scope composes a different request declares it per fixture index with `pinsChildToolSchemas` and `pinsChildSystemPrompts`. A scenario that changes the request header mid-run declares `expectedHeaderChanges`.
83
+ A pin owns its generated `system-prompt.expected.md` or `tool-schemas.expected.json` sidecar by default; `systemPromptSource` and `toolSchemasSource` name another pin when the complete corresponding sequence is identical, so each distinct version is committed once. The selected highest parent fixture stores `"system":"{{system}}","tools":"{{tools}}"` while retaining config, reason, and any model-visible prefix. A child Session whose own scope composes a different request declares it per fixture index with `pinsChildToolSchemas` and `pinsChildSystemPrompts`. A scenario that changes the request header mid-run declares `expectedHeaderChanges`.
82
84
 
83
85
  ### Platform and composition variants
84
86
 
@@ -102,7 +104,7 @@ This section explains the design of the kit; the observable behavior is fully co
102
104
 
103
105
  ### Design
104
106
 
105
- The shared core owns manifests, workspace setup/comparison, typed identity mapping, normalizers, and fixture invariants. The ACP adapter adds four composable layers: launcher, scenario harness, normalizers, and suite factory. `launchAcpTestAgent` boots a source profile under tsx or a built `lib` profile under plain Node, connects the SDK client over a raw-byte stdout tee, collects session updates and stderr, fails closed on unhandled permission requests, and owns shutdown. `runScenario` drives ACP JSON-RPC stdio and harvests every persisted raw JSONL session log. The pure normalizers replace cwd paths and typed identities with stable tokens, zero times, expand physical provenance ranges, and scrub request-header bulk. `defineAcpSnapshotSuite` registers comparisons, fixture write-back, and the live uniformity guard.
107
+ The shared core owns manifests, generation-qualified role selection, workspace setup/comparison, typed identity mapping, normalizers, and fixture invariants. The ACP adapter adds four composable layers: launcher, scenario harness, normalizers, and suite factory. `launchAcpTestAgent` boots a source profile under tsx or a built `lib` profile under plain Node, connects the SDK client over a raw-byte stdout tee, collects Session updates and stderr, fails closed on unhandled permission requests, and owns shutdown. `runScenario` drives ACP JSON-RPC stdio and harvests the numerically highest persisted raw JSONL generation for every Session directory. The pure normalizers replace cwd paths and typed identities with stable tokens, zero times, expand physical provenance ranges, and scrub request-header bulk. `defineAcpSnapshotSuite` registers comparisons, generation-qualified fixture write-back, and the live uniformity guard.
106
108
 
107
109
  ### Source map
108
110
 
@@ -111,6 +113,7 @@ The shared core owns manifests, workspace setup/comparison, typed identity mappi
111
113
  | [`src/launcher.ts`](src/launcher.ts) | Subprocess/client launcher and shutdown ownership |
112
114
  | [`src/harness.ts`](src/harness.ts) | Scripted scenario driver and session-log harvest |
113
115
  | [`src/manifest.ts`](src/manifest.ts) | Closed `snapshot.yml` schema, collection, and ownership rules |
116
+ | [`src/session-files.ts`](src/session-files.ts) | Canonical parent/child generation grammar, header agreement, and highest-role selection |
114
117
  | [`src/identity.ts`](src/identity.ts) | Typed first-seen identity tokenization across parent and child logs |
115
118
  | [`src/normalize.ts`](src/normalize.ts) | Pure normalizers and scrubbing helpers |
116
119
  | [`src/workspace.ts`](src/workspace.ts) | Scenario workspace setup and complete expected-state comparison |
package/README.zh.md CHANGED
@@ -68,17 +68,19 @@ defineAcpSnapshotSuite({
68
68
  })
69
69
  ```
70
70
 
71
- 每个已记录会话目录携带封闭的 `snapshot.yml` manifest,以及自身的 `session.jsonl` 与连续的 `session.<n>.jsonl` 子会话日志。manifest 指名场景、随附 profile、组合/header 类别、录制来源,以及已完成会话无法重建的 replay、平台、权限、环境、workspace 或输入事实。适配器注册预期输出、会话日志与可选 `workspace.expected/` 比较;保护会拒绝遗留目录、缺失文件、绝对路径、畸形 manifest 与平台专用分隔符。
71
+ 每个已记录 Session 目录携带封闭的 `snapshot.yml` manifest,以及规范 parent 与连续 child 角色。parent 文件名是 `session[.vN].jsonl`;child 是 `session.<ordinal>[.vN].jsonl`;v0 省略 `.v0`,正版本使用小写 `.vN`,且每个文件名与其 header 一致。一个角色可以保留旧 generation,但 harness 会选择数值最高的一项。拥有 fixture 的 manifest 可以声明 `sessionFormat.version` 与一个或多个封闭 `coverage` 名称,把该历史 generation 保留为显式迁移 fixture;省略此字段时跟随当前 writer。manifest 还会指名场景、随附 profile、组合/header 类别、录制来源,以及已完成 Session 无法重建的 replay、平台、权限、环境、workspace 或输入事实。适配器注册预期输出、Session 日志与可选 `workspace.expected/` 比较;保护会拒绝遗留目录、缺失角色、非规范名称、绝对路径、malformed manifest 与平台专用分隔符。
72
72
 
73
- `normalizeSessionSnapshot` 在规范化路径并清理 request header 后,会保留完整会话 header 与事件 payload,但从已提交 fixture 中省略普通行的 `seq`/`time` 和打包行的 `seq0`/`time0` envelope。回放只在内存中合成这些 envelope,而运行时持久化仍写入完整日志。fixture 使用规范打包行;[临时仓库迁移器](../../../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)负责删除该迁移器。
73
+ `normalizeSessionSnapshot` 在规范化路径并清理 request header 后,会保留完整 Session header 与事件 payload,但从已提交 fixture 中省略顶层 `seq`/`time` envelope;它还会规范化嵌入式 stream clock 与历史 packed-row 的 `seq0`/`time0` envelope。Replay 只在内存中合成顶层 envelope,而运行时持久化仍写入完整日志。多 Session 比较会先通过当前构建期静态 Session 格式目录恢复每个选定的持久化或投影 fixture,再进行身份脱敏与规范化,因此保留的 v0/v1 replay 输入与新生成的 `session.v2.jsonl` writer 输出会作为同一个 v2 logical Session 比较,且不会重写或重命名历史文件。预期日志与收集日志使用同一条严格恢复路径;来源文件名不能改变格式校验。无版本的协议适配器单元测试 fixture 不属于已发布 Session 格式语料。当前 v2 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)负责删除该迁移器。
74
+
75
+ 已知的快照 spill 路径会规范化为稳定的定位信息 token,包括 JSON 省略通知中带引号、使用 JSON 转义 Windows 分隔符的路径。刷新提取会保留匹配路径的序列化写法,以便进行字面替换。规范化只改变定位信息:保存字节数与省略计数仍作为比较证据。
74
76
 
75
77
  ### 录制、回放与刷新
76
78
 
77
- `pnpm run test:snapshot:record` 调用在线 LLM(大语言模型),并重写已录制的模型 fixture;`pnpm run test:snapshot:refresh` 保持无密钥,运行回放 overlay,并从已提交模型脚本重写 stdout、可比较会话日志预期输出,以及各 pin 自有的提示词与工具 schema 伴随文件。每个组合 owner 把 replay patch 放在 live patch 旁;顶层 `snapshots/` 拥有会话驱动场景,其他预期输出留在其包 owner 旁。[`dsh-llm-replay`](../llm-replay/README.zh.md) 提供通过 `DSH_SNAPSHOT_*` 环境值选择的已记录流。
79
+ `pnpm run test:snapshot:record` 调用在线 LLM(大语言模型),并在规范具名版本文件下写入收集到的当前 generation。record 与 refresh 绝不重命名或删除已完成的 generation,即使后续运行不再产生某个 child 角色也一样;受审阅的源树整理只有在同角色存在已验证的当前替代文件后才移除前代。显式声明 `sessionFormat` 的场景在录制模式下保持只读。`pnpm run test:snapshot:refresh` 保持无密钥,运行选定的最高 replay 输入,并写入 stdout、各 pin 自有的 prompt 与工具 schema sidecar;只有 manifest 未保留历史 generation 时,才写入新鲜当前 generation 的可比较 Session 输出。每个组合 owner 把 replay patch 放在 live patch 旁;顶层 `snapshots/` 拥有 Session 驱动场景,其他预期输出留在其 package owner 旁。[`dsh-llm-replay`](../llm-replay/README.zh.md) 提供通过 `DSH_SNAPSHOT_*` 环境值选择的已记录流。
78
80
 
79
81
  ### 固定请求 header
80
82
 
81
- 每个 pin 默认拥有其生成的 `system-prompt.expected.md` 或 `tool-schemas.expected.json` 伴随文件;当完整的对应序列相同时,`systemPromptSource` 与 `toolSchemasSource` 指定另一个 pin 作为来源,因此每个不同版本只提交一次。该 pin 的 `session.jsonl` 存储 `"system":"{{system}}","tools":"{{tools}}"`,同时保留配置、原因与任何模型可见前缀。自身作用域组合出不同请求的子会话按 fixture 索引以 `pinsChildToolSchemas` 与 `pinsChildSystemPrompts` 单独声明。运行中改变请求 header 的场景声明 `expectedHeaderChanges`。
83
+ 每个 pin 默认拥有其生成的 `system-prompt.expected.md` 或 `tool-schemas.expected.json` sidecar;当完整的对应序列相同时,`systemPromptSource` 与 `toolSchemasSource` 指定另一个 pin 作为来源,因此每个不同版本只提交一次。选定的最高 parent fixture 存储 `"system":"{{system}}","tools":"{{tools}}"`,同时保留配置、原因与任何模型可见前缀。自身 scope 组合出不同请求的 child Session 按 fixture index 以 `pinsChildToolSchemas` 与 `pinsChildSystemPrompts` 单独声明。运行中改变请求 header 的场景声明 `expectedHeaderChanges`。
82
84
 
83
85
  ### 平台与组合变体
84
86
 
@@ -102,7 +104,7 @@ defineAcpSnapshotSuite({
102
104
 
103
105
  ### 设计
104
106
 
105
- 共享核心拥有 manifest、workspace 设置/比较、类型化身份映射、规范化器与 fixture 不变式。ACP 适配器增加四个可组合层:启动器、场景 harness、规范化器与套件工厂。`launchAcpTestAgent` 在 tsx 下启动源码 profile,或在普通 Node 下启动已构建 `lib` profile,通过原始字节 stdout tee 连接 SDK 客户端,收集会话更新与 stderr,默认拒绝未处理的权限请求,并负责关闭。`runScenario` 驱动 ACP JSON-RPC stdio,并收集每个持久化原始 JSONL 会话日志。纯规范化器把 cwd 路径与类型化身份变为稳定 token,将时间归零、展开物理来源区间,并擦除请求 header bulk。`defineAcpSnapshotSuite` 注册比较、fixture 回写与实时一致性保护。
107
+ 共享核心拥有 manifest、generation 限定角色选择、workspace 设置/比较、类型化身份映射、normalizer 与 fixture 不变式。ACP 适配器增加四个可组合层:launcher、场景 harness、normalizer 与 suite factory。`launchAcpTestAgent` 在 tsx 下启动源码 profile,或在普通 Node 下启动已构建 `lib` profile,通过原始字节 stdout tee 连接 SDK client,收集 Session update 与 stderr,默认拒绝未处理的权限请求,并负责关闭。`runScenario` 驱动 ACP JSON-RPC stdio,并收集每个 Session 目录中数值最高的持久原始 JSONL generation。纯 normalizer 把 cwd 路径与类型化身份变为稳定 token,将时间归零、展开物理来源区间,并清理 request header bulk。`defineAcpSnapshotSuite` 注册比较、generation 限定 fixture 回写与实时一致性保护。
106
108
 
107
109
  ### 源码地图
108
110
 
@@ -111,6 +113,7 @@ defineAcpSnapshotSuite({
111
113
  | [`src/launcher.ts`](src/launcher.ts) | 子进程/客户端启动器与关闭所有权 |
112
114
  | [`src/harness.ts`](src/harness.ts) | 脚本化场景驱动与会话日志收集 |
113
115
  | [`src/manifest.ts`](src/manifest.ts) | 封闭 `snapshot.yml` schema、收集与归属规则 |
116
+ | [`src/session-files.ts`](src/session-files.ts) | 规范 parent/child generation grammar、header 一致性与最高角色选择 |
114
117
  | [`src/identity.ts`](src/identity.ts) | 跨父子日志的类型化首次出现身份 token 化 |
115
118
  | [`src/normalize.ts`](src/normalize.ts) | 纯规范化器与擦除辅助 |
116
119
  | [`src/workspace.ts`](src/workspace.ts) | 场景 workspace 设置与完整预期状态比较 |
package/lib/index.js CHANGED
@@ -12,7 +12,10 @@ import { pathToFileURL } from "node:url";
12
12
  import * as yaml from "js-yaml";
13
13
  import { entryListSchema } from "@deepseek-ai/cordis-plugin-include";
14
14
  import { resolveExampleLaunch } from "@deepseek-ai/dsh-loader-smoke";
15
- import { SessionLogOffset, SessionSeq, decodeSeqRanges, decodeStorageRecord, packChunkRuns } from "@deepseek-ai/dsh-session";
15
+ import { clearedProxyEnv } from "@deepseek-ai/dsh-http-proxy";
16
+ import { parseSessionFormatLogFilename, sessionFormatLogFilename } from "@deepseek-ai/dsh-session-format";
17
+ import { decodeSeqRanges } from "@deepseek-ai/dsh-session";
18
+ import { prepareSessionSnapshotFixtureForComparison } from "@deepseek-ai/dsh-llm-replay";
16
19
  import { isSurfaceEligibleType } from "@deepseek-ai/dsh-session/surface";
17
20
  //#region lib/types/identity.js
18
21
  /** Relationship-preserving identity redaction for committed session snapshots. */
@@ -88,7 +91,10 @@ function redactSessionSnapshotIds(logs) {
88
91
  collect(item, recordType);
89
92
  }
90
93
  };
91
- for (const log of parsed) for (const record of log.records) collect(record, record.type);
94
+ for (const log of parsed) for (const record of log.records) {
95
+ if (record.type === "feedback/message-put" && isRecord$1(record.data) && isRecord$1(record.data.item)) claim(record.data.item.version, "id");
96
+ collect(record, record.type);
97
+ }
92
98
  const replacements = [...tokenByValue].sort(([left], [right]) => right.length - left.length);
93
99
  const replace = (value) => {
94
100
  if (typeof value === "string") {
@@ -398,6 +404,188 @@ function isRunning(child) {
398
404
  return child.exitCode === null && child.signalCode === null;
399
405
  }
400
406
  //#endregion
407
+ //#region lib/types/session-files.js
408
+ /** Immutable Session-generation filenames used by recorded-session fixtures. */
409
+ const FIXTURE_FILE = /^session(?:\.([1-9]\d*))?(?:\.v([1-9]\d*))?\.jsonl$/u;
410
+ function nonNegativeSafeInteger(value, label) {
411
+ if (!Number.isSafeInteger(value) || value < 0 || Object.is(value, -0)) throw new Error(`${label} must be a non-negative safe integer`);
412
+ }
413
+ /**
414
+ * Return the canonical fixture filename for one parent/ordinal and generation.
415
+ *
416
+ * @param index - Parent `0` or a positive child/ordinal slot.
417
+ * @param version - Physical format generation; `0` is omitted.
418
+ * @returns The lowercase canonical JSONL filename.
419
+ */
420
+ function sessionFixtureName(index, version) {
421
+ nonNegativeSafeInteger(index, "session fixture index");
422
+ nonNegativeSafeInteger(version, "Session format version");
423
+ return `session${index === 0 ? "" : `.${index}`}${version === 0 ? "" : `.v${version}`}.jsonl`;
424
+ }
425
+ /**
426
+ * Parse one canonical recorded-session fixture filename.
427
+ *
428
+ * @param name - Basename from a scenario directory.
429
+ * @returns Parsed role and generation, or `undefined` for an unrelated file.
430
+ */
431
+ function parseSessionFixtureName(name) {
432
+ const match = FIXTURE_FILE.exec(name);
433
+ if (match === null) {
434
+ if (name.startsWith("session") && name.endsWith(".jsonl")) throw new Error(`invalid session fixture name: ${name}`);
435
+ return;
436
+ }
437
+ const index = match[1] === void 0 ? 0 : Number(match[1]);
438
+ const version = match[2] === void 0 ? 0 : Number(match[2]);
439
+ if (!Number.isSafeInteger(index) || !Number.isSafeInteger(version)) throw new Error(`invalid session fixture name: ${name}`);
440
+ return {
441
+ index,
442
+ version,
443
+ name
444
+ };
445
+ }
446
+ /**
447
+ * Select the highest generation for every parent/ordinal fixture role.
448
+ * Older generations remain in the directory but do not count as extra Sessions.
449
+ *
450
+ * @param names - File basenames in one scenario directory.
451
+ * @returns Parent first, followed by contiguous child/ordinal roles.
452
+ */
453
+ function sessionFixtureFiles(names) {
454
+ const selected = /* @__PURE__ */ new Map();
455
+ const identities = /* @__PURE__ */ new Set();
456
+ for (const name of names) {
457
+ const fixture = parseSessionFixtureName(name);
458
+ if (fixture === void 0) continue;
459
+ const identity = `${fixture.index}/${fixture.version}`;
460
+ if (identities.has(identity)) throw new Error(`duplicate session fixture generation: ${name}`);
461
+ identities.add(identity);
462
+ const previous = selected.get(fixture.index);
463
+ if (previous === void 0 || fixture.version > previous.version) selected.set(fixture.index, fixture);
464
+ }
465
+ if (selected.get(0) === void 0) throw new Error("missing parent session fixture");
466
+ const ordered = [...selected.values()].sort((left, right) => left.index - right.index);
467
+ for (const [offset, fixture] of ordered.entries()) if (fixture.index !== offset) throw new Error(`session fixture roles must be contiguous: expected index ${offset}, found ${fixture.name}`);
468
+ return ordered;
469
+ }
470
+ /**
471
+ * Validate and order a scenario directory's selected Session fixture filenames.
472
+ *
473
+ * @param names - File basenames in one scenario directory.
474
+ * @returns Highest-generation parent and child/ordinal filenames.
475
+ */
476
+ function sessionFixtureNames(names) {
477
+ return sessionFixtureFiles(names).map((file) => file.name);
478
+ }
479
+ /**
480
+ * Read the declared physical generation from one Session JSONL header.
481
+ *
482
+ * @param content - Complete UTF-8 JSONL content.
483
+ * @param label - Diagnostic filename or path.
484
+ * @returns The declared non-negative format generation.
485
+ */
486
+ function sessionHeaderVersion(content, label) {
487
+ const line = content.split(/\r?\n/u).find((candidate) => candidate.trim().length > 0);
488
+ if (line === void 0) throw new Error(`${label}: session fixture is empty`);
489
+ let value;
490
+ try {
491
+ value = JSON.parse(line);
492
+ } catch (error) {
493
+ throw new Error(`${label}: session header contains invalid JSON`, { cause: error });
494
+ }
495
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value.type !== "session") throw new Error(`${label}: first record must be a Session header`);
496
+ const version = value.version;
497
+ if (!Number.isSafeInteger(version) || version < 0 || Object.is(version, -0)) throw new Error(`${label}: Session header version must be a non-negative safe integer`);
498
+ return version;
499
+ }
500
+ /**
501
+ * Require one fixture's canonical filename generation to equal its header.
502
+ *
503
+ * @param name - Canonical fixture basename.
504
+ * @param content - Complete UTF-8 JSONL content.
505
+ * @returns The validated format generation.
506
+ */
507
+ function assertSessionFixtureVersion(name, content) {
508
+ const fixture = parseSessionFixtureName(name);
509
+ if (fixture === void 0) throw new Error(`not a session fixture name: ${name}`);
510
+ const first = content.split(/\r?\n/u).find((candidate) => candidate.trim().length > 0);
511
+ if (first !== void 0) {
512
+ let projected;
513
+ try {
514
+ projected = JSON.parse(first);
515
+ } catch {
516
+ projected = void 0;
517
+ }
518
+ if (projected !== null && typeof projected === "object" && !Array.isArray(projected) && projected.type === "session" && !Object.hasOwn(projected, "version")) {
519
+ if (fixture.version !== 0) throw new Error(`${name}: a versionless projected Session header is format v0`);
520
+ return 0;
521
+ }
522
+ }
523
+ const headerVersion = sessionHeaderVersion(content, name);
524
+ if (headerVersion !== fixture.version) throw new Error(`${name}: filename declares Session format v${fixture.version}, header declares v${headerVersion}`);
525
+ return headerVersion;
526
+ }
527
+ /**
528
+ * Return the canonical persistence basename for one generation and compression.
529
+ *
530
+ * @param version - Physical format generation; `0` is omitted.
531
+ * @param compression - Backend compression mode.
532
+ * @returns The canonical persistence basename.
533
+ */
534
+ function persistedSessionFilename(version, compression = "raw") {
535
+ return `${sessionFormatLogFilename(version)}${compression === "zstd" ? ".zstd" : ""}`;
536
+ }
537
+ /**
538
+ * Parse a canonical persistence basename from a Session's own directory.
539
+ *
540
+ * @param name - Candidate basename.
541
+ * @returns Its generation and compression, or `undefined` for noise and noncanonical names.
542
+ */
543
+ function parsePersistedSessionFilename(name) {
544
+ const compression = name.endsWith(".zstd") ? "zstd" : "raw";
545
+ const version = parseSessionFormatLogFilename(compression === "zstd" ? name.slice(0, -5) : name);
546
+ if (version === void 0) return void 0;
547
+ return {
548
+ version,
549
+ compression,
550
+ name
551
+ };
552
+ }
553
+ /**
554
+ * Select one highest-generation persistence path per physical Session directory.
555
+ *
556
+ * @param paths - Relative or absolute paths beneath a sessions root.
557
+ * @param compression - Compression selected by the snapshot composition.
558
+ * @returns Stable path order with older generations and filesystem noise omitted.
559
+ */
560
+ function latestPersistedSessionPaths(paths, compression = "raw") {
561
+ const selected = /* @__PURE__ */ new Map();
562
+ for (const path of paths) {
563
+ const parsed = parsePersistedSessionFilename(basename(path));
564
+ if (parsed === void 0 || parsed.compression !== compression) continue;
565
+ const directory = dirname(path);
566
+ const previous = selected.get(directory);
567
+ if (previous === void 0 || parsed.version > previous.version) selected.set(directory, {
568
+ path,
569
+ version: parsed.version
570
+ });
571
+ }
572
+ return [...selected.values()].map((entry) => entry.path).sort();
573
+ }
574
+ /**
575
+ * Require one persistence basename's generation to equal its Session header.
576
+ *
577
+ * @param name - Canonical persistence basename.
578
+ * @param content - Complete uncompressed UTF-8 JSONL content.
579
+ * @returns The validated generation.
580
+ */
581
+ function assertPersistedSessionVersion(name, content) {
582
+ const persisted = parsePersistedSessionFilename(name);
583
+ if (persisted === void 0) throw new Error(`not a canonical Session persistence filename: ${name}`);
584
+ const headerVersion = sessionHeaderVersion(content, name);
585
+ if (headerVersion !== persisted.version) throw new Error(`${name}: filename declares Session format v${persisted.version}, header declares v${headerVersion}`);
586
+ return headerVersion;
587
+ }
588
+ //#endregion
401
589
  //#region lib/types/workspace.js
402
590
  /** Capture readable, path-stable workspace state for recorded-session tests. */
403
591
  /** Marker that lets Git retain an expected empty directory without becoming expected workspace state. */
@@ -519,6 +707,7 @@ async function runScenario(input, opts) {
519
707
  ] });
520
708
  const env = {
521
709
  ...opts.env,
710
+ ...clearedProxyEnv(),
522
711
  DSH_SNAPSHOT: opts.mode,
523
712
  DSH_SNAPSHOT_FILE: opts.fixtureFile,
524
713
  DSH_SNAPSHOT_SESSIONS_ROOT: sessionsRoot,
@@ -902,13 +1091,14 @@ function latestOpenTurn(content) {
902
1091
  return turn;
903
1092
  }
904
1093
  /**
905
- * Harvest EVERY persisted `.jsonl` session log under a sessions root, parse each
1094
+ * Harvest every latest-generation raw JSONL Session under a sessions root, parse each
906
1095
  * header line, and return them ordered primary-first: the top-level session (no
907
1096
  * `parentSession`) leads, then each subagent child by ascending `createdAt`.
908
1097
  *
909
- * Snapshot configs select the JSONL backend's raw mode, which lays sessions
910
- * out as `<root>/<project>/<session-id>/session.jsonl`. Recursive collection
911
- * catches the primary and every child session. Returns `[]` if no log was
1098
+ * Snapshot configs select the JSONL backend's raw mode, which lays each immutable
1099
+ * generation beneath `<root>/<project>/<session-id>/`. Recursive collection
1100
+ * chooses the numerically highest generation for the primary and every child.
1101
+ * Returns `[]` if no log was
912
1102
  * produced (a no-session scenario).
913
1103
  */
914
1104
  async function harvestSessionLogs(root) {
@@ -919,9 +1109,10 @@ async function harvestSessionLogs(root) {
919
1109
  return [];
920
1110
  }
921
1111
  const logs = [];
922
- for (const file of files) {
923
- if (basename(file) !== "session.jsonl") continue;
1112
+ for (const file of latestPersistedSessionPaths(files)) {
924
1113
  const content = await readFile(join(root, file), "utf8");
1114
+ assertPersistedSessionVersion(basename(file), content);
1115
+ /* v8 ignore next -- the generation validator above rejects header-less content. */
925
1116
  const firstLine = content.split("\n").find((line) => line.trim().length > 0) ?? "{}";
926
1117
  const header = JSON.parse(firstLine);
927
1118
  logs.push({
@@ -978,8 +1169,8 @@ const PATH_TEXT_BOUNDARY_RE = /[\s<>'"`()\[\]{},;:!?=]/;
978
1169
  const FILE_URI_PATH_PREFIX_RE = /(?:^|[^a-z0-9+.-])file:\/\/\/?$/i;
979
1170
  /** A UUID v4 string, the shape `randomUUID()` produces for session ids. */
980
1171
  const UUID_RE$1 = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
981
- const LOCAL_SPILL_PATH_RE = new RegExp(String.raw`\{\{cwd\}\}[\\/]\.spill[\\/]session-[0-9a-f]{12}[\\/][0-9a-f]{12}-([A-Za-z0-9._~-]+?)` + String.raw`(?=\. Use read with offset/limit|[\s)]|$)`, "g");
982
- const SNAPSHOT_SPILL_PATH_RE = new RegExp(String.raw`(?:[A-Za-z]:)?[\\/](?:tmp|t)[\\/](?:dsh-acp-snap-[0-9a-f]{9}|dsh-acp-snapshot-spill)[\\/]session-[0-9a-f]{12}[\\/][0-9a-f]{12}-([A-Za-z0-9._~-]+?)` + String.raw`(?=\. Use read with offset/limit|[\s)]|$)`, "g");
1172
+ const LOCAL_SPILL_PATH_RE = new RegExp(String.raw`\{\{cwd\}\}[\\/]+\.spill[\\/]+session-[0-9a-f]{12}[\\/]+[0-9a-f]{12}-([A-Za-z0-9._~-]+?)` + String.raw`(?=\. Use read with offset/limit|[\s)"]|\\+"|$)`, "g");
1173
+ const SNAPSHOT_SPILL_PATH_RE = new RegExp(String.raw`(?:[A-Za-z]:)?[\\/]+(?:tmp|t)[\\/]+(?:dsh-acp-snap-[0-9a-f]{9}|dsh-acp-snapshot-spill)[\\/]+session-[0-9a-f]{12}[\\/]+[0-9a-f]{12}-([A-Za-z0-9._~-]+?)` + String.raw`(?=\. Use read with offset/limit|[\s)"]|\\+"|$)`, "g");
983
1174
  /**
984
1175
  * Extract every snapshot-mode spill path from a session log, keyed by spill
985
1176
  * filename. Used by refresh write-back to keep spill paths stable across runs.
@@ -1151,9 +1342,9 @@ function normalizeStdout(rawStdout, ctx, options = {}) {
1151
1342
  }
1152
1343
  /**
1153
1344
  * Normalize a session JSONL log into a stable expected output: the header line's
1154
- * volatile fields (`createdAt`, `id`, `cwd`) are zeroed/scrubbed, ordinary
1155
- * event `time`, packed-row `time0`, and goal-change lifecycle clock values are
1156
- * zeroed, and all volatile strings are scrubbed. Projected inputs remain
1345
+ * volatile fields (`createdAt`, `id`, `cwd`) are zeroed/scrubbed; event,
1346
+ * historical packed-row, embedded Assistant-stream, and goal lifecycle clocks
1347
+ * are zeroed; and all volatile strings are scrubbed. Projected inputs remain
1157
1348
  * projected. Packed `data.dt` gaps are normalized even when the projected row
1158
1349
  * omits its `time0` anchor.
1159
1350
  * Output is JSONL in the same shape as the input — one compact record per
@@ -1176,10 +1367,21 @@ function normalizeSessionLog(rawLog, ctx, options = {}) {
1176
1367
  const data = record.data;
1177
1368
  if (data !== null && typeof data === "object" && Array.isArray(data.dt)) data.dt = data.dt.map(() => 0);
1178
1369
  } else if ("time" in record) record.time = 0;
1370
+ if ((record.type === "assistant/message" || record.type === "assistant/attempt") && record.data !== null && typeof record.data === "object") {
1371
+ const stream = record.data.stream;
1372
+ if (Array.isArray(stream)) for (const member of stream) {
1373
+ if (member === null || typeof member !== "object") continue;
1374
+ const timed = member;
1375
+ if (typeof timed.time === "number") timed.time = 0;
1376
+ if (typeof timed.time0 === "number") timed.time0 = 0;
1377
+ if (Array.isArray(timed.dt)) timed.dt = timed.dt.map(() => 0);
1378
+ }
1379
+ }
1179
1380
  if (record.type === "hook/result" && record.data !== null && typeof record.data === "object") {
1180
1381
  const data = record.data;
1181
1382
  if ("durationMs" in data) data.durationMs = 0;
1182
1383
  }
1384
+ normalizeFeedbackClocks(record);
1183
1385
  if (record.type === "goal/change" && record.data !== null && typeof record.data === "object") {
1184
1386
  const data = record.data;
1185
1387
  if ("createdAt" in data) data.createdAt = 0;
@@ -1190,36 +1392,15 @@ function normalizeSessionLog(rawLog, ctx, options = {}) {
1190
1392
  }).map((r) => JSON.stringify(r)).join("\n") + "\n";
1191
1393
  }
1192
1394
  /**
1193
- * Repack projected body records so persistence flush boundaries do not affect
1194
- * committed snapshots. Synthetic envelopes exist only while the storage codec
1195
- * reconstructs and packs the logical event stream; returned rows stay projected.
1395
+ * Canonicalize projected v2 body records. Compact streams are nested event data,
1396
+ * so persistence flush boundaries cannot change the row layout.
1196
1397
  */
1197
- function repackSessionSnapshot(rawLog) {
1398
+ function projectSessionSnapshot(rawLog) {
1198
1399
  const lines = rawLog.split("\n").filter((line) => line.trim().length > 0);
1199
- const header = lines.shift();
1200
- let nextSeq = SessionLogOffset(0);
1201
1400
  return [
1202
- header,
1203
- ...packChunkRuns(lines.flatMap((line) => {
1204
- const record = JSON.parse(line);
1205
- if (isPackedFixtureRow(record)) {
1206
- const decoded = decodeStorageRecord({
1207
- ...record,
1208
- seq0: nextSeq,
1209
- time0: 0
1210
- });
1211
- nextSeq = SessionLogOffset(nextSeq + decoded.length);
1212
- return decoded;
1213
- }
1214
- const event = {
1215
- ...record,
1216
- seq: SessionSeq(nextSeq),
1217
- time: 0
1218
- };
1219
- nextSeq = SessionLogOffset(nextSeq + 1);
1220
- return [event];
1221
- })).map((stored) => {
1222
- const projected = { ...stored };
1401
+ lines.shift(),
1402
+ ...lines.map((line) => {
1403
+ const projected = { ...JSON.parse(line) };
1223
1404
  omitFixtureEnvelope(projected);
1224
1405
  return JSON.stringify(projected);
1225
1406
  }),
@@ -1229,8 +1410,8 @@ function repackSessionSnapshot(rawLog) {
1229
1410
  /**
1230
1411
  * Normalize and project persisted session JSONL for a committed fixture.
1231
1412
  * This composes ordinary log normalization with request-header scrubbing and
1232
- * persistence-envelope projection, then packs the logical event stream into a
1233
- * canonical layout independent of persistence flush boundaries.
1413
+ * persistence-envelope projection, then writes the v2 logical event stream as
1414
+ * one record per event, independent of persistence flush boundaries.
1234
1415
  *
1235
1416
  * @param rawLog - persisted or already-projected session JSONL.
1236
1417
  * @param ctx - the run's volatile values to scrub.
@@ -1238,7 +1419,7 @@ function repackSessionSnapshot(rawLog) {
1238
1419
  * @returns normalized committed session snapshot JSONL.
1239
1420
  */
1240
1421
  function normalizeSessionSnapshot(rawLog, ctx, options = {}) {
1241
- return repackSessionSnapshot(scrubSessionSnapshot(normalizeSessionLog(rawLog, ctx, options)));
1422
+ return projectSessionSnapshot(scrubSessionSnapshot(normalizeSessionLog(rawLog, ctx, options)));
1242
1423
  }
1243
1424
  /**
1244
1425
  * Normalize one scenario's primary and child logs with shared typed identity redaction.
@@ -1248,7 +1429,7 @@ function normalizeSessionSnapshot(rawLog, ctx, options = {}) {
1248
1429
  * @returns normalized session fixtures in input order.
1249
1430
  */
1250
1431
  function normalizeSessionSnapshots(rawLogs, ctx, options = {}) {
1251
- return redactSessionSnapshotIds(rawLogs).map((log) => repackSessionSnapshot(scrubSessionSnapshot(normalizeSessionLog(log, {
1432
+ return redactSessionSnapshotIds(rawLogs.map((log) => hasSessionFormatVersion(log) ? prepareSessionSnapshotFixtureForComparison(log) : log).map(normalizeSessionFormatProvenance)).map((log) => projectSessionSnapshot(scrubSessionSnapshot(normalizeSessionLog(log, {
1252
1433
  ...ctx,
1253
1434
  sessionIds: []
1254
1435
  }, {
@@ -1257,6 +1438,60 @@ function normalizeSessionSnapshots(rawLogs, ctx, options = {}) {
1257
1438
  }))));
1258
1439
  }
1259
1440
  /**
1441
+ * Omit only generation-qualified operational provenance from expected-output comparison.
1442
+ * @param rawLog - Session records or events as compact JSON lines.
1443
+ * @returns the same records without delivery or captured-source generation qualifiers.
1444
+ */
1445
+ function normalizeSessionFormatProvenance(rawLog) {
1446
+ return rawLog.split("\n").map((line) => {
1447
+ if (line.trim().length === 0) return line;
1448
+ const record = JSON.parse(line);
1449
+ let changed = normalizeCapturedFormatProvenance(record);
1450
+ if (record.type === "session" && Object.hasOwn(record, "version")) {
1451
+ delete record.version;
1452
+ changed = true;
1453
+ }
1454
+ if (record.type === "session-log-deepseek/delivery-accepted" && record.data !== null && typeof record.data === "object" && !Array.isArray(record.data)) {
1455
+ const data = { ...record.data };
1456
+ if (Object.hasOwn(data, "sessionFormatVersion")) {
1457
+ delete data.sessionFormatVersion;
1458
+ record.data = data;
1459
+ changed = true;
1460
+ }
1461
+ }
1462
+ return changed ? JSON.stringify(record) : line;
1463
+ }).join("\n");
1464
+ }
1465
+ /** Omit captured generations only from an actual current Message source position. */
1466
+ function normalizeCapturedFormatProvenance(event) {
1467
+ if (event.data === null || typeof event.data !== "object" || Array.isArray(event.data)) return false;
1468
+ const data = event.data;
1469
+ const message = event.type === "user/message" ? data : event.type === "assistant/message" || event.type === "tool/result" ? data.message : void 0;
1470
+ if (message === null || typeof message !== "object" || Array.isArray(message)) return false;
1471
+ const source = message.source;
1472
+ if (source === null || typeof source !== "object" || Array.isArray(source)) return false;
1473
+ const record = source;
1474
+ if (record.kind !== "session-reference" || record.form !== "recall" || record.version !== 1 || !Array.isArray(record.references)) return false;
1475
+ let changed = false;
1476
+ for (const reference of record.references) {
1477
+ if (reference === null || typeof reference !== "object" || Array.isArray(reference)) continue;
1478
+ const captured = reference;
1479
+ if (Object.hasOwn(captured, "capturedFormatVersion")) {
1480
+ delete captured.capturedFormatVersion;
1481
+ changed = true;
1482
+ }
1483
+ }
1484
+ return changed;
1485
+ }
1486
+ /** Whether a fixture declares a released Session format and therefore participates in migration burn-in. */
1487
+ function hasSessionFormatVersion(rawLog) {
1488
+ const firstLine = rawLog.split(/\r?\n/).find((line) => line.trim().length > 0);
1489
+ if (firstLine === void 0) throw new Error("session snapshot must start with a session header");
1490
+ const header = JSON.parse(firstLine);
1491
+ if (header === null || typeof header !== "object" || Array.isArray(header) || header["type"] !== "session") throw new Error("session snapshot must start with a session header");
1492
+ return Object.hasOwn(header, "version");
1493
+ }
1494
+ /**
1260
1495
  * Replace system-prompt content in request headers with `{{system}}` tokens
1261
1496
  * while retaining field presence.
1262
1497
  * Other header content stays verbatim, so a header-pinning fixture can keep
@@ -1320,9 +1555,19 @@ function scrubSessionSnapshot(rawLog) {
1320
1555
  return line;
1321
1556
  }
1322
1557
  omitFixtureEnvelope(record);
1558
+ normalizeFeedbackClocks(record);
1323
1559
  return JSON.stringify(record);
1324
1560
  }).join("\n");
1325
1561
  }
1562
+ /** Normalize service-owned feedback clocks without touching user-authored payloads. */
1563
+ function normalizeFeedbackClocks(record) {
1564
+ if (record.type !== "feedback/message-put" || record.data === null || typeof record.data !== "object") return;
1565
+ const item = record.data.item;
1566
+ if (item === null || typeof item !== "object") return;
1567
+ const clocks = item;
1568
+ if ("createdAt" in clocks) clocks.createdAt = 0;
1569
+ if ("updatedAt" in clocks) clocks.updatedAt = 0;
1570
+ }
1326
1571
  /** Transform the selected request-header payloads. */
1327
1572
  function scrubHeaderContent(rawLog, options) {
1328
1573
  return rawLog.split("\n").map((line) => {
@@ -1350,6 +1595,18 @@ function scrubHeaderContent(rawLog, options) {
1350
1595
  //#endregion
1351
1596
  //#region lib/types/manifest.js
1352
1597
  /** Parse and validate one recorded-session snapshot manifest. */
1598
+ /**
1599
+ * Whether one run writes current-writer Session fixtures for this scenario.
1600
+ * Explicit historical generations remain immutable replay inputs; record and
1601
+ * refresh may still update their non-Session expected outputs.
1602
+ *
1603
+ * @param manifest - Parsed scenario ownership and retained-generation metadata.
1604
+ * @param mode - Snapshot execution mode.
1605
+ * @returns True only when a write-capable mode tracks the current writer.
1606
+ */
1607
+ function writesCurrentSessionFixtures(manifest, mode) {
1608
+ return mode !== "replay" && manifest.session === void 0 && manifest.sessionFormat === void 0;
1609
+ }
1353
1610
  const PROFILES = new Set([
1354
1611
  "headless",
1355
1612
  "sdk",
@@ -1363,6 +1620,13 @@ const PERMISSIONS = new Set([
1363
1620
  "workspace-write",
1364
1621
  "danger-full-access"
1365
1622
  ]);
1623
+ const SESSION_FORMAT_COVERAGE = new Set([
1624
+ "multi-hop",
1625
+ "packed-row",
1626
+ "retry-failure",
1627
+ "shipped-profile",
1628
+ "adjacent-migration"
1629
+ ]);
1366
1630
  const NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1367
1631
  function record(value, label) {
1368
1632
  if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be a mapping`);
@@ -1412,7 +1676,8 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
1412
1676
  "environment",
1413
1677
  "workspace",
1414
1678
  "input",
1415
- "session"
1679
+ "session",
1680
+ "sessionFormat"
1416
1681
  ], "manifest");
1417
1682
  if (root.version !== 1) throw new Error("manifest.version must equal 1");
1418
1683
  const scenario = root.scenario === void 0 ? void 0 : name(root.scenario, "manifest.scenario");
@@ -1527,6 +1792,18 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
1527
1792
  if (isAbsolute(value.source) || value.source.includes("\\") || value.source.includes("\0")) throw new Error("manifest.session.source must be a relative POSIX path");
1528
1793
  session = { source: value.source };
1529
1794
  }
1795
+ let sessionFormat;
1796
+ if (root.sessionFormat !== void 0) {
1797
+ const value = record(root.sessionFormat, "manifest.sessionFormat");
1798
+ exactKeys(value, ["version", "coverage"], "manifest.sessionFormat");
1799
+ if (!Number.isSafeInteger(value.version) || Number(value.version) < 0 || Object.is(value.version, -0)) throw new Error("manifest.sessionFormat.version must be a non-negative safe integer");
1800
+ if (!Array.isArray(value.coverage) || value.coverage.length === 0 || value.coverage.some((item) => typeof item !== "string" || !SESSION_FORMAT_COVERAGE.has(item)) || new Set(value.coverage).size !== value.coverage.length) throw new Error("manifest.sessionFormat.coverage must be a non-empty array of unique supported coverage names");
1801
+ if (session !== void 0) throw new Error("manifest.sessionFormat is only valid when the scenario owns its Session fixtures");
1802
+ sessionFormat = {
1803
+ version: Number(value.version),
1804
+ coverage: [...value.coverage]
1805
+ };
1806
+ }
1530
1807
  return {
1531
1808
  version: 1,
1532
1809
  ...scenario === void 0 ? {} : { scenario },
@@ -1540,7 +1817,8 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
1540
1817
  ...environment === void 0 ? {} : { environment },
1541
1818
  ...workspace === void 0 ? {} : { workspace },
1542
1819
  ...input === void 0 ? {} : { input },
1543
- ...session === void 0 ? {} : { session }
1820
+ ...session === void 0 ? {} : { session },
1821
+ ...sessionFormat === void 0 ? {} : { sessionFormat }
1544
1822
  };
1545
1823
  } catch (error) {
1546
1824
  /* v8 ignore next -- every parser and validator above throws Error instances. */
@@ -1592,9 +1870,10 @@ const PACKED_CHUNK_ROW_TYPES = new Set([
1592
1870
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1593
1871
  /**
1594
1872
  * Whether a scenario's run test is skipped for this mode and host: record mode
1595
- * skips authored (non-`recorded`) scenarios, {@link Scenario.posixOnly}
1596
- * scenarios skip on Windows, and {@link Scenario.pwshOnly} scenarios skip
1597
- * when the caller's `hasPwsh` probe is false.
1873
+ * skips authored (non-`recorded`) scenarios and explicit historical Session
1874
+ * generations, {@link Scenario.posixOnly} scenarios skip on Windows, and
1875
+ * {@link Scenario.pwshOnly} scenarios skip when the caller's `hasPwsh` probe
1876
+ * is false.
1598
1877
  *
1599
1878
  * @param scenario The scenario whose run test is being registered.
1600
1879
  * @param recording Whether the suite runs in record mode.
@@ -1604,7 +1883,7 @@ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
1604
1883
  * @returns True when the scenario's run test must not execute.
1605
1884
  */
1606
1885
  function scenarioSkipped(scenario, recording, platform = process.platform, hasPwsh) {
1607
- if (recording && !scenario.recorded) return true;
1886
+ if (recording && (!scenario.recorded || scenario.sessionFormat !== void 0)) return true;
1608
1887
  if (scenario.posixOnly === true && platform === "win32") return true;
1609
1888
  return scenario.pwshOnly === true && hasPwsh !== true;
1610
1889
  }
@@ -1660,47 +1939,20 @@ function assertUniqueSnapshotContents(kind, snapshots) {
1660
1939
  firstPathByContent.set(snapshot.content, snapshot.path);
1661
1940
  }
1662
1941
  }
1663
- /**
1664
- * Validate and order a scenario directory's session-fixture filenames.
1665
- *
1666
- * The primary fixture is always `session.jsonl`; child sessions are discovered
1667
- * from contiguous `session.1.jsonl` … filenames. The directory is the source of
1668
- * truth, so scenario tables do not duplicate a child count that can drift from
1669
- * the files. A session-like JSONL with any other suffix fails loud.
1670
- *
1671
- * @param names File names in one scenario directory.
1672
- * @returns The primary and child fixture names in replay/harvest order.
1673
- */
1674
- function sessionFixtureNames(names) {
1675
- if (!names.includes("session.jsonl")) throw new Error("missing session.jsonl");
1676
- const children = [];
1677
- for (const name of names) {
1678
- if (name === "session.jsonl") continue;
1679
- if (!name.startsWith("session.") || !name.endsWith(".jsonl")) continue;
1680
- const match = /^session\.([1-9]\d*)\.jsonl$/.exec(name);
1681
- if (match === null) throw new Error(`invalid child session fixture name: ${name}`);
1682
- children.push({
1683
- name,
1684
- index: Number(match[1])
1685
- });
1686
- }
1687
- children.sort((a, b) => a.index - b.index);
1688
- for (const [offset, child] of children.entries()) {
1689
- const expected = offset + 1;
1690
- if (child.index !== expected) throw new Error(`child session fixtures must be contiguous: expected session.${expected}.jsonl, found ${child.name}`);
1691
- }
1692
- return ["session.jsonl", ...children.map((child) => child.name)];
1693
- }
1694
1942
  /** Read one scenario directory's validated session-fixture inventory. */
1695
1943
  async function sessionFixtures(dir) {
1696
- return sessionFixtureNames((await readdir(dir, { withFileTypes: true })).filter((entry) => entry.isFile()).map((entry) => entry.name));
1944
+ const names = sessionFixtureNames((await readdir(dir, { withFileTypes: true })).filter((entry) => entry.isFile()).map((entry) => entry.name));
1945
+ await Promise.all(names.map(async (name) => {
1946
+ assertSessionFixtureVersion(name, await readFile(join(dir, name), "utf8"));
1947
+ }));
1948
+ return names;
1697
1949
  }
1698
1950
  /**
1699
1951
  * Derive normalization values from a fixture's own session header. Recorded ids and cwd differ
1700
1952
  * from the live replay run; the non-empty sentinel for missing cwd avoids accidental empty-
1701
1953
  * string replacement.
1702
1954
  *
1703
- * @param fixture The committed `session.jsonl` content.
1955
+ * @param fixture The selected committed parent Session fixture content.
1704
1956
  * @returns The fixture's own volatile values, ready for {@link normalizeSessionLog}.
1705
1957
  */
1706
1958
  function fixtureContext(fixture) {
@@ -2271,11 +2523,12 @@ function defineAcpSnapshotSuite(options) {
2271
2523
  const workspaceDir = join(dir, "workspace");
2272
2524
  let fixtureFiles = RECORDING ? [] : await sessionFixtures(dir);
2273
2525
  const childFixtureFiles = fixtureFiles.slice(1);
2526
+ const primaryFixtureFile = fixtureFiles[0] ?? sessionFixtureName(0, 0);
2274
2527
  const comparesLog = scenario.comparesLog ?? scenario.hasModelTurn;
2275
2528
  const result = await runScenario(input, {
2276
2529
  agent,
2277
2530
  mode: childMode,
2278
- fixtureFile: join(dir, "session.jsonl"),
2531
+ fixtureFile: join(dir, primaryFixtureFile),
2279
2532
  ...scenario.env !== void 0 ? { env: scenario.env } : {},
2280
2533
  ...existsSync(overrideFile) ? { overrideFile } : {},
2281
2534
  ...!RECORDING && childFixtureFiles.length > 0 ? { childFiles: childFixtureFiles.map((file) => join(dir, file)) } : {},
@@ -2293,23 +2546,19 @@ function defineAcpSnapshotSuite(options) {
2293
2546
  const childSchemaPins = new Set(scenario.pinsChildToolSchemas ?? []);
2294
2547
  const childPromptPins = new Set(scenario.pinsChildSystemPrompts ?? []);
2295
2548
  const portableFixture = scenario.workspaceParent === void 0 ? tokenizeSessionFixtureCwd : (log) => log;
2296
- if (RECORDING && scenario.recorded && scenario.hasModelTurn || REFRESHING && comparesLog) {
2549
+ if (writesCurrentSessionFixtures(manifest, mode) && (RECORDING && scenario.recorded && scenario.hasModelTurn || REFRESHING && comparesLog)) {
2297
2550
  expect(result.sessionLogs.length, `${mode} produced no session log to harvest`).toBeGreaterThan(0);
2298
2551
  if (REFRESHING) expect(result.sessionLogs.length, `expected ${fixtureFiles.length} session logs (parent + children)`).toBe(fixtureFiles.length);
2299
- const outputFixtureFiles = ["session.jsonl", ...Array.from({ length: result.sessionLogs.length - 1 }, (_, i) => `session.${i + 1}.jsonl`)];
2300
- const existingFixtures = await Promise.all(outputFixtureFiles.map(async (file) => {
2301
- const path = join(dir, file);
2302
- return existsSync(path) ? readFile(path, "utf8") : "";
2552
+ const outputFixtureFiles = result.sessionLogs.map((log, index) => sessionFixtureName(index, sessionHeaderVersion(log.content, `harvested Session ${index}`)));
2553
+ const existingFixtures = await Promise.all(outputFixtureFiles.map(async (_file, index) => {
2554
+ const file = fixtureFiles[index];
2555
+ if (file === void 0) return "";
2556
+ return readFile(join(dir, file), "utf8");
2303
2557
  }));
2304
2558
  const refreshReplacements = REFRESHING ? refreshFixtureReplacements(result.sessionLogs, existingFixtures) : [];
2305
2559
  const outputFixtures = redactSessionSnapshotIds(stabilizeFixtureMessageIds(REFRESHING ? result.sessionLogs.map((log, index) => scrubSessionSnapshot(portableFixture(stabilizeRefreshLog(log.content, existingFixtures[index], refreshReplacements, ctx)))) : result.sessionLogs.map((log) => scrubSessionSnapshot(portableFixture(log.content))), existingFixtures));
2306
2560
  await Promise.all(outputFixtures.map((fixture, index) => writeFile(join(dir, outputFixtureFiles[index]), fixture)));
2307
- if (RECORDING) {
2308
- const outputNames = new Set(outputFixtureFiles);
2309
- const entries = await readdir(dir, { withFileTypes: true });
2310
- await Promise.all(entries.filter((entry) => entry.isFile() && /^session\.[1-9]\d*\.jsonl$/.test(entry.name) && !outputNames.has(entry.name)).map((entry) => rm(join(dir, entry.name))));
2311
- fixtureFiles = outputFixtureFiles;
2312
- }
2561
+ fixtureFiles = outputFixtureFiles;
2313
2562
  if (scenario.pinsHeader === true) {
2314
2563
  const primary = result.sessionLogs[0];
2315
2564
  const pinningHeaders = pinningHeaderPayloads(primary.content, ctx);
@@ -2366,7 +2615,9 @@ function defineAcpSnapshotSuite(options) {
2366
2615
  const promptSource = promptSourceByClass.get(classOf(scenario)) ?? pinningScenario;
2367
2616
  /* v8 ignore next -- registration guarantees every scenario class has resolved sources. */
2368
2617
  const schemaSource = schemaSourceByClass.get(classOf(scenario)) ?? pinningScenario;
2369
- const pinnedFixture = await readFile(join(join(snapshotsDir, pinningScenario.name), "session.jsonl"), "utf8");
2618
+ const pinningDir = join(snapshotsDir, pinningScenario.name);
2619
+ const [pinningFixtureFile] = await sessionFixtures(pinningDir);
2620
+ const pinnedFixture = await readFile(join(pinningDir, pinningFixtureFile), "utf8");
2370
2621
  const pinned = pinningHeaderPayloads(pinnedFixture, fixtureContext(pinnedFixture));
2371
2622
  const promptSnapshot = await readFile(join(snapshotsDir, promptSource.name, SYSTEM_PROMPT_SNAPSHOT), "utf8");
2372
2623
  const initialPromptSnapshot = initialSystemPromptSnapshot(promptSnapshot);
@@ -2444,7 +2695,6 @@ function defineAcpSnapshotSuite(options) {
2444
2695
  expect(existsSync(join(dir, "input.json")), `${name}/input.json`).toBe(true);
2445
2696
  expect(existsSync(join(dir, "stdout.expected.jsonl")), `${name}/stdout.expected.jsonl`).toBe(true);
2446
2697
  expect(existsSync(join(dir, WINDOWS_STDOUT_SNAPSHOT)), `${name}/${WINDOWS_STDOUT_SNAPSHOT} presence must match \`pinsNativeWindowsStdout\``).toBe(pinsNativeWindowsStdout === true);
2447
- expect(existsSync(join(dir, "session.jsonl")), `${name}/session.jsonl`).toBe(true);
2448
2698
  expect(existsSync(join(dir, "replay.override.json")), `${name}/replay.override.json presence must match \`overridden\``).toBe(overridden === true);
2449
2699
  expect(existsSync(join(dir, SYSTEM_PROMPT_SNAPSHOT)), `${name}/${SYSTEM_PROMPT_SNAPSHOT} presence must match snapshot-source ownership`).toBe(promptOwners.has(name));
2450
2700
  expect(existsSync(join(dir, TOOL_SCHEMAS_SNAPSHOT)), `${name}/${TOOL_SCHEMAS_SNAPSHOT} presence must match snapshot-source ownership`).toBe(schemaOwners.has(name));
@@ -2466,7 +2716,9 @@ function defineAcpSnapshotSuite(options) {
2466
2716
  const promptSource = promptSourceByClass.get(classOf(scenario)) ?? scenario;
2467
2717
  /* v8 ignore next -- registration guarantees every pin has resolved sources. */
2468
2718
  const schemaSource = schemaSourceByClass.get(classOf(scenario)) ?? scenario;
2469
- const fixture = await readFile(join(snapshotsDir, scenario.name, "session.jsonl"), "utf8");
2719
+ const fixtureDir = join(snapshotsDir, scenario.name);
2720
+ const [fixtureFile] = await sessionFixtures(fixtureDir);
2721
+ const fixture = await readFile(join(fixtureDir, fixtureFile), "utf8");
2470
2722
  const headers = pinningHeaderPayloads(fixture, fixtureContext(fixture));
2471
2723
  const promptSnapshot = await readFile(join(snapshotsDir, promptSource.name, SYSTEM_PROMPT_SNAPSHOT), "utf8");
2472
2724
  expect(headers.length, `${scenario.name}: unexpected request/header count`).toBe(1 + (scenario.expectedHeaderChanges ?? 0));
@@ -2531,4 +2783,4 @@ function defineAcpSnapshotSuite(options) {
2531
2783
  });
2532
2784
  }
2533
2785
  //#endregion
2534
- export { EMPTY_WORKSPACE_MARKER, captureExpectedWorkspaceSnapshot, captureWorkspaceSnapshot, defineAcpSnapshotSuite, extractSnapshotSpillPaths, fixtureContext, formatSystemPromptSnapshot, formatToolSchemasSnapshot, headerChangeCount, launchAcpTestAgent, materializeProfilePatch, normalizeSessionLog, normalizeSessionSnapshot, normalizeSessionSnapshots, normalizeStdout, normalizedHeaders, normalizedSystemPrompts, normalizedToolSchemas, parseSnapshotManifest, parseToolSchemasSnapshot, redactSessionSnapshotIds, refreshFixtureReplacements, restorePinnedToolSchemas, runScenario, scrubRequestHeaders, scrubSessionSnapshot, scrubSystemPrompts, scrubToolSchemas, sessionFixtureNames, snapshotSpillRoot, stabilizeFixtureMessageIds, stabilizeRefreshLog, tokenizeSessionFixtureCwd };
2786
+ export { EMPTY_WORKSPACE_MARKER, assertPersistedSessionVersion, assertSessionFixtureVersion, captureExpectedWorkspaceSnapshot, captureWorkspaceSnapshot, defineAcpSnapshotSuite, extractSnapshotSpillPaths, fixtureContext, formatSystemPromptSnapshot, formatToolSchemasSnapshot, headerChangeCount, latestPersistedSessionPaths, launchAcpTestAgent, materializeProfilePatch, normalizeSessionFormatProvenance, normalizeSessionLog, normalizeSessionSnapshot, normalizeSessionSnapshots, normalizeStdout, normalizedHeaders, normalizedSystemPrompts, normalizedToolSchemas, parsePersistedSessionFilename, parseSessionFixtureName, parseSnapshotManifest, parseToolSchemasSnapshot, persistedSessionFilename, redactSessionSnapshotIds, refreshFixtureReplacements, restorePinnedToolSchemas, runScenario, scrubRequestHeaders, scrubSessionSnapshot, scrubSystemPrompts, scrubToolSchemas, sessionFixtureFiles, sessionFixtureName, sessionFixtureNames, sessionHeaderVersion, snapshotSpillRoot, stabilizeFixtureMessageIds, stabilizeRefreshLog, tokenizeSessionFixtureCwd, writesCurrentSessionFixtures };
@@ -19,8 +19,9 @@
19
19
  export { redactSessionSnapshotIds, } from './identity.ts';
20
20
  export { runScenario, snapshotSpillRoot, type HarvestedLog, type InputScript, type InputStep, type PermissionAnswer, type RunOptions, type RunResult, } from './harness.ts';
21
21
  export { launchAcpTestAgent, materializeProfilePatch, type AcpTestLaunchOptions, type AgentUnderTest, type LaunchedAcpTestAgent, } from './launcher.ts';
22
- export { extractSnapshotSpillPaths, normalizeSessionLog, normalizeSessionSnapshot, normalizeSessionSnapshots, normalizeStdout, scrubRequestHeaders, scrubSessionSnapshot, scrubSystemPrompts, scrubToolSchemas, tokenizeSessionFixtureCwd, type CwdPathMode, type NormalizeContext, type NormalizeOptions, } from './normalize.ts';
23
- export { parseSnapshotManifest, type SnapshotHeaderManifest, type SnapshotInputAttachment, type SnapshotInputManifest, type SnapshotManifest, type SnapshotPermission, type SnapshotPlatform, type SnapshotProfile, type SnapshotRecording, type SnapshotReplayManifest, type SnapshotSessionReference, type SnapshotWorkspaceManifest, } from './manifest.ts';
24
- export { formatSystemPromptSnapshot, formatToolSchemasSnapshot, fixtureContext, headerChangeCount, defineAcpSnapshotSuite, normalizedHeaders, normalizedSystemPrompts, normalizedToolSchemas, parseToolSchemasSnapshot, refreshFixtureReplacements, restorePinnedToolSchemas, sessionFixtureNames, stabilizeFixtureMessageIds, stabilizeRefreshLog, type Scenario, type SnapshotSuiteOptions, } from './suite.ts';
22
+ export { extractSnapshotSpillPaths, normalizeSessionFormatProvenance, normalizeSessionLog, normalizeSessionSnapshot, normalizeSessionSnapshots, normalizeStdout, scrubRequestHeaders, scrubSessionSnapshot, scrubSystemPrompts, scrubToolSchemas, tokenizeSessionFixtureCwd, type CwdPathMode, type NormalizeContext, type NormalizeOptions, } from './normalize.ts';
23
+ export { parseSnapshotManifest, writesCurrentSessionFixtures, type SnapshotHeaderManifest, type SnapshotInputAttachment, type SnapshotInputManifest, type SnapshotManifest, type SnapshotPermission, type SnapshotPlatform, type SnapshotProfile, type SnapshotRecording, type SnapshotReplayManifest, type SnapshotSessionReference, type SnapshotSessionFormatCoverage, type SnapshotSessionFormatManifest, type SnapshotSessionWriteMode, type SnapshotWorkspaceManifest, } from './manifest.ts';
24
+ export { assertPersistedSessionVersion, assertSessionFixtureVersion, latestPersistedSessionPaths, parsePersistedSessionFilename, parseSessionFixtureName, persistedSessionFilename, sessionFixtureFiles, sessionFixtureName, sessionFixtureNames, sessionHeaderVersion, type PersistedSessionFile, type SessionFixtureFile, } from './session-files.ts';
25
+ export { formatSystemPromptSnapshot, formatToolSchemasSnapshot, fixtureContext, headerChangeCount, defineAcpSnapshotSuite, normalizedHeaders, normalizedSystemPrompts, normalizedToolSchemas, parseToolSchemasSnapshot, refreshFixtureReplacements, restorePinnedToolSchemas, stabilizeFixtureMessageIds, stabilizeRefreshLog, type Scenario, type SnapshotSuiteOptions, } from './suite.ts';
25
26
  export { captureExpectedWorkspaceSnapshot, captureWorkspaceSnapshot, EMPTY_WORKSPACE_MARKER, type CaptureWorkspaceSnapshotOptions, type WorkspaceBinaryFileSnapshot, type WorkspaceEmptyDirectorySnapshot, type WorkspaceSnapshotEntry, type WorkspaceSymlinkSnapshot, type WorkspaceTextFileSnapshot, } from './workspace.ts';
26
27
  //# sourceMappingURL=index.d.ts.map
@@ -56,9 +56,18 @@ export interface SnapshotInputManifest {
56
56
  }
57
57
  /** Optional reference to another scenario's canonical session. */
58
58
  export interface SnapshotSessionReference {
59
- /** Repository-relative POSIX path from this scenario directory to the owning `session.jsonl`. */
59
+ /** Repository-relative POSIX path to the owning scenario's selected parent Session fixture. */
60
60
  source: string;
61
61
  }
62
+ /** Historical-format behavior one retained scenario permanently exercises. */
63
+ export type SnapshotSessionFormatCoverage = 'multi-hop' | 'packed-row' | 'retry-failure' | 'shipped-profile' | 'adjacent-migration';
64
+ /** Explicit historical generation retained by an owning scenario. */
65
+ export interface SnapshotSessionFormatManifest {
66
+ /** Selected fixture generation; absent manifest metadata tracks the current writer. */
67
+ readonly version: number;
68
+ /** Migration behaviors that require this historical fixture. */
69
+ readonly coverage: readonly SnapshotSessionFormatCoverage[];
70
+ }
62
71
  /** Declarative ownership metadata stored beside a recorded session. */
63
72
  export interface SnapshotManifest {
64
73
  /** Manifest format version. */
@@ -85,9 +94,23 @@ export interface SnapshotManifest {
85
94
  workspace?: SnapshotWorkspaceManifest;
86
95
  /** Exceptional controller input absent for ordinary log-driven scenarios. */
87
96
  input?: SnapshotInputManifest;
88
- /** Absent when this directory owns `session.jsonl`; present for a read-only borrower. */
97
+ /** Absent when this directory owns its selected parent fixture; present for a read-only borrower. */
89
98
  session?: SnapshotSessionReference;
99
+ /** Historical generation retained by an owner instead of tracking the current writer. */
100
+ sessionFormat?: SnapshotSessionFormatManifest;
90
101
  }
102
+ /** Snapshot execution modes that may read or replace committed fixture generations. */
103
+ export type SnapshotSessionWriteMode = 'replay' | 'record' | 'refresh';
104
+ /**
105
+ * Whether one run writes current-writer Session fixtures for this scenario.
106
+ * Explicit historical generations remain immutable replay inputs; record and
107
+ * refresh may still update their non-Session expected outputs.
108
+ *
109
+ * @param manifest - Parsed scenario ownership and retained-generation metadata.
110
+ * @param mode - Snapshot execution mode.
111
+ * @returns True only when a write-capable mode tracks the current writer.
112
+ */
113
+ export declare function writesCurrentSessionFixtures(manifest: SnapshotManifest, mode: SnapshotSessionWriteMode): boolean;
91
114
  /**
92
115
  * Parse one `snapshot.yml` without admitting JavaScript YAML tags or unknown fields.
93
116
  * @param source - complete manifest text.
@@ -55,9 +55,9 @@ export declare function tokenizeSessionFixtureCwd(rawLog: string): string;
55
55
  export declare function normalizeStdout(rawStdout: string, ctx: NormalizeContext, options?: NormalizeOptions): string;
56
56
  /**
57
57
  * Normalize a session JSONL log into a stable expected output: the header line's
58
- * volatile fields (`createdAt`, `id`, `cwd`) are zeroed/scrubbed, ordinary
59
- * event `time`, packed-row `time0`, and goal-change lifecycle clock values are
60
- * zeroed, and all volatile strings are scrubbed. Projected inputs remain
58
+ * volatile fields (`createdAt`, `id`, `cwd`) are zeroed/scrubbed; event,
59
+ * historical packed-row, embedded Assistant-stream, and goal lifecycle clocks
60
+ * are zeroed; and all volatile strings are scrubbed. Projected inputs remain
61
61
  * projected. Packed `data.dt` gaps are normalized even when the projected row
62
62
  * omits its `time0` anchor.
63
63
  * Output is JSONL in the same shape as the input — one compact record per
@@ -72,8 +72,8 @@ export declare function normalizeSessionLog(rawLog: string, ctx: NormalizeContex
72
72
  /**
73
73
  * Normalize and project persisted session JSONL for a committed fixture.
74
74
  * This composes ordinary log normalization with request-header scrubbing and
75
- * persistence-envelope projection, then packs the logical event stream into a
76
- * canonical layout independent of persistence flush boundaries.
75
+ * persistence-envelope projection, then writes the v2 logical event stream as
76
+ * one record per event, independent of persistence flush boundaries.
77
77
  *
78
78
  * @param rawLog - persisted or already-projected session JSONL.
79
79
  * @param ctx - the run's volatile values to scrub.
@@ -89,6 +89,12 @@ export declare function normalizeSessionSnapshot(rawLog: string, ctx: NormalizeC
89
89
  * @returns normalized session fixtures in input order.
90
90
  */
91
91
  export declare function normalizeSessionSnapshots(rawLogs: readonly string[], ctx: NormalizeContext, options?: Omit<NormalizeOptions, 'identityMode'>): string[];
92
+ /**
93
+ * Omit only generation-qualified operational provenance from expected-output comparison.
94
+ * @param rawLog - Session records or events as compact JSON lines.
95
+ * @returns the same records without delivery or captured-source generation qualifiers.
96
+ */
97
+ export declare function normalizeSessionFormatProvenance(rawLog: string): string;
92
98
  /**
93
99
  * Replace system-prompt content in request headers with `{{system}}` tokens
94
100
  * while retaining field presence.
@@ -0,0 +1,97 @@
1
+ /** Immutable Session-generation filenames used by recorded-session fixtures. */
2
+ /** One canonical recorded-session fixture filename. */
3
+ export interface SessionFixtureFile {
4
+ /** Parent is `0`; positive values are child/ordinal slots. */
5
+ readonly index: number;
6
+ /** Physical Session format generation; zero is encoded by omission. */
7
+ readonly version: number;
8
+ /** Canonical filename. */
9
+ readonly name: string;
10
+ }
11
+ /** One canonical persistence filename in a Session's own storage directory. */
12
+ export interface PersistedSessionFile {
13
+ /** Physical Session format generation; zero is encoded by omission. */
14
+ readonly version: number;
15
+ /** Physical compression selected by the backend. */
16
+ readonly compression: 'raw' | 'zstd';
17
+ /** Canonical basename. */
18
+ readonly name: string;
19
+ }
20
+ /**
21
+ * Return the canonical fixture filename for one parent/ordinal and generation.
22
+ *
23
+ * @param index - Parent `0` or a positive child/ordinal slot.
24
+ * @param version - Physical format generation; `0` is omitted.
25
+ * @returns The lowercase canonical JSONL filename.
26
+ */
27
+ export declare function sessionFixtureName(index: number, version: number): string;
28
+ /**
29
+ * Parse one canonical recorded-session fixture filename.
30
+ *
31
+ * @param name - Basename from a scenario directory.
32
+ * @returns Parsed role and generation, or `undefined` for an unrelated file.
33
+ */
34
+ export declare function parseSessionFixtureName(name: string): SessionFixtureFile | undefined;
35
+ /**
36
+ * Select the highest generation for every parent/ordinal fixture role.
37
+ * Older generations remain in the directory but do not count as extra Sessions.
38
+ *
39
+ * @param names - File basenames in one scenario directory.
40
+ * @returns Parent first, followed by contiguous child/ordinal roles.
41
+ */
42
+ export declare function sessionFixtureFiles(names: readonly string[]): SessionFixtureFile[];
43
+ /**
44
+ * Validate and order a scenario directory's selected Session fixture filenames.
45
+ *
46
+ * @param names - File basenames in one scenario directory.
47
+ * @returns Highest-generation parent and child/ordinal filenames.
48
+ */
49
+ export declare function sessionFixtureNames(names: readonly string[]): string[];
50
+ /**
51
+ * Read the declared physical generation from one Session JSONL header.
52
+ *
53
+ * @param content - Complete UTF-8 JSONL content.
54
+ * @param label - Diagnostic filename or path.
55
+ * @returns The declared non-negative format generation.
56
+ */
57
+ export declare function sessionHeaderVersion(content: string, label: string): number;
58
+ /**
59
+ * Require one fixture's canonical filename generation to equal its header.
60
+ *
61
+ * @param name - Canonical fixture basename.
62
+ * @param content - Complete UTF-8 JSONL content.
63
+ * @returns The validated format generation.
64
+ */
65
+ export declare function assertSessionFixtureVersion(name: string, content: string): number;
66
+ /**
67
+ * Return the canonical persistence basename for one generation and compression.
68
+ *
69
+ * @param version - Physical format generation; `0` is omitted.
70
+ * @param compression - Backend compression mode.
71
+ * @returns The canonical persistence basename.
72
+ */
73
+ export declare function persistedSessionFilename(version: number, compression?: 'raw' | 'zstd'): string;
74
+ /**
75
+ * Parse a canonical persistence basename from a Session's own directory.
76
+ *
77
+ * @param name - Candidate basename.
78
+ * @returns Its generation and compression, or `undefined` for noise and noncanonical names.
79
+ */
80
+ export declare function parsePersistedSessionFilename(name: string): PersistedSessionFile | undefined;
81
+ /**
82
+ * Select one highest-generation persistence path per physical Session directory.
83
+ *
84
+ * @param paths - Relative or absolute paths beneath a sessions root.
85
+ * @param compression - Compression selected by the snapshot composition.
86
+ * @returns Stable path order with older generations and filesystem noise omitted.
87
+ */
88
+ export declare function latestPersistedSessionPaths(paths: readonly string[], compression?: 'raw' | 'zstd'): string[];
89
+ /**
90
+ * Require one persistence basename's generation to equal its Session header.
91
+ *
92
+ * @param name - Canonical persistence basename.
93
+ * @param content - Complete uncompressed UTF-8 JSONL content.
94
+ * @returns The validated generation.
95
+ */
96
+ export declare function assertPersistedSessionVersion(name: string, content: string): number;
97
+ //# sourceMappingURL=session-files.d.ts.map
@@ -17,6 +17,7 @@
17
17
  * @module @deepseek-ai/dsh-session-snapshot/suite
18
18
  */
19
19
  import { type AgentUnderTest, type HarvestedLog } from './harness.ts';
20
+ import { type SnapshotSessionFormatManifest } from './manifest.ts';
20
21
  import { type CwdPathMode, type NormalizeContext } from './normalize.ts';
21
22
  /** A snapshot scenario and how its fixtures are produced. */
22
23
  export interface Scenario {
@@ -27,24 +28,26 @@ export interface Scenario {
27
28
  hasModelTurn: boolean;
28
29
  /**
29
30
  * Whether the run persists a comparable session log to diff against the
30
- * `session.jsonl` fixture. Defaults to {@link hasModelTurn} (a model turn
31
+ * selected parent Session fixture. Defaults to {@link hasModelTurn} (a model turn
31
32
  * always produces a log worth comparing). Set it independently for a scenario
32
33
  * that produces a non-trivial durable log without calling the model.
33
34
  */
34
35
  comparesLog?: boolean;
35
36
  /**
36
- * Whether `test:snapshot:record` regenerates this scenario's `session.jsonl`
37
- * from the LIVE API. `recorded` scenarios are model-driven and reproducible;
37
+ * Whether `test:snapshot:record` regenerates this scenario's current-version
38
+ * Session fixtures from the LIVE API. `recorded` scenarios are model-driven and reproducible;
38
39
  * `authored` scenarios (fixtures hand-written or hand-harvested — e.g. a
39
40
  * provider error or a cancel the live API can't be coaxed into
40
41
  * deterministically, a deterministic hook scenario, or a scripted repetition
41
42
  * a live model won't reproduce) are NEVER re-recorded.
42
43
  */
43
44
  recorded: boolean;
45
+ /** Historical generation retained as a read-only migration fixture. */
46
+ sessionFormat?: SnapshotSessionFormatManifest;
44
47
  /**
45
48
  * Whether replay is driven by a hand-written `replay.override.json` sidecar
46
49
  * (a `ReplayOverrideDoc` that replaces or patches the script derived from
47
- * `session.jsonl`) — the throw/hang cases chunks cannot express. The fixture
50
+ * selected parent Session fixture) — the throw/hang cases chunks cannot express. The fixture
48
51
  * guard requires the sidecar exactly when this is set: the harness forwards
49
52
  * the file purely on existence, so an unregistered stray sidecar would
50
53
  * silently alter the derived script. The guard fails loud on either
@@ -142,9 +145,10 @@ export interface Scenario {
142
145
  }
143
146
  /**
144
147
  * Whether a scenario's run test is skipped for this mode and host: record mode
145
- * skips authored (non-`recorded`) scenarios, {@link Scenario.posixOnly}
146
- * scenarios skip on Windows, and {@link Scenario.pwshOnly} scenarios skip
147
- * when the caller's `hasPwsh` probe is false.
148
+ * skips authored (non-`recorded`) scenarios and explicit historical Session
149
+ * generations, {@link Scenario.posixOnly} scenarios skip on Windows, and
150
+ * {@link Scenario.pwshOnly} scenarios skip when the caller's `hasPwsh` probe
151
+ * is false.
148
152
  *
149
153
  * @param scenario The scenario whose run test is being registered.
150
154
  * @param recording Whether the suite runs in record mode.
@@ -223,24 +227,12 @@ export declare function claimSharedSnapshot(claims: Map<string, SharedSnapshotCl
223
227
  * @returns Nothing.
224
228
  */
225
229
  export declare function assertUniqueSnapshotContents(kind: string, snapshots: readonly NamedSnapshotContent[]): void;
226
- /**
227
- * Validate and order a scenario directory's session-fixture filenames.
228
- *
229
- * The primary fixture is always `session.jsonl`; child sessions are discovered
230
- * from contiguous `session.1.jsonl` … filenames. The directory is the source of
231
- * truth, so scenario tables do not duplicate a child count that can drift from
232
- * the files. A session-like JSONL with any other suffix fails loud.
233
- *
234
- * @param names File names in one scenario directory.
235
- * @returns The primary and child fixture names in replay/harvest order.
236
- */
237
- export declare function sessionFixtureNames(names: readonly string[]): string[];
238
230
  /**
239
231
  * Derive normalization values from a fixture's own session header. Recorded ids and cwd differ
240
232
  * from the live replay run; the non-empty sentinel for missing cwd avoids accidental empty-
241
233
  * string replacement.
242
234
  *
243
- * @param fixture The committed `session.jsonl` content.
235
+ * @param fixture The selected committed parent Session fixture content.
244
236
  * @returns The fixture's own volatile values, ready for {@link normalizeSessionLog}.
245
237
  */
246
238
  export declare function fixtureContext(fixture: string): NormalizeContext;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-session-snapshot",
3
3
  "description": "Session-log snapshot core with an ACP protocol adapter, expected-output normalization, and fixture invariants",
4
- "version": "0.1.2-rc.1",
4
+ "version": "0.1.3-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -30,25 +30,29 @@
30
30
  "@agentclientprotocol/sdk": "1.4.0",
31
31
  "js-yaml": "^4.2.0",
32
32
  "vitest": "^4.1.8",
33
- "@deepseek-ai/dsh-loader-smoke": "0.1.2-rc.1",
34
- "@deepseek-ai/cordis-plugin-include": "1.0.7"
33
+ "@deepseek-ai/cordis-plugin-include": "1.0.7",
34
+ "@deepseek-ai/dsh-llm-replay": "^0.1.3-alpha.2",
35
+ "@deepseek-ai/dsh-loader-smoke": "0.1.3-alpha.2",
36
+ "@deepseek-ai/dsh-session-format": "^0.1.3-alpha.2"
35
37
  },
36
38
  "peerDependencies": {
37
39
  "@deepseek-ai/cordis": "^4.0.2",
38
- "@deepseek-ai/dsh-session": "^0.1.2-rc.1"
40
+ "@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
41
+ "@deepseek-ai/dsh-http-proxy": "^0.1.3-alpha.2"
39
42
  },
40
43
  "devDependencies": {
41
44
  "@types/js-yaml": "^4.0.9",
42
45
  "@deepseek-ai/cordis": "^4.0.2",
43
- "@deepseek-ai/dsh-agent": "^0.1.2-rc.1",
44
- "@deepseek-ai/dsh-agent-loop": "^0.1.2-rc.1",
45
- "@deepseek-ai/dsh-compaction": "^0.1.2-rc.1",
46
- "@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
47
- "@deepseek-ai/dsh-sandbox": "^0.1.2-rc.1",
48
- "@deepseek-ai/dsh-sandbox-policy": "^0.1.2-rc.1",
49
- "@deepseek-ai/dsh-session": "^0.1.2-rc.1",
50
- "@deepseek-ai/dsh-subagent": "^0.1.2-rc.1",
51
- "@deepseek-ai/dsh-tools": "^0.1.2-rc.1",
52
- "@deepseek-ai/dsh-user-questions": "^0.1.2-rc.1"
46
+ "@deepseek-ai/dsh-agent": "^0.1.3-alpha.2",
47
+ "@deepseek-ai/dsh-agent-loop": "^0.1.3-alpha.2",
48
+ "@deepseek-ai/dsh-compaction": "^0.1.3-alpha.2",
49
+ "@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
50
+ "@deepseek-ai/dsh-sandbox": "^0.1.3-alpha.2",
51
+ "@deepseek-ai/dsh-sandbox-policy": "^0.1.3-alpha.2",
52
+ "@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
53
+ "@deepseek-ai/dsh-subagent": "^0.1.3-alpha.2",
54
+ "@deepseek-ai/dsh-tools": "^0.1.3-alpha.2",
55
+ "@deepseek-ai/dsh-user-questions": "^0.1.3-alpha.2",
56
+ "@deepseek-ai/dsh-http-proxy": "^0.1.3-alpha.2"
53
57
  }
54
58
  }