@deepseek-ai/dsh-session-snapshot 0.1.3-alpha.2 → 0.1.5-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/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}}";
@@ -1343,8 +1361,9 @@ function normalizeStdout(rawStdout, ctx, options = {}) {
1343
1361
  /**
1344
1362
  * Normalize a session JSONL log into a stable expected output: the header line's
1345
1363
  * 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
1364
+ * historical packed-row, embedded Assistant-stream, goal lifecycle, and
1365
+ * catalog child-creation clocks are zeroed; and all volatile strings are
1366
+ * scrubbed. Projected inputs remain
1348
1367
  * projected. Packed `data.dt` gaps are normalized even when the projected row
1349
1368
  * omits its `time0` anchor.
1350
1369
  * Output is JSONL in the same shape as the input — one compact record per
@@ -1387,12 +1406,16 @@ function normalizeSessionLog(rawLog, ctx, options = {}) {
1387
1406
  if ("createdAt" in data) data.createdAt = 0;
1388
1407
  if ("updatedAt" in data) data.updatedAt = 0;
1389
1408
  }
1409
+ if (record.type === "subagent/catalog" && record.data !== null && typeof record.data === "object") {
1410
+ const data = record.data;
1411
+ if ("childCreatedAt" in data) data.childCreatedAt = 0;
1412
+ }
1390
1413
  if (Object.hasOwn(record, "sourceEventSeqs")) record.sourceEventSeqs = decodeSeqRanges(record.sourceEventSeqs);
1391
1414
  return scrubValue(record, ctx, cwdPathMode, identityMode);
1392
1415
  }).map((r) => JSON.stringify(r)).join("\n") + "\n";
1393
1416
  }
1394
1417
  /**
1395
- * Canonicalize projected v2 body records. Compact streams are nested event data,
1418
+ * Canonicalize projected v3 body records. Compact streams are nested event data,
1396
1419
  * so persistence flush boundaries cannot change the row layout.
1397
1420
  */
1398
1421
  function projectSessionSnapshot(rawLog) {
@@ -1400,9 +1423,9 @@ function projectSessionSnapshot(rawLog) {
1400
1423
  return [
1401
1424
  lines.shift(),
1402
1425
  ...lines.map((line) => {
1403
- const projected = { ...JSON.parse(line) };
1404
- omitFixtureEnvelope(projected);
1405
- return JSON.stringify(projected);
1426
+ const record = JSON.parse(line);
1427
+ omitFixtureEnvelope(record);
1428
+ return JSON.stringify(record);
1406
1429
  }),
1407
1430
  ""
1408
1431
  ].join("\n");
@@ -1410,8 +1433,9 @@ function projectSessionSnapshot(rawLog) {
1410
1433
  /**
1411
1434
  * Normalize and project persisted session JSONL for a committed fixture.
1412
1435
  * This composes ordinary log normalization with request-header scrubbing and
1413
- * persistence-envelope projection, then writes the v2 logical event stream as
1414
- * one record per event, independent of persistence flush boundaries.
1436
+ * persistence-envelope projection, then writes the v3 logical event stream as
1437
+ * one record per event, independent of persistence flush boundaries. Event order
1438
+ * and source-event references are preserved.
1415
1439
  *
1416
1440
  * @param rawLog - persisted or already-projected session JSONL.
1417
1441
  * @param ctx - the run's volatile values to scrub.
@@ -1438,51 +1462,20 @@ function normalizeSessionSnapshots(rawLogs, ctx, options = {}) {
1438
1462
  }))));
1439
1463
  }
1440
1464
  /**
1441
- * Omit only generation-qualified operational provenance from expected-output comparison.
1465
+ * Omit the artifact header generation after official migration for comparison.
1466
+ * Delivery and captured-source generations retain their opaque recorded values.
1442
1467
  * @param rawLog - Session records or events as compact JSON lines.
1443
- * @returns the same records without delivery or captured-source generation qualifiers.
1468
+ * @returns the same records with only the Session header version omitted.
1444
1469
  */
1445
1470
  function normalizeSessionFormatProvenance(rawLog) {
1446
1471
  return rawLog.split("\n").map((line) => {
1447
1472
  if (line.trim().length === 0) return line;
1448
1473
  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;
1474
+ if (record.type !== "session" || !Object.hasOwn(record, "version")) return line;
1475
+ delete record.version;
1476
+ return JSON.stringify(record);
1463
1477
  }).join("\n");
1464
1478
  }
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
1479
  /** Whether a fixture declares a released Session format and therefore participates in migration burn-in. */
1487
1480
  function hasSessionFormatVersion(rawLog) {
1488
1481
  const firstLine = rawLog.split(/\r?\n/).find((line) => line.trim().length > 0);
@@ -1492,60 +1485,58 @@ function hasSessionFormatVersion(rawLog) {
1492
1485
  return Object.hasOwn(header, "version");
1493
1486
  }
1494
1487
  /**
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.
1488
+ * Replace the rendered prompt text of every `system/message` event with the
1489
+ * `{{system}}` token. The text block keeps its position and type, so the
1490
+ * fixture still shows one system node per prompt version; an empty `content`
1491
+ * (no system prompt) stays empty. Request headers and every other line pass
1492
+ * through byte-for-byte; the transform is idempotent.
1501
1493
  *
1502
1494
  * @param rawLog The raw session `.jsonl` content.
1503
- * @returns The JSONL with system-prompt content tokenized.
1495
+ * @returns The JSONL with system-prompt text tokenized.
1504
1496
  */
1505
1497
  function scrubSystemPrompts(rawLog) {
1506
- return scrubHeaderContent(rawLog, { system: true });
1498
+ return scrubModelRequestContent(rawLog, { system: true });
1507
1499
  }
1508
1500
  /**
1509
1501
  * 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.
1502
+ * tokens while retaining field presence. System-prompt text stays verbatim so
1503
+ * pinning fixtures can move only schema bulk into their dedicated JSON
1504
+ * sidecar. Lines without a tool payload pass through byte-for-byte; the
1505
+ * transform is idempotent.
1514
1506
  *
1515
1507
  * @param rawLog The raw session `.jsonl` content.
1516
1508
  * @returns The JSONL with tool-schema content tokenized.
1517
1509
  */
1518
1510
  function scrubToolSchemas(rawLog) {
1519
- return scrubHeaderContent(rawLog, { tools: true });
1511
+ return scrubModelRequestContent(rawLog, { tools: true });
1520
1512
  }
1521
1513
  /**
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.
1514
+ * Replace all bulky model-request content in a session JSONL with stable
1515
+ * tokens: the `system/message` prompt text handled by
1516
+ * {@link scrubSystemPrompts} and the request-header tool schemas handled by
1517
+ * {@link scrubToolSchemas}. Field presence, config, and reason are kept.
1518
+ * Lines without content to scrub pass through byte-for-byte, and the
1519
+ * transform is idempotent.
1528
1520
  *
1529
1521
  * @param rawLog The raw session `.jsonl` content.
1530
- * @returns The JSONL with all header bulk tokenized, other lines byte-identical.
1522
+ * @returns The JSONL with prompt text and schema bulk tokenized, other lines byte-identical.
1531
1523
  */
1532
- function scrubRequestHeaders(rawLog) {
1533
- return scrubHeaderContent(rawLog, {
1524
+ function scrubModelRequestBulk(rawLog) {
1525
+ return scrubModelRequestContent(rawLog, {
1534
1526
  system: true,
1535
1527
  tools: true
1536
1528
  });
1537
1529
  }
1538
1530
  /**
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.
1531
+ * Project a persisted session log while tokenizing prompt text and schema
1532
+ * bulk. Each non-empty line is parsed at most once; the session header stays
1533
+ * byte-identical. Body records omit their persistence-only envelopes.
1543
1534
  *
1544
1535
  * @param rawLog - persisted or already-projected session JSONL.
1545
- * @returns committed snapshot JSONL with request headers tokenized.
1536
+ * @returns committed snapshot JSONL with prompt text and tool schemas tokenized.
1546
1537
  */
1547
1538
  function scrubSessionSnapshot(rawLog) {
1548
- const scrubbed = scrubRequestHeaders(rawLog);
1539
+ const scrubbed = scrubModelRequestBulk(rawLog);
1549
1540
  let recordIndex = 0;
1550
1541
  return scrubbed.split("\n").map((line) => {
1551
1542
  if (line.trim().length === 0) return line;
@@ -1568,26 +1559,31 @@ function normalizeFeedbackClocks(record) {
1568
1559
  if ("createdAt" in clocks) clocks.createdAt = 0;
1569
1560
  if ("updatedAt" in clocks) clocks.updatedAt = 0;
1570
1561
  }
1571
- /** Transform the selected request-header payloads. */
1572
- function scrubHeaderContent(rawLog, options) {
1562
+ /** Return the first text block of a `system/message` payload, or `undefined` when it carries none. */
1563
+ function systemPromptBlock(data) {
1564
+ const message = data.message;
1565
+ if (message === null || typeof message !== "object" || !Array.isArray(message.content)) return void 0;
1566
+ const block = message.content[0];
1567
+ return block !== null && typeof block === "object" && typeof block.text === "string" ? block : void 0;
1568
+ }
1569
+ /** Transform the selected model-request payloads. */
1570
+ function scrubModelRequestContent(rawLog, options) {
1573
1571
  return rawLog.split("\n").map((line) => {
1574
1572
  if (line.trim().length === 0) return line;
1575
1573
  const record = JSON.parse(line);
1576
1574
  const data = record.data;
1577
1575
  if (data === null || typeof data !== "object") return line;
1578
- if (record.type === "request/header") {
1576
+ if (options.system === true && record.type === "system/message") {
1577
+ const block = systemPromptBlock(data);
1578
+ if (block === void 0) return line;
1579
+ block.text = SYSTEM;
1580
+ return JSON.stringify(record);
1581
+ }
1582
+ if (options.tools === true && record.type === "request/header") {
1579
1583
  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;
1584
+ if (header === null || typeof header !== "object" || !("tools" in header)) return line;
1585
+ header.tools = TOOLS;
1586
+ return JSON.stringify(record);
1591
1587
  }
1592
1588
  return line;
1593
1589
  }).join("\n");
@@ -1698,10 +1694,11 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
1698
1694
  "toolSchemasSource",
1699
1695
  "childSystemPrompts",
1700
1696
  "childToolSchemas",
1701
- "changes"
1697
+ "changes",
1698
+ "promptChanges"
1702
1699
  ], "manifest.header");
1703
1700
  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");
1701
+ 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
1702
  header = {
1706
1703
  class: name(value.class, "manifest.header.class"),
1707
1704
  ...value.pin === true ? { pin: true } : {},
@@ -1709,7 +1706,8 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
1709
1706
  ...value.toolSchemasSource === void 0 ? {} : { toolSchemasSource: scenarioSource(value.toolSchemasSource, "manifest.header.toolSchemasSource") },
1710
1707
  ...value.childSystemPrompts === void 0 ? {} : { childSystemPrompts: positiveIndexes(value.childSystemPrompts, "manifest.header.childSystemPrompts") },
1711
1708
  ...value.childToolSchemas === void 0 ? {} : { childToolSchemas: positiveIndexes(value.childToolSchemas, "manifest.header.childToolSchemas") },
1712
- ...value.changes === void 0 ? {} : { changes: Number(value.changes) }
1709
+ ...value.changes === void 0 ? {} : { changes: Number(value.changes) },
1710
+ ...value.promptChanges === void 0 ? {} : { promptChanges: Number(value.promptChanges) }
1713
1711
  };
1714
1712
  }
1715
1713
  let replay;
@@ -1744,11 +1742,11 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
1744
1742
  "parent"
1745
1743
  ], "manifest.workspace");
1746
1744
  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");
1745
+ if (value.parent !== void 0 && value.parent !== "outside-temp") throw new Error("manifest.workspace.parent must equal outside-temp");
1748
1746
  workspace = {
1749
1747
  ...value.setup === void 0 ? {} : { setup: name(value.setup, "manifest.workspace.setup") },
1750
1748
  ...value.final === true ? { final: true } : {},
1751
- ...value.parent === "home" ? { parent: "home" } : {}
1749
+ ...value.parent === "outside-temp" ? { parent: "outside-temp" } : {}
1752
1750
  };
1753
1751
  if (Object.keys(workspace).length === 0) throw new Error("manifest.workspace must not be empty");
1754
1752
  }
@@ -1838,9 +1836,10 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
1838
1836
  * refresh stay serial while writing.
1839
1837
  *
1840
1838
  * 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
1839
+ * sequence and the tokenized `system/message` sequence. Its prompt and
1840
+ * tool-schema sequences live in independent sidecars, each of which may be
1841
+ * shared with another class pin when the bytes are identical. Every live
1842
+ * header and system prompt is checked against the composed pin, so
1844
1843
  * session-dependent composition must declare a separate class instead of
1845
1844
  * escaping coverage.
1846
1845
  * @module @deepseek-ai/dsh-session-snapshot/suite
@@ -1965,10 +1964,13 @@ function fixtureContext(fixture) {
1965
1964
  }
1966
1965
  /** Normalize request-header payloads while retaining the reason that selects a pin revision. */
1967
1966
  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
- }));
1967
+ return parseJsonlRecords(normalizeSessionLog(rawLog, ctx)).filter((record) => record.type === "request/header").map((record) => {
1968
+ const data = record.data;
1969
+ return {
1970
+ header: data?.header,
1971
+ reason: data?.reason
1972
+ };
1973
+ });
1972
1974
  }
1973
1975
  /**
1974
1976
  * Header revisions that own sidecar content. `series` reuses the current revision, while
@@ -1979,13 +1981,17 @@ function normalizedHeaderEvents(rawLog, ctx) {
1979
1981
  function pinningHeaderPayloads(rawLog, ctx) {
1980
1982
  return normalizedHeaderEvents(rawLog, ctx).filter((event) => event.reason !== "series").map((event) => event.header);
1981
1983
  }
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
- });
1984
+ /**
1985
+ * The rendered prompt text of one parsed `system/message` record: its single
1986
+ * text block, or `''` when the empty `content` records "no system prompt".
1987
+ * Any other record yields `undefined`.
1988
+ */
1989
+ function systemPromptOf(record) {
1990
+ if (record.type !== "system/message") return void 0;
1991
+ const content = record.data?.message?.content;
1992
+ if (!Array.isArray(content)) return void 0;
1993
+ const block = content[0];
1994
+ return content.length === 0 ? "" : typeof block?.text === "string" ? block.text : void 0;
1989
1995
  }
1990
1996
  /** Extract every array-valued tool catalog from a normalized header sequence. */
1991
1997
  function toolSchemasFrom(headers) {
@@ -1999,8 +2005,7 @@ function toolSchemasFrom(headers) {
1999
2005
  * The `data.header` payload of every `request/header` event in a session
2000
2006
  * JSONL, in log order, with the log's volatile values scrubbed first
2001
2007
  * ({@link normalizeSessionLog}) so headers harvested from different runs —
2002
- * each embedding its own generated cwd in the composed prompt — compare on equal
2003
- * footing.
2008
+ * each embedding its own generated cwd — compare on equal footing.
2004
2009
  *
2005
2010
  * @param rawLog The session `.jsonl` content to extract headers from.
2006
2011
  * @param ctx The volatile values of the run that produced it.
@@ -2010,16 +2015,35 @@ function normalizedHeaders(rawLog, ctx) {
2010
2015
  return normalizedHeaderEvents(rawLog, ctx).map((event) => event.header);
2011
2016
  }
2012
2017
  /**
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.
2018
+ * The normalized prompt text of every `system/message` event in a session
2019
+ * JSONL, in log order: the first is the initial system prompt (surface node 0)
2020
+ * and each later one replaced it or, on an in-history route, appended the
2021
+ * changed prompt after the cached history. An empty `content` yields `''`; a
2022
+ * `system/message` without a text block is omitted.
2016
2023
  *
2017
2024
  * @param rawLog The session `.jsonl` content to inspect.
2018
2025
  * @param ctx The volatile values of the run that produced it.
2019
- * @returns The normalized system prompts, in header order.
2026
+ * @returns The normalized system prompts, in log order.
2020
2027
  */
2021
2028
  function normalizedSystemPrompts(rawLog, ctx) {
2022
- return systemPromptsFrom(normalizedHeaders(rawLog, ctx));
2029
+ return parseJsonlRecords(normalizeSessionLog(rawLog, ctx)).flatMap((record) => {
2030
+ const prompt = systemPromptOf(record);
2031
+ return prompt === void 0 ? [] : [prompt];
2032
+ });
2033
+ }
2034
+ /**
2035
+ * Whether every model request in a session JSONL is preceded by its system
2036
+ * prompt: the log has no `request/header`, or a `system/message` event comes
2037
+ * before its first `request/header`.
2038
+ *
2039
+ * @param rawLog The session `.jsonl` content to inspect.
2040
+ * @returns True when the first `request/header` (if any) follows a `system/message`.
2041
+ */
2042
+ function systemPromptPrecedesRequests(rawLog) {
2043
+ const types = parseJsonlRecords(rawLog).map((record) => record.type);
2044
+ const firstHeader = types.indexOf("request/header");
2045
+ const firstPrompt = types.indexOf("system/message");
2046
+ return firstHeader < 0 || firstPrompt >= 0 && firstPrompt < firstHeader;
2023
2047
  }
2024
2048
  /**
2025
2049
  * The normalized tool-schema arrays carried by request headers in a session
@@ -2077,24 +2101,40 @@ function restorePinnedToolSchemas(header, schemas) {
2077
2101
  tools: schemas
2078
2102
  };
2079
2103
  }
2104
+ /** Marker line separating one replaced prompt from the previous one in a prompt sidecar. */
2105
+ const SYSTEM_PROMPT_CHANGE_MARKER = "\n<!-- system/message change ";
2080
2106
  /**
2081
2107
  * Render a normalized prompt as a repository-friendly Markdown snapshot.
2082
2108
  * Prompt text is unchanged except that a missing terminal newline is added so
2083
2109
  * the committed file follows the repository newline contract.
2084
2110
  *
2085
- * @param prompt The normalized system prompt.
2086
- * @param changes Full normalized prompts from later changed-header snapshots.
2111
+ * @param prompt The normalized initial system prompt (surface node 0).
2112
+ * @param changes Full normalized prompts from later `system/message` events, replacements or in-history appends.
2087
2113
  * @returns Markdown snapshot text ending in a newline.
2088
2114
  */
2089
2115
  function formatSystemPromptSnapshot(prompt, changes = []) {
2090
2116
  let snapshot = prompt.endsWith("\n") ? prompt : `${prompt}\n`;
2091
2117
  for (const [index, change] of changes.entries()) {
2092
- snapshot += `\n<!-- request/header change ${index + 1} -->\n\n`;
2118
+ snapshot += `${SYSTEM_PROMPT_CHANGE_MARKER}${index + 1} -->\n\n`;
2093
2119
  snapshot += change.endsWith("\n") ? change : `${change}\n`;
2094
2120
  }
2095
2121
  return snapshot;
2096
2122
  }
2097
2123
  /**
2124
+ * Split a prompt sidecar into its initial prompt and each later change, the
2125
+ * inverse of {@link formatSystemPromptSnapshot}.
2126
+ *
2127
+ * @param snapshot The Markdown sidecar text.
2128
+ * @returns The initial prompt snapshot plus one entry per later `system/message`.
2129
+ */
2130
+ function parseSystemPromptSnapshot(snapshot) {
2131
+ const parts = snapshot.split(/\n<!-- system\/message change [1-9]\d* -->\n\n/);
2132
+ return {
2133
+ initial: parts[0],
2134
+ changes: parts.slice(1)
2135
+ };
2136
+ }
2137
+ /**
2098
2138
  * Reject a child prompt sidecar that cannot own distinct, canonical prompt text.
2099
2139
  * @param sidecar - committed child prompt snapshot.
2100
2140
  * @param classPin - initial prompt snapshot owned by the scenario's header class.
@@ -2105,10 +2145,9 @@ function assertChildSystemPromptSnapshot(sidecar, classPin, label) {
2105
2145
  if (!sidecar.endsWith("\n")) throw new Error(`${label} must end in a newline`);
2106
2146
  if (sidecar === classPin) throw new Error(`${label} must differ from its class pin`);
2107
2147
  }
2108
- /** Return the initial-prompt portion of a possibly multi-header snapshot. */
2148
+ /** Return the initial-prompt portion of a possibly multi-prompt snapshot. */
2109
2149
  function initialSystemPromptSnapshot(snapshot) {
2110
- const marker = snapshot.indexOf("\n<!-- request/header change ");
2111
- return marker < 0 ? snapshot : snapshot.slice(0, marker);
2150
+ return parseSystemPromptSnapshot(snapshot).initial;
2112
2151
  }
2113
2152
  /**
2114
2153
  * Count changed `request/header` snapshots in a session JSONL.
@@ -2141,6 +2180,7 @@ function surfaceEventMessage(record) {
2141
2180
  case "user/message":
2142
2181
  message = data;
2143
2182
  break;
2183
+ case "system/message":
2144
2184
  case "assistant/message":
2145
2185
  case "tool/result":
2146
2186
  message = data.message;
@@ -2458,11 +2498,34 @@ function stabilizeRefreshLog(fresh, existing, replacements, freshContext) {
2458
2498
  return records.map((record) => JSON.stringify(record)).join("\n") + "\n";
2459
2499
  }
2460
2500
  /**
2501
+ * Check every selected role for tool/path defects and current generations for canonical prompt/identity storage.
2502
+ * Historical roles retain their released bytes, including roles retired by the current writer.
2503
+ * @param dir Scenario directory containing canonical Session fixtures.
2504
+ * @param scenarioName Scenario name used in failure diagnostics.
2505
+ * @returns Resolves when all selected fixtures satisfy the storage checks.
2506
+ */
2507
+ async function assertSessionFixtureStorage(dir, scenarioName) {
2508
+ const files = await sessionFixtures(dir);
2509
+ const currentFixtures = [];
2510
+ for (const file of files) {
2511
+ const fixture = await readFile(join(dir, file), "utf8");
2512
+ const version = assertSessionFixtureVersion(file, fixture);
2513
+ expect(unknownToolCallIds(fixture), `${scenarioName}/${file} contains UNKNOWN_TOOL`).toEqual([]);
2514
+ expect(fixture, `${scenarioName}/${file} carries a non-canonical macOS cwd token`).not.toContain("/private{{cwd}}");
2515
+ if (version !== SESSION_FORMAT_VERSION) continue;
2516
+ currentFixtures.push(fixture);
2517
+ expect(scrubSystemPrompts(fixture), `${scenarioName}/${file} carries an unscrubbed system prompt`).toEqual(fixture);
2518
+ expect(scrubToolSchemas(fixture), `${scenarioName}/${file} carries unscrubbed tool schemas`).toEqual(fixture);
2519
+ expect(systemPromptPrecedesRequests(fixture), `${scenarioName}/${file} has a request/header with no preceding system/message`).toBe(true);
2520
+ }
2521
+ expect(redactSessionSnapshotIds(currentFixtures), `${scenarioName}: identity redaction fixed point`).toEqual(currentFixtures);
2522
+ }
2523
+ /**
2461
2524
  * 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
2525
+ * the header and prompt uniformity guard) plus the fixture guard block (no orphan
2463
2526
  * 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
2527
+ * shared sidecars unique and well-formed, every JSONL prompt- and
2528
+ * schema-scrubbed with a `system/message` before its first request). Must
2466
2529
  * run at vitest collection time — it calls `describe`/`it`. Throws
2467
2530
  * immediately if any header class lacks a pinning scenario or carries two
2468
2531
  * (the uniformity guard needs exactly one comparison anchor per class).
@@ -2481,7 +2544,12 @@ function defineAcpSnapshotSuite(options) {
2481
2544
  for (const scenario of scenarios) {
2482
2545
  if (scenariosByName.has(scenario.name)) throw new Error(`acp-snapshot: duplicate scenario name "${scenario.name}"`);
2483
2546
  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`);
2547
+ for (const field of [
2548
+ "systemPromptSource",
2549
+ "toolSchemasSource",
2550
+ "expectedHeaderChanges",
2551
+ "expectedPromptChanges"
2552
+ ]) 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
2553
  }
2486
2554
  /** Each header class's single pinning scenario. Guarded here (and by meta-tests) so a pin cannot silently vanish or split. */
2487
2555
  const pinningByClass = /* @__PURE__ */ new Map();
@@ -2493,14 +2561,19 @@ function defineAcpSnapshotSuite(options) {
2493
2561
  pinningByClass.set(cls, scenario);
2494
2562
  }
2495
2563
  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})`);
2564
+ /**
2565
+ * Resolve one pin's sidecar source. A prompt sidecar spans `1 + expectedPromptChanges`
2566
+ * prompts and a schema sidecar spans `1 + expectedHeaderChanges` schema sets, so a shared
2567
+ * source must declare the count that sizes the sidecar it lends.
2568
+ */
2496
2569
  const sourceFor = (pinningScenario, field, label) => {
2497
2570
  const sourceName = pinningScenario[field] ?? pinningScenario.name;
2498
2571
  const source = scenariosByName.get(sourceName);
2499
2572
  if (source === void 0) throw new Error(`acp-snapshot: ${pinningScenario.name} names unknown ${label} source "${sourceName}"`);
2500
2573
  if (source.pinsHeader !== true) throw new Error(`acp-snapshot: ${pinningScenario.name} names non-pinning ${label} source "${sourceName}"`);
2501
2574
  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}`);
2575
+ const countField = field === "systemPromptSource" ? "expectedPromptChanges" : "expectedHeaderChanges";
2576
+ if ((source[countField] ?? 0) !== (pinningScenario[countField] ?? 0)) throw new Error(`acp-snapshot: ${pinningScenario.name} and ${sourceName} declare different ${countField} counts for shared ${label}`);
2504
2577
  return source;
2505
2578
  };
2506
2579
  const promptSourceByClass = /* @__PURE__ */ new Map();
@@ -2524,7 +2597,7 @@ function defineAcpSnapshotSuite(options) {
2524
2597
  let fixtureFiles = RECORDING ? [] : await sessionFixtures(dir);
2525
2598
  const childFixtureFiles = fixtureFiles.slice(1);
2526
2599
  const primaryFixtureFile = fixtureFiles[0] ?? sessionFixtureName(0, 0);
2527
- const comparesLog = scenario.comparesLog ?? scenario.hasModelTurn;
2600
+ const comparesLog = scenario.comparesLog ?? (scenario.hasModelTurn && manifest.sessionFormat === void 0);
2528
2601
  const result = await runScenario(input, {
2529
2602
  agent,
2530
2603
  mode: childMode,
@@ -2561,16 +2634,14 @@ function defineAcpSnapshotSuite(options) {
2561
2634
  fixtureFiles = outputFixtureFiles;
2562
2635
  if (scenario.pinsHeader === true) {
2563
2636
  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);
2637
+ const prompts = normalizedSystemPrompts(primary.content, ctx);
2638
+ expect(prompts.length, `${mode} produced a system prompt count that differs from 1 + expectedPromptChanges`).toBe(1 + (scenario.expectedPromptChanges ?? 0));
2567
2639
  const promptSnapshot = formatSystemPromptSnapshot(prompts[0], prompts.slice(1));
2568
2640
  const promptPath = join(snapshotsDir, (promptSourceByClass.get(classOf(scenario)) ?? scenario).name, SYSTEM_PROMPT_SNAPSHOT);
2569
2641
  claimSharedSnapshot(promptClaims, promptPath, scenario.name, promptSnapshot);
2570
2642
  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);
2643
+ const schemaSets = toolSchemasFrom(pinningHeaderPayloads(primary.content, ctx));
2644
+ expect(schemaSets.length, `${mode} produced a tool-schema count that differs from 1 + expectedHeaderChanges`).toBe(1 + (scenario.expectedHeaderChanges ?? 0));
2574
2645
  const toolSchemasSnapshot = formatToolSchemasSnapshot(schemaSets[0], schemaSets.slice(1));
2575
2646
  const schemaPath = join(snapshotsDir, (schemaSourceByClass.get(classOf(scenario)) ?? scenario).name, TOOL_SCHEMAS_SNAPSHOT);
2576
2647
  claimSharedSnapshot(schemaClaims, schemaPath, scenario.name, toolSchemasSnapshot);
@@ -2586,7 +2657,7 @@ function defineAcpSnapshotSuite(options) {
2586
2657
  for (const index of childPromptPins) {
2587
2658
  const log = result.sessionLogs[index];
2588
2659
  expect(log, `${mode}: no child session log at index ${index} to snapshot a prompt from`).toBeDefined();
2589
- const prompts = systemPromptsFrom(pinningHeaderPayloads(log.content, ctx));
2660
+ const prompts = normalizedSystemPrompts(log.content, ctx);
2590
2661
  expect(prompts.length, `${mode}: child ${index} produced no system prompt to snapshot`).toBeGreaterThan(0);
2591
2662
  await writeFile(join(dir, childSystemPromptSnapshot(index)), formatSystemPromptSnapshot(prompts[0]));
2592
2663
  }
@@ -2622,6 +2693,7 @@ function defineAcpSnapshotSuite(options) {
2622
2693
  const promptSnapshot = await readFile(join(snapshotsDir, promptSource.name, SYSTEM_PROMPT_SNAPSHOT), "utf8");
2623
2694
  const initialPromptSnapshot = initialSystemPromptSnapshot(promptSnapshot);
2624
2695
  expect(pinned.length, `the pinning fixture (${pinningScenario.name}) has an unexpected request/header count`).toBe(1 + (pinningScenario.expectedHeaderChanges ?? 0));
2696
+ expect(parseSystemPromptSnapshot(promptSnapshot).changes.length, `the prompt source (${promptSource.name}) has an unexpected system prompt change count`).toBe(pinningScenario.expectedPromptChanges ?? 0);
2625
2697
  const toolSchemasSnapshot = await readFile(join(snapshotsDir, schemaSource.name, TOOL_SCHEMAS_SNAPSHOT), "utf8");
2626
2698
  const toolSchemas = parseToolSchemasSnapshot(toolSchemasSnapshot);
2627
2699
  const pinnedSchemaSets = [toolSchemas.initial, ...toolSchemas.changes];
@@ -2636,14 +2708,18 @@ function defineAcpSnapshotSuite(options) {
2636
2708
  for (const index of childPromptPins) childPinnedPrompts.set(index, await readFile(join(dir, childSystemPromptSnapshot(index)), "utf8"));
2637
2709
  for (const [logIndex, log] of result.sessionLogs.entries()) {
2638
2710
  const childSchemas = childPinnedSchemas.get(logIndex);
2639
- const expectedChanges = scenario.pinsHeader === true && logIndex === 0 ? scenario.expectedHeaderChanges ?? 0 : 0;
2711
+ const pinsPrimary = scenario.pinsHeader === true && logIndex === 0;
2712
+ const expectedChanges = pinsPrimary ? scenario.expectedHeaderChanges ?? 0 : 0;
2713
+ const expectedPromptChanges = pinsPrimary ? scenario.expectedPromptChanges ?? 0 : 0;
2640
2714
  expect(headerChangeCount(log.content), `session ${log.id}: changed request/header count`).toBe(expectedChanges);
2641
- const headerEvents = normalizedHeaderEvents(scrubSystemPrompts(log.content), ctx);
2715
+ const headerEvents = normalizedHeaderEvents(log.content, ctx);
2642
2716
  const headers = headerEvents.map((event) => event.header);
2643
2717
  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);
2718
+ expect(normalizedToolSchemas(log.content, ctx).length, `session ${log.id}: every request/header must carry an array-valued tools field`).toBe(headers.length);
2719
+ if (headers.length > 0) {
2720
+ expect(systemPromptPrecedesRequests(log.content), `session ${log.id}: a system/message must precede the first request/header`).toBe(true);
2721
+ expect(prompts.length, `session ${log.id}: system/message count`).toBe(1 + expectedPromptChanges);
2722
+ }
2647
2723
  if (childSchemas !== void 0) expect(childSchemas.length, `session ${log.id}: ${childToolSchemasSnapshot(logIndex)} has an unexpected tool-schema count`).toBe(1 + headerChangeCount(log.content));
2648
2724
  let revision = 0;
2649
2725
  for (const [k, header] of headers.entries()) {
@@ -2654,17 +2730,15 @@ function defineAcpSnapshotSuite(options) {
2654
2730
  tools: childSchemas[revision]
2655
2731
  };
2656
2732
  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
2733
  }
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);
2734
+ if (expectedPromptChanges === 0) {
2735
+ const childPrompt = childPinnedPrompts.get(logIndex);
2736
+ const promptOrigin = childPrompt === void 0 ? `${promptSource.name}/${SYSTEM_PROMPT_SNAPSHOT}` : childSystemPromptSnapshot(logIndex);
2737
+ for (const [k, prompt] of prompts.entries()) expect(formatSystemPromptSnapshot(prompt), `session ${log.id}: system prompt #${k + 1} diverged from ${promptOrigin}`).toEqual(childPrompt ?? initialPromptSnapshot);
2738
+ }
2739
+ if (pinsPrimary) {
2740
+ const pinningSchemas = toolSchemasFrom(pinningHeaderPayloads(log.content, ctx));
2741
+ expect(formatSystemPromptSnapshot(prompts[0], prompts.slice(1)), `session ${log.id}: changed system prompts diverged from ${promptSource.name}/${SYSTEM_PROMPT_SNAPSHOT}`).toEqual(promptSnapshot);
2668
2742
  expect(formatToolSchemasSnapshot(pinningSchemas[0], pinningSchemas.slice(1)), `session ${log.id}: changed tool schemas diverged from ${schemaSource.name}/${TOOL_SCHEMAS_SNAPSHOT}`).toEqual(toolSchemasSnapshot);
2669
2743
  }
2670
2744
  }
@@ -2729,6 +2803,8 @@ function defineAcpSnapshotSuite(options) {
2729
2803
  for (const [index, header] of headers.entries()) expect(() => restorePinnedToolSchemas(header, schemaSets[index]), `${scenario.name}: tools must use the sidecar token`).not.toThrow();
2730
2804
  expect(promptSnapshot.length, `${promptSource.name}/${SYSTEM_PROMPT_SNAPSHOT} must not be empty`).toBeGreaterThan(0);
2731
2805
  expect(promptSnapshot.endsWith("\n"), `${promptSource.name}/${SYSTEM_PROMPT_SNAPSHOT} must end in a newline`).toBe(true);
2806
+ expect(parseSystemPromptSnapshot(promptSnapshot).changes.length, `${promptSource.name}: prompt change count must match ${scenario.name}'s expectedPromptChanges`).toBe(scenario.expectedPromptChanges ?? 0);
2807
+ 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
2808
  expect(toolSchemasSnapshot, `${schemaSource.name}/${TOOL_SCHEMAS_SNAPSHOT} must use canonical JSON formatting`).toBe(formatToolSchemasSnapshot(toolSchemas.initial, toolSchemas.changes));
2733
2809
  expect(headerChangeCount(fixture), `${scenario.name}: a pinning fixture must carry exactly its declared changed headers`).toBe(scenario.expectedHeaderChanges ?? 0);
2734
2810
  }
@@ -2765,22 +2841,9 @@ function defineAcpSnapshotSuite(options) {
2765
2841
  }
2766
2842
  });
2767
2843
  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
- }
2844
+ for (const scenario of scenarios) await assertSessionFixtureStorage(join(snapshotsDir, scenario.name), scenario.name);
2782
2845
  });
2783
2846
  });
2784
2847
  }
2785
2848
  //#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 };
2849
+ 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 };