@deepseek-ai/dsh-session-snapshot 0.2.0-rc.2 → 0.2.1-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -18,11 +18,11 @@
18
18
  en: 594ad1f3e6f02054
19
19
  zh: 7ca4f565976704be
20
20
  /deepseek-ai-dsh-session-snapshot/use-this-package/writing-a-snapshot-suite:
21
- en: b753e44790d4df99
22
- zh: 5547c21757797d9b
21
+ en: 57ff7c89e7696ee8
22
+ zh: b885ac1b4edc8c56
23
23
  /deepseek-ai-dsh-session-snapshot/use-this-package/record-replay-and-refresh:
24
- en: 4c4b1d6a459d2dc6
25
- zh: 10c1bb216c50fea8
24
+ en: 2bea67c686715289
25
+ zh: aecc6ea7b4ae5623
26
26
  /deepseek-ai-dsh-session-snapshot/use-this-package/pinning-request-headers-and-system-prompts:
27
27
  en: 6b11d5111e1a3222
28
28
  zh: de4d3889b1a5ebb4
@@ -30,8 +30,8 @@
30
30
  en: 64666ea2894acc4a
31
31
  zh: 0e78df32bcfd3633
32
32
  /deepseek-ai-dsh-session-snapshot/use-this-package/what-can-go-wrong:
33
- en: f9dc0b285401e01e
34
- zh: e2b365b082dbaa82
33
+ en: 0b03592947ca43d6
34
+ zh: b14a64238c4b5f80
35
35
  /deepseek-ai-dsh-session-snapshot/understand-the-implementation:
36
36
  en: 18ff76b440fe40da
37
37
  zh: 4dbf2eb5f9ee98a2
@@ -39,8 +39,8 @@
39
39
  en: 8771607f15c4f4e1
40
40
  zh: 7c2cf395f704717c
41
41
  /deepseek-ai-dsh-session-snapshot/understand-the-implementation/source-map:
42
- en: 4c3ff235ca8fe261
43
- zh: 0f03425b926721cc
42
+ en: 1510b3b09c3bd553
43
+ zh: 1931fba0891f7d65
44
44
  /deepseek-ai-dsh-session-snapshot/understand-the-implementation/data-flow:
45
45
  en: a71c8ab2e78798c5
46
46
  zh: 419cebed1c023fbf
package/README.md CHANGED
@@ -68,7 +68,7 @@ defineAcpSnapshotSuite({
68
68
  })
