Persist the seed boundary so fork-child replay routes correctly

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.
This commit is contained in:
Tianyi Cui
2026-06-22 20:55:32 +08:00
parent c4ba1bd65a
commit b3d40d427e
19 changed files with 209 additions and 40 deletions

View File

@@ -106,6 +106,10 @@ describe('dsh-subagent-fork', () => {
expect(seededUser).toBeDefined()
// Lineage stamped.
expect(child.session.header.parentSession).toBe(parent.session.header.id)
// The seed boundary is recorded on the header (= the seeded prefix length),
// so a reload / replay harness can tell the inherited prefix from the
// child's own events.
expect(child.session.header.seedLength).toBe(parentPrefixLen)
await run.dispose()
})

View File

@@ -122,6 +122,9 @@ export function startInProcessRun(
meta: {
...parentHeader.cwd !== undefined ? { cwd: parentHeader.cwd } : {},
parentSession: parentHeader.id,
// Record the seed boundary so a reload (and a replay harness) can tell the
// inherited prefix from the child's OWN events. 0 for a fresh spawn.
...seedLength > 0 ? { seedLength } : {},
},
...options.seed !== undefined ? { seed: options.seed } : {},
agentOptions,