A fork subagent seeds its child session with a prefix of the parent's log, and that seed becomes the child's persisted log — so a fork child's .jsonl begins with the PARENT's events, including the parent's assistant/chunk events. The snapshot replay harness derived a child's script from its whole log, which would replay the parent's recorded responses as the child's model calls. Spawn-only scenarios never hit it, but a fork snapshot would mis-route silently. Record the seed boundary and skip the inherited prefix at replay: - SessionHeader gains an optional `seedLength` (how many leading events were inherited via a seed), threaded through CreateSessionOptions/CreateAgentOptions meta and stamped by the fork backend (= seeded-prefix length; absent for spawn). It is EXPLICIT, never inferred from seed.length: a resume seeds the whole stored log, so the resume path passes the persisted boundary back. - Both persistence backends round-trip it: JSONL header line, SQLite seed_length column. The SQLite table change bumps SCHEMA_VERSION 2->3; per the pre-release stance the backend rejects an older user_version on open with NO migration. - llm-replay's parseSessionHeader reads seedLength and loadSessionScripts derives a child script from events AFTER the boundary. seedLength is 0 for spawn, so spawn replay is byte-for-byte unchanged. Closes the routing-correctness gap the per-session snapshot replay RFC under- stated; a recorded fork scenario remains a future addition but now derives correctly. RFC: docs/rfc/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md. Regression coverage: a fork child fixture whose seeded prefix carries a parent chunk (derived script must exclude it, proven red without the slice); a seedLength persistence round-trip through the shared coordinator contract (both backends); the fork backend stamping it; resume preserving it from the persisted header.
@deepseek-ai/dsh-llm-replay
A replay LLM plugin for keyless snapshot tests. It installs a single llm/stream waterfall listener that short-circuits the waterfall (never calls next()) and yields model streams reconstructed from a recorded session JSONL fixture — so a test can boot the real agent against a fixed model transcript with no API key.
Its consumer is the ACP snapshot harness in examples/acp-agent, which loads this plugin (via cordis.snapshot.yml) in place of a real LLM adapter. The package exists so its derive/parse/replay logic falls under the per-file 100% coverage gate on packages/*/src (the same logic, while it lived under examples/, was outside the gate).
How the fixture works
The fixture IS the persisted session log (<scenario>/session.jsonl). Its assistant/chunk events carry every StreamChunk, so grouping them by (turn, step) reconstructs each stream() call's chunk sequence (one model call per loop step). Recording is therefore "run the real agent once and harvest the .jsonl", done by the snapshot harness — this plugin does not record.
Two failure modes are not reconstructable from assistant/chunk alone — a pure throw before any chunk (e.g. an HTTP 401, where the log holds only a turn/end {error} and no chunks) and a cancel/hang (timing, not chunk content). A scenario that needs those supplies an optional sidecar (<scenario>/replay.override.json: a ReplayEntry[]) that REPLACES the derived script.
Nested agents: per-session keying
A scenario where a parent agent delegates to in-process subagents records more than one log: the parent (session.jsonl) plus one per child (session.1.jsonl, …). Each agent runs as its own Session on the same context, so replay must serve each one its own script.
Replay keys every call by its calling session id (GenerateOptions.sessionId, stamped by the agent loop). Live session ids are freshly random each run and never equal the recorded ones, so a live session binds to a recorded script by first-call order: scripts are ordered by header createdAt (parent first — it streams before it can delegate), and the first live session to make any call claims the first script, the next new session the next, and so on. Each session then advances its own cursor. A call with no sessionId is one anonymous session bound to the primary script, so single-session scenarios behave exactly as before. More distinct live sessions than recorded scripts fails loud.
Config
| Key | Type | Default | Notes |
|---|---|---|---|
file |
string | $DSH_SNAPSHOT_FILE |
Path to the primary (parent) session.jsonl fixture. Required (config or env). |
overrideFile |
string | $DSH_SNAPSHOT_OVERRIDE |
Optional path to a ReplayEntry[] sidecar that replaces the PRIMARY session's derived script. |
childFiles |
string[] | $DSH_SNAPSHOT_CHILD_FILES (path-delimited) |
Recorded subagent child-session logs for a nested scenario; empty for a single-session scenario. |
- id: llm-replay
name: '@deepseek-ai/dsh-llm-replay'
# file/overrideFile/childFiles default to $DSH_SNAPSHOT_FILE /
# $DSH_SNAPSHOT_OVERRIDE / $DSH_SNAPSHOT_CHILD_FILES, set by the snapshot
# harness per scenario.
Exports
installLlmReplay(ctx, config)— install thellm/streamlistener; returns the disposer (HMR safety). Use this in tests to drive replay without the Loader or env vars.loadSessionScripts(config)— resolve the orderedSessionScript[](primary + children) for a scenario, ready to bind to live sessions in first-call order.loadReplayScript(config)— resolve theReplayEntry[]for the PRIMARY session only (sidecar override if present, else derived from the JSONL; fail-loud if the fixture is missing).deriveReplayScript(events)/parseSessionLog(text)/parseSessionHeader(text)— the pure helpers that turn a recorded session log into a script and read its headerid/createdAt. A derived group must end in afinishchunk; a group without one is the fingerprint of a thrownstream()and must instead be expressed via an override sidecar.- Types
ReplayEntry/SessionScript/ReplayConfig/Config.
Plugin export shape
Named name / inject / Config / apply, with no default export: the cordis Loader's unwrapExports does exports.default ?? exports, so a stray default would collapse the module to the bare function and drop the inject namespace (see docs/postmortem/0001).