69
69
  ```
70
70
 
71
- Each recorded-session directory carries a closed `snapshot.yml` manifest plus canonical parent and contiguous child roles. Parent filenames are `session[.vN].jsonl`; children are `session.<ordinal>[.vN].jsonl`; v0 omits `.v0`, positive versions use lowercase `.vN`, and every filename agrees with its header. A role may retain older generations, but the harness selects the numerically highest one. An owning manifest may declare `sessionFormat.version` plus one or more closed `coverage` names to keep that historical generation as an explicit migration fixture. Without that declaration, record and refresh write the current generation; replay also accepts the retained V3 baseline under the [corpus policy](../../../snapshots/AGENTS.md). The manifest also names the scenario, shipped profile, composition/header class, recording source, and only the replay, platform, permission, environment, workspace, or input facts the completed Session cannot reconstruct. Storage guards check tool results and portable paths in every selected parent and child role. Prompt/schema scrubbing, message identity, and prompt-before-request ordering apply to current generations; retained predecessors keep their historical representation. The adapter registers expected-output, Session-log, and optional `workspace.expected/` comparisons; guards reject orphan directories, missing roles, noncanonical names, absolute paths, malformed manifests, and platform-specific separators.
71
+ Each recorded-session directory carries a closed `snapshot.yml` manifest plus canonical parent and contiguous child roles. Parent filenames are `session[.vN].jsonl`; children are `session.<ordinal>[.vN].jsonl`; v0 omits `.v0`, positive versions use lowercase `.vN`, and every filename agrees with its header. A role may retain older generations, but the harness selects the numerically highest one. An owning manifest may declare `sessionFormat.version` plus one or more closed `coverage` names to keep that historical generation as an explicit migration fixture. Without that declaration, record and refresh write the current generation; replay independently accepts the retained V3 baseline or the current generation for each parent/child role under the [corpus policy](../../../snapshots/AGENTS.md). An explicit `sessionFormat.version` requires every selected role to match that version. The manifest also names the scenario, shipped profile, composition/header class, recording source, and only the replay, platform, permission, environment, workspace, or input facts the completed Session cannot reconstruct. Storage guards check tool results and portable paths in every selected parent and child role. Prompt/schema scrubbing, message identity, and prompt-before-request ordering apply to current generations; retained predecessors keep their historical representation. The adapter registers expected-output, Session-log, and optional `workspace.expected/` comparisons; guards reject orphan directories, missing roles, noncanonical names, absolute paths, malformed manifests, and platform-specific separators.
72
72
 
73
73
  `normalizeSessionSnapshot` retains the complete Session header and event payloads but omits top-level `seq`/`time` envelopes from committed fixtures after normalizing paths and scrubbing system-prompt text and tool schemas; it also normalizes embedded stream clocks and historical packed-row `seq0`/`time0` envelopes, and catalog child-creation clocks. Event order and source-event reference order are preserved. Record and refresh write top-level `sourceEventSeqs` as expanded arrays. Replay synthesizes the top-level envelopes in memory, while runtime persistence continues to write complete logs. Multi-session comparison validates expected and harvested logs through the strict build-static Session format catalog before identity redaction and normalization; source filenames cannot alter format validation. A retained historical replay input is not the native current-format writer-output oracle: structural migration preserves request meaning but can produce a different event layout. Normalization preserves unexpected request-header fields, including `system`, so regressions remain visible. Versionless protocol-adapter unit fixtures remain outside the released Session format corpus. Fixtures in the [current writer format](../../../docs/session-format-status.md) use one row per event; retained v0/v1 fixtures may use canonical packed rows. The [temporary repository migrator](../../../scripts/migrate-packed-session-fixtures.ts) (`pnpm run migrate:packed-session-fixtures`) rewrites older historical layouts, and its [removal proposal](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md) owns its deletion.
74
74
 
@@ -80,7 +80,7 @@ Headless/ACP and SDK adapters compare raw catalog child-creation times with harv
80
80
 
81
81
  ### Record, replay, and refresh
82
82
 
83
- `pnpm run test:snapshot:record` calls the live LLM and writes the harvested current generation under its canonical versioned filename. Record and refresh never rename or delete a completed generation, including generations of a child role absent from a later run; reviewed source-tree curation removes a predecessor only after the same role has a verified current replacement. Scenarios with an explicit `sessionFormat` remain read-only in record mode. `pnpm run test:snapshot:refresh` stays keyless, runs the selected highest replay input, and writes stdout, owned prompt and tool-schema sidecars, and a fresh current-generation comparable Session output; retained historical scenarios write the separate writer-output oracles instead of a canonical current-format replay generation. Each composition owner keeps its replay patch beside its live patch; top-level `snapshots/` owns Session-driven scenarios, while other expected outputs stay beside their owning package. [`dsh-llm-replay`](../llm-replay/README.md) serves the recorded streams selected through `DSH_SNAPSHOT_*` environment values.
83
+ `pnpm run test:snapshot:record` calls the live LLM and writes the harvested current generation under its canonical versioned filename. Record and refresh never rename or delete a completed generation, including generations of a child role absent from a later run; reviewed source-tree curation removes a predecessor only after the same role has a verified current replacement. Scenarios with an explicit `sessionFormat` remain read-only in record mode. `pnpm run test:snapshot:refresh` stays keyless, runs the selected highest replay input, and writes stdout, owned prompt and tool-schema sidecars, and a fresh current-generation comparable Session output; retained historical scenarios write the separate writer-output oracles instead of a canonical current-format replay generation. Headless and SDK scenarios may add a local `runtime.cordis.yml` patch for execution fixtures. Each composition owner keeps its replay patch beside its live patch; top-level `snapshots/` owns Session-driven scenarios, while other expected outputs stay beside their owning package. [`dsh-llm-replay`](../llm-replay/README.md) serves the recorded streams selected through `DSH_SNAPSHOT_*` environment values.
84
84
 
85
85
  ### Pinning request headers and system prompts
86
86
 
@@ -92,7 +92,7 @@ A scenario requiring a non-Windows host declares `posixOnly`, which skips its ru
92
92
 
93
93
  ### What can go wrong
94
94
 
95
- - **A child turn wait fails** — `waitForSubagentTurnEnd` identifies the child, requested turn, and deadline even when the first log harvest exceeds that deadline, and retains the underlying failure as the error cause.
95
+ - **A durable wait times out** — `waitForTurnStart`, `waitForTurnEnd`, `waitForSubagentTurnEnd`, `waitForGoalPhase`, `waitForInboxMessage`, `waitForTitleAfterTurnEnd`, and `waitForEventAfterTurnEnd` identify the session or child and deadline even when the first log harvest exceeds that deadline. Child waits also name the requested turn. All retain the underlying failure as the error cause. `waitForTurnStart` rejects malformed turn records immediately.
96
96
  - **A fixture guard rejects the committed files** — orphan scenario dirs, missing files, multiple pins for one header class, duplicate sidecar content, unscrubbed prompt text or tool schemas, a `request/header` with no preceding `system/message`, and malformed pinning headers all fail the suite before comparisons run.
97
97
  - **The session harvest needs raw JSONL mode** — snapshot configs set the JSONL backend's `compression: 'none'`; compressed JSONL has no snapshot-harvest path.
98
98
  - **Built mode needs current artifacts** — run `pnpm run build` before selecting `DSH_EXAMPLE_MODE=lib`; source mode remains the zero-build path.
@@ -124,7 +124,6 @@ The shared core owns manifests, generation-qualified role selection, workspace s
124
124
  | [`src/workspace.ts`](src/workspace.ts) | Scenario workspace setup and complete expected-state comparison |
125
125
  | [`src/suite.ts`](src/suite.ts) | Scenario-table suite factory, fixture guards, record/refresh write-back |
126
126
  | [`src/index.ts`](src/index.ts) | Package entry re-exporting the four layers |
127
- | — | No runtime invariant companion is published; this test-support package owns no production event stream or mutable data; consuming test suites exercise its behavior. |
128
127
 
129
128
  ### Data flow
130
129
 
package/README.zh.md CHANGED
@@ -68,7 +68,7 @@ defineAcpSnapshotSuite({
68
68
  })
69
69
  ```
