@deepseek-ai/dsh-session-snapshot 0.2.1-alpha.1 → 0.2.1-alpha.3
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 +8 -8
- package/README.md +4 -4
- package/README.zh.md +4 -4
- package/lib/index.js +108 -54
- package/lib/types/harness.d.ts +4 -2
- package/lib/types/manifest.d.ts +1 -1
- package/package.json +24 -22
package/README.i18n.yaml
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
en: b184b97e2d1e1fa7
|
|
10
10
|
zh: 7d4b4cc8d73446af
|
|
11
11
|
/deepseek-ai-dsh-session-snapshot/summary:
|
|
12
|
-
en:
|
|
13
|
-
zh:
|
|
12
|
+
en: 5d11e1dbbf5dd1cf
|
|
13
|
+
zh: 7a54089ed26ddd8f
|
|
14
14
|
/deepseek-ai-dsh-session-snapshot/table-of-contents:
|
|
15
15
|
en: d152484eb41ac6b4
|
|
16
16
|
zh: 09388d293f9be9cb
|
|
@@ -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:
|
|
22
|
-
zh:
|
|
21
|
+
en: 57ff7c89e7696ee8
|
|
22
|
+
zh: b885ac1b4edc8c56
|
|
23
23
|
/deepseek-ai-dsh-session-snapshot/use-this-package/record-replay-and-refresh:
|
|
24
|
-
en:
|
|
25
|
-
zh:
|
|
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:
|
|
34
|
-
zh:
|
|
33
|
+
en: 0b03592947ca43d6
|
|
34
|
+
zh: b14a64238c4b5f80
|
|
35
35
|
/deepseek-ai-dsh-session-snapshot/understand-the-implementation:
|
|
36
36
|
en: 18ff76b440fe40da
|
|
37
37
|
zh: 4dbf2eb5f9ee98a2
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
|
|
9
9
|
|
|
10
10
|
## Summary
|
|
11
11
|
|
|
12
|
-
`dsh-session-snapshot` provides the shared support behind keyless recorded-session tests (`pnpm run test:snapshot`): closed manifests, typed identity redaction, normalization, workspace comparison, fixture guards, and protocol adapters for headless, SDK, ACP, and Web owners. The ACP adapter launches the tested profile as a real subprocess, drives a deterministic input script, and registers the complete record, replay, and refresh suite. Every scenario owns enough committed evidence to prove model-visible output and filesystem effects without trusting the agent's report. The package entry imports vitest and is therefore available only inside a vitest run.
|
|
12
|
+
`dsh-session-snapshot` provides the shared support behind keyless recorded-session tests (`pnpm run test:snapshot`): closed manifests, typed identity redaction, normalization, workspace comparison, fixture guards, and protocol adapters for headless, SDK (`sdk` and `sdk-minimal`), ACP, and Web owners. The ACP adapter launches the tested profile as a real subprocess, drives a deterministic input script, and registers the complete record, replay, and refresh suite. Every scenario owns enough committed evidence to prove model-visible output and filesystem effects without trusting the agent's report. The package entry imports vitest and is therefore available only inside a vitest run.
|
|
13
13
|
|
|
14
14
|
## Table of Contents
|
|
15
15
|
|
|
@@ -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
|
|
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
|
|
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.
|
package/README.zh.md
CHANGED
|
@@ -9,7 +9,7 @@ kind: "package-library"
|
|
|
9
9
|
|
|
10
10
|
## 概述
|
|
11
11
|
|
|
12
|
-
`dsh-session-snapshot` 提供无密钥已记录会话测试(`pnpm run test:snapshot`)背后的共享支持:封闭 manifest、类型化身份脱敏、规范化、workspace 比较、fixture(测试前置数据)保护,以及 headless、SDK
|
|
12
|
+
`dsh-session-snapshot` 提供无密钥已记录会话测试(`pnpm run test:snapshot`)背后的共享支持:封闭 manifest、类型化身份脱敏、规范化、workspace 比较、fixture(测试前置数据)保护,以及 headless、SDK(`sdk` 与 `sdk-minimal`)、ACP(Agent Client Protocol)与 Web owner 使用的协议适配器。ACP 适配器以真实子进程启动被测 profile,驱动确定性输入脚本,并注册完整的录制、回放与刷新套件。每个场景都提交足够证据来证明模型可见输出与文件系统效果,不依赖 agent(智能体)自述。包入口会导入 vitest,因此只能在 vitest 运行中使用。
|
|
13
13
|
|
|
14
14
|
## 目录
|
|
15
15
|
|
|
@@ -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
|
|
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
|
|
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
|
-
-
|
|
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 仍是零构建路径。
|
package/lib/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { isSurfaceEligibleType } from "@deepseek-ai/dsh-session/surface";
|
|
|
20
20
|
//#region lib/types/identity.js
|
|
21
21
|
/** Relationship-preserving identity redaction for committed session snapshots. */
|
|
22
22
|
const UUID_FRAGMENT_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
23
|
+
const SHORT_JOB_ID_RE$1 = /-[0-9a-f]{16}$/i;
|
|
23
24
|
const LEGACY_TOKEN_RE = /^\{\{(?:sessionId|messageId)\}\}$/;
|
|
24
25
|
const CANONICAL_TOKEN_RE = /^\{\{(session|message|approval|workflow|command|rpc|retry|id):([1-9]\d*)\}\}$/;
|
|
25
26
|
const ID_KEY_RE = /(?:^id$|Id$|Ids$)/;
|
|
@@ -37,7 +38,7 @@ function messageId(value) {
|
|
|
37
38
|
return value.id;
|
|
38
39
|
}
|
|
39
40
|
function redactedCandidate(value) {
|
|
40
|
-
return UUID_FRAGMENT_RE.test(value) || LEGACY_TOKEN_RE.test(value) || CANONICAL_TOKEN_RE.test(value);
|
|
41
|
+
return UUID_FRAGMENT_RE.test(value) || SHORT_JOB_ID_RE$1.test(value) || LEGACY_TOKEN_RE.test(value) || CANONICAL_TOKEN_RE.test(value);
|
|
41
42
|
}
|
|
42
43
|
/**
|
|
43
44
|
* Replace volatile opaque ids while preserving equality relationships across a parent and its child logs.
|
|
@@ -958,23 +959,25 @@ async function runStep(client, step, cwd, waitForUpdate, getSessionId, setSessio
|
|
|
958
959
|
/** Wait until persistence exposes an open turn for the selected session. */
|
|
959
960
|
async function waitForPersistedTurnStart(root, sessionId, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS, minimumTurn) {
|
|
960
961
|
let invalidRecord;
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
openTurn
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
})
|
|
962
|
+
const message = `snapshot-harness: session "${sessionId}" did not persist ${minimumTurn === void 0 ? "turn/start" : `turn/start at or beyond turn ${minimumTurn}`} within ${timeoutMs}ms`;
|
|
963
|
+
try {
|
|
964
|
+
await vi.waitFor(async () => {
|
|
965
|
+
const log = (await harvestSessionLogs(root)).find((candidate) => candidate.id === sessionId);
|
|
966
|
+
let openTurn;
|
|
967
|
+
try {
|
|
968
|
+
openTurn = log === void 0 ? void 0 : latestOpenTurn(log.content);
|
|
969
|
+
} catch (error) {
|
|
970
|
+
invalidRecord = { error };
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
if (openTurn === void 0 || minimumTurn !== void 0 && openTurn < minimumTurn) throw new Error(message);
|
|
974
|
+
}, {
|
|
975
|
+
interval: WAIT_POLL_INTERVAL_MS,
|
|
976
|
+
timeout: timeoutMs
|
|
977
|
+
});
|
|
978
|
+
} catch (cause) {
|
|
979
|
+
throw new Error(message, { cause });
|
|
980
|
+
}
|
|
978
981
|
if (invalidRecord !== void 0) throw invalidRecord.error;
|
|
979
982
|
}
|
|
980
983
|
/**
|
|
@@ -1028,28 +1031,38 @@ function hasClosedTurn(content, turn) {
|
|
|
1028
1031
|
}
|
|
1029
1032
|
/** Wait until the latest durable goal snapshot reaches one phase. */
|
|
1030
1033
|
async function waitForPersistedGoalPhase(root, sessionId, phase, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1034
|
+
const message = `snapshot-harness: session "${sessionId}" did not persist goal phase "${phase}" within ${timeoutMs}ms`;
|
|
1035
|
+
try {
|
|
1036
|
+
await vi.waitFor(async () => {
|
|
1037
|
+
if (!(((await harvestSessionLogs(root)).find((log) => log.id === sessionId)?.content)?.split("\n").filter(Boolean).some((line) => {
|
|
1038
|
+
const event = JSON.parse(line);
|
|
1039
|
+
return event.type === "goal/change" && event.data?.goal?.phase === phase;
|
|
1040
|
+
}) ?? false)) throw new Error(message);
|
|
1041
|
+
}, {
|
|
1042
|
+
interval: WAIT_POLL_INTERVAL_MS,
|
|
1043
|
+
timeout: timeoutMs
|
|
1044
|
+
});
|
|
1045
|
+
} catch (cause) {
|
|
1046
|
+
throw new Error(message, { cause });
|
|
1047
|
+
}
|
|
1040
1048
|
}
|
|
1041
1049
|
/** Wait until an inserted inbox message contains scenario-owned text. */
|
|
1042
1050
|
async function waitForPersistedInboxMessage(root, sessionId, text, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1051
|
+
const message = `snapshot-harness: session "${sessionId}" did not persist expected inbox message within ${timeoutMs}ms`;
|
|
1052
|
+
try {
|
|
1053
|
+
await vi.waitFor(async () => {
|
|
1054
|
+
if (!((await harvestSessionLogs(root)).find((candidate) => candidate.id === sessionId)?.content.split("\n").some((line) => {
|
|
1055
|
+
if (line.length === 0) return false;
|
|
1056
|
+
const record = JSON.parse(line);
|
|
1057
|
+
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;
|
|
1058
|
+
}) ?? false)) throw new Error(message);
|
|
1059
|
+
}, {
|
|
1060
|
+
interval: WAIT_POLL_INTERVAL_MS,
|
|
1061
|
+
timeout: timeoutMs
|
|
1062
|
+
});
|
|
1063
|
+
} catch (cause) {
|
|
1064
|
+
throw new Error(message, { cause });
|
|
1065
|
+
}
|
|
1053
1066
|
}
|
|
1054
1067
|
/** Whether a child log contains model work after its own descriptor event. */
|
|
1055
1068
|
function hasRequestHeaderAfterDescriptor(content) {
|
|
@@ -1059,23 +1072,33 @@ function hasRequestHeaderAfterDescriptor(content) {
|
|
|
1059
1072
|
}
|
|
1060
1073
|
/** Wait until a complete provider or fallback title record follows the latest closed turn. */
|
|
1061
1074
|
async function waitForPersistedTitleAfterTurnEnd(root, sessionId, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1075
|
+
const message = `snapshot-harness: session "${sessionId}" did not persist session/title after turn/end within ${timeoutMs}ms`;
|
|
1076
|
+
try {
|
|
1077
|
+
await vi.waitFor(async () => {
|
|
1078
|
+
const log = (await harvestSessionLogs(root)).find((candidate) => candidate.id === sessionId);
|
|
1079
|
+
if (log === void 0 || !latestTitleFollowsTurnEnd(log.content)) throw new Error(message);
|
|
1080
|
+
}, {
|
|
1081
|
+
interval: WAIT_POLL_INTERVAL_MS,
|
|
1082
|
+
timeout: timeoutMs
|
|
1083
|
+
});
|
|
1084
|
+
} catch (cause) {
|
|
1085
|
+
throw new Error(message, { cause });
|
|
1086
|
+
}
|
|
1069
1087
|
}
|
|
1070
1088
|
/** Wait until a complete record of `type` follows the latest closed turn. */
|
|
1071
1089
|
async function waitForPersistedEventAfterTurnEnd(root, sessionId, type, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1090
|
+
const message = `snapshot-harness: session "${sessionId}" did not persist ${type} after turn/end within ${timeoutMs}ms`;
|
|
1091
|
+
try {
|
|
1092
|
+
await vi.waitFor(async () => {
|
|
1093
|
+
const log = (await harvestSessionLogs(root)).find((candidate) => candidate.id === sessionId);
|
|
1094
|
+
if (log === void 0 || !latestEventFollowsTurnEnd(log.content, type)) throw new Error(message);
|
|
1095
|
+
}, {
|
|
1096
|
+
interval: WAIT_POLL_INTERVAL_MS,
|
|
1097
|
+
timeout: timeoutMs
|
|
1098
|
+
});
|
|
1099
|
+
} catch (cause) {
|
|
1100
|
+
throw new Error(message, { cause });
|
|
1101
|
+
}
|
|
1079
1102
|
}
|
|
1080
1103
|
/** Wait for a cwd-relative marker proving an external action reached readiness. */
|
|
1081
1104
|
async function waitForWorkspaceFile(cwd, path, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
|
|
@@ -1194,6 +1217,7 @@ const PATH_TEXT_BOUNDARY_RE = /[\s<>'"`()\[\]{},;:!?=]/;
|
|
|
1194
1217
|
const FILE_URI_PATH_PREFIX_RE = /(?:^|[^a-z0-9+.-])file:\/\/\/?$/i;
|
|
1195
1218
|
/** A UUID v4 string, the shape `randomUUID()` produces for session ids. */
|
|
1196
1219
|
const UUID_RE$1 = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
|
|
1220
|
+
const SHORT_JOB_ID_RE = /-[0-9a-f]{16}$/i;
|
|
1197
1221
|
const LOCAL_SPILL_PATH_RE = new RegExp(String.raw`\{\{cwd\}\}[\\/]+\.spill[\\/]+session-[0-9a-f]{12}[\\/]+[0-9a-f]{12}-([A-Za-z0-9._~-]+?)` + String.raw`(?=\. Use read with offset/limit|[\s)"]|\\+"|$)`, "g");
|
|
1198
1222
|
const SNAPSHOT_SPILL_PATH_RE = new RegExp(String.raw`(?:[A-Za-z]:)?[\\/]+(?:tmp|t)[\\/]+(?:dsh-acp-snap-[0-9a-f]{9}|dsh-acp-snapshot-spill)[\\/]+session-[0-9a-f]{12}[\\/]+[0-9a-f]{12}-([A-Za-z0-9._~-]+?)` + String.raw`(?=\. Use read with offset/limit|[\s)"]|\\+"|$)`, "g");
|
|
1199
1223
|
/**
|
|
@@ -1384,8 +1408,32 @@ function normalizeStdout(rawStdout, ctx, options = {}) {
|
|
|
1384
1408
|
function normalizeSessionLog(rawLog, ctx, options = {}) {
|
|
1385
1409
|
const cwdPathMode = options.cwdPathMode ?? "canonical";
|
|
1386
1410
|
const identityMode = options.identityMode ?? "legacy";
|
|
1387
|
-
|
|
1388
|
-
|
|
1411
|
+
const parsed = rawLog.split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
|
|
1412
|
+
const jobTokens = /* @__PURE__ */ new Map();
|
|
1413
|
+
const reservedTokens = new Set(rawLog.match(/\{\{id:[1-9]\d*\}\}/g) ?? []);
|
|
1414
|
+
let nextJobToken = 1;
|
|
1415
|
+
if (identityMode === "legacy") for (const record of parsed) {
|
|
1416
|
+
if (record.type !== "job/registered" && record.type !== "job/settled" || record.data === null || typeof record.data !== "object") continue;
|
|
1417
|
+
const id = record.data.jobId;
|
|
1418
|
+
if (typeof id !== "string" || id.search(UUID_RE$1) < 0 && !SHORT_JOB_ID_RE.test(id) || jobTokens.has(id)) continue;
|
|
1419
|
+
let token;
|
|
1420
|
+
do
|
|
1421
|
+
token = `{{id:${nextJobToken++}}}`;
|
|
1422
|
+
while (reservedTokens.has(token));
|
|
1423
|
+
jobTokens.set(id, token);
|
|
1424
|
+
}
|
|
1425
|
+
const replaceJobs = (value) => {
|
|
1426
|
+
if (typeof value === "string") {
|
|
1427
|
+
let output = value;
|
|
1428
|
+
for (const [id, token] of jobTokens) output = output.split(id).join(token);
|
|
1429
|
+
return output;
|
|
1430
|
+
}
|
|
1431
|
+
if (Array.isArray(value)) return value.map(replaceJobs);
|
|
1432
|
+
if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, replaceJobs(item)]));
|
|
1433
|
+
return value;
|
|
1434
|
+
};
|
|
1435
|
+
return parsed.map((raw) => {
|
|
1436
|
+
const record = jobTokens.size === 0 ? raw : replaceJobs(raw);
|
|
1389
1437
|
if (record.type === "session") {
|
|
1390
1438
|
if ("createdAt" in record) record.createdAt = 0;
|
|
1391
1439
|
} else if (isPackedFixtureRow(record)) {
|
|
@@ -1408,6 +1456,11 @@ function normalizeSessionLog(rawLog, ctx, options = {}) {
|
|
|
1408
1456
|
if ("durationMs" in data) data.durationMs = 0;
|
|
1409
1457
|
}
|
|
1410
1458
|
normalizeFeedbackClocks(record);
|
|
1459
|
+
if ((record.type === "job/registered" || record.type === "job/settled") && record.data !== null && typeof record.data === "object") {
|
|
1460
|
+
const data = record.data;
|
|
1461
|
+
if ("startedAt" in data) data.startedAt = 0;
|
|
1462
|
+
if ("finishedAt" in data) data.finishedAt = 0;
|
|
1463
|
+
}
|
|
1411
1464
|
if (record.type === "goal/change" && record.data !== null && typeof record.data === "object") {
|
|
1412
1465
|
const data = record.data;
|
|
1413
1466
|
if ("createdAt" in data) data.createdAt = 0;
|
|
@@ -1631,6 +1684,7 @@ function writesCurrentSessionFixtures(manifest, mode) {
|
|
|
1631
1684
|
const PROFILES = new Set([
|
|
1632
1685
|
"headless",
|
|
1633
1686
|
"sdk",
|
|
1687
|
+
"sdk-minimal",
|
|
1634
1688
|
"acp",
|
|
1635
1689
|
"web"
|
|
1636
1690
|
]);
|
|
@@ -1703,7 +1757,7 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
|
|
|
1703
1757
|
], "manifest");
|
|
1704
1758
|
if (root.version !== 1) throw new Error("manifest.version must equal 1");
|
|
1705
1759
|
const scenario = root.scenario === void 0 ? void 0 : name(root.scenario, "manifest.scenario");
|
|
1706
|
-
if (typeof root.profile !== "string" || !PROFILES.has(root.profile)) throw new Error("manifest.profile must be headless, sdk, acp, or web");
|
|
1760
|
+
if (typeof root.profile !== "string" || !PROFILES.has(root.profile)) throw new Error("manifest.profile must be headless, sdk, sdk-minimal, acp, or web");
|
|
1707
1761
|
const composition = root.composition === void 0 ? void 0 : name(root.composition, "manifest.composition");
|
|
1708
1762
|
let recording;
|
|
1709
1763
|
if (root.recording !== void 0) {
|
package/lib/types/harness.d.ts
CHANGED
|
@@ -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.
|
|
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/lib/types/manifest.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Parse and validate one recorded-session snapshot manifest. */
|
|
2
2
|
/** Public `dsh` profile used to control a recorded-session scenario. */
|
|
3
|
-
export type SnapshotProfile = 'headless' | 'sdk' | 'acp' | 'web';
|
|
3
|
+
export type SnapshotProfile = 'headless' | 'sdk' | 'sdk-minimal' | 'acp' | 'web';
|
|
4
4
|
/** How a canonical session may be regenerated. */
|
|
5
5
|
export type SnapshotRecording = 'live' | 'authored';
|
|
6
6
|
/** Request-header ownership metadata for one composition. */
|
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.1-alpha.
|
|
4
|
+
"version": "0.2.1-alpha.3",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -28,31 +28,33 @@
|
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@agentclientprotocol/sdk": "1.4.0",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"@deepseek-ai/dsh-
|
|
34
|
-
"@deepseek-ai/dsh-session-format": "0.2.1-alpha.
|
|
35
|
-
"
|
|
36
|
-
"
|
|
31
|
+
"@deepseek-ai/cordis-plugin-include": "~1.0.10-alpha.2",
|
|
32
|
+
"@deepseek-ai/dsh-llm-replay": "0.2.1-alpha.3",
|
|
33
|
+
"@deepseek-ai/dsh-loader-smoke": "0.2.1-alpha.3",
|
|
34
|
+
"@deepseek-ai/dsh-session-format": "0.2.1-alpha.3",
|
|
35
|
+
"js-yaml": "^4.3.2",
|
|
36
|
+
"vitest": "^4.1.11"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"@deepseek-ai/
|
|
40
|
-
"@deepseek-ai/dsh-
|
|
41
|
-
"@deepseek-ai/
|
|
39
|
+
"@deepseek-ai/cordis": "~4.0.5-alpha.2",
|
|
40
|
+
"@deepseek-ai/dsh-session": "0.2.1-alpha.3",
|
|
41
|
+
"@deepseek-ai/dsh-http-proxy": "0.2.1-alpha.3"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
+
"@deepseek-ai/cordis": "~4.0.5-alpha.2",
|
|
45
|
+
"@deepseek-ai/dsh-agent": "0.2.1-alpha.3",
|
|
46
|
+
"@deepseek-ai/dsh-agent-loop": "0.2.1-alpha.3",
|
|
47
|
+
"@deepseek-ai/dsh-agent-preset-registry": "0.2.1-alpha.3",
|
|
48
|
+
"@deepseek-ai/dsh-compaction": "0.2.1-alpha.3",
|
|
49
|
+
"@deepseek-ai/dsh-fs": "0.2.1-alpha.3",
|
|
50
|
+
"@deepseek-ai/dsh-llm": "0.2.1-alpha.3",
|
|
51
|
+
"@deepseek-ai/dsh-sandbox": "0.2.1-alpha.3",
|
|
52
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.3",
|
|
53
|
+
"@deepseek-ai/dsh-session": "0.2.1-alpha.3",
|
|
54
|
+
"@deepseek-ai/dsh-subagent": "0.2.1-alpha.3",
|
|
55
|
+
"@deepseek-ai/dsh-tools": "0.2.1-alpha.3",
|
|
56
|
+
"@deepseek-ai/dsh-user-questions": "0.2.1-alpha.3",
|
|
44
57
|
"@types/js-yaml": "^4.0.9",
|
|
45
|
-
"@deepseek-ai/
|
|
46
|
-
"@deepseek-ai/dsh-agent": "0.2.1-alpha.1",
|
|
47
|
-
"@deepseek-ai/dsh-agent-loop": "0.2.1-alpha.1",
|
|
48
|
-
"@deepseek-ai/dsh-compaction": "0.2.1-alpha.1",
|
|
49
|
-
"@deepseek-ai/dsh-llm": "0.2.1-alpha.1",
|
|
50
|
-
"@deepseek-ai/dsh-sandbox": "0.2.1-alpha.1",
|
|
51
|
-
"@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.1",
|
|
52
|
-
"@deepseek-ai/dsh-session": "0.2.1-alpha.1",
|
|
53
|
-
"@deepseek-ai/dsh-subagent": "0.2.1-alpha.1",
|
|
54
|
-
"@deepseek-ai/dsh-user-questions": "0.2.1-alpha.1",
|
|
55
|
-
"@deepseek-ai/dsh-http-proxy": "0.2.1-alpha.1",
|
|
56
|
-
"@deepseek-ai/dsh-tools": "0.2.1-alpha.1"
|
|
58
|
+
"@deepseek-ai/dsh-http-proxy": "0.2.1-alpha.3"
|
|
57
59
|
}
|
|
58
60
|
}
|