@deepseek-ai/dsh-session-snapshot 0.1.3-alpha.2 → 0.1.5-alpha.1

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/lib/index.js CHANGED
@@ -14,7 +14,7 @@ import { entryListSchema } from "@deepseek-ai/cordis-plugin-include";
14
14
  import { resolveExampleLaunch } from "@deepseek-ai/dsh-loader-smoke";
15
15
  import { clearedProxyEnv } from "@deepseek-ai/dsh-http-proxy";
16
16
  import { parseSessionFormatLogFilename, sessionFormatLogFilename } from "@deepseek-ai/dsh-session-format";
17
- import { decodeSeqRanges } from "@deepseek-ai/dsh-session";
17
+ import { SESSION_FORMAT_VERSION, decodeSeqRanges } from "@deepseek-ai/dsh-session";
18
18
  import { prepareSessionSnapshotFixtureForComparison } from "@deepseek-ai/dsh-llm-replay";
19
19
  import { isSurfaceEligibleType } from "@deepseek-ai/dsh-session/surface";
20
20
  //#region lib/types/identity.js
@@ -423,6 +423,16 @@ function sessionFixtureName(index, version) {
423
423
  return `session${index === 0 ? "" : `.${index}`}${version === 0 ? "" : `.v${version}`}.jsonl`;
424
424
  }
425
425
  /**
426
+ * Name the native-writer oracle for a retained historical replay role.
427
+ * This expected output never participates in replay generation selection.
428
+ * @param index - Parent `0` or a positive child/ordinal slot.
429
+ * @returns The expected-output JSONL basename.
430
+ */
431
+ function writerSnapshotName(index) {
432
+ nonNegativeSafeInteger(index, "writer snapshot index");
433
+ return `writer${index === 0 ? "" : `.${index}`}.expected.jsonl`;
434
+ }
435
+ /**
426
436
  * Parse one canonical recorded-session fixture filename.
427
437
  *
428
438
  * @param name - Basename from a scenario directory.
@@ -668,7 +678,7 @@ function captureExpectedWorkspaceSnapshot(root) {
668
678
  const DEFAULT_WAIT_TIMEOUT_MS = 1e4;
669
679
  const WAIT_POLL_INTERVAL_MS = 10;
670
680
  /**
671
- * Derive one stable, fixed-length spill root owned by this scenario.
681
+ * Derive the stable, fixed-length logical spill prefix; never allocate files here.
672
682
  * Windows uses a two-character-shorter root because drive resolution adds its drive prefix.
673
683
  * @param fixtureFile - The scenario fixture whose parent directory provides the stable identity.
674
684
  * @param platform - the host platform, injectable for unit coverage.
@@ -692,11 +702,12 @@ async function runScenario(input, opts) {
692
702
  const cwd = await mkdtemp(join(opts.workspaceParent ?? tmpdir(), "acp-snap-cwd-"));
693
703
  const cwdAliases = [...new Set([realpathSync(cwd), realpathSync.native(cwd)])];
694
704
  const sessionsRoot = await mkdtemp(join(tmpdir(), "acp-snap-sessions-"));
695
- const spillRoot = snapshotSpillRoot(opts.fixtureFile);
705
+ let spillRoot;
696
706
  let launched;
697
707
  let sessionId;
698
708
  let sessionLogs = [];
699
709
  const outcome = await (async () => {
710
+ spillRoot = await mkdtemp(join(tmpdir(), "acp-snap-spill-"));
700
711
  if (opts.workspaceDir !== void 0 && existsSync(opts.workspaceDir)) await cp(opts.workspaceDir, cwd, { recursive: true });
701
712
  await opts.prepareWorkspace?.(cwd);
702
713
  const initialWorkspace = await captureWorkspaceSnapshot(cwd, { ignoredRootEntries: [
@@ -712,6 +723,7 @@ async function runScenario(input, opts) {
712
723
  DSH_SNAPSHOT_FILE: opts.fixtureFile,
713
724
  DSH_SNAPSHOT_SESSIONS_ROOT: sessionsRoot,
714
725
  DSH_SNAPSHOT_SPILL_ROOT: spillRoot,
726
+ DSH_SNAPSHOT_SPILL_LOCATOR_ROOT: snapshotSpillRoot(opts.fixtureFile),
715
727
  DSH_HOME: join(cwd, ".dsh"),
716
728
  DSH_AGENTS_HOME: join(cwd, ".agents"),
717
729
  ...opts.overrideFile !== void 0 ? { DSH_SNAPSHOT_OVERRIDE: opts.overrideFile } : {},
@@ -789,7 +801,8 @@ async function runScenario(input, opts) {
789
801
  recursive: true,
790
802
  force: true
791
803
  }));
792
- await cleanup(() => rm(spillRoot, {
804
+ const allocatedSpillRoot = spillRoot;
805
+ if (allocatedSpillRoot !== void 0) await cleanup(() => rm(allocatedSpillRoot, {
793
806
  recursive: true,
794
807
  force: true
795
808
  }));
@@ -987,13 +1000,18 @@ async function waitForPersistedTurnEnd(root, sessionId, timeoutMs = DEFAULT_WAIT
987
1000
  * own model work reached a closed turn.
988
1001
  */
989
1002
  async function waitForPersistedChildTurnEnd(root, child, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS, minimumTurn = 1) {
990
- await vi.waitFor(async () => {
991
- const log = (await harvestSessionLogs(root))[child];
992
- if (log === void 0 || !latestTurnIsClosed(log.content) || !hasRequestHeaderAfterDescriptor(log.content) || !hasClosedTurn(log.content, minimumTurn)) throw new Error(`snapshot-harness: subagent child #${child} did not persist closed turn ${minimumTurn} within ${timeoutMs}ms`);
993
- }, {
994
- interval: WAIT_POLL_INTERVAL_MS,
995
- timeout: timeoutMs
996
- });
1003
+ const message = `snapshot-harness: subagent child #${child} did not persist closed turn ${minimumTurn} within ${timeoutMs}ms`;
1004
+ try {
1005
+ await vi.waitFor(async () => {
1006
+ const log = (await harvestSessionLogs(root))[child];
1007
+ if (log === void 0 || !latestTurnIsClosed(log.content) || !hasRequestHeaderAfterDescriptor(log.content) || !hasClosedTurn(log.content, minimumTurn)) throw new Error(message);
1008
+ }, {
1009
+ interval: WAIT_POLL_INTERVAL_MS,
1010
+ timeout: timeoutMs
1011
+ });
1012
+ } catch (cause) {
1013
+ throw new Error(message, { cause });
1014
+ }
997
1015
  }