70
70
 
71
- 每个已记录 Session 目录携带封闭的 `snapshot.yml` manifest,以及规范 parent 与连续 child 角色。parent 文件名是 `session[.vN].jsonl`;child 是 `session.<ordinal>[.vN].jsonl`;v0 省略 `.v0`,正版本使用小写 `.vN`,且每个文件名与其 header 一致。一个角色可以保留旧 generation,但 harness 会选择数值最高的一项。拥有 fixture 的 manifest 可以声明 `sessionFormat.version` 与一个或多个封闭 `coverage` 名称,把该历史 generation 保留为显式迁移 fixture。未声明时,record 与 refresh 写入当前 generation;replay 也按[语料策略](../../../snapshots/AGENTS.md)接受保留的 V3 基线。manifest 还会指名场景、随附 profile、组合/header 类别、录制来源,以及已完成 Session 无法重建的 replay、平台、权限、环境、workspace 或输入事实。存储保护检查每个选定 parent 与 child 角色的工具结果和可移植路径。提示词/schema 擦除、消息身份及提示词先于请求的顺序检查适用于当前 generation;保留的前代维持其历史表示。适配器注册预期输出、Session 日志与可选 `workspace.expected/` 比较;保护会拒绝遗留目录、缺失角色、非规范名称、绝对路径、格式错误的 manifest 与平台专用分隔符。
71
+ 每个已记录 Session 目录携带封闭的 `snapshot.yml` manifest,以及规范 parent 与连续 child 角色。parent 文件名是 `session[.vN].jsonl`;child 是 `session.<ordinal>[.vN].jsonl`;v0 省略 `.v0`,正版本使用小写 `.vN`,且每个文件名与其 header 一致。一个角色可以保留旧 generation,但 harness 会选择数值最高的一项。拥有 fixture 的 manifest 可以声明 `sessionFormat.version` 与一个或多个封闭 `coverage` 名称,把该历史 generation 保留为显式迁移 fixture。未声明时,record 与 refresh 写入当前 generation;replay 按[语料策略](../../../snapshots/AGENTS.md)对每个 parent/child 角色独立接受保留的 V3 基线或当前 generation。显式声明 `sessionFormat.version` 时,每个选定角色都必须匹配该版本。manifest 还会指名场景、随附 profile、组合/header 类别、录制来源,以及已完成 Session 无法重建的 replay、平台、权限、环境、workspace 或输入事实。存储保护检查每个选定 parent 与 child 角色的工具结果和可移植路径。提示词/schema 擦除、消息身份及提示词先于请求的顺序检查适用于当前 generation;保留的前代维持其历史表示。适配器注册预期输出、Session 日志与可选 `workspace.expected/` 比较;保护会拒绝遗留目录、缺失角色、非规范名称、绝对路径、格式错误的 manifest 与平台专用分隔符。
72
72
 
