diff --git a/docs/specs/2026-08-30-130-dsh-alpha1-session-compatibility.md b/docs/specs/2026-08-30-130-dsh-alpha1-session-compatibility.md new file mode 100644 index 00000000..4de8952e --- /dev/null +++ b/docs/specs/2026-08-30-130-dsh-alpha1-session-compatibility.md @@ -0,0 +1,202 @@ +# DeepSeek Harness alpha.1 Session compatibility + +## Traceability + +- Spec ID: `2026-08-30-130-dsh-alpha1-session-compatibility` +- Story: `#130` +- Status: `Implemented` + +## Intent + +Extend the existing fail-closed DeepSeek Harness (`dsh`) Session persistence +adapter from the qualified `0.1.1-rc.2` contract to the known +`0.1.2-alpha.1` persisted contract without expanding Better Harness P0 +semantics. + +This is forward compatibility for a candidate prerelease. It does not replace +rc.2 as the qualified baseline. The compatibility boundary is pinned to: + +- original Better Harness implementation base `057ce8689a9f4f38399f2f0ff72049c37c380446`; +- final delivery base `177714970ca204ec98defcead9d1510b259202e1`; +- DSH `0.1.1-rc.2` at `b150a551b8d465e31e418e1b2eaf5e79bbb7d28e`; +- DSH `0.1.2-alpha.1` at `cd5ef8148158c3a752a658978873241fdf8e2bbc`. + +The canonical owners remain the existing DSH Session adapter and its focused +Session regression test. The change recognizes only the observed physical +provenance encoding and three required metadata events described below. + +## Acceptance scenarios + +### AC-1 — Alpha provenance ranges + +Valid alpha.1 `Array` `sourceEventSeqs` persistence +is validated and expanded to the existing scalar semantic representation. +Valid mixed scalar/range forms follow native alpha.1 semantics. Malformed, +unordered, overlapping, unsafe, reversed, or out-of-bound range forms remain +fail-closed where native invariants require it. + +### AC-2 — rc.2 provenance compatibility + +Existing rc.2 scalar provenance behavior is unchanged. Alpha range-only strict +ordering rules are not imposed on legacy pure-scalar input where rc.2 did not +require them. + +### AC-3 — New required event vocabulary + +Exactly these three alpha.1 persisted events are recognized: + +- `model/selection`; +- `session-log-deepseek/delivery-accepted`; +- `subagent/model-selection-policy`. + +Their native payload contracts are exactly validated. They remain +non-conversation, non-normalized metadata unless existing Better Harness +semantics explicitly require otherwise. + +### AC-4 — Delivery invariants + +`session-log-deepseek/delivery-accepted` enforces native alpha.1 relationship +semantics: + +- `sessionId` is non-empty; +- `throughSeq` is a safe non-negative integer; +- `throughSeq < event.seq`; +- a non-inherited event identifies the containing Session; +- a valid inherited parent marker retains the parent Session identity. + +Tests explicitly cover foreign non-inherited Session rejection, equal and +future watermark rejection, and valid inherited parent-marker acceptance. + +### AC-5 — Subagent policy invariants + +`subagent/model-selection-policy` requires a non-empty `allowedModels` array, +non-empty provider/model routes, and rejection of duplicate provider/model +routes. A direct duplicate-route regression exists. + +### AC-6 — Fail-closed safety + +Malformed known alpha events, malformed provenance ranges, genuinely unknown +required future events, and committed corrupt Session records remain rejected. +Existing rc.2 unknown-ignorable behavior remains unchanged. + +### AC-7 — Persistence parity + +Compatible semantics are verified for raw JSONL, Zstandard, and packed/chunked +alpha persistence. All formats converge through one canonical +physical-to-semantic normalization seam. + +### AC-8 — Native compatibility evidence + +At least one native alpha.1 artifact produced through first-party Session +persistence proves the new physical contract is accepted by Better Harness. +Existing qualification evidence may be used only when it remains reproducible +and tied to the exact alpha.1 SHA. + +### AC-9 — Runtime matrix + +Focused compatibility checks pass under Node `22.20.0` and a repository- +supported Node 24.x runtime. + +### AC-10 — Scope + +No changes are made to Profile, Preset, Cordis, Skill discovery, Instructions, +report rendering, Evidence Bundle schema, installation, dependencies, +capability flags, or the official qualified DSH baseline. + +## Non-goals + +- Full DSH `0.1.2-alpha.1` feature parity. +- Profile, Preset, or Cordis runtime evaluation. +- Reporting the new model-selection metadata. +- MCP, ACP, or plugin expansion. +- Changing DSH installation guidance. +- Promoting alpha.1 as the supported baseline. +- Generic future-event acceptance. +- A generic future-proof Session schema framework. +- Changing torn-tail semantics. + +## Plan and tasks + +1. Confirm Issue #130 and the pinned native upstream contracts. +2. Lock provenance range semantics in regression tests. +3. Lock the three event payload contracts. +4. Add the missing delivery relationship regressions. +5. Add the duplicate model-route regression. +6. Update the pinned known-event test catalog. +7. Verify rc.2 backward compatibility. +8. Verify alpha raw, Zstandard, and packed behavior. +9. Run focused validation under Node 22 and Node 24. +10. Run required repository gates and generated-document checks. +11. Perform the Review Readiness Check. + +No additional production implementation is planned. If a new regression proves +the existing implementation wrong, work stops for a separate correctness +decision rather than silently changing the adapter. + +## Risks + +### Fail-open risk + +Overly permissive validation could accept unknown future DSH semantics. + +Mitigation: retain the exact event allowlist, exact payload schemas, and the +unknown-required-event regression. + +### Backward-compatibility risk + +Alpha range validation could accidentally tighten valid rc.2 scalar +provenance. + +Mitigation: retain an explicit legacy scalar-order regression. + +### Resource-exhaustion risk + +Range expansion could allocate unbounded sequence lists. + +Mitigation: bound expansion before allocation using the authoritative event +sequence and native constraints. + +### Semantic-expansion risk + +New metadata events could accidentally become conversation or report +semantics. + +Mitigation: keep them known, exactly validated, and non-normalized. + +## Test and review evidence + +The implementation qualification supplied before this traceability phase is +the native evidence for AC-8: first-party alpha.1 raw, Zstandard, packed, and +metadata artifacts at exact DSH commit +`cd5ef8148158c3a752a658978873241fdf8e2bbc` were accepted after the compatibility +change, while the qualified rc.2 baseline remained green. This phase did not +recreate those native artifacts. An independent source review additionally +verified the normalization seam, range decoder, three event validators, rc.2 +compatibility, fail-closed behavior, and scope against both pinned DSH commits. + +Current local evidence from this phase: + +| Acceptance criteria | Command or review | Observed result | +| --- | --- | --- | +| AC-3, AC-4, AC-5 | Disposable detached `upstream/main` worktree with only the five new relationship/policy assertions, then `vitest run test/sessions/session-analysis-dsh-discovery.test.mjs` | RED: 1 file ran; 62 tests passed and all 5 new regressions failed because the pre-fix adapter returned `DSH_UNKNOWN_REQUIRED_EVENT`. The disposable worktree was removed. | +| AC-1 through AC-7, AC-9 | Post-rebase Node `22.20.0`: `node ./node_modules/vitest/vitest.mjs run test/sessions/session-analysis-dsh-discovery.test.mjs test/sessions/session-analysis-dsh-fixtures.test.mjs test/sessions/session-analysis-dsh-provider.test.mjs` | 3 files passed; 112 tests passed; 0 skipped. | +| AC-1 through AC-7, AC-9 | Post-rebase Node `24.15.0`: the same three-file Vitest command | 3 files passed; 112 tests passed; 0 skipped. | +| AC-1 through AC-7 | Post-rebase `npm test` with the required local loopback access | 107 files passed; 1,613 tests passed; 2 skipped. | +| Repository documentation | `node scripts/doc-link-graph/cli.mjs skills/better-harness` | Parsed 13 seed docs; generated a 39-file/56-link graph; `docs/better-harness-doc-links.mmd` remained unchanged. | +| Repository documentation | `npx vitest run test/skills-docs/doc-link-graph.test.mjs` | 1 file passed; 8 tests passed. | +| Repository generated sources | `npm run harness:generated` | PASS; Langium generation completed and the generated-source diff was clean. | +| Repository Harness package | `npm run harness:test` | 20 files passed; 173 tests passed. | +| Repository package boundary | Post-rebase `npm run pack:verify` | PASS; npm package contained 619 entries and the runtime zip contained 881 entries. | +| AC-10 and repository hygiene | Rebase and scope review; `git diff --check`; production-diff SHA-256 before and after rebasing onto `177714970ca204ec98defcead9d1510b259202e1` | No overlapping upstream owner or out-of-scope file changed; whitespace check passed; the adapter diff remained `da4e9887eb4b418da043ac61e9f97c8127727150ab60968b2d8a5bd4476ff74b`. | + +Issue #130, this Spec, the AC-mapped regressions, risk controls, and current +gate receipts form the implementation evidence chain. GitHub did not apply the +required `bug` label during Issue creation, and the authenticated account lacks +`AddLabelsToLabelable`. Maintainer phodal subsequently confirmed +`@Cobb04 welcome to PR`; the form-controlled label limitation therefore does +not block delivery and does not change the implemented AC status. + +AI involvement: Codex generated the local Spec and regression additions and ran +the recorded commands. Cobb04 supplied the compatibility boundary, independent +review requirements, and final delivery authorization. The delivery commit +uses the repository's single Codex co-author marker convention. diff --git a/scripts/session-analysis/platforms/dsh.mjs b/scripts/session-analysis/platforms/dsh.mjs index a9b89d92..ab6118e7 100644 --- a/scripts/session-analysis/platforms/dsh.mjs +++ b/scripts/session-analysis/platforms/dsh.mjs @@ -34,8 +34,9 @@ const VALIDATED_KNOWN_UNSUPPORTED_TYPES = new Set([ "approval/policy", "command/done", "command/run", "compaction/end", "compaction/prune", "compaction/start", "compaction/summary", "feedback/record", "goal/change", "hook/invoked", "hook/result", "llm/retry", "llm/retry-started", "permission/preset", "plan/mode", - "request/context", "request/header", "sandbox/mode", "session/end-seed", "session/title", - "session/title-llm-request", "subagent/descriptor", "todo/write", "tool-workflow/agent-end", + "model/selection", "request/context", "request/header", "sandbox/mode", "session/end-seed", + "session/title", "session/title-llm-request", "session-log-deepseek/delivery-accepted", + "subagent/descriptor", "subagent/model-selection-policy", "todo/write", "tool-workflow/agent-end", "team/member", "team/message/delivered", "team/message/queued", "team/task", "tool-workflow/agent-start", "tool-workflow/run-end", "tool-workflow/run-start", "tool/code-dispatch", "tool/code-dispatch-start", "schedule/change", "web/deepseek-search-llm-request", @@ -46,9 +47,10 @@ const KNOWN_EVENT_TYPES = new Set([ "approval/policy", "assistant/chunk", "assistant/message", "command/done", "command/run", "compaction/end", "compaction/prune", "compaction/start", "compaction/summary", "feedback/record", "goal/change", "hook/invoked", "hook/result", "llm/retry", - "llm/retry-started", "permission/preset", "plan/mode", "request/context", "request/header", - "sandbox/mode", "schedule/change", "session/end-seed", "session/title", - "session/title-llm-request", "step/end", "step/start", "subagent/descriptor", "todo/write", + "llm/retry-started", "model/selection", "permission/preset", "plan/mode", "request/context", + "request/header", "sandbox/mode", "schedule/change", "session/end-seed", "session/title", + "session/title-llm-request", "session-log-deepseek/delivery-accepted", "step/end", "step/start", + "subagent/descriptor", "subagent/model-selection-policy", "todo/write", "team/member", "team/message/delivered", "team/message/queued", "team/task", "tool-workflow/agent-end", "tool-workflow/agent-start", "tool-workflow/run-end", "tool-workflow/run-start", "tool/call", "tool/code-dispatch", "tool/code-dispatch-start", @@ -698,10 +700,35 @@ function validateSupportedEvent(event) { } } +function validateProviderModelRoute(route) { + return exactKeys(route, ["provider", "model"]) + && nonemptyString(route.provider) && nonemptyString(route.model); +} + // Pinned core payloads that are required for replay but intentionally omitted from BH normalization. function validateKnownUnsupportedEvent(event) { const data = event.data; switch (event.type) { + case "model/selection": + if (!exactKeys(data, ["provider", "model"], ["reasoningEffort"]) + || !nonemptyString(data.provider) || !nonemptyString(data.model) + || (Object.hasOwn(data, "reasoningEffort") && !nonemptyString(data.reasoningEffort))) { + fail("DSH_EVENT_SHAPE_DRIFT"); + } + break; + case "session-log-deepseek/delivery-accepted": + if (!exactKeys(data, ["sessionId", "throughSeq"]) || !nonemptyString(data.sessionId) + || !safeNonnegative(data.throughSeq)) fail("DSH_EVENT_SHAPE_DRIFT"); + break; + case "subagent/model-selection-policy": { + if (!exactKeys(data, ["allowedModels"]) || !Array.isArray(data.allowedModels) + || data.allowedModels.length === 0 || data.allowedModels.some((route) => !validateProviderModelRoute(route))) { + fail("DSH_EVENT_SHAPE_DRIFT"); + } + const routes = data.allowedModels.map((route) => `${route.provider}\0${route.model}`); + if (new Set(routes).size !== routes.length) fail("DSH_EVENT_SHAPE_DRIFT"); + break; + } case "permission/preset": if (!exactKeys(data, ["preset"], ["origin"]) || typeof data.preset !== "string" || (Object.hasOwn(data, "origin") @@ -1606,6 +1633,13 @@ function validateSequence(events, header) { applyCompactionFold(compaction, event, openTurn, durableSuffixStart); applySurfaceEvent(event, surfaceNodes, events); const data = event.data; + if (event.type === "session-log-deepseek/delivery-accepted") { + const inherited = header.parentSession !== undefined && header.seedLength !== undefined + && event.seq < header.seedLength; + if ((!inherited && data.sessionId !== header.id) || data.throughSeq >= event.seq) { + fail("DSH_EVENT_RELATIONSHIP_INVALID"); + } + } if (event.type === "agent/inbox/spliced" && index >= durableSuffixStart) applyInboxSplice(data, queues); if (event.type === "schedule/change" && index >= durableSuffixStart) applyScheduleChange(data, schedules); applyGoalFold(goal, event); @@ -1790,11 +1824,37 @@ function parseDshJsonlLine(input) { } } -function expandDshStorageRecord(record) { - const expanded = packedRow(record); +function expandDshSourceEventSeqs(record, expectedSeq) { + if (!Object.hasOwn(record, "sourceEventSeqs")) return record; + if (record.seq !== expectedSeq || !safeNonnegative(record.seq) + || !Array.isArray(record.sourceEventSeqs)) fail("DSH_EVENT_SHAPE_DRIFT"); + const decoded = []; + let hasRange = false; + for (const entry of record.sourceEventSeqs) { + if (typeof entry === "number") { + if (!safeNonnegative(entry) || decoded.length >= expectedSeq) fail("DSH_EVENT_SHAPE_DRIFT"); + decoded.push(entry); + continue; + } + if (!Array.isArray(entry) || entry.length !== 2) fail("DSH_EVENT_SHAPE_DRIFT"); + const [start, end] = entry; + if (!safeNonnegative(start) || !safeNonnegative(end) || end < start + || end - start + 1 > expectedSeq - decoded.length) fail("DSH_EVENT_SHAPE_DRIFT"); + for (let seq = start; seq <= end; seq += 1) decoded.push(seq); + hasRange = true; + } + if (hasRange && !strictlyIncreasing(decoded)) fail("DSH_EVENT_SHAPE_DRIFT"); + return { ...record, sourceEventSeqs: decoded }; +} + +function expandDshStorageRecord(record, expectedSeq) { + const semanticRecord = expandDshSourceEventSeqs(record, expectedSeq); + const expanded = packedRow(semanticRecord); if (expanded) return expanded; - if (typeof record.type === "string" && record.type.endsWith("-chunks")) fail("DSH_UNSUPPORTED_PACKED_ROW"); - return [record]; + if (typeof semanticRecord.type === "string" && semanticRecord.type.endsWith("-chunks")) { + fail("DSH_UNSUPPORTED_PACKED_ROW"); + } + return [semanticRecord]; } function scanDshJsonlPrefix(input, { requireComplete = false } = {}) { @@ -1811,7 +1871,7 @@ function scanDshJsonlPrefix(input, { requireComplete = false } = {}) { newline = buffer.indexOf(0x0A, lineStart)) { let expanded; try { - expanded = expandDshStorageRecord(parseDshJsonlLine(buffer.subarray(lineStart, newline))); + expanded = expandDshStorageRecord(parseDshJsonlLine(buffer.subarray(lineStart, newline)), events.length); } catch (error) { issue ??= error; lineStart = newline + 1; diff --git a/test/sessions/session-analysis-dsh-discovery.test.mjs b/test/sessions/session-analysis-dsh-discovery.test.mjs index 4ef215e2..6064891d 100644 --- a/test/sessions/session-analysis-dsh-discovery.test.mjs +++ b/test/sessions/session-analysis-dsh-discovery.test.mjs @@ -97,6 +97,37 @@ function insertBeforeTurnEnd(rows, events) { return output; } +function makeAlpha1PackedRangeRows() { + const header = makeDshHeader({ parentSession: undefined, seedLength: undefined, origin: undefined, + delegationDepth: 0, agentPreset: undefined }); + const storage = makePackedDshStorageRows().map((row) => ({ ...row, seq0: row.seq0 + 2 })); + const assistant = structuredClone(makeSupportedDshSessionRows() + .find((row) => row.type === "assistant/message")); + assistant.seq = 11; + assistant.time = header.createdAt + 2_000; + assistant.sourceEventSeqs = [[2, 10]]; + return [ + header, + makeDshEvent("turn/start", { turn: 1 }, { seq: 0, time: header.createdAt + 900 }), + makeDshEvent("step/start", { turn: 1, step: 1 }, { seq: 1, time: header.createdAt + 950 }), + ...storage, + assistant, + makeDshEvent("step/end", { turn: 1, step: 1 }, { seq: 12, time: header.createdAt + 2_010 }), + makeDshEvent("turn/end", { turn: 1, reason: { kind: "completed" } }, { + seq: 13, time: header.createdAt + 2_020, + }), + ]; +} + +function makeAlpha1MetadataEvent(type, header, seq, time) { + const data = type === "model/selection" + ? { provider: "fixture-provider", model: "fixture-model", reasoningEffort: "high" } + : type === "session-log-deepseek/delivery-accepted" + ? { sessionId: header.id, throughSeq: seq - 1 } + : { allowedModels: [{ provider: "fixture-provider", model: "fixture-model" }] }; + return makeDshEvent(type, data, { seq, time }); +} + function requestHeaderEvent({ reason = "initial", config = { provider: "fixture-provider", model: "fixture-model" }, @@ -128,9 +159,10 @@ const PINNED_KNOWN_EVENT_TYPES = [ "approval/policy", "assistant/chunk", "assistant/message", "command/done", "command/run", "compaction/end", "compaction/prune", "compaction/start", "compaction/summary", "feedback/record", "goal/change", "hook/invoked", "hook/result", "llm/retry", - "llm/retry-started", "permission/preset", "plan/mode", "request/context", "request/header", + "llm/retry-started", "model/selection", "permission/preset", "plan/mode", "request/context", "request/header", "sandbox/mode", "schedule/change", "session/end-seed", "session/title", - "session/title-llm-request", "step/end", "step/start", "subagent/descriptor", "todo/write", + "session/title-llm-request", "session-log-deepseek/delivery-accepted", "step/end", "step/start", + "subagent/descriptor", "subagent/model-selection-policy", "todo/write", "team/member", "team/message/delivered", "team/message/queued", "team/task", "tool-workflow/agent-end", "tool-workflow/agent-start", "tool-workflow/run-end", "tool-workflow/run-start", "tool/call", "tool/code-dispatch", "tool/code-dispatch-start", @@ -375,6 +407,49 @@ test("all three pinned packed rows expand losslessly and malformed packed shapes } }); +test("alpha.1 storage provenance ranges expand before raw and zstd Session validation", () => { + const rows = makeAlpha1PackedRangeRows(); + const raw = decodeDshJsonl(encodeDshRawJsonl(rows)); + assert.deepEqual(raw.events[11].sourceEventSeqs, [2, 3, 4, 5, 6, 7, 8, 9, 10]); + assert.deepEqual(raw.events.map((event) => event.seq), raw.events.map((_, index) => index)); + + if (typeof zlib.zstdCompressSync !== "function" || typeof zlib.zstdDecompressSync !== "function" + || !Number.isSafeInteger(zlib.constants?.ZSTD_c_checksumFlag)) return; + const compressed = makeDshZstdArtifact([[rows[0]], rows.slice(1)]); + const zstd = decodeDshArtifact(compressed.artifact, { compressed: true }); + assert.deepEqual(zstd.events, raw.events); +}); + +test("alpha.1 storage provenance accepts native mixed form while preserving scalar rc.2 order", () => { + const ranged = makeSupportedDshSessionRows(); + ranged[5].sourceEventSeqs = [0, [1, 2]]; + assert.deepEqual(decodeDshJsonl(encodeDshRawJsonl(ranged)).events[4].sourceEventSeqs, [0, 1, 2]); + + const scalar = makeSupportedDshSessionRows(); + scalar[5].sourceEventSeqs = [2, 0]; + assert.deepEqual(decodeDshJsonl(encodeDshRawJsonl(scalar)).events[4].sourceEventSeqs, [2, 0]); +}); + +test("alpha.1 storage provenance ranges reject malformed or unsafe expansion", () => { + const invalid = [ + [[2, 1]], + [[1]], + [[0, 1, 2]], + [["0", 2]], + [[0.5, 2]], + [[[0, 1], 2]], + [[0, 2], [2, 3]], + [[2, 3], 0], + [[0, 4]], + [[0, Number.MAX_SAFE_INTEGER]], + ]; + for (const sourceEventSeqs of invalid) { + const rows = makeSupportedDshSessionRows(); + rows[5].sourceEventSeqs = sourceEventSeqs; + assert.throws(() => decodeDshJsonl(encodeDshRawJsonl(rows)), stableError("DSH_EVENT_SHAPE_DRIFT")); + } +}); + test("pinned types.ts accepts all seven raw StreamChunk variants and accounts them as unsupported evidence", () => { const chunks = [ { type: "block-start", index: 0, blockType: "text" }, @@ -838,6 +913,84 @@ test("known unsupported and unknown ignorable events are accounted, while open t assert.deepEqual(await readFile(written.filePath), before); }); +test.each([ + "model/selection", + "session-log-deepseek/delivery-accepted", + "subagent/model-selection-policy", +])("alpha.1 required metadata event %s is validated and accounted without normalization", async (type) => { + const base = makeSupportedDshSessionRows(); + const turnEnd = base.at(-1); + const event = makeAlpha1MetadataEvent(type, base[0], turnEnd.seq, turnEnd.time - 1); + const rows = appendEvent(base, event); + const decoded = decodeDshJsonl(encodeDshRawJsonl(rows)); + assert.equal(decoded.events.some((candidate) => candidate.type === type), true); + assert.equal(decoded.diagnostics.knownUnsupportedTypes.includes(type), true); + + const root = await tempRoot("dsh-alpha1-metadata-"); + const home = path.join(root, "home"); + const workspace = path.join(root, "workspace"); + rows[0].cwd = workspace; + await writeNestedDshArtifact({ dshHome: home, rows }); + const { analyzer, scope, sessions } = await inventory(home, workspace); + const normalized = await analyzer.readSession(sessions[0], scope); + assert.equal(normalized.some((candidate) => candidate.nativeType === type), false); +}); + +test.each([ + ["model/selection", { provider: "fixture-provider" }], + ["session-log-deepseek/delivery-accepted", { sessionId: "", throughSeq: 0 }], + ["subagent/model-selection-policy", { allowedModels: [] }], +])("alpha.1 required metadata event %s rejects malformed payloads", (type, data) => { + const base = makeSupportedDshSessionRows(); + const turnEnd = base.at(-1); + const rows = appendEvent(base, makeDshEvent(type, data, { seq: turnEnd.seq, time: turnEnd.time - 1 })); + assert.throws(() => decodeDshJsonl(encodeDshRawJsonl(rows)), stableError("DSH_EVENT_SHAPE_DRIFT")); +}); + +test.each([ + ["foreign non-inherited session", (header, seq) => ({ sessionId: `${header.id}-foreign`, throughSeq: seq - 1 })], + ["equal watermark", (header, seq) => ({ sessionId: header.id, throughSeq: seq })], + ["future watermark", (header, seq) => ({ sessionId: header.id, throughSeq: seq + 1 })], +])("alpha.1 delivery marker rejects %s", (_case, makeData) => { + const base = makeSupportedDshSessionRows({ parentSession: undefined, seedLength: undefined, + origin: undefined, delegationDepth: 0 }); + const turnEnd = base.at(-1); + const event = makeDshEvent("session-log-deepseek/delivery-accepted", makeData(base[0], turnEnd.seq), { + seq: turnEnd.seq, time: turnEnd.time - 1, + }); + assert.throws(() => decodeDshJsonl(encodeDshRawJsonl(appendEvent(base, event))), + stableError("DSH_EVENT_RELATIONSHIP_INVALID")); +}); + +test("alpha.1 delivery marker accepts inherited parent identity before seedLength", () => { + const rows = makeSupportedDshSessionRows({ sessionId: "fixture-child", parentSession: "fixture-parent", + seedLength: 2, origin: "subagent", delegationDepth: 1 }); + rows.splice(2, 0, makeDshEvent("session-log-deepseek/delivery-accepted", { + sessionId: rows[0].parentSession, + throughSeq: 0, + }, { seq: 1, time: rows[1].time + 1 })); + rows.slice(1).forEach((event, index) => { event.seq = index; }); + assert.doesNotThrow(() => decodeDshJsonl(encodeDshRawJsonl(rows))); +}); + +test("alpha.1 model-selection policy rejects duplicate provider and model routes", () => { + const base = makeSupportedDshSessionRows(); + const turnEnd = base.at(-1); + const route = { provider: "fixture-provider", model: "fixture-model" }; + const event = makeDshEvent("subagent/model-selection-policy", { + allowedModels: [route, { ...route }], + }, { seq: turnEnd.seq, time: turnEnd.time - 1 }); + assert.throws(() => decodeDshJsonl(encodeDshRawJsonl(appendEvent(base, event))), + stableError("DSH_EVENT_SHAPE_DRIFT")); +}); + +test("alpha.1 compatibility keeps truly unknown required events fail closed", () => { + const base = makeSupportedDshSessionRows(); + const turnEnd = base.at(-1); + const rows = appendEvent(base, makeUnknownRequiredDshEvent({ seq: turnEnd.seq, time: turnEnd.time - 1 })); + assert.throws(() => decodeDshJsonl(encodeDshRawJsonl(rows)), stableError("DSH_UNKNOWN_REQUIRED_EVENT")); +}); + test("unknown ignorable events accept every JSON data class without projecting their payloads", async () => { const root = await tempRoot(); const home = path.join(root, "home");