998
1016
  /** Whether a raw session log contains the requested closed turn. */
999
1017
  function hasClosedTurn(content, turn) {
@@ -1132,8 +1150,8 @@ async function harvestSessionLogs(root) {
1132
1150
  /**
1133
1151
  * Pure ACP transcript and session-log normalizers. They scrub session ids, run cwd, RPC ids,
1134
1152
  * timestamps, goal lifecycle clocks, and hook duration while preserving semantic payload values.
1135
- * Request-header scrubbers stay composable so one scenario per header class can pin prompt and
1136
- * tool-schema sidecars.
1153
+ * The prompt-text and tool-schema scrubbers stay composable so one scenario per header class can
1154
+ * pin prompt and tool-schema sidecars.
1137
1155
  * @module @deepseek-ai/dsh-session-snapshot/normalize
1138
1156
  */
1139
1157
  const SESSION_ID = "{{sessionId}}";
@@ -1392,7 +1410,7 @@ function normalizeSessionLog(rawLog, ctx, options = {}) {
1392
1410
  }).map((r) => JSON.stringify(r)).join("\n") + "\n";
1393
1411
  }
1394
1412
  /**
1395
- * Canonicalize projected v2 body records. Compact streams are nested event data,
1413
+ * Canonicalize projected v3 body records. Compact streams are nested event data,
1396
1414
  * so persistence flush boundaries cannot change the row layout.
1397
1415
  */