73
73
  `normalizeSessionSnapshot` 在规范化路径并擦除系统提示文本与工具 schema 后,会保留完整 Session header 与事件 payload,但从已提交 fixture 中省略顶层 `seq`/`time` envelope;它还会规范化嵌入式 stream clock 与历史 packed-row 的 `seq0`/`time0` envelope 与 catalog child 创建时钟。事件顺序与来源事件引用的顺序保持不变。录制与刷新会将顶层 `sourceEventSeqs` 以展开数组的形式写入 fixture。Replay 只在内存中合成顶层 envelope,而运行时持久化仍写入完整日志。多 Session 比较会先通过严格的构建期静态 Session 格式目录校验预期日志与收集日志,再进行身份脱敏与规范化;来源文件名不能改变格式校验。保留的历史 replay 输入不是原生当前格式 writer 输出的比较基准:结构迁移保留请求含义,但可以产生不同的事件布局。归一化保留意外的 request-header 字段(包括 `system`),使回归保持可见。无版本的协议适配器单元测试 fixture 不属于已发布 Session 格式语料。[当前写入器格式](../../../docs/session-format-status.zh.md)的 fixture 每个事件占一行;保留的 v0/v1 fixture 可以使用规范 packed row。[临时仓库迁移器](../../../scripts/migrate-packed-session-fixtures.ts)(`pnpm run migrate:packed-session-fixtures`)会改写更旧的历史布局,由其[移除提案](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.zh.md)负责删除该迁移器。
74
74
 
@@ -80,7 +80,7 @@ Headless/ACP 与 SDK 适配器在规范化之前,将原始目录中的子创
80
80
 
81
81
  ### 录制、回放与刷新
82
82
 
83
- `pnpm run test:snapshot:record` 调用在线 LLM(大语言模型),并在规范具名版本文件下写入收集到的当前 generation。record 与 refresh 绝不重命名或删除已完成的 generation,即使后续运行不再产生某个 child 角色也一样;受审阅的源树整理只有在同角色存在已验证的当前替代文件后才移除前代。显式声明 `sessionFormat` 的场景在录制模式下保持只读。`pnpm run test:snapshot:refresh` 保持无密钥,运行选定的最高 replay 输入,并写入 stdout、各 pin 自有的提示词与工具 schema 伴随文件,以及新鲜当前 generation 的可比较 Session 输出;保留历史输入的场景写入单独的 writer 输出比较基准,而非规范当前格式 replay 代际。每个组合 owner 把 replay patch 放在 live patch 旁;顶层 `snapshots/` 拥有 Session 驱动场景,其他预期输出留在其 package owner 旁。[`dsh-llm-replay`](../llm-replay/README.zh.md) 提供通过 `DSH_SNAPSHOT_*` 环境值选择的已记录流。
83
+ `pnpm run test:snapshot:record` 调用在线 LLM(大语言模型),并在规范具名版本文件下写入收集到的当前 generation。record 与 refresh 绝不重命名或删除已完成的 generation,即使后续运行不再产生某个 child 角色也一样;受审阅的源树整理只有在同角色存在已验证的当前替代文件后才移除前代。显式声明 `sessionFormat` 的场景在录制模式下保持只读。`pnpm run test:snapshot:refresh` 保持无密钥,运行选定的最高 replay 输入,并写入 stdout、各 pin 自有的提示词与工具 schema 伴随文件,以及新鲜当前 generation 的可比较 Session 输出;保留历史输入的场景写入单独的 writer 输出比较基准,而非规范当前格式 replay 代际。Headless 与 SDK 场景可通过本地 `runtime.cordis.yml` patch 添加执行 fixture。每个组合 owner 把 replay patch 放在 live patch 旁;顶层 `snapshots/` 拥有 Session 驱动场景,其他预期输出留在其 package owner 旁。[`dsh-llm-replay`](../llm-replay/README.zh.md) 提供通过 `DSH_SNAPSHOT_*` 环境值选择的已记录流。
84
84
 
85
85
  ### 固定请求 header 与系统提示
86
86
 
@@ -92,7 +92,7 @@ Headless/ACP 与 SDK 适配器在规范化之前,将原始目录中的子创
92
92
 
93
93
  ### 可能出什么问题
94
94
 