1398
1416
  function projectSessionSnapshot(rawLog) {
@@ -1410,7 +1428,7 @@ function projectSessionSnapshot(rawLog) {
1410
1428
  /**
1411
1429
  * Normalize and project persisted session JSONL for a committed fixture.
1412
1430
  * This composes ordinary log normalization with request-header scrubbing and
1413
- * persistence-envelope projection, then writes the v2 logical event stream as
1431
+ * persistence-envelope projection, then writes the v3 logical event stream as
1414
1432
  * one record per event, independent of persistence flush boundaries.
1415
1433
  *
1416
1434
  * @param rawLog - persisted or already-projected session JSONL.
@@ -1438,51 +1456,20 @@ function normalizeSessionSnapshots(rawLogs, ctx, options = {}) {
1438
1456
  }))));
1439
1457
  }
1440
1458
  /**
1441
- * Omit only generation-qualified operational provenance from expected-output comparison.
1459
+ * Omit the artifact header generation after official migration for comparison.
1460
+ * Delivery and captured-source generations retain their opaque recorded values.
1442
1461
  * @param rawLog - Session records or events as compact JSON lines.
1443
- * @returns the same records without delivery or captured-source generation qualifiers.
1462
+ * @returns the same records with only the Session header version omitted.
1444
1463
  */
1445
1464
  function normalizeSessionFormatProvenance(rawLog) {
1446
1465
  return rawLog.split("\n").map((line) => {
1447
1466
  if (line.trim().length === 0) return line;
1448
1467
  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;
1468
+ if (record.type !== "session" || !Object.hasOwn(record, "version")) return line;
1469
+ delete record.version;
1470
+ return JSON.stringify(record);
1463
1471
  }).join("\n");
1464
1472
  }
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
1473
  /** Whether a fixture declares a released Session format and therefore participates in migration burn-in. */
1487
1474
  function hasSessionFormatVersion(rawLog) {
1488
1475
  const firstLine = rawLog.split(/\r?\n/).find((line) => line.trim().length > 0);
@@ -1492,60 +1479,58 @@ function hasSessionFormatVersion(rawLog) {
1492
1479
  return Object.hasOwn(header, "version");
1493
1480
  }
1494
1481
  /**
1495
- * Replace system-prompt content in request headers with `{{system}}` tokens
1496
- * while retaining field presence.
1497
- * Other header content stays verbatim, so a header-pinning fixture can keep
1498
- * its complete tool schemas while every JSONL fixture omits the prompt text.
1499
- * Lines without a system payload pass through byte-for-byte; the transform is
1500
- * idempotent.
1482
+ * Replace the rendered prompt text of every `system/message` event with the
1483
+ * `{{system}}` token. The text block keeps its position and type, so the
1484
+ * fixture still shows one system node per prompt version; an empty `content`
1485
+ * (no system prompt) stays empty. Request headers and every other line pass
1486
+ * through byte-for-byte; the transform is idempotent.
1501
1487
  *
1502
1488
  * @param rawLog The raw session `.jsonl` content.
1503
- * @returns The JSONL with system-prompt content tokenized.
1489
+ * @returns The JSONL with system-prompt text tokenized.
1504
1490
  */
1505
1491
  function scrubSystemPrompts(rawLog) {
1506
- return scrubHeaderContent(rawLog, { system: true });
1492
+ return scrubModelRequestContent(rawLog, { system: true });
1507
1493
  }
1508
1494
  /**
1509
1495
  * Replace tool schemas in full request-header snapshots with `{{tools}}`
1510
- * tokens while retaining field presence. System prompts and session-prefix
1511
- * messages stay verbatim so pinning fixtures can move only schema bulk into
1512
- * their dedicated JSON sidecar. Lines without a tool payload pass through
1513
- * byte-for-byte; the transform is idempotent.
1496
+ * tokens while retaining field presence. System-prompt text stays verbatim so
1497
+ * pinning fixtures can move only schema bulk into their dedicated JSON
1498
+ * sidecar. Lines without a tool payload pass through byte-for-byte; the
1499
+ * transform is idempotent.
1514
1500
  *
1515
1501
  * @param rawLog The raw session `.jsonl` content.
1516
1502
  * @returns The JSONL with tool-schema content tokenized.
1517
1503
  */
1518
1504
  function scrubToolSchemas(rawLog) {
1519
- return scrubHeaderContent(rawLog, { tools: true });
1505
+ return scrubModelRequestContent(rawLog, { tools: true });
1520
1506
  }
1521
1507
  /**
1522
- * Replace all bulky request-header content in a session JSONL with stable
1523
- * tokens. This includes the system-prompt fields handled by
1524
- * {@link scrubSystemPrompts}, tool schemas, and session-prefix messages. It
1525
- * keeps prefix message counts, field presence, config, and reason. Lines
1526
- * without content to scrub pass through byte-for-byte, and the transform is
1527
- * idempotent.
1508
+ * Replace all bulky model-request content in a session JSONL with stable
1509
+ * tokens: the `system/message` prompt text handled by
1510
+ * {@link scrubSystemPrompts} and the request-header tool schemas handled by
1511
+ * {@link scrubToolSchemas}. Field presence, config, and reason are kept.
1512
+ * Lines without content to scrub pass through byte-for-byte, and the
1513
+ * transform is idempotent.
1528
1514
  *
1529
1515
  * @param rawLog The raw session `.jsonl` content.
1530
- * @returns The JSONL with all header bulk tokenized, other lines byte-identical.
1516
+ * @returns The JSONL with prompt text and schema bulk tokenized, other lines byte-identical.
1531
1517
  */
1532
- function scrubRequestHeaders(rawLog) {
1533
- return scrubHeaderContent(rawLog, {
1518
+ function scrubModelRequestBulk(rawLog) {
1519
+ return scrubModelRequestContent(rawLog, {
1534
1520
  system: true,
1535
1521
  tools: true
1536
1522
  });
1537
1523
  }
1538
1524
  /**
1539
- * Project a persisted session log while tokenizing all request-header bulk.
1540
- * Each non-empty line is parsed at most once; the session header stays
1541
- * byte-identical. Body records omit their persistence-only envelopes, and
1542
- * request-header payloads are tokenized.
1525
+ * Project a persisted session log while tokenizing prompt text and schema
1526
+ * bulk. Each non-empty line is parsed at most once; the session header stays
1527
+ * byte-identical. Body records omit their persistence-only envelopes.
1543
1528
  *
1544
1529
  * @param rawLog - persisted or already-projected session JSONL.
1545
- * @returns committed snapshot JSONL with request headers tokenized.
1530
+ * @returns committed snapshot JSONL with prompt text and tool schemas tokenized.
1546
1531
  */
1547
1532
  function scrubSessionSnapshot(rawLog) {
1548
- const scrubbed = scrubRequestHeaders(rawLog);
1533
+ const scrubbed = scrubModelRequestBulk(rawLog);
1549
1534
  let recordIndex = 0;
1550
1535
  return scrubbed.split("\n").map((line) => {
1551
1536
  if (line.trim().length === 0) return line;
@@ -1568,26 +1553,31 @@ function normalizeFeedbackClocks(record) {
1568
1553
  if ("createdAt" in clocks) clocks.createdAt = 0;
1569
1554
  if ("updatedAt" in clocks) clocks.updatedAt = 0;
1570
1555
  }
1571
- /** Transform the selected request-header payloads. */
1572
- function scrubHeaderContent(rawLog, options) {
1556
+ /** Return the first text block of a `system/message` payload, or `undefined` when it carries none. */
1557
+ function systemPromptBlock(data) {
1558
+ const message = data.message;
1559
+ if (message === null || typeof message !== "object" || !Array.isArray(message.content)) return void 0;
1560
+ const block = message.content[0];
1561
+ return block !== null && typeof block === "object" && typeof block.text === "string" ? block : void 0;
1562
+ }
1563
+ /** Transform the selected model-request payloads. */
1564
+ function scrubModelRequestContent(rawLog, options) {
1573
1565
  return rawLog.split("\n").map((line) => {
1574
1566
  if (line.trim().length === 0) return line;
1575
1567
  const record = JSON.parse(line);
1576
1568
  const data = record.data;
1577
1569
  if (data === null || typeof data !== "object") return line;
1578
- if (record.type === "request/header") {
1570
+ if (options.system === true && record.type === "system/message") {
1571
+ const block = systemPromptBlock(data);
1572
+ if (block === void 0) return line;
1573
+ block.text = SYSTEM;
1574
+ return JSON.stringify(record);
1575
+ }
1576
+ if (options.tools === true && record.type === "request/header") {
1579
1577
  const header = data.header;
1580
- if (header === null || typeof header !== "object") return line;
1581
- let touched = false;
1582
- if (options.system === true && "system" in header) {
1583
- header.system = SYSTEM;
1584
- touched = true;
1585
- }
1586
- if (options.tools === true && "tools" in header) {
1587
- header.tools = TOOLS;
1588
- touched = true;
1589
- }
1590
- return touched ? JSON.stringify(record) : line;
1578
+ if (header === null || typeof header !== "object" || !("tools" in header)) return line;
1579
+ header.tools = TOOLS;
1580
+ return JSON.stringify(record);
1591
1581
  }
1592
1582
  return line;
1593
1583
  }).join("\n");
@@ -1698,10 +1688,11 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
1698
1688
  "toolSchemasSource",
1699
1689
  "childSystemPrompts",
1700
1690
  "childToolSchemas",
1701
- "changes"
1691
+ "changes",
1692
+ "promptChanges"
1702
1693
  ], "manifest.header");
1703
1694
  if (value.pin !== void 0 && value.pin !== true) throw new Error("manifest.header.pin must equal true when present");
1704
- if (value.changes !== void 0 && (!Number.isInteger(value.changes) || Number(value.changes) < 0)) throw new Error("manifest.header.changes must be a non-negative integer");
1695
+ for (const field of ["changes", "promptChanges"]) if (value[field] !== void 0 && (!Number.isInteger(value[field]) || Number(value[field]) < 0)) throw new Error(`manifest.header.${field} must be a non-negative integer`);
1705
1696
  header = {
1706
1697
  class: name(value.class, "manifest.header.class"),
1707
1698
  ...value.pin === true ? { pin: true } : {},
@@ -1709,7 +1700,8 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
1709
1700
  ...value.toolSchemasSource === void 0 ? {} : { toolSchemasSource: scenarioSource(value.toolSchemasSource, "manifest.header.toolSchemasSource") },
1710
1701
  ...value.childSystemPrompts === void 0 ? {} : { childSystemPrompts: positiveIndexes(value.childSystemPrompts, "manifest.header.childSystemPrompts") },
1711
1702
  ...value.childToolSchemas === void 0 ? {} : { childToolSchemas: positiveIndexes(value.childToolSchemas, "manifest.header.childToolSchemas") },
1712
- ...value.changes === void 0 ? {} : { changes: Number(value.changes) }
1703
+ ...value.changes === void 0 ? {} : { changes: Number(value.changes) },
1704
+ ...value.promptChanges === void 0 ? {} : { promptChanges: Number(value.promptChanges) }
1713
1705
  };
1714
1706
  }
1715
1707
  let replay;
@@ -1744,11 +1736,11 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
1744
1736
  "parent"
1745
1737
  ], "manifest.workspace");
1746
1738
  if (value.final !== void 0 && value.final !== true) throw new Error("manifest.workspace.final must equal true when present");
1747
- if (value.parent !== void 0 && value.parent !== "home") throw new Error("manifest.workspace.parent must equal home");
1739
+ if (value.parent !== void 0 && value.parent !== "outside-temp") throw new Error("manifest.workspace.parent must equal outside-temp");
1748
1740
  workspace = {
1749
1741
  ...value.setup === void 0 ? {} : { setup: name(value.setup, "manifest.workspace.setup") },
1750
1742
  ...value.final === true ? { final: true } : {},
1751
- ...value.parent === "home" ? { parent: "home" } : {}
1743
+ ...value.parent === "outside-temp" ? { parent: "outside-temp" } : {}
1752
1744
  };
1753
1745
  if (Object.keys(workspace).length === 0) throw new Error("manifest.workspace must not be empty");
1754
1746
  }
@@ -1838,9 +1830,10 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
1838
1830
  * refresh stay serial while writing.
1839
1831
  *
1840
1832
  * Exactly one scenario per header-composition class pins the tokenized header
1841
- * sequence. Its prompt and tool-schema sequences live in independent
1842
- * sidecars, each of which may be shared with another class pin when the bytes
1843
- * are identical. Every live header is checked against the composed pin, so
1833
+ * sequence and the tokenized `system/message` sequence. Its prompt and
1834
+ * tool-schema sequences live in independent sidecars, each of which may be
1835
+ * shared with another class pin when the bytes are identical. Every live
1836
+ * header and system prompt is checked against the composed pin, so
1844
1837
  * session-dependent composition must declare a separate class instead of
1845
1838
  * escaping coverage.
1846
1839
  * @module @deepseek-ai/dsh-session-snapshot/suite
@@ -1965,10 +1958,13 @@ function fixtureContext(fixture) {
1965
1958
  }
1966
1959
  /** Normalize request-header payloads while retaining the reason that selects a pin revision. */
1967
1960
  function normalizedHeaderEvents(rawLog, ctx) {
1968
- return normalizeSessionLog(rawLog, ctx).split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line)).filter((record) => record.type === "request/header").map((record) => ({
1969
- header: record.data?.header,
1970
- reason: record.data?.reason
1971
- }));
1961
+ return parseJsonlRecords(normalizeSessionLog(rawLog, ctx)).filter((record) => record.type === "request/header").map((record) => {
1962
+ const data = record.data;
1963
+ return {
1964
+ header: data?.header,
1965
+ reason: data?.reason
1966
+ };
1967
+ });
1972
1968
  }
1973
1969
  /**
1974
1970
  * Header revisions that own sidecar content. `series` reuses the current revision, while
@@ -1979,13 +1975,17 @@ function normalizedHeaderEvents(rawLog, ctx) {
1979
1975
  function pinningHeaderPayloads(rawLog, ctx) {
1980
1976
  return normalizedHeaderEvents(rawLog, ctx).filter((event) => event.reason !== "series").map((event) => event.header);
1981
1977
  }
1982
- /** Extract every string system prompt from a normalized header sequence. */
1983
- function systemPromptsFrom(headers) {
1984
- return headers.flatMap((header) => {
1985
- if (header === null || typeof header !== "object") return [];
1986
- const system = header.system;
1987
- return typeof system === "string" ? [system] : [];
1988
- });
1978
+ /**
1979
+ * The rendered prompt text of one parsed `system/message` record: its single
1980
+ * text block, or `''` when the empty `content` records "no system prompt".
1981
+ * Any other record yields `undefined`.
1982
+ */
1983
+ function systemPromptOf(record) {
1984
+ if (record.type !== "system/message") return void 0;
1985
+ const content = record.data?.message?.content;
1986
+ if (!Array.isArray(content)) return void 0;
1987
+ const block = content[0];
1988
+ return content.length === 0 ? "" : typeof block?.text === "string" ? block.text : void 0;
1989
1989
  }
1990
1990
  /** Extract every array-valued tool catalog from a normalized header sequence. */
1991
1991
  function toolSchemasFrom(headers) {
@@ -1999,8 +1999,7 @@ function toolSchemasFrom(headers) {
1999
1999
  * The `data.header` payload of every `request/header` event in a session
2000
2000
  * JSONL, in log order, with the log's volatile values scrubbed first
2001
2001
  * ({@link normalizeSessionLog}) so headers harvested from different runs —
2002
- * each embedding its own generated cwd in the composed prompt — compare on equal
2003
- * footing.
2002
+ * each embedding its own generated cwd — compare on equal footing.
2004
2003
  *
2005
2004
  * @param rawLog The session `.jsonl` content to extract headers from.
2006
2005
  * @param ctx The volatile values of the run that produced it.
@@ -2010,16 +2009,35 @@ function normalizedHeaders(rawLog, ctx) {
2010
2009
  return normalizedHeaderEvents(rawLog, ctx).map((event) => event.header);
2011
2010
  }
2012
2011
  /**
2013
- * The normalized string-valued system prompts carried by request headers in a
2014
- * session JSONL, in log order. Headers without a string prompt are omitted so
2015
- * callers can assert one prompt per header explicitly.
2012
+ * The normalized prompt text of every `system/message` event in a session
2013
+ * JSONL, in log order: the first is the initial system prompt (surface node 0)
2014
+ * and each later one replaced it or, on an in-history route, appended the
2015
+ * changed prompt after the cached history. An empty `content` yields `''`; a
2016
+ * `system/message` without a text block is omitted.
2016
2017
  *
2017
2018
  * @param rawLog The session `.jsonl` content to inspect.
2018
2019
  * @param ctx The volatile values of the run that produced it.
2019
- * @returns The normalized system prompts, in header order.
2020
+ * @returns The normalized system prompts, in log order.
2020
2021
  */
2021
2022
  function normalizedSystemPrompts(rawLog, ctx) {
2022
- return systemPromptsFrom(normalizedHeaders(rawLog, ctx));
2023
+ return parseJsonlRecords(normalizeSessionLog(rawLog, ctx)).flatMap((record) => {
2024
+ const prompt = systemPromptOf(record);
2025
+ return prompt === void 0 ? [] : [prompt];
2026
+ });
2027
+ }
2028
+ /**
2029
+ * Whether every model request in a session JSONL is preceded by its system
2030
+ * prompt: the log has no `request/header`, or a `system/message` event comes
2031
+ * before its first `request/header`.
2032
+ *
2033
+ * @param rawLog The session `.jsonl` content to inspect.
2034
+ * @returns True when the first `request/header` (if any) follows a `system/message`.
2035
+ */
2036
+ function systemPromptPrecedesRequests(rawLog) {
2037
+ const types = parseJsonlRecords(rawLog).map((record) => record.type);
2038
+ const firstHeader = types.indexOf("request/header");
2039
+ const firstPrompt = types.indexOf("system/message");
2040
+ return firstHeader < 0 || firstPrompt >= 0 && firstPrompt < firstHeader;
2023
2041
  }
2024
2042
  /**
2025
2043
  * The normalized tool-schema arrays carried by request headers in a session
@@ -2077,24 +2095,40 @@ function restorePinnedToolSchemas(header, schemas) {
2077
2095
  tools: schemas
2078
2096
  };
2079
2097
  }
2098
+ /** Marker line separating one replaced prompt from the previous one in a prompt sidecar. */
2099
+ const SYSTEM_PROMPT_CHANGE_MARKER = "\n<!-- system/message change ";
2080
2100
  /**
2081
2101
  * Render a normalized prompt as a repository-friendly Markdown snapshot.
2082
2102
  * Prompt text is unchanged except that a missing terminal newline is added so
2083
2103
  * the committed file follows the repository newline contract.
2084
2104
  *
2085
- * @param prompt The normalized system prompt.
2086
- * @param changes Full normalized prompts from later changed-header snapshots.
2105
+ * @param prompt The normalized initial system prompt (surface node 0).
2106
+ * @param changes Full normalized prompts from later `system/message` events, replacements or in-history appends.
2087
2107
  * @returns Markdown snapshot text ending in a newline.
2088
2108
  */
2089
2109
  function formatSystemPromptSnapshot(prompt, changes = []) {
2090
2110
  let snapshot = prompt.endsWith("\n") ? prompt : `${prompt}\n`;
2091
2111
  for (const [index, change] of changes.entries()) {
2092
- snapshot += `\n<!-- request/header change ${index + 1} -->\n\n`;
2112
+ snapshot += `${SYSTEM_PROMPT_CHANGE_MARKER}${index + 1} -->\n\n`;
2093
2113
  snapshot += change.endsWith("\n") ? change : `${change}\n`;
2094
2114
  }
2095
2115
  return snapshot;
2096
2116
  }
2097
2117
  /**
2118
+ * Split a prompt sidecar into its initial prompt and each later change, the
2119
+ * inverse of {@link formatSystemPromptSnapshot}.
2120
+ *
2121
+ * @param snapshot The Markdown sidecar text.
2122
+ * @returns The initial prompt snapshot plus one entry per later `system/message`.
2123
+ */
2124
+ function parseSystemPromptSnapshot(snapshot) {
2125
+ const parts = snapshot.split(/\n<!-- system\/message change [1-9]\d* -->\n\n/);
2126
+ return {
2127
+ initial: parts[0],
2128
+ changes: parts.slice(1)
2129
+ };
2130
+ }
2131
+ /**
2098
2132
  * Reject a child prompt sidecar that cannot own distinct, canonical prompt text.
2099
2133
  * @param sidecar - committed child prompt snapshot.
2100
2134
  * @param classPin - initial prompt snapshot owned by the scenario's header class.
@@ -2105,10 +2139,9 @@ function assertChildSystemPromptSnapshot(sidecar, classPin, label) {
2105
2139
  if (!sidecar.endsWith("\n")) throw new Error(`${label} must end in a newline`);
2106
2140
  if (sidecar === classPin) throw new Error(`${label} must differ from its class pin`);
2107
2141
  }
2108
- /** Return the initial-prompt portion of a possibly multi-header snapshot. */
2142
+ /** Return the initial-prompt portion of a possibly multi-prompt snapshot. */
2109
2143
  function initialSystemPromptSnapshot(snapshot) {
2110
- const marker = snapshot.indexOf("\n<!-- request/header change ");
2111
- return marker < 0 ? snapshot : snapshot.slice(0, marker);
2144
+ return parseSystemPromptSnapshot(snapshot).initial;
2112
2145
  }
2113
2146
  /**
2114
2147
  * Count changed `request/header` snapshots in a session JSONL.
@@ -2141,6 +2174,7 @@ function surfaceEventMessage(record) {
2141
2174
  case "user/message":
2142
2175
  message = data;
2143
2176
  break;
2177
+ case "system/message":
2144
2178
  case "assistant/message":
2145
2179
  case "tool/result":
2146
2180
  message = data.message;
@@ -2458,11 +2492,34 @@ function stabilizeRefreshLog(fresh, existing, replacements, freshContext) {
2458
2492
  return records.map((record) => JSON.stringify(record)).join("\n") + "\n";
2459
2493
  }
2460
2494
  /**
2495
+ * Check every selected role for tool/path defects and current generations for canonical prompt/identity storage.
2496
+ * Historical roles retain their released bytes, including roles retired by the current writer.
2497
+ * @param dir Scenario directory containing canonical Session fixtures.
2498
+ * @param scenarioName Scenario name used in failure diagnostics.
2499
+ * @returns Resolves when all selected fixtures satisfy the storage checks.
2500
+ */
2501
+ async function assertSessionFixtureStorage(dir, scenarioName) {
2502
+ const files = await sessionFixtures(dir);
2503
+ const currentFixtures = [];
2504
+ for (const file of files) {
2505
+ const fixture = await readFile(join(dir, file), "utf8");
2506
+ const version = assertSessionFixtureVersion(file, fixture);
2507
+ expect(unknownToolCallIds(fixture), `${scenarioName}/${file} contains UNKNOWN_TOOL`).toEqual([]);
2508
+ expect(fixture, `${scenarioName}/${file} carries a non-canonical macOS cwd token`).not.toContain("/private{{cwd}}");
2509
+ if (version !== SESSION_FORMAT_VERSION) continue;
2510
+ currentFixtures.push(fixture);
2511
+ expect(scrubSystemPrompts(fixture), `${scenarioName}/${file} carries an unscrubbed system prompt`).toEqual(fixture);
2512
+ expect(scrubToolSchemas(fixture), `${scenarioName}/${file} carries unscrubbed tool schemas`).toEqual(fixture);
2513
+ expect(systemPromptPrecedesRequests(fixture), `${scenarioName}/${file} has a request/header with no preceding system/message`).toBe(true);
2514
+ }
2515
+ expect(redactSessionSnapshotIds(currentFixtures), `${scenarioName}: identity redaction fixed point`).toEqual(currentFixtures);
2516
+ }
2517
+ /**
2461
2518
  * Register the suite: one test per scenario (the expected-output and log comparisons and
2462
- * the header-uniformity guard) plus the fixture guard block (no orphan
2519
+ * the header and prompt uniformity guard) plus the fixture guard block (no orphan
2463
2520
  * scenario dirs, required files present, exactly one pin per header class,
2464
- * shared sidecars unique and well-formed, every JSONL prompt-scrubbed,
2465
- * non-pinning fixtures fully header-scrubbed). Must
2521
+ * shared sidecars unique and well-formed, every JSONL prompt- and
2522
+ * schema-scrubbed with a `system/message` before its first request). Must
2466
2523
  * run at vitest collection time — it calls `describe`/`it`. Throws
2467
2524
  * immediately if any header class lacks a pinning scenario or carries two
2468
2525
  * (the uniformity guard needs exactly one comparison anchor per class).
@@ -2481,7 +2538,12 @@ function defineAcpSnapshotSuite(options) {
2481
2538
  for (const scenario of scenarios) {
2482
2539
  if (scenariosByName.has(scenario.name)) throw new Error(`acp-snapshot: duplicate scenario name "${scenario.name}"`);
2483
2540
  scenariosByName.set(scenario.name, scenario);
2484
- for (const field of ["systemPromptSource", "toolSchemasSource"]) if (scenario[field] !== void 0 && scenario.pinsHeader !== true) throw new Error(`acp-snapshot: ${scenario.name}.${field} is only valid on a header-pinning scenario`);
2541
+ for (const field of [
2542
+ "systemPromptSource",
2543
+ "toolSchemasSource",
2544
+ "expectedHeaderChanges",
2545
+ "expectedPromptChanges"
2546
+ ]) if (scenario[field] !== void 0 && scenario.pinsHeader !== true) throw new Error(`acp-snapshot: ${scenario.name}.${field} is only valid on a header-pinning scenario`);
2485
2547
  }
2486
2548
  /** Each header class's single pinning scenario. Guarded here (and by meta-tests) so a pin cannot silently vanish or split. */
2487
2549
  const pinningByClass = /* @__PURE__ */ new Map();
@@ -2493,14 +2555,19 @@ function defineAcpSnapshotSuite(options) {
2493
2555
  pinningByClass.set(cls, scenario);
2494
2556
  }
2495
2557
  for (const scenario of scenarios) if (!pinningByClass.has(classOf(scenario))) throw new Error(`acp-snapshot: no scenario pins the request-header content of class "${classOf(scenario)}" (needed by ${scenario.name})`);
2558
+ /**
2559
+ * Resolve one pin's sidecar source. A prompt sidecar spans `1 + expectedPromptChanges`
2560
+ * prompts and a schema sidecar spans `1 + expectedHeaderChanges` schema sets, so a shared
2561
+ * source must declare the count that sizes the sidecar it lends.
2562
+ */
2496
2563
  const sourceFor = (pinningScenario, field, label) => {
2497
2564
  const sourceName = pinningScenario[field] ?? pinningScenario.name;
2498
2565
  const source = scenariosByName.get(sourceName);
2499
2566
  if (source === void 0) throw new Error(`acp-snapshot: ${pinningScenario.name} names unknown ${label} source "${sourceName}"`);
2500
2567
  if (source.pinsHeader !== true) throw new Error(`acp-snapshot: ${pinningScenario.name} names non-pinning ${label} source "${sourceName}"`);
2501
2568
  if (source[field] !== void 0 && source[field] !== source.name) throw new Error(`acp-snapshot: ${pinningScenario.name} names ${label} source "${sourceName}", which does not own its sidecar`);
2502
- const expectedChanges = pinningScenario.expectedHeaderChanges ?? 0;
2503
- if ((source.expectedHeaderChanges ?? 0) !== expectedChanges) throw new Error(`acp-snapshot: ${pinningScenario.name} and ${sourceName} declare different header-change counts for shared ${label}`);
2569
+ const countField = field === "systemPromptSource" ? "expectedPromptChanges" : "expectedHeaderChanges";
2570
+ if ((source[countField] ?? 0) !== (pinningScenario[countField] ?? 0)) throw new Error(`acp-snapshot: ${pinningScenario.name} and ${sourceName} declare different ${countField} counts for shared ${label}`);
2504
2571
  return source;
2505
2572
  };
2506
2573
  const promptSourceByClass = /* @__PURE__ */ new Map();
@@ -2524,7 +2591,7 @@ function defineAcpSnapshotSuite(options) {
2524
2591
  let fixtureFiles = RECORDING ? [] : await sessionFixtures(dir);
2525
2592
  const childFixtureFiles = fixtureFiles.slice(1);
2526
2593
  const primaryFixtureFile = fixtureFiles[0] ?? sessionFixtureName(0, 0);
2527
- const comparesLog = scenario.comparesLog ?? scenario.hasModelTurn;
2594
+ const comparesLog = scenario.comparesLog ?? (scenario.hasModelTurn && manifest.sessionFormat === void 0);
2528
2595
  const result = await runScenario(input, {
2529
2596
  agent,
2530
2597
  mode: childMode,
@@ -2561,16 +2628,14 @@ function defineAcpSnapshotSuite(options) {
2561
2628
  fixtureFiles = outputFixtureFiles;
2562
2629
  if (scenario.pinsHeader === true) {
2563
2630
  const primary = result.sessionLogs[0];
2564
- const pinningHeaders = pinningHeaderPayloads(primary.content, ctx);
2565
- const prompts = systemPromptsFrom(pinningHeaders);
2566
- expect(prompts.length, `${mode} produced no system prompt to snapshot`).toBeGreaterThan(0);
2631
+ const prompts = normalizedSystemPrompts(primary.content, ctx);
2632
+ expect(prompts.length, `${mode} produced a system prompt count that differs from 1 + expectedPromptChanges`).toBe(1 + (scenario.expectedPromptChanges ?? 0));
2567
2633
  const promptSnapshot = formatSystemPromptSnapshot(prompts[0], prompts.slice(1));
2568
2634
  const promptPath = join(snapshotsDir, (promptSourceByClass.get(classOf(scenario)) ?? scenario).name, SYSTEM_PROMPT_SNAPSHOT);
2569
2635
  claimSharedSnapshot(promptClaims, promptPath, scenario.name, promptSnapshot);
2570
2636
  await writeFile(promptPath, promptSnapshot);
2571
- const schemaSets = toolSchemasFrom(pinningHeaders);
2572
- expect(schemaSets.length, `${mode} produced no tool schemas to snapshot`).toBeGreaterThan(0);
2573
- expect(schemaSets.length, `${mode} produced a tool-schema sequence that differs from its prompt sequence`).toBe(prompts.length);
2637
+ const schemaSets = toolSchemasFrom(pinningHeaderPayloads(primary.content, ctx));
2638
+ expect(schemaSets.length, `${mode} produced a tool-schema count that differs from 1 + expectedHeaderChanges`).toBe(1 + (scenario.expectedHeaderChanges ?? 0));
2574
2639
  const toolSchemasSnapshot = formatToolSchemasSnapshot(schemaSets[0], schemaSets.slice(1));
2575
2640
  const schemaPath = join(snapshotsDir, (schemaSourceByClass.get(classOf(scenario)) ?? scenario).name, TOOL_SCHEMAS_SNAPSHOT);
2576
2641
  claimSharedSnapshot(schemaClaims, schemaPath, scenario.name, toolSchemasSnapshot);
@@ -2586,7 +2651,7 @@ function defineAcpSnapshotSuite(options) {
2586
2651
  for (const index of childPromptPins) {
2587
2652
  const log = result.sessionLogs[index];
2588
2653
  expect(log, `${mode}: no child session log at index ${index} to snapshot a prompt from`).toBeDefined();
2589
- const prompts = systemPromptsFrom(pinningHeaderPayloads(log.content, ctx));
2654
+ const prompts = normalizedSystemPrompts(log.content, ctx);
2590
2655
  expect(prompts.length, `${mode}: child ${index} produced no system prompt to snapshot`).toBeGreaterThan(0);
2591
2656
  await writeFile(join(dir, childSystemPromptSnapshot(index)), formatSystemPromptSnapshot(prompts[0]));
2592
2657
  }
@@ -2622,6 +2687,7 @@ function defineAcpSnapshotSuite(options) {
2622
2687
  const promptSnapshot = await readFile(join(snapshotsDir, promptSource.name, SYSTEM_PROMPT_SNAPSHOT), "utf8");
2623
2688
  const initialPromptSnapshot = initialSystemPromptSnapshot(promptSnapshot);
2624
2689
  expect(pinned.length, `the pinning fixture (${pinningScenario.name}) has an unexpected request/header count`).toBe(1 + (pinningScenario.expectedHeaderChanges ?? 0));
2690
+ expect(parseSystemPromptSnapshot(promptSnapshot).changes.length, `the prompt source (${promptSource.name}) has an unexpected system prompt change count`).toBe(pinningScenario.expectedPromptChanges ?? 0);
2625
2691
  const toolSchemasSnapshot = await readFile(join(snapshotsDir, schemaSource.name, TOOL_SCHEMAS_SNAPSHOT), "utf8");
2626
2692
  const toolSchemas = parseToolSchemasSnapshot(toolSchemasSnapshot);
2627
2693
  const pinnedSchemaSets = [toolSchemas.initial, ...toolSchemas.changes];
@@ -2636,14 +2702,18 @@ function defineAcpSnapshotSuite(options) {
2636
2702
  for (const index of childPromptPins) childPinnedPrompts.set(index, await readFile(join(dir, childSystemPromptSnapshot(index)), "utf8"));
2637
2703
  for (const [logIndex, log] of result.sessionLogs.entries()) {
2638
2704
  const childSchemas = childPinnedSchemas.get(logIndex);
2639
- const expectedChanges = scenario.pinsHeader === true && logIndex === 0 ? scenario.expectedHeaderChanges ?? 0 : 0;
2705
+ const pinsPrimary = scenario.pinsHeader === true && logIndex === 0;
2706
+ const expectedChanges = pinsPrimary ? scenario.expectedHeaderChanges ?? 0 : 0;
2707
+ const expectedPromptChanges = pinsPrimary ? scenario.expectedPromptChanges ?? 0 : 0;
2640
2708
  expect(headerChangeCount(log.content), `session ${log.id}: changed request/header count`).toBe(expectedChanges);
2641
- const headerEvents = normalizedHeaderEvents(scrubSystemPrompts(log.content), ctx);
2709
+ const headerEvents = normalizedHeaderEvents(log.content, ctx);
2642
2710
  const headers = headerEvents.map((event) => event.header);
2643
2711
  const prompts = normalizedSystemPrompts(log.content, ctx);
2644
- const schemaSets = normalizedToolSchemas(log.content, ctx);
2645
- expect(prompts.length, `session ${log.id}: every request/header must carry a string system prompt`).toBe(headers.length);
2646
- expect(schemaSets.length, `session ${log.id}: every request/header must carry an array-valued tools field`).toBe(headers.length);
2712
+ expect(normalizedToolSchemas(log.content, ctx).length, `session ${log.id}: every request/header must carry an array-valued tools field`).toBe(headers.length);
2713
+ if (headers.length > 0) {
2714
+ expect(systemPromptPrecedesRequests(log.content), `session ${log.id}: a system/message must precede the first request/header`).toBe(true);
2715
+ expect(prompts.length, `session ${log.id}: system/message count`).toBe(1 + expectedPromptChanges);
2716
+ }
2647
2717
  if (childSchemas !== void 0) expect(childSchemas.length, `session ${log.id}: ${childToolSchemasSnapshot(logIndex)} has an unexpected tool-schema count`).toBe(1 + headerChangeCount(log.content));
2648
2718
  let revision = 0;
2649
2719
  for (const [k, header] of headers.entries()) {
@@ -2654,17 +2724,15 @@ function defineAcpSnapshotSuite(options) {
2654
2724
  tools: childSchemas[revision]
2655
2725
  };
2656
2726
  expect(header, `session ${log.id}: request/header #${k + 1} diverged from the pinned (${pinningScenario.name}) header`).toEqual(expected);
2657
- if (expectedChanges === 0) {
2658
- const childPrompt = childPinnedPrompts.get(logIndex);
2659
- const promptOrigin = childPrompt === void 0 ? `${promptSource.name}/${SYSTEM_PROMPT_SNAPSHOT}` : childSystemPromptSnapshot(logIndex);
2660
- expect(formatSystemPromptSnapshot(prompts[k]), `session ${log.id}: initial system prompt #${k + 1} diverged from ${promptOrigin}`).toEqual(childPrompt ?? initialPromptSnapshot);
2661
- }
2662
2727
  }
2663
- if (scenario.pinsHeader === true && logIndex === 0) {
2664
- const pinningHeaders = pinningHeaderPayloads(log.content, ctx);
2665
- const pinningPrompts = systemPromptsFrom(pinningHeaders);
2666
- const pinningSchemas = toolSchemasFrom(pinningHeaders);
2667
- expect(formatSystemPromptSnapshot(pinningPrompts[0], pinningPrompts.slice(1)), `session ${log.id}: changed system prompts diverged from ${promptSource.name}/${SYSTEM_PROMPT_SNAPSHOT}`).toEqual(promptSnapshot);
2728
+ if (expectedPromptChanges === 0) {
2729
+ const childPrompt = childPinnedPrompts.get(logIndex);
2730
+ const promptOrigin = childPrompt === void 0 ? `${promptSource.name}/${SYSTEM_PROMPT_SNAPSHOT}` : childSystemPromptSnapshot(logIndex);
2731
+ for (const [k, prompt] of prompts.entries()) expect(formatSystemPromptSnapshot(prompt), `session ${log.id}: system prompt #${k + 1} diverged from ${promptOrigin}`).toEqual(childPrompt ?? initialPromptSnapshot);
2732
+ }
2733
+ if (pinsPrimary) {
2734
+ const pinningSchemas = toolSchemasFrom(pinningHeaderPayloads(log.content, ctx));
2735
+ expect(formatSystemPromptSnapshot(prompts[0], prompts.slice(1)), `session ${log.id}: changed system prompts diverged from ${promptSource.name}/${SYSTEM_PROMPT_SNAPSHOT}`).toEqual(promptSnapshot);
2668
2736
  expect(formatToolSchemasSnapshot(pinningSchemas[0], pinningSchemas.slice(1)), `session ${log.id}: changed tool schemas diverged from ${schemaSource.name}/${TOOL_SCHEMAS_SNAPSHOT}`).toEqual(toolSchemasSnapshot);
2669
2737
  }
2670
2738
  }
@@ -2729,6 +2797,8 @@ function defineAcpSnapshotSuite(options) {
2729
2797
  for (const [index, header] of headers.entries()) expect(() => restorePinnedToolSchemas(header, schemaSets[index]), `${scenario.name}: tools must use the sidecar token`).not.toThrow();
2730
2798
  expect(promptSnapshot.length, `${promptSource.name}/${SYSTEM_PROMPT_SNAPSHOT} must not be empty`).toBeGreaterThan(0);
2731
2799
  expect(promptSnapshot.endsWith("\n"), `${promptSource.name}/${SYSTEM_PROMPT_SNAPSHOT} must end in a newline`).toBe(true);
2800
+ expect(parseSystemPromptSnapshot(promptSnapshot).changes.length, `${promptSource.name}: prompt change count must match ${scenario.name}'s expectedPromptChanges`).toBe(scenario.expectedPromptChanges ?? 0);
2801
+ expect(normalizedSystemPrompts(fixture, fixtureContext(fixture)).length, `${scenario.name}: a pinning fixture must carry exactly its declared system/message count`).toBe(1 + (scenario.expectedPromptChanges ?? 0));
2732
2802
  expect(toolSchemasSnapshot, `${schemaSource.name}/${TOOL_SCHEMAS_SNAPSHOT} must use canonical JSON formatting`).toBe(formatToolSchemasSnapshot(toolSchemas.initial, toolSchemas.changes));
2733
2803
  expect(headerChangeCount(fixture), `${scenario.name}: a pinning fixture must carry exactly its declared changed headers`).toBe(scenario.expectedHeaderChanges ?? 0);
2734
2804
  }
@@ -2765,22 +2835,9 @@ function defineAcpSnapshotSuite(options) {
2765
2835
  }
2766
2836
  });
2767
2837
  it("every committed JSONL has valid tool results and canonical fixture storage", async () => {
2768
- for (const scenario of scenarios) {
2769
- const dir = join(snapshotsDir, scenario.name);
2770
- const files = await sessionFixtures(dir);
2771
- for (const file of files) {
2772
- const fixture = await readFile(join(dir, file), "utf8");
2773
- expect(unknownToolCallIds(fixture), `${scenario.name}/${file} contains UNKNOWN_TOOL`).toEqual([]);
2774
- expect(fixture, `${scenario.name}/${file} carries a non-canonical macOS cwd token`).not.toContain("/private{{cwd}}");
2775
- expect(scrubSystemPrompts(fixture), `${scenario.name}/${file} carries an unscrubbed system prompt`).toEqual(fixture);
2776
- expect(scrubToolSchemas(fixture), `${scenario.name}/${file} carries unscrubbed tool schemas`).toEqual(fixture);
2777
- if (scenario.pinsHeader !== true) expect(scrubRequestHeaders(fixture), `${scenario.name}/${file} carries unscrubbed header content`).toEqual(fixture);
2778
- }
2779
- const fixtures = await Promise.all(files.map((file) => readFile(join(dir, file), "utf8")));
2780
- expect(redactSessionSnapshotIds(fixtures), `${scenario.name}: identity redaction fixed point`).toEqual(fixtures);
2781
- }
2838
+ for (const scenario of scenarios) await assertSessionFixtureStorage(join(snapshotsDir, scenario.name), scenario.name);
2782
2839
  });
2783
2840
  });
2784
2841
  }
2785
2842
  //#endregion
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 };
2843
+ 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, parseSystemPromptSnapshot, parseToolSchemasSnapshot, persistedSessionFilename, redactSessionSnapshotIds, refreshFixtureReplacements, restorePinnedToolSchemas, runScenario, scrubModelRequestBulk, scrubSessionSnapshot, scrubSystemPrompts, scrubToolSchemas, sessionFixtureFiles, sessionFixtureName, sessionFixtureNames, sessionHeaderVersion, snapshotSpillRoot, stabilizeFixtureMessageIds, stabilizeRefreshLog, systemPromptPrecedesRequests, tokenizeSessionFixtureCwd, writerSnapshotName, writesCurrentSessionFixtures };