95
- - **子会话轮次等待失败**——即使首次日志收集就超过期限,`waitForSubagentTurnEnd` 也会指出子会话、目标轮次与等待期限,并通过错误的 cause 保留底层失败。
95
+ - **持久记录等待超时**——即使首次日志收集就超过期限,`waitForTurnStart`、`waitForTurnEnd`、`waitForSubagentTurnEnd`、`waitForGoalPhase`、`waitForInboxMessage`、`waitForTitleAfterTurnEnd` 和 `waitForEventAfterTurnEnd` 也会指出会话或子会话及等待期限。子会话等待还会指出目标轮次。这些等待都通过错误的 cause 保留底层失败。`waitForTurnStart` 会立即拒绝格式错误的轮次记录。
96
96
  - **fixture 保护拒绝已提交文件**——遗留场景目录、缺失文件、一个 header 类别包含多个 pin、重复的伴随文件内容、未擦除的提示文本或工具 schema、没有前置 `system/message` 的 `request/header`,以及格式错误的 pin header 都会在比较运行前使套件失败。
97
97
  - **会话收集需要原始 JSONL mode**——快照配置使用 JSONL 后端的 `compression: 'none'`;压缩 JSONL 没有快照收集路径。
98
98
  - **构建 mode 需要当前产物**——选择 `DSH_EXAMPLE_MODE=lib` 前先运行 `pnpm run build`;源 mode 仍是零构建路径。
@@ -124,7 +124,6 @@ Headless/ACP 与 SDK 适配器在规范化之前,将原始目录中的子创
124
124
  | [`src/workspace.ts`](src/workspace.ts) | 场景 workspace 设置与完整预期状态比较 |
125
125
  | [`src/suite.ts`](src/suite.ts) | 场景表套件工厂、fixture 保护、录制/刷新回写 |
126
126
  | [`src/index.ts`](src/index.ts) | 再导出四个层的包入口 |
127
- | — | 不发布运行时不变式伴生入口;该测试支持包不拥有任何生产事件流或可变数据;消费它的测试套件会检验该工具包。 |
128
127
 
129
128
  ### 数据流
130
129
 
package/lib/index.js CHANGED
@@ -672,7 +672,7 @@ function captureExpectedWorkspaceSnapshot(root) {
672
672
  * shutdown flush. The pure normalizers in ./normalize.ts turn the captured
673
673
  * stdout frames and the session-log events into stable, snapshot-able text.
674
674
  *
675
- * See .agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md.
675
+ * See packages/test-support/session-snapshot/README.md.
676
676
  *
677
677
  * @module @deepseek-ai/dsh-session-snapshot/harness
678
678
  */
@@ -958,23 +958,25 @@ async function runStep(client, step, cwd, waitForUpdate, getSessionId, setSessio
958
958
  /** Wait until persistence exposes an open turn for the selected session. */
959
959
  async function waitForPersistedTurnStart(root, sessionId, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS, minimumTurn) {
960
960
  let invalidRecord;
961
- await vi.waitFor(async () => {
962
- const log = (await harvestSessionLogs(root)).find((candidate) => candidate.id === sessionId);
963
- let openTurn;
964
- try {
965
- openTurn = log === void 0 ? void 0 : latestOpenTurn(log.content);
966
- } catch (error) {
967
- invalidRecord = { error };
968
- return;
969
- }
970
- if (openTurn === void 0 || minimumTurn !== void 0 && openTurn < minimumTurn) {
971
- const detail = minimumTurn === void 0 ? "turn/start" : `turn/start at or beyond turn ${minimumTurn}`;
972
- throw new Error(`snapshot-harness: session "${sessionId}" did not persist ${detail} within ${timeoutMs}ms`);
973
- }
974
- }, {
975
- interval: WAIT_POLL_INTERVAL_MS,
976
- timeout: timeoutMs
977
- });
961
+ const message = `snapshot-harness: session "${sessionId}" did not persist ${minimumTurn === void 0 ? "turn/start" : `turn/start at or beyond turn ${minimumTurn}`} within ${timeoutMs}ms`;
962
+ try {
963
+ await vi.waitFor(async () => {
964
+ const log = (await harvestSessionLogs(root)).find((candidate) => candidate.id === sessionId);
965
+ let openTurn;
966
+ try {
967
+ openTurn = log === void 0 ? void 0 : latestOpenTurn(log.content);
968
+ } catch (error) {
969
+ invalidRecord = { error };
970
+ return;
971
+ }
972
+ if (openTurn === void 0 || minimumTurn !== void 0 && openTurn < minimumTurn) throw new Error(message);
973
+ }, {
974
+ interval: WAIT_POLL_INTERVAL_MS,
975
+ timeout: timeoutMs
976
+ });
977
+ } catch (cause) {
978
+ throw new Error(message, { cause });
979
+ }
978
980
  if (invalidRecord !== void 0) throw invalidRecord.error;
979
981
  }
980
982
  /**
@@ -984,13 +986,18 @@ async function waitForPersistedTurnStart(root, sessionId, timeoutMs = DEFAULT_WA
984
986
  * keep subprocess disposal from changing an `aborted` turn into `disposed`.
985
987
  */
986
988
  async function waitForPersistedTurnEnd(root, sessionId, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
987
- await vi.waitFor(async () => {
988
- const log = (await harvestSessionLogs(root)).find((candidate) => candidate.id === sessionId);
989
- if (log === void 0 || !latestTurnIsClosed(log.content)) throw new Error(`snapshot-harness: session "${sessionId}" did not persist turn/end within ${timeoutMs}ms`);
990
- }, {
991
- interval: WAIT_POLL_INTERVAL_MS,
992
- timeout: timeoutMs
993
- });
989
+ const message = `snapshot-harness: session "${sessionId}" did not persist turn/end within ${timeoutMs}ms`;
990
+ try {
991
+ await vi.waitFor(async () => {
992
+ const log = (await harvestSessionLogs(root)).find((candidate) => candidate.id === sessionId);
993
+ if (log === void 0 || !latestTurnIsClosed(log.content)) throw new Error(message);
994
+ }, {
995
+ interval: WAIT_POLL_INTERVAL_MS,
996
+ timeout: timeoutMs
997
+ });
998
+ } catch (cause) {
999
+ throw new Error(message, { cause });
1000
+ }
994
1001
  }
995
1002
  /**
996
1003
  * Wait until the Nth harvested child Session closes a model work turn.
@@ -1023,28 +1030,38 @@ function hasClosedTurn(content, turn) {
1023
1030
  }
1024
1031
  /** Wait until the latest durable goal snapshot reaches one phase. */
1025
1032
  async function waitForPersistedGoalPhase(root, sessionId, phase, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
1026
- await vi.waitFor(async () => {
1027
- if (!(((await harvestSessionLogs(root)).find((log) => log.id === sessionId)?.content)?.split("\n").filter(Boolean).some((line) => {
1028
- const event = JSON.parse(line);
1029
- return event.type === "goal/change" && event.data?.goal?.phase === phase;
1030
- }) ?? false)) throw new Error(`snapshot-harness: session "${sessionId}" did not persist goal phase "${phase}" within ${timeoutMs}ms`);
1031
- }, {
1032
- interval: WAIT_POLL_INTERVAL_MS,
1033
- timeout: timeoutMs
1034
- });
1033
+ const message = `snapshot-harness: session "${sessionId}" did not persist goal phase "${phase}" within ${timeoutMs}ms`;
1034
+ try {
1035
+ await vi.waitFor(async () => {
1036
+ if (!(((await harvestSessionLogs(root)).find((log) => log.id === sessionId)?.content)?.split("\n").filter(Boolean).some((line) => {
1037
+ const event = JSON.parse(line);
1038
+ return event.type === "goal/change" && event.data?.goal?.phase === phase;
1039
+ }) ?? false)) throw new Error(message);
1040
+ }, {
1041
+ interval: WAIT_POLL_INTERVAL_MS,
1042
+ timeout: timeoutMs
1043
+ });
1044
+ } catch (cause) {
1045
+ throw new Error(message, { cause });
1046
+ }
1035
1047
  }
1036
1048
  /** Wait until an inserted inbox message contains scenario-owned text. */
1037
1049
  async function waitForPersistedInboxMessage(root, sessionId, text, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
1038
- await vi.waitFor(async () => {
1039
- if (!((await harvestSessionLogs(root)).find((candidate) => candidate.id === sessionId)?.content.split("\n").some((line) => {
1040
- if (line.length === 0) return false;
1041
- const record = JSON.parse(line);
1042
- return record.type === "agent/inbox/spliced" && record.data?.inserted?.some((message) => message.content?.some((block) => block.type === "text" && typeof block.text === "string" && block.text.includes(text))) === true;
1043
- }) ?? false)) throw new Error(`snapshot-harness: session "${sessionId}" did not persist expected inbox message within ${timeoutMs}ms`);
1044
- }, {
1045
- interval: WAIT_POLL_INTERVAL_MS,
1046
- timeout: timeoutMs
1047
- });
1050
+ const message = `snapshot-harness: session "${sessionId}" did not persist expected inbox message within ${timeoutMs}ms`;
1051
+ try {
1052
+ await vi.waitFor(async () => {
1053
+ if (!((await harvestSessionLogs(root)).find((candidate) => candidate.id === sessionId)?.content.split("\n").some((line) => {
1054
+ if (line.length === 0) return false;
1055
+ const record = JSON.parse(line);
1056
+ return record.type === "agent/inbox/spliced" && record.data?.inserted?.some((message) => message.content?.some((block) => block.type === "text" && typeof block.text === "string" && block.text.includes(text))) === true;
1057
+ }) ?? false)) throw new Error(message);
1058
+ }, {
1059
+ interval: WAIT_POLL_INTERVAL_MS,
1060
+ timeout: timeoutMs
1061
+ });
1062
+ } catch (cause) {
1063
+ throw new Error(message, { cause });
1064
+ }
1048
1065
  }
1049
1066
  /** Whether a child log contains model work after its own descriptor event. */
1050
1067
  function hasRequestHeaderAfterDescriptor(content) {
@@ -1054,23 +1071,33 @@ function hasRequestHeaderAfterDescriptor(content) {
1054
1071
  }
1055
1072
  /** Wait until a complete provider or fallback title record follows the latest closed turn. */
1056
1073
  async function waitForPersistedTitleAfterTurnEnd(root, sessionId, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
1057
- await vi.waitFor(async () => {
1058
- const log = (await harvestSessionLogs(root)).find((candidate) => candidate.id === sessionId);
1059
- if (log === void 0 || !latestTitleFollowsTurnEnd(log.content)) throw new Error(`snapshot-harness: session "${sessionId}" did not persist session/title after turn/end within ${timeoutMs}ms`);
1060
- }, {
1061
- interval: WAIT_POLL_INTERVAL_MS,
1062
- timeout: timeoutMs
1063
- });
1074
+ const message = `snapshot-harness: session "${sessionId}" did not persist session/title after turn/end within ${timeoutMs}ms`;
1075
+ try {
1076
+ await vi.waitFor(async () => {
1077
+ const log = (await harvestSessionLogs(root)).find((candidate) => candidate.id === sessionId);
1078
+ if (log === void 0 || !latestTitleFollowsTurnEnd(log.content)) throw new Error(message);
1079
+ }, {
1080
+ interval: WAIT_POLL_INTERVAL_MS,
1081
+ timeout: timeoutMs
1082
+ });
1083
+ } catch (cause) {
1084
+ throw new Error(message, { cause });
1085
+ }
1064
1086
  }
1065
1087
  /** Wait until a complete record of `type` follows the latest closed turn. */
1066
1088
  async function waitForPersistedEventAfterTurnEnd(root, sessionId, type, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
1067
- await vi.waitFor(async () => {
1068
- const log = (await harvestSessionLogs(root)).find((candidate) => candidate.id === sessionId);
1069
- if (log === void 0 || !latestEventFollowsTurnEnd(log.content, type)) throw new Error(`snapshot-harness: session "${sessionId}" did not persist ${type} after turn/end within ${timeoutMs}ms`);
1070
- }, {
1071
- interval: WAIT_POLL_INTERVAL_MS,
1072
- timeout: timeoutMs
1073
- });
1089
+ const message = `snapshot-harness: session "${sessionId}" did not persist ${type} after turn/end within ${timeoutMs}ms`;
1090
+ try {
1091
+ await vi.waitFor(async () => {
1092
+ const log = (await harvestSessionLogs(root)).find((candidate) => candidate.id === sessionId);
1093
+ if (log === void 0 || !latestEventFollowsTurnEnd(log.content, type)) throw new Error(message);
1094
+ }, {
1095
+ interval: WAIT_POLL_INTERVAL_MS,
1096
+ timeout: timeoutMs
1097
+ });
1098
+ } catch (cause) {
1099
+ throw new Error(message, { cause });
1100
+ }
1074
1101
  }
1075
1102
  /** Wait for a cwd-relative marker proving an external action reached readiness. */
1076
1103
  async function waitForWorkspaceFile(cwd, path, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
@@ -11,7 +11,7 @@
11
11
  * shutdown flush. The pure normalizers in ./normalize.ts turn the captured
12
12
  * stdout frames and the session-log events into stable, snapshot-able text.
13
13
  *
14
- * See .agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md.
14
+ * See packages/test-support/session-snapshot/README.md.
15
15
  *
16
16
  * @module @deepseek-ai/dsh-session-snapshot/harness
17
17
  */
@@ -39,12 +39,14 @@ export type { AgentUnderTest } from './launcher.ts';
39
39
  * `waitForInboxMessage` waits for inserted inbox text containing a scenario marker.
40
40
  * `waitForSubagentTurnEnd` waits until one background child has persisted a
41
41
  * closed model-work turn after its own descriptor; child progress has no ACP
42
- * update to wait on. Failures identify the child, turn, and deadline even if
43
- * the first log read is still pending; the underlying failure is retained as cause.
42
+ * update to wait on.
44
43
  * `waitForTitleAfterTurnEnd` additionally waits for a later durable title.
45
44
  * `waitForEventAfterTurnEnd` waits until a complete record of the given event
46
45
  * type follows the latest closed turn — for scenarios whose asserted state
47
46
  * (e.g. a goal pause) is appended only after cancellation reaches idle.
47
+ * Session-log wait timeouts identify the session or child and deadline even
48
+ * if the first log read is still pending; they retain the underlying failure
49
+ * as cause. Child waits also name the requested turn.
48
50
  * A standalone `cancel` may also wait for a cwd-relative readiness marker.
49
51
  * All wait timeouts default to 10s.
50
52
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-session-snapshot",
3
3
  "description": "Session-log snapshot core with an ACP protocol adapter, expected-output normalization, and fixture invariants",
4
- "version": "0.2.0-rc.2",
4
+ "version": "0.2.1-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -28,31 +28,32 @@
28
28
  "license": "MIT",
29
29
  "dependencies": {
30
30
  "@agentclientprotocol/sdk": "1.4.0",
31
- "js-yaml": "^4.2.0",
32
- "vitest": "^4.1.8",
33
- "@deepseek-ai/cordis-plugin-include": "~1.0.9",
34
- "@deepseek-ai/dsh-llm-replay": "0.2.0-rc.2",
35
- "@deepseek-ai/dsh-loader-smoke": "0.2.0-rc.2",
36
- "@deepseek-ai/dsh-session-format": "0.2.0-rc.2"
31
+ "@deepseek-ai/cordis-plugin-include": "~1.0.10-alpha.1",
32
+ "@deepseek-ai/dsh-llm-replay": "0.2.1-alpha.2",
33
+ "@deepseek-ai/dsh-loader-smoke": "0.2.1-alpha.2",
34
+ "@deepseek-ai/dsh-session-format": "0.2.1-alpha.2",
35
+ "js-yaml": "^4.3.2",
36
+ "vitest": "^4.1.11"
37
37
  },
38
38
  "peerDependencies": {
39
- "@deepseek-ai/cordis": "~4.0.4",
40
- "@deepseek-ai/dsh-session": "0.2.0-rc.2",
41
- "@deepseek-ai/dsh-http-proxy": "0.2.0-rc.2"
39
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1",
40
+ "@deepseek-ai/dsh-session": "0.2.1-alpha.2",
41
+ "@deepseek-ai/dsh-http-proxy": "0.2.1-alpha.2"
42
42
  },
43
43
  "devDependencies": {
44
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1",
45
+ "@deepseek-ai/dsh-agent": "0.2.1-alpha.2",
46
+ "@deepseek-ai/dsh-agent-loop": "0.2.1-alpha.2",
47
+ "@deepseek-ai/dsh-compaction": "0.2.1-alpha.2",
48
+ "@deepseek-ai/dsh-fs": "0.2.1-alpha.2",
49
+ "@deepseek-ai/dsh-llm": "0.2.1-alpha.2",
50
+ "@deepseek-ai/dsh-sandbox": "0.2.1-alpha.2",
51
+ "@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.2",
52
+ "@deepseek-ai/dsh-session": "0.2.1-alpha.2",
53
+ "@deepseek-ai/dsh-subagent": "0.2.1-alpha.2",
54
+ "@deepseek-ai/dsh-tools": "0.2.1-alpha.2",
55
+ "@deepseek-ai/dsh-user-questions": "0.2.1-alpha.2",
44
56
  "@types/js-yaml": "^4.0.9",
45
- "@deepseek-ai/cordis": "~4.0.4",
46
- "@deepseek-ai/dsh-agent": "0.2.0-rc.2",
47
- "@deepseek-ai/dsh-agent-loop": "0.2.0-rc.2",
48
- "@deepseek-ai/dsh-compaction": "0.2.0-rc.2",
49
- "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
50
- "@deepseek-ai/dsh-sandbox": "0.2.0-rc.2",
51
- "@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.2",
52
- "@deepseek-ai/dsh-session": "0.2.0-rc.2",
53
- "@deepseek-ai/dsh-subagent": "0.2.0-rc.2",
54
- "@deepseek-ai/dsh-tools": "0.2.0-rc.2",
55
- "@deepseek-ai/dsh-user-questions": "0.2.0-rc.2",
56
- "@deepseek-ai/dsh-http-proxy": "0.2.0-rc.2"
57
+ "@deepseek-ai/dsh-http-proxy": "0.2.1-alpha.2"
57
58
  }
58
59
  }