perf(session-format): stream released v0-to-v2 migrations

This commit is contained in:
imccyu committed 2026-09-06 02:05:27 +08:00
1 parent a84a8da9e1
commit 46196d6f95
31 files changed
+2692 -1122

No files matched your search

@@ -242,6 +242,7 @@ function galleryFixture(imageUrl: string): string {
...(call.meta === undefined ? {} : { meta: call.meta }),
}, { surfaceOp: 'append', sourceEventSeqs: [source.seq] })
}
session.append('step/end', { turn: 1, step: 1 })
session.append('step/start', { turn: 1, step: 2 })
session.append('assistant/message', {
stream: [],
@@ -201,4 +201,4 @@
- text: DeepSeek-V4-Flash
- img
- button "Send message" [disabled]
- text: 1 turns · 1 steps LLM {{duration}} · Tool call {{duration}}
- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}}
+8 -12
View File
@@ -869,19 +869,15 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
* in-memory record-mode harvest, so the on-disk zstd default never matters.
*/
function rawSessionLog(session: Session): string {
const encoded = sessionFormatCatalog.encodeCurrent({
header: {
...session.header,
delegationDepth: session.header.delegationDepth ?? 0,
},
inheritedEventCount: session.inheritedEventCount,
// Session validates durable payloads as JSON; its closed event unions do
// not carry the index signature used by the format package's JSON types.
events: session.snapshotEvents() as unknown as readonly SessionFormatEvent[],
})
const encodedEvents = (session.snapshotEvents() as unknown as readonly SessionFormatEvent[])
.map(event => sessionFormatCatalog.encodeCurrentEvent(event))
const header = sessionFormatCatalog.encodeCurrentHeader({
...session.header,
delegationDepth: session.header.delegationDepth ?? 0,
}, session.inheritedEventCount)
return [
JSON.stringify(encoded.header),
...encoded.rows.map(record => JSON.stringify(record)),
JSON.stringify(header),
...encodedEvents.map(record => JSON.stringify(record)),
'',
].join('\n')
}
@@ -13,13 +13,16 @@ import { assertReleasedV2Header, releasedV2SessionFormatCodec, restoreReleasedV2
export const sessionFormatCatalog = createSessionFormatCatalog({
currentVersion: 2,
codecs: [releasedV0SessionFormatCodec, releasedV1SessionFormatCodec, releasedV2SessionFormatCodec],
encodeCurrentArtifact: artifact => releasedV2SessionFormatCodec.encodeArtifact(artifact),
currentEncoder: releasedV2SessionFormatCodec,
migrations: [sessionFormatV0ToV1, sessionFormatV1ToV2],
restoreCurrent(artifact) {
const restored = restoreReleasedV2Artifact(artifact, KNOWN_SESSION_EVENT_TYPES)
validateInstalledCurrentSessionArtifact(restored)
return restored
},
restoreTransformedCurrent(artifact) {
return restoreReleasedV2Artifact(artifact, KNOWN_SESSION_EVENT_TYPES)
},
restoreCurrentHeader(header) {
assertReleasedV2Header(header)
validateInstalledCurrentSessionHeader(header)
@@ -27,10 +27,11 @@ describe('first-party Session format catalog', () => {
})
const v1Header = { ...header, version: 1 }
const current = sessionFormatCatalog.decodeArtifact(v1Header, [
{ type: 'turn/start', seq: 0, time: 2, data: { turn: 1 } },
])
expect(sessionFormatCatalog.migrate(current)).toMatchObject({
const restore = sessionFormatCatalog.createRestore(v1Header, {
recovery: 'strict', validation: 'current',
})
restore.decodeRow({ type: 'turn/start', seq: 0, time: 2, data: { turn: 1 } })
expect(restore.finish()).toMatchObject({
header: { version: 2, id: 'catalog' },
})
})
@@ -39,21 +40,38 @@ describe('first-party Session format catalog', () => {
const header = {
type: 'session', version: 2, id: 'current-growth', createdAt: 1, isSeeded: false, delegationDepth: 0,
}
const extended = sessionFormatCatalog.decodeArtifact(header, [{
const restore = (rows: readonly unknown[]) => {
const current = sessionFormatCatalog.createRestore(header, {
recovery: 'strict', validation: 'current',
})
for (const row of rows) current.decodeRow(row)
return current.finish()
}
const extended = restore([{
type: 'turn/start', seq: 0, time: 1, data: { turn: 1, postReleaseMember: true },
}])
expect(sessionFormatCatalog.migrate(extended).events).toEqual(extended.events)
const unknownRequired = sessionFormatCatalog.decodeArtifact(header, [{
type: 'ordinary/not-installed', seq: 0, time: 1, data: 'future',
expect(extended.events).toEqual([{
type: 'turn/start', seq: 0, time: 1, data: { turn: 1, postReleaseMember: true },
}])
expect(() => sessionFormatCatalog.migrate(unknownRequired)).toThrow(/unknown event type/)
const extension = sessionFormatCatalog.decodeArtifact(header, [{
expect(() => restore([{
type: 'ordinary/not-installed', seq: 0, time: 1, data: 'future',
}])).toThrow(/unknown event type/)
const extension = restore([{
type: 'ordinary/external', seq: 0, time: 1, data: null, ignorable: true,
}])
expect(sessionFormatCatalog.migrate(extension).events).toEqual([{
expect(extension.events).toEqual([{
type: 'ordinary/external', seq: 0, time: 1, data: null, ignorable: true,
}])
})
it('validates complete relationships after streaming migration', () => {
const stream = sessionFormatCatalog.createRestore({
type: 'session', version: 1, id: 'invalid-stream', createdAt: 1, delegationDepth: 0,
}, { recovery: 'strict', validation: 'current' })
stream.decodeRow({ type: 'step/start', seq: 0, time: 2, data: { turn: 1, step: 1 } })
expect(() => stream.finish()).toThrow(/open turn/)
})
})
@@ -1,32 +1,46 @@
import {
SessionFormatError,
isSessionFormatJsonObject,
sessionFormatCount,
sessionFormatSafeInteger,
snapshotSessionFormatArtifact,
snapshotSessionFormatJson,
} from '@deepseek-ai/dsh-session-format'
import type {
EncodedSessionFormatArtifact,
SessionFormatArtifact,
SessionFormatArtifactDecoder,
SessionFormatCodec,
SessionFormatEncodeOptions,
SessionFormatEvent,
SessionFormatEventRun,
SessionFormatHeader,
SessionFormatJsonObject,
SessionFormatJsonValue,
SessionFormatMigrationContext,
SessionFormatRecovery,
} from '@deepseek-ai/dsh-session-format'
import {
assertReleasedSessionFormatHeader,
assertReleasedV0SourceArtifact,
assertReleasedV1PhysicalArtifact,
} from './validation.ts'
import { assertReleasedSessionFormatHeader } from './validation.ts'
import { assertReleasedV0Keys, releasedV0Record } from './validation-helpers.ts'
const PHYSICAL_HEADER_REQUIRED = ['type', 'version', 'id', 'createdAt', 'delegationDepth'] as const
const PHYSICAL_HEADER_OPTIONAL = ['cwd', 'parentSession', 'seedLength', 'origin', 'agentPreset'] as const
const PACKED_TAGS = new Set(['text-chunks', 'reasoning-chunks', 'tool-call-chunks'])
/** A released packed Assistant row retained until v1-to-v2 embeds its compact stream. */
export interface ReleasedAssistantChunkRun extends SessionFormatEventRun {
readonly runType: 'released-assistant-chunks'
readonly turn: number
readonly step: number
readonly lastSeq: number
readonly lastTime: number
readonly stream: SessionFormatJsonObject
}
/**
* Test whether a compact migration item is a released packed Assistant row.
* @param run - compact migration item to classify.
* @returns whether the item carries the released Assistant chunk representation.
*/
export function isReleasedAssistantChunkRun(run: SessionFormatEventRun): run is ReleasedAssistantChunkRun {
return run.runType === 'released-assistant-chunks'
}
/** Frozen physical JSON codec for the released v0 layout. */
export const releasedV0SessionFormatCodec = createReleasedCodec(0)
@@ -37,40 +51,88 @@ function createReleasedCodec(version: 0 | 1) {
return Object.freeze({
version,
decodeHeader: (value: unknown) => decodeHeader(value, version),
decodeArtifact(headerValue: unknown, rowValues: readonly unknown[]) {
createDecoder(headerValue: unknown, recovery: SessionFormatRecovery) {
const physical = decodePhysicalHeader(headerValue, version)
const artifact = snapshotSessionFormatArtifact({
const scanner = scanRows(recovery === 'recoverable')
return {
header: physical.header,
inheritedEventCount: physical.inheritedEventCount,
events: scanRows(rowValues, false).events,
}, `released v${version} artifact`)
if (version === 0) assertReleasedV0SourceArtifact(artifact)
else assertReleasedV1PhysicalArtifact(artifact)
return artifact
headerInheritedEventCount: physical.inheritedEventCount,
decodeRow: (rowValue, context) => { scanner.decodeRow(rowValue, context) },
finish(_context) {
scanner.finish(physical.inheritedEventCount)
return physical.inheritedEventCount
},
} satisfies SessionFormatArtifactDecoder
},
decodeRecoverableArtifact(headerValue: unknown, rowValues: readonly unknown[]) {
const physical = decodePhysicalHeader(headerValue, version)
const recovered = scanRows(rowValues, true)
const artifact = snapshotSessionFormatArtifact({
header: physical.header,
inheritedEventCount: physical.inheritedEventCount,
events: recovered.events,
}, `released v${version} recoverable artifact`)
if (version === 0) assertReleasedV0SourceArtifact(artifact)
else assertReleasedV1PhysicalArtifact(artifact)
return artifact
} satisfies SessionFormatCodec)
}
function scanRows(
recoverable: boolean,
): {
decodeRow(
rowValue: unknown,
context: SessionFormatMigrationContext,
): void
finish(inheritedEventCount: number): void
} {
let rowIndex = 0
let eventCount = 0
let issue: SessionFormatError | undefined
return {
decodeRow(rowValue, context) {
const currentRow = rowIndex
rowIndex += 1
let packed = false
let decoded: SessionFormatEvent | ReleasedAssistantChunkRun
try {
const record = releasedV0Record(rowValue, `released Session row ${currentRow}`)
const type = record['type']
if (typeof type === 'string' && PACKED_TAGS.has(type)) {
packed = true
decoded = decodePackedRun(record, type, currentRow)
} else {
decoded = decodeEvent(record, currentRow)
}
} catch (error: unknown) {
const current = error instanceof SessionFormatError
? error
: new SessionFormatError(`released Session row ${currentRow} is malformed`, { cause: error })
if (!recoverable) throw current
issue ??= current
return
}
if (issue !== undefined) {
if (!packed && (decoded as SessionFormatEvent).type === 'turn/end') throw issue
return
}
const seq = packed
? (decoded as ReleasedAssistantChunkRun).firstSeq
: (decoded as SessionFormatEvent).seq
if (seq !== eventCount) {
const gap = new SessionFormatError(
`released Session row ${currentRow} has seq gap (expected ${eventCount}, got ${seq})`,
)
if (!recoverable) throw gap
issue = gap
if (!packed && (decoded as SessionFormatEvent).type === 'turn/end') throw gap
return
}
if (packed) {
const run = decoded as ReleasedAssistantChunkRun
eventCount += run.eventCount
context.emitRun(run)
} else {
eventCount += 1
context.emitEvent(decoded as SessionFormatEvent)
}
},
encodeArtifact(artifact: SessionFormatArtifact, options: SessionFormatEncodeOptions) {
if (version === 0) assertReleasedV0SourceArtifact(artifact)
else assertReleasedV1PhysicalArtifact(artifact)
return encodeArtifact(artifact, options, version)
finish(inheritedEventCount) {
if (inheritedEventCount > eventCount) {
throw new SessionFormatError('Session inheritedEventCount exceeds its event count')
}
},
} satisfies SessionFormatCodec & {
encodeArtifact(
artifact: SessionFormatArtifact,
options: SessionFormatEncodeOptions,
): EncodedSessionFormatArtifact
})
}
}
function decodeHeader(value: unknown, version: 0 | 1): SessionFormatHeader {
@@ -109,7 +171,7 @@ function decodePhysicalHeader(
if (record['origin'] !== undefined && record['origin'] !== 'subagent') {
throw new SessionFormatError(`released v${version} header origin must be "subagent"`)
}
const header = snapshotSessionFormatJson({
const header = {
version,
id: record['id'],
createdAt,
@@ -119,101 +181,34 @@ function decodePhysicalHeader(
...(record['origin'] === undefined ? {} : { origin: record['origin'] }),
delegationDepth,
...(record['agentPreset'] === undefined ? {} : { agentPreset: record['agentPreset'] }),
}, `released v${version} logical header`) as SessionFormatHeader
} as SessionFormatHeader
assertReleasedSessionFormatHeader(header, version)
return { header, inheritedEventCount: seedLength }
}
function encodeArtifact(
artifact: SessionFormatArtifact,
options: SessionFormatEncodeOptions,
version: 0 | 1,
): EncodedSessionFormatArtifact {
const header = artifact.header
const physicalHeader = snapshotSessionFormatJson({
type: 'session',
version,
id: header.id,
createdAt: header.createdAt,
...(header.cwd === undefined ? {} : { cwd: header.cwd }),
...(header.parentSession === undefined ? {} : { parentSession: header.parentSession }),
...(header.isSeeded ? { seedLength: artifact.inheritedEventCount } : {}),
...(header.origin === undefined ? {} : { origin: header.origin }),
delegationDepth: header.delegationDepth,
...(header.agentPreset === undefined ? {} : { agentPreset: header.agentPreset }),
}, `released v${version} encoded header`) as SessionFormatJsonObject
const records = options.packChunks ? packChunkRuns(artifact.events) : [...artifact.events]
const rows = Object.freeze(records.map(record => encodeProvenance(record)))
return Object.freeze({ header: physicalHeader, rows })
}
function scanRows(
rowValues: readonly unknown[],
recoverable: boolean,
): { readonly events: readonly SessionFormatEvent[] } {
const events: SessionFormatEvent[] = []
let issue: SessionFormatError | undefined
for (const [rowIndex, value] of rowValues.entries()) {
let decoded: readonly SessionFormatEvent[]
try {
const row = snapshotSessionFormatJson(value, `released Session row ${rowIndex}`)
decoded = decodeRow(row, rowIndex)
} catch (error: unknown) {
const current = error instanceof SessionFormatError
? error
: new SessionFormatError(`released Session row ${rowIndex} is malformed`, { cause: error })
if (!recoverable) throw current
issue ??= current
continue
}
if (issue !== undefined) {
if (decoded.some(event => event.type === 'turn/end')) throw issue
continue
}
const rowStart = events.length
for (const event of decoded) {
if (event.seq !== events.length) {
const gap = new SessionFormatError(
`released Session row ${rowIndex} has seq gap (expected ${events.length}, got ${event.seq})`,
)
events.length = rowStart
if (!recoverable) throw gap
issue = gap
break
}
events.push(event)
}
if (issue !== undefined) {
if (decoded.some(event => event.type === 'turn/end')) throw issue
continue
}
}
return Object.freeze({ events: Object.freeze(events) })
}
function decodeRow(value: SessionFormatJsonValue, rowIndex: number): readonly SessionFormatEvent[] {
const record = releasedV0Record(value, `released Session row ${rowIndex}`)
const type = record['type']
if (typeof type === 'string' && PACKED_TAGS.has(type)) return expandPackedRow(record, type, rowIndex)
function decodeEvent(
record: Record<string, SessionFormatJsonValue>,
rowIndex: number,
): SessionFormatEvent {
if (record['sourceEventSeqs'] !== undefined) {
const seq = sessionFormatCount(record['seq'], `released Session row ${rowIndex} seq`)
return Object.freeze([{
return {
...record,
sourceEventSeqs: decodeSeqRanges(record['sourceEventSeqs'], seq),
} as unknown as SessionFormatEvent])
} as unknown as SessionFormatEvent
}
return Object.freeze([record as unknown as SessionFormatEvent])
return record as unknown as SessionFormatEvent
}
function expandPackedRow(
function decodePackedRun(
row: Record<string, SessionFormatJsonValue>,
type: string,
rowIndex: number,
): readonly SessionFormatEvent[] {
): ReleasedAssistantChunkRun {
const label = `released ${type} row ${rowIndex}`
assertReleasedV0Keys(row, ['type', 'seq0', 'time0', 'data'], [], label)
const seq0 = sessionFormatCount(row['seq0'], `${label} seq0`)
let time = sessionFormatSafeInteger(row['time0'], `${label} time0`)
const time0 = sessionFormatSafeInteger(row['time0'], `${label} time0`)
const data = releasedV0Record(row['data'], `${label} data`)
const isTool = type === 'tool-call-chunks'
assertReleasedV0Keys(
@@ -230,37 +225,73 @@ function expandPackedRow(
if (!Array.isArray(gaps) || gaps.length !== payload.length - 1) {
throw new SessionFormatError(`${label} dt length must match its payload`)
}
for (const gap of gaps) sessionFormatSafeInteger(gap, `${label} dt member`)
if (typeof data['turn'] !== 'number' || typeof data['step'] !== 'number' || typeof data['index'] !== 'number') {
throw new SessionFormatError(`${label} turn, step, and index must be numbers`)
let lastTime = time0
for (const gap of gaps) {
const validGap = sessionFormatSafeInteger(gap, `${label} dt member`)
lastTime = sessionFormatSafeInteger(lastTime + validGap, `${label} member time`)
}
const turn = sessionFormatCount(data['turn'], `${label} turn`)
const step = sessionFormatCount(data['step'], `${label} step`)
const chunkIndex = sessionFormatCount(data['index'], `${label} index`)
if (isTool && (typeof data['id'] !== 'string'
|| data['id'].length === 0
|| (data['name'] !== undefined && typeof data['name'] !== 'string'))) {
throw new SessionFormatError(`${label} id and optional name must be strings`)
}
const output: SessionFormatEvent[] = []
for (let index = 0; index < payload.length; index += 1) {
if (index > 0) time = sessionFormatSafeInteger(time + (gaps[index - 1] as number), `${label} member time`)
const member = payload[index] as string
const chunk = type === 'text-chunks'
? { type: 'text-delta', index: data['index'], text: member }
: type === 'reasoning-chunks'
? { type: 'reasoning-delta', index: data['index'], text: member }
const lastSeq = sessionFormatCount(seq0 + payload.length - 1, `${label} final seq`)
const stream = (type === 'tool-call-chunks'
? {
type,
time0,
index: chunkIndex,
dt: gaps,
id: data['id'],
...(data['name'] === undefined ? {} : { name: data['name'] }),
args: payload,
}
: { type, time0, index: chunkIndex, dt: gaps, texts: payload }) as SessionFormatJsonObject
const run: ReleasedAssistantChunkRun = {
runType: 'released-assistant-chunks',
firstSeq: seq0,
eventCount: payload.length,
turn,
step,
lastSeq,
lastTime,
stream,
expand: () => expandAssistantChunkRun(run),
}
return run
}
function* expandAssistantChunkRun(run: ReleasedAssistantChunkRun): Iterable<SessionFormatEvent> {
const stream = run.stream
const gaps = stream['dt'] as readonly number[]
const members = stream['type'] === 'tool-call-chunks'
? stream['args'] as readonly string[]
: stream['texts'] as readonly string[]
let time = run.stream['time0'] as number
for (let index = 0; index < members.length; index += 1) {
if (index > 0) time += gaps[index - 1] as number
const member = members[index] as string
const chunk = stream['type'] === 'text-chunks'
? { type: 'text-delta', index: stream['index'], text: member }
: stream['type'] === 'reasoning-chunks'
? { type: 'reasoning-delta', index: stream['index'], text: member }
: {
type: 'tool-call-delta',
index: data['index'],
id: data['id'],
...(data['name'] === undefined ? {} : { name: data['name'] }),
index: stream['index'],
id: stream['id'],
...(stream['name'] === undefined ? {} : { name: stream['name'] }),
argumentsDelta: member,
}
output.push(snapshotSessionFormatJson({
yield {
type: 'assistant/chunk',
seq: sessionFormatCount(seq0 + index, `${label} member seq`),
seq: run.firstSeq + index,
time,
data: { turn: data['turn'], step: data['step'], chunk },
}, `${label} member`) as SessionFormatEvent)
data: { turn: run.turn, step: run.step, chunk },
} as SessionFormatEvent
}
return Object.freeze(output)
}
function decodeSeqRanges(value: SessionFormatJsonValue, maxEntries: number): readonly SessionFormatJsonValue[] {
@@ -287,130 +318,5 @@ function decodeSeqRanges(value: SessionFormatJsonValue, maxEntries: number): rea
if (hasRange && output.some((member, index) => index > 0 && member <= (output[index - 1] as number))) {
throw new SessionFormatError('sourceEventSeqs ranges must be strictly increasing')
}
return Object.freeze(output)
}
function encodeProvenance(record: SessionFormatEvent | SessionFormatJsonObject): SessionFormatJsonObject {
if (!Object.hasOwn(record, 'sourceEventSeqs')) return record
const sourceEventSeqs = record['sourceEventSeqs'] as readonly SessionFormatJsonValue[]
const values = sourceEventSeqs.map(value => sessionFormatCount(value, 'sourceEventSeqs member'))
return snapshotSessionFormatJson({ ...record, sourceEventSeqs: encodeSeqRanges(values) }) as SessionFormatJsonObject
}
function encodeSeqRanges(values: readonly number[]): readonly SessionFormatJsonValue[] {
if (values.some((value, index) => index > 0 && value <= (values[index - 1] as number))) return Object.freeze([...values])
const output: SessionFormatJsonValue[] = []
for (let start = 0; start < values.length;) {
let end = start
while (end + 1 < values.length && values[end + 1] === (values[end] as number) + 1) end += 1
if (end - start >= 2) output.push(Object.freeze([values[start] as number, values[end] as number]))
else for (let index = start; index <= end; index += 1) output.push(values[index] as number)
start = end + 1
}
return Object.freeze(output)
}
type ChunkKind = 'text-delta' | 'reasoning-delta' | 'tool-call-delta'
function packChunkRuns(events: readonly SessionFormatEvent[]): readonly (SessionFormatEvent | SessionFormatJsonObject)[] {
const output: Array<SessionFormatEvent | SessionFormatJsonObject> = []
let kind: ChunkKind | undefined
let run: SessionFormatEvent[] = []
const flush = (): void => {
if (kind !== undefined && run.length >= 3) output.push(buildPackedRow(kind, run))
else output.push(...run)
kind = undefined
run = []
}
for (const event of events) {
const candidate = classifyChunk(event)
const previous = run.at(-1)
if (candidate !== undefined && candidate === kind && previous !== undefined && continuesChunk(previous, event, candidate)) {
run.push(event)
continue
}
flush()
if (candidate === undefined) output.push(event)
else {
kind = candidate
run = [event]
}
}
flush()
return Object.freeze(output)
}
function classifyChunk(event: SessionFormatEvent): ChunkKind | undefined {
if (event.type !== 'assistant/chunk' || !hasExactKeys(event, ['type', 'seq', 'time', 'data'])) return undefined
const data = event.data
if (!isSessionFormatJsonObject(data) || !hasExactKeys(data, ['turn', 'step', 'chunk'])) return undefined
const chunk = data['chunk']
if (!isSessionFormatJsonObject(chunk)
|| typeof chunk['index'] !== 'number'
|| typeof chunk['type'] !== 'string') return undefined
if (chunk['type'] === 'text-delta' || chunk['type'] === 'reasoning-delta') {
return hasExactKeys(chunk, ['type', 'index', 'text']) && typeof chunk['text'] === 'string'
? chunk['type']
: undefined
}
if (chunk['type'] !== 'tool-call-delta') return undefined
const exact = hasExactKeys(chunk, ['type', 'index', 'id', 'argumentsDelta'])
|| hasExactKeys(chunk, ['type', 'index', 'id', 'name', 'argumentsDelta'])
return exact && typeof chunk['id'] === 'string'
&& typeof chunk['argumentsDelta'] === 'string'
&& (chunk['name'] === undefined || typeof chunk['name'] === 'string')
? 'tool-call-delta'
: undefined
}
function continuesChunk(previous: SessionFormatEvent, next: SessionFormatEvent, kind: ChunkKind): boolean {
const previousData = previous.data as SessionFormatJsonObject
const nextData = next.data as SessionFormatJsonObject
const previousChunk = previousData['chunk'] as SessionFormatJsonObject
const nextChunk = nextData['chunk'] as SessionFormatJsonObject
if (!Number.isSafeInteger(next.time - previous.time)) return false
if (nextData['turn'] !== previousData['turn'] || nextData['step'] !== previousData['step']) return false
if (nextChunk['index'] !== previousChunk['index']) return false
if (kind !== 'tool-call-delta') return true
return nextChunk['id'] === previousChunk['id']
&& Object.hasOwn(nextChunk, 'name') === Object.hasOwn(previousChunk, 'name')
&& nextChunk['name'] === previousChunk['name']
}
function buildPackedRow(kind: ChunkKind, run: readonly SessionFormatEvent[]): SessionFormatJsonObject {
const first = run[0] as SessionFormatEvent
const firstData = first.data as SessionFormatJsonObject
const firstChunk = firstData['chunk'] as SessionFormatJsonObject
const base = {
turn: firstData['turn'],
step: firstData['step'],
index: firstChunk['index'],
dt: run.slice(1).map((event, index) => event.time - (run[index] as SessionFormatEvent).time),
}
if (kind === 'tool-call-delta') {
return snapshotSessionFormatJson({
type: 'tool-call-chunks',
seq0: first.seq,
time0: first.time,
data: {
...base,
id: firstChunk['id'],
...(firstChunk['name'] === undefined ? {} : { name: firstChunk['name'] }),
args: run.map(event => ((event.data as SessionFormatJsonObject)['chunk'] as SessionFormatJsonObject)['argumentsDelta']),
},
}) as SessionFormatJsonObject
}
return snapshotSessionFormatJson({
type: kind === 'text-delta' ? 'text-chunks' : 'reasoning-chunks',
seq0: first.seq,
time0: first.time,
data: {
...base,
texts: run.map(event => ((event.data as SessionFormatJsonObject)['chunk'] as SessionFormatJsonObject)['text']),
},
}) as SessionFormatJsonObject
}
function hasExactKeys(record: Readonly<Record<string, unknown>>, keys: readonly string[]): boolean {
return Object.keys(record).length === keys.length && keys.every(key => Object.hasOwn(record, key))
return output
}
@@ -7,7 +7,7 @@ export { assertReleasedPayloadSemantics } from './payload-validation.ts'
export { assertReleasedArtifactRelationships } from './relationships.ts'
export {
assertReleasedSurfaceMetadata,
assertReleasedV1Artifact,
assertReleasedEventPayload,
assertReleasedV1Header,
restoreReleasedV1Artifact,
} from './validation.ts'
@@ -3,19 +3,20 @@ import {
SessionFormatUnsupportedMigrationError,
defineSessionFormatMigration,
sessionFormatCount,
snapshotSessionFormatArtifact,
} from '@deepseek-ai/dsh-session-format'
import type {
SessionFormatEvent,
SessionFormatEventRun,
SessionFormatHeader,
SessionFormatJsonObject,
SessionFormatJsonValue,
SessionFormatMigrationContext,
SessionFormatMigrationStage,
SessionFormatMigrationStageInput,
} from '@deepseek-ai/dsh-session-format'
import { isReleasedAssistantChunkRun } from './codec.ts'
import {
assertReleasedEventPayload,
assertNormalizedReleasedV0Artifact,
assertReleasedV0SourceArtifact,
assertReleasedV1Artifact,
assertReleasedV1Header,
} from './validation.ts'
import { assertReleasedV0Keys, releasedV0Record } from './validation-helpers.ts'
@@ -29,45 +30,179 @@ export const sessionFormatV0ToV1 = defineSessionFormatMigration({
assertHeaderVersion(header, 0)
return { ...header, version: 1 }
},
migrate(source) {
assertReleasedV0SourceArtifact(source)
const events = normalizeReleasedV0Events(source.events, source.header.id)
assertNormalizedReleasedV0Artifact({ ...source, events })
const target = snapshotSessionFormatArtifact({
header: { ...source.header, version: 1 },
inheritedEventCount: source.inheritedEventCount,
events,
}, 'released v0-to-v1 target')
assertReleasedV1Artifact(target)
return target
createStage(input) {
return new ReleasedV0ToV1Stage(input)
},
validateTarget: assertReleasedV1Artifact,
validateTargetHeader: assertReleasedV1Header,
})
class ReleasedV0ToV1Stage implements SessionFormatMigrationStage {
readonly headerInheritedEventCount: number
private readonly state: LegacyNormalizationState = { messageIds: new Map(), retryIds: new Map() }
constructor(private readonly input: SessionFormatMigrationStageInput) {
assertHeaderVersion(input.sourceHeader, 0)
this.headerInheritedEventCount = input.sourceInheritedEventCount
}
transformEvent(
event: SessionFormatEvent,
context: SessionFormatMigrationContext,
): void {
const normalized = normalizeReleasedV0Event(event, this.input.sourceHeader.id, this.state)
assertSourceDeliveryMarker(normalized, this.input)
context.emitEvent(normalized)
}
transformRun(
run: SessionFormatEventRun,
context: SessionFormatMigrationContext,
): void {
if (isReleasedAssistantChunkRun(run)) {
context.emitRun(run)
return
}
for (const event of run.expand()) this.transformEvent(event, context)
}
finish(_context: SessionFormatMigrationContext): number {
return this.input.sourceInheritedEventCount
}
}
function assertHeaderVersion(header: SessionFormatHeader, version: 0 | 1): void {
if (header.version !== version) throw new SessionFormatError(`expected format v${version} header`)
}
function normalizeReleasedV0Events(
events: readonly SessionFormatEvent[],
interface LegacyNormalizationState {
readonly messageIds: Map<number, string>
readonly retryIds: Map<string, string>
compactionId?: string
}
function normalizeReleasedV0Event(
event: SessionFormatEvent,
sessionId: string,
): readonly SessionFormatEvent[] {
const messageIds = new Map<number, string>()
const output: SessionFormatEvent[] = []
for (const event of events) {
assertSupportedLegacyType(event, sessionId)
const start = normalizeLegacyTurnStart(event, sessionId)
const end = normalizeLegacyTurnEnd(start, sessionId)
const header = normalizeLegacyRequestHeader(end, sessionId)
const steering = normalizeLegacySteering(header, sessionId)
const message = normalizeLegacyMessage(steering, sessionId, messageIds)
assertReleasedEventPayload(message, 0)
output.push(message)
const messageId = eventMessageId(message)
if (messageId !== undefined) messageIds.set(message.seq, messageId)
state: LegacyNormalizationState,
): SessionFormatEvent {
const named = normalizeLegacyCompactionType(event)
assertSupportedLegacyType(named, sessionId)
const start = normalizeLegacyTurnStart(named, sessionId)
const end = normalizeLegacyTurnEnd(start, sessionId)
const header = normalizeLegacyRequestHeader(end, sessionId)
const steering = normalizeLegacySteering(header, sessionId)
const retry = normalizeLegacyRetry(steering, sessionId, state.retryIds)
const compaction = normalizeLegacyCompaction(retry, sessionId, state)
const message = normalizeLegacyMessage(compaction, sessionId, state.messageIds)
if (message.type !== 'assistant/chunk') assertReleasedEventPayload(message, 0)
const messageId = eventMessageId(message)
if (messageId !== undefined) state.messageIds.set(message.seq, messageId)
return message
}
function normalizeLegacyCompactionType(event: SessionFormatEvent): SessionFormatEvent {
const type: string = event.type
switch (type) {
case 'compact/start':
return { ...event, type: 'compaction/start' }
case 'compact/summary':
return { ...event, type: 'compaction/summary' }
case 'compact/end':
return { ...event, type: 'compaction/end' }
case 'compact/prune':
return { ...event, type: 'compaction/prune' }
default:
return event
}
return Object.freeze(output)
}
function assertSourceDeliveryMarker(
event: SessionFormatEvent,
input: SessionFormatMigrationStageInput,
): void {
if (event.type !== 'session-log-deepseek/delivery-accepted') return
const data = releasedV0Record(event.data, `${event.type} ${event.seq} data`)
const acceptedVersion = data['sessionFormatVersion'] ?? 0
const inherited = input.sourceHeader.parentSession !== undefined
&& event.seq < input.sourceInheritedEventCount
if (acceptedVersion === 0 && !inherited && data['sessionId'] !== input.sourceHeader.id) {
throw new SessionFormatError('current-generation delivery marker names the wrong Session')
}
}
function normalizeLegacyRetry(
event: SessionFormatEvent,
sessionId: string,
retryIds: Map<string, string>,
): SessionFormatEvent {
if (event.type !== 'llm/retry') return event
const data = releasedV0Record(event.data, `llm/retry ${event.seq} data`)
const chain = [data['turn'], data['step'], data['provider'], data['policyKey']]
.map(value => JSON.stringify(value))
.join('\0')
const retryId = data['retryId']
if (typeof retryId === 'string' && retryId.length > 0) {
retryIds.set(chain, retryId)
return event
}
if (Object.hasOwn(data, 'retryId')) return event
const migratedRetryId = retryIds.get(chain) ?? `legacy-retry:${sessionId}:${event.seq}`
retryIds.set(chain, migratedRetryId)
return { ...event, data: { ...data, retryId: migratedRetryId } }
}
function normalizeLegacyCompaction(
event: SessionFormatEvent,
sessionId: string,
state: LegacyNormalizationState,
): SessionFormatEvent {
if (event.type === 'session/end-seed') {
delete state.compactionId
return event
}
if (event.type === 'compaction/start') {
const data = releasedV0Record(event.data, `compaction/start ${event.seq} data`)
const existing = data['compactionId']
if (typeof existing === 'string' && existing.length > 0) {
state.compactionId = existing
return event
}
if (Object.hasOwn(data, 'compactionId')) return event
const id = `legacy-compaction:${sessionId}:${event.seq}`
state.compactionId = id
return { ...event, data: { ...data, compactionId: id } }
}
const compactionId = state.compactionId
if (compactionId === undefined) return event
if (event.type === 'compaction/summary' || event.type === 'compaction/end') {
const normalized = addLegacyCompactionId(event, compactionId)
if (event.type === 'compaction/end') delete state.compactionId
return normalized
}
if (event.type !== 'user/message') return event
const data = releasedV0Record(event.data, `user/message ${event.seq} data`)
const source = data['source']
if (!releasedIsRecord(source) || source['kind'] !== 'plugin' || source['plugin'] !== 'compact'
|| Object.hasOwn(source, 'compactionId')) return event
return {
...event,
data: {
...data,
source: {
...source,
compactionId,
},
},
}
}
function addLegacyCompactionId(
event: SessionFormatEvent,
compactionId: string,
): SessionFormatEvent {
const data = releasedV0Record(event.data, `${event.type} ${event.seq} data`)
if (Object.hasOwn(data, 'compactionId')) return event
return { ...event, data: { ...data, compactionId } }
}
function normalizeLegacyRequestHeader(event: SessionFormatEvent, sessionId: string): SessionFormatEvent {
@@ -1,4 +1,5 @@
import { SessionFormatError, sessionFormatCount, sessionFormatSafeInteger } from '@deepseek-ai/dsh-session-format'
import { deepEqualJson } from '@deepseek-ai/dsh-util-values'
import type {
SessionFormatEvent,
SessionFormatJsonValue,
@@ -489,7 +490,12 @@ function messageValue(
if (role === undefined) literalValue(message['role'], ['system', 'user', 'assistant'], `${label} role`)
else literalValue(message['role'], [role], `${label} role`)
contentBlocksValue(message['content'], `${label} content`, version)
messageSourceValue(message['source'], `${label} source`, version, expected)
const source = releasedV0Record(message['source'], `${label} source`)
if (version < 2 && expected === 'user' && source['kind'] === 'goal' && source['change'] !== undefined) {
legacyGoalMessageValue(message, source, label)
} else {
messageSourceValue(source, `${label} source`, version, expected)
}
if (expected === 'tool') {
const content = message['content']
const block = Array.isArray(content) && content.length === 1
@@ -502,6 +508,34 @@ function messageValue(
}
}
function legacyGoalMessageValue(message: JsonRecord, source: JsonRecord, label: string): void {
assertReleasedV0Keys(source, ['kind', 'goalId', 'revision', 'round', 'change'], [], `${label} source`)
nonEmptyString(source['goalId'], `${label} source goalId`)
positiveIntegerValue(source['revision'], `${label} source revision`)
if (source['round'] !== 0) throw new SessionFormatError(`${label} legacy goal source round must be 0`)
const change = releasedV0Record(source['change'], `${label} source change`)
goalChangeValue(change, `${label} source change`)
const ref = releasedV0Record(
change['operation'] === 'clear' ? change['cleared'] : change['goal'],
`${label} source change ref`,
)
if (source['goalId'] !== ref['id'] || source['revision'] !== ref['revision']) {
throw new SessionFormatError(`${label} legacy goal source does not match its change`)
}
const payload = change['operation'] === 'clear'
? { cleared: change['cleared'], clearedAt: change['clearedAt'] }
: {
goal: change['goal'],
roundsStarted: change['roundsStarted'],
createdAt: change['createdAt'],
updatedAt: change['updatedAt'],
}
const expected = [{ type: 'text', text: `<goal_state>${JSON.stringify(payload)}</goal_state>` }]
if (!deepEqualJson(message['content'], expected)) {
throw new SessionFormatError(`${label} legacy goal content does not match its change`)
}
}
function messageSourceValue(
value: SessionFormatJsonValue | undefined,
label: string,
@@ -34,6 +34,8 @@ export interface ReleasedRelationshipExtensions {
readonly stepEvents?: ReadonlySet<string>
/** Title-request model input was source-validated and preserved across sequence remapping. */
readonly preservedSourceTitleRequestText?: true
/** Admit the released resume pattern whose next-turn inbox insert omitted the prior turn/end. */
readonly legacyInterruptedTurnRestart?: true
}
/**
@@ -75,7 +77,22 @@ export function assertReleasedArtifactRelationships(
}
switch (event.type) {
case 'turn/start':
case 'turn/start': {
const previous = artifact.events[event.seq - 1]
if (extensions.legacyInterruptedTurnRestart === true
&& openTurn !== null
&& openStep === null
&& data['turn'] === openTurn + 1
&& nextTurn === openTurn
&& previous?.type === 'agent/inbox/spliced') {
const splice = releasedV0Record(previous.data, `agent/inbox/spliced ${previous.seq} data`)
if (splice['target'] === 'next-turn'
&& Array.isArray(splice['inserted'])
&& splice['inserted'].length > 0) {
openTurn = null
nextTurn += 1
}
}
if (openTurn !== null || data['turn'] !== nextTurn) {
throw new SessionFormatError(`turn/start ${JSON.stringify(data['turn'])} does not open expected turn ${nextTurn}`)
}
@@ -84,6 +101,7 @@ export function assertReleasedArtifactRelationships(
toolLifecycles.clear()
nextStep = 1
break
}
case 'turn/end':
if (openTurn !== data['turn']) {
throw new SessionFormatError(`turn/end ${JSON.stringify(data['turn'])} has no matching open turn`)
@@ -200,7 +218,11 @@ export function assertReleasedArtifactRelationships(
break
}
case 'llm/retry':
requireOpenStep(event, data, openTurn, openStep)
if (openTurn !== data['turn']
|| data['step'] !== (openStep ?? nextStep - 1)
|| openTurn === null) {
throw new SessionFormatError('llm/retry does not match the current turn and step')
}
if (data['provider'] !== openStepProvider) {
throw new SessionFormatError('llm/retry provider does not match the open request/header')
}
@@ -0,0 +1,81 @@
import { createSessionFormatCatalog } from '@deepseek-ai/dsh-session-format'
import type {
SessionFormatArtifact,
SessionFormatCurrentEncoder,
SessionFormatRecovery,
} from '@deepseek-ai/dsh-session-format'
import { releasedV0SessionFormatCodec, releasedV1SessionFormatCodec } from '../codec.ts'
import { sessionFormatV0ToV1 } from '../migration.ts'
import { assertReleasedV1Header } from '../validation.ts'
import {
assertReleasedV1Artifact,
assertReleasedV1MigrationSource,
} from './validation.ts'
const unusedEncoder: SessionFormatCurrentEncoder = {
/* v8 ignore next -- this restore-only test facade never encodes a header. */
encodeHeader: header => header,
/* v8 ignore next -- this restore-only test facade never encodes an event. */
encodeEvent: event => event,
}
const catalog = createSessionFormatCatalog({
currentVersion: 1,
codecs: [releasedV0SessionFormatCodec, releasedV1SessionFormatCodec],
currentEncoder: unusedEncoder,
migrations: [sessionFormatV0ToV1],
restoreCurrent(artifact) {
assertReleasedV1Artifact(artifact)
return artifact
},
restoreTransformedCurrent(artifact) {
assertReleasedV1MigrationSource(artifact)
return artifact
},
/* v8 ignore next 3 -- this restore-only test facade never performs a header-only read. */
restoreCurrentHeader(header) {
assertReleasedV1Header(header)
return header
},
})
function restore(
header: unknown,
rows: readonly unknown[],
recovery: SessionFormatRecovery,
validation: 'transformed' | 'current',
): SessionFormatArtifact {
const current = catalog.createRestore(header, { recovery, validation })
for (const row of rows) current.decodeRow(row)
return current.finish()
}
/**
* Restore released v0 rows through the production decoder and v0-to-v1 stage.
* @param header - released v0 physical header.
* @param rows - released v0 physical rows.
* @param recovery - strict or recoverable row policy.
* @returns the transformed released v1 artifact.
*/
export function restoreV0ToV1(
header: unknown,
rows: readonly unknown[],
recovery: SessionFormatRecovery = 'strict',
): SessionFormatArtifact {
return restore(header, rows, recovery, 'transformed')
}
/**
* Restore released v1 rows through the production decoder and current collector.
* @param header - released v1 physical header.
* @param rows - released v1 physical rows.
* @param recovery - strict or recoverable row policy.
* @returns the restored released v1 artifact.
*/
export function restoreV1(
header: unknown,
rows: readonly unknown[],
recovery: SessionFormatRecovery = 'strict',
): SessionFormatArtifact {
return restore(header, rows, recovery, 'current')
}
@@ -0,0 +1,65 @@
import type { SessionFormatArtifact } from '@deepseek-ai/dsh-session-format'
import { RELEASED_V0_EVENT_DISPOSITIONS } from '../dispositions.ts'
import { assertReleasedArtifactRelationships } from '../relationships.ts'
import {
assertReleasedArtifactCoordinates,
assertReleasedEventPayload,
assertReleasedSessionFormatHeader,
} from '../validation.ts'
const RELEASED_V0_EVENT_TYPE_SET: ReadonlySet<string> = new Set(Object.keys(RELEASED_V0_EVENT_DISPOSITIONS))
/**
* Validate one decoded released-v0 source artifact in tests.
* @param artifact - released-v0 source artifact.
*/
export function assertReleasedV0SourceArtifact(artifact: SessionFormatArtifact): void {
assertReleasedSessionFormatHeader(artifact.header, 0)
assertReleasedArtifactCoordinates(artifact, true, RELEASED_V0_EVENT_TYPE_SET, false, true)
}
/**
* Validate normalized v0 output before the identity header bump in tests.
* @param artifact - normalized v0 artifact.
*/
export function assertNormalizedReleasedV0Artifact(artifact: SessionFormatArtifact): void {
assertReleasedSessionFormatHeader(artifact.header, 0)
assertReleasedArtifactCoordinates(artifact, false, RELEASED_V0_EVENT_TYPE_SET, false, true)
for (const event of artifact.events) assertReleasedEventPayload(event, 0)
assertReleasedArtifactRelationships(artifact, { legacyInterruptedTurnRestart: true })
}
/**
* Validate v1 input accepted by the v1-to-v2 migration in tests.
* @param artifact - v1 migration source artifact.
*/
export function assertReleasedV1MigrationSource(artifact: SessionFormatArtifact): void {
assertReleasedSessionFormatHeader(artifact.header, 1)
assertReleasedArtifactCoordinates(artifact, false, RELEASED_V0_EVENT_TYPE_SET, false, true)
for (const event of artifact.events) {
if (RELEASED_V0_EVENT_DISPOSITIONS[event.type] !== undefined) assertReleasedEventPayload(event, 1)
}
assertReleasedArtifactRelationships(artifact, { legacyInterruptedTurnRestart: true })
}
/**
* Validate the exact released-v1 logical artifact in tests.
* @param artifact - released-v1 logical artifact.
*/
export function assertReleasedV1Artifact(artifact: SessionFormatArtifact): void {
assertReleasedSessionFormatHeader(artifact.header, 1)
assertReleasedArtifactCoordinates(artifact, false, RELEASED_V0_EVENT_TYPE_SET, false, true)
for (const event of artifact.events) {
if (RELEASED_V0_EVENT_DISPOSITIONS[event.type] !== undefined) assertReleasedEventPayload(event, 1)
}
assertReleasedArtifactRelationships(artifact)
}
/**
* Validate released-v1 physical decoding without interpreting event vocabulary in tests.
* @param artifact - released-v1 physical artifact.
*/
export function assertReleasedV1PhysicalArtifact(artifact: SessionFormatArtifact): void {
assertReleasedSessionFormatHeader(artifact.header, 1)
assertReleasedArtifactCoordinates(artifact, false, undefined, true)
}
@@ -4,8 +4,8 @@ import {
SessionFormatUnsupportedMigrationError,
sessionFormatCount,
sessionFormatSafeInteger,
snapshotSessionFormatJson,
} from '@deepseek-ai/dsh-session-format'
import { isJsonValue } from '@deepseek-ai/dsh-util-values'
import type {
SessionFormatArtifact,
SessionFormatEvent,
@@ -14,7 +14,6 @@ import type {
} from '@deepseek-ai/dsh-session-format'
import { RELEASED_V0_EVENT_DISPOSITIONS } from './dispositions.ts'
import { assertReleasedPayloadSemantics } from './payload-validation.ts'
import { assertReleasedArtifactRelationships } from './relationships.ts'
import { assertReleasedV0Keys, releasedV0Record } from './validation-helpers.ts'
const HEADER_REQUIRED = ['version', 'id', 'createdAt', 'isSeeded', 'delegationDepth'] as const
@@ -23,8 +22,15 @@ const EVENT_REQUIRED = ['type', 'seq', 'time', 'data'] as const
const SURFACE_EVENT_TYPES = new Set(['user/message', 'assistant/message', 'tool/result'])
const SURFACE_OPTIONAL = ['ignorable', 'sourceEventSeqs', 'surfaceOp'] as const
const LOG_OPTIONAL = ['ignorable'] as const
const LEGACY_SOURCE_TYPES = new Set(['steering/message', 'request/header-delta', 'mode/set'])
const RELEASED_V0_EVENT_TYPE_SET: ReadonlySet<string> = new Set(Object.keys(RELEASED_V0_EVENT_DISPOSITIONS))
const LEGACY_SOURCE_TYPES = new Set([
'steering/message',
'request/header-delta',
'mode/set',
'compact/start',
'compact/summary',
'compact/end',
'compact/prune',
])
/**
* Validate the logical header shared by released v0 and v1.
@@ -62,39 +68,6 @@ export function assertReleasedV1Header(header: SessionFormatHeader): void {
assertReleasedSessionFormatHeader(header, 1)
}
/**
* Validate v0 before historical normalizers run.
* @param artifact - decoded released-v0 source.
*/
export function assertReleasedV0SourceArtifact(artifact: SessionFormatArtifact): void {
assertReleasedSessionFormatHeader(artifact.header, 0)
assertArtifactCoordinates(artifact, true, RELEASED_V0_EVENT_TYPE_SET)
}
/**
* Validate normalized v0 events before the identity header version changes.
* @param artifact - normalized released-v0 artifact.
*/
export function assertNormalizedReleasedV0Artifact(artifact: SessionFormatArtifact): void {
assertReleasedSessionFormatHeader(artifact.header, 0)
assertArtifactCoordinates(artifact, false, RELEASED_V0_EVENT_TYPE_SET)
for (const event of artifact.events) assertReleasedEventPayload(event, 0)
assertReleasedArtifactRelationships(artifact)
}
/**
* Validate the exact logical image emitted by the released v1 writer.
* @param artifact - decoded or migration-produced v1 artifact.
*/
export function assertReleasedV1Artifact(artifact: SessionFormatArtifact): void {
assertReleasedV1Header(artifact.header)
assertArtifactCoordinates(artifact, false, RELEASED_V0_EVENT_TYPE_SET)
for (const event of artifact.events) {
if (RELEASED_V0_EVENT_DISPOSITIONS[event.type] !== undefined) assertReleasedEventPayload(event, 1)
}
assertReleasedArtifactRelationships(artifact)
}
/**
* Restore v1 against the installed build's ordinary event vocabulary without freezing payload additions.
* @param artifact - vocabulary-neutral released-v1 physical decode.
@@ -106,24 +79,24 @@ export function restoreReleasedV1Artifact(
knownEventTypes: ReadonlySet<string>,
): SessionFormatArtifact {
assertReleasedV1Header(artifact.header)
assertArtifactCoordinates(artifact, false, knownEventTypes)
assertReleasedArtifactCoordinates(artifact, false, knownEventTypes)
return artifact
}
/**
* Validate released-v1 physical layout without interpreting event vocabulary.
* @param artifact - physical-codec output.
* Validate released-v0/v1 artifact coordinates under an explicit vocabulary policy.
* @param artifact - logical artifact to validate.
* @param allowLegacySteering - whether to accept the retired steering event name.
* @param knownEventTypes - installed event vocabulary, when vocabulary-aware.
* @param vocabularyNeutral - whether unknown event types remain opaque.
* @param frozenEnvelope - whether event envelopes must already be frozen.
*/
export function assertReleasedV1PhysicalArtifact(artifact: SessionFormatArtifact): void {
assertReleasedV1Header(artifact.header)
assertArtifactCoordinates(artifact, false, undefined, true)
}
function assertArtifactCoordinates(
export function assertReleasedArtifactCoordinates(
artifact: SessionFormatArtifact,
allowLegacySteering: boolean,
knownEventTypes?: ReadonlySet<string>,
vocabularyNeutral = false,
frozenEnvelope = false,
): void {
const inheritedEventCount = sessionFormatCount(artifact.inheritedEventCount, 'Session inheritedEventCount')
if (inheritedEventCount > artifact.events.length) {
@@ -151,7 +124,6 @@ function assertArtifactCoordinates(
`format v1 contains unknown required event type ${JSON.stringify(type)} at seq ${index}`,
)
}
const frozenEnvelope = !vocabularyNeutral && knownEventTypes === RELEASED_V0_EVENT_TYPE_SET
const surface = disposition !== undefined ? SURFACE_EVENT_TYPES.has(type) : type === 'steering/message'
const optional = frozenEnvelope
? surface ? SURFACE_OPTIONAL : LOG_OPTIONAL
@@ -195,8 +167,7 @@ export function assertReleasedSurfaceMetadata(
}
seen.add(current)
}
if (sources.length === 0
&& (type !== 'assistant/message' || assistantSources === 'forbid-assistant')) {
if (sources.length === 0 && type !== 'assistant/message') {
throw new SessionFormatError(`${type} ${seq} sourceEventSeqs must be non-empty`)
}
}
@@ -220,7 +191,7 @@ export function assertReleasedEventPayload(event: SessionFormatEvent, version: 0
/* v8 ignore next -- artifact coordinate validation admits only the frozen inventory before payload validation. */
if (disposition === undefined) {
throw new SessionFormatUnsupportedMigrationError(
`format v0 contains unknown event type ${JSON.stringify(event.type)} at seq ${event.seq}`,
`format v0 contains unknown historical event type ${JSON.stringify(event.type)} at seq ${event.seq}; migration refuses unknown historical events even when ignorable`,
)
}
const data = releasedV0Record(event.data, `${event.type} ${event.seq} data`)
@@ -238,7 +209,9 @@ export function assertReleasedEventPayload(event: SessionFormatEvent, version: 0
: disposition.optional
assertReleasedV0Keys(data, disposition.required, versionOptional, `${event.type} ${event.seq} data`)
for (const key of disposition.opaque) {
if (Object.hasOwn(data, key)) snapshotSessionFormatJson(data[key], `${event.type} ${event.seq} opaque ${key}`)
if (Object.hasOwn(data, key) && !isJsonValue(data[key])) {
throw new SessionFormatError(`${event.type} ${event.seq} opaque ${key} is not lossless JSON`)
}
}
assertReleasedPayloadSemantics(event, version)
}
@@ -1,5 +1,14 @@
import { describe, expect, it } from 'vitest'
import type { SessionFormatArtifact, SessionFormatEvent } from '@deepseek-ai/dsh-session-format'
import { SessionFormatEventCollector } from '@deepseek-ai/dsh-session-format'
import type {
SessionFormatArtifact,
SessionFormatArtifactDecoder,
SessionFormatEvent,
SessionFormatEventRun,
SessionFormatJsonValue,
SessionFormatMigrationContext,
SessionFormatRecovery,
} from '@deepseek-ai/dsh-session-format'
import {
releasedV0SessionFormatCodec,
releasedV1SessionFormatCodec,
@@ -11,33 +20,42 @@ const fullHeader = {
} as const
const textBlock = { type: 'text', text: 'text' } as const
function chunk(
seq: number,
type: 'text-delta' | 'reasoning-delta' | 'tool-call-delta',
value: string,
options: { turn?: number; step?: number; index?: number; time?: number; name?: string } = {},
): SessionFormatEvent {
const stream = type === 'tool-call-delta'
? { type, index: options.index ?? 0, id: 'call', ...(options.name === undefined ? {} : { name: options.name }), argumentsDelta: value }
: { type, index: options.index ?? 0, text: value }
return {
type: 'assistant/chunk', seq, time: options.time ?? seq + 1,
data: { turn: options.turn ?? 1, step: options.step ?? 0, chunk: stream },
class DecodedItemCollector implements SessionFormatMigrationContext {
readonly values: Array<SessionFormatEvent | SessionFormatEventRun> = []
emitEvent(event: SessionFormatEvent): void {
this.values.push(event)
}
emitRun(run: SessionFormatEventRun): void {
this.values.push(run)
}
}
function artifact(events: readonly SessionFormatEvent[], overrides: Partial<SessionFormatArtifact> = {}): SessionFormatArtifact {
return {
header: { version: 1, id: 'codec', createdAt: 1, isSeeded: false, delegationDepth: 0 },
inheritedEventCount: 0,
events,
...overrides,
}
function decodeRow(
decoder: SessionFormatArtifactDecoder,
row: SessionFormatJsonValue,
): Array<SessionFormatEvent | SessionFormatEventRun> {
const output = new DecodedItemCollector()
decoder.decodeRow(row, output)
return output.values
}
function decodeArtifact(
codec: typeof releasedV0SessionFormatCodec,
header: unknown,
rows: readonly unknown[],
recovery: SessionFormatRecovery = 'strict',
): SessionFormatArtifact {
const decoder = codec.createDecoder(header, recovery)
const context = new SessionFormatEventCollector()
for (const row of rows) decoder.decodeRow(row, context)
return { header: decoder.header, inheritedEventCount: decoder.finish(context), events: context.values }
}
describe('released v0/v1 physical codecs', () => {
it('round-trips every physical header field and seeded zero cut', () => {
const decoded = releasedV1SessionFormatCodec.decodeArtifact(fullHeader, [])
it('decodes every physical header field and seeded zero cut', () => {
const decoded = decodeArtifact(releasedV1SessionFormatCodec, fullHeader, [])
expect(decoded).toEqual({
header: {
version: 1, id: 'codec', createdAt: 1, cwd: '/work', parentSession: 'parent',
@@ -46,11 +64,8 @@ describe('released v0/v1 physical codecs', () => {
inheritedEventCount: 0,
events: [],
})
expect(releasedV1SessionFormatCodec.encodeArtifact(decoded, { packChunks: false }).header).toEqual(fullHeader)
expect(releasedV0SessionFormatCodec.encodeArtifact({
...decoded,
header: { ...decoded.header, version: 0 },
}, { packChunks: false }).header).toEqual({ ...fullHeader, version: 0 })
expect(releasedV0SessionFormatCodec.decodeHeader({ ...fullHeader, version: 0 }))
.toEqual({ ...decoded.header, version: 0 })
})
it.each([
@@ -69,43 +84,60 @@ describe('released v0/v1 physical codecs', () => {
expect(() => releasedV1SessionFormatCodec.decodeHeader(header)).toThrow()
})
it('packs and expands text, reasoning, and named tool-call runs exactly', () => {
const events = [
chunk(0, 'text-delta', 'a'), chunk(1, 'text-delta', 'b'), chunk(2, 'text-delta', 'c'),
chunk(3, 'reasoning-delta', 'd'), chunk(4, 'reasoning-delta', 'e'), chunk(5, 'reasoning-delta', 'f'),
chunk(6, 'tool-call-delta', '{', { name: 'read' }),
chunk(7, 'tool-call-delta', '}', { name: 'read' }),
chunk(8, 'tool-call-delta', '', { name: 'read' }),
]
const v0 = { ...artifact(events), header: { ...artifact(events).header, version: 0 } }
const encoded = releasedV0SessionFormatCodec.encodeArtifact(v0, { packChunks: true })
expect(encoded.rows.map(row => row['type'])).toEqual(['text-chunks', 'reasoning-chunks', 'tool-call-chunks'])
expect(releasedV0SessionFormatCodec.decodeArtifact(encoded.header, encoded.rows).events).toEqual(events)
it('expands valid text, reasoning, and named or unnamed tool-call rows exactly', () => {
const cases = [
[{
type: 'text-chunks', seq0: 0, time0: 1,
data: { turn: 1, step: 2, index: 3, dt: [2], texts: ['a', 'b'] },
}, [
{ type: 'assistant/chunk', seq: 0, time: 1, data: {
turn: 1, step: 2, chunk: { type: 'text-delta', index: 3, text: 'a' },
} },
{ type: 'assistant/chunk', seq: 1, time: 3, data: {
turn: 1, step: 2, chunk: { type: 'text-delta', index: 3, text: 'b' },
} },
]],
[{
type: 'reasoning-chunks', seq0: 0, time0: 4,
data: { turn: 2, step: 3, index: 1, dt: [1], texts: ['c', 'd'] },
}, [
{ type: 'assistant/chunk', seq: 0, time: 4, data: {
turn: 2, step: 3, chunk: { type: 'reasoning-delta', index: 1, text: 'c' },
} },
{ type: 'assistant/chunk', seq: 1, time: 5, data: {
turn: 2, step: 3, chunk: { type: 'reasoning-delta', index: 1, text: 'd' },
} },
]],
[{
type: 'tool-call-chunks', seq0: 0, time0: 6,
data: { turn: 3, step: 4, index: 2, id: 'call', name: 'read', dt: [1], args: ['{', '}'] },
}, [
{ type: 'assistant/chunk', seq: 0, time: 6, data: {
turn: 3, step: 4,
chunk: { type: 'tool-call-delta', index: 2, id: 'call', name: 'read', argumentsDelta: '{' },
} },
{ type: 'assistant/chunk', seq: 1, time: 7, data: {
turn: 3, step: 4,
chunk: { type: 'tool-call-delta', index: 2, id: 'call', name: 'read', argumentsDelta: '}' },
} },
]],
[{
type: 'tool-call-chunks', seq0: 0, time0: 8,
data: { turn: 4, step: 5, index: 0, id: 'call', dt: [], args: ['x'] },
}, [
{ type: 'assistant/chunk', seq: 0, time: 8, data: {
turn: 4, step: 5,
chunk: { type: 'tool-call-delta', index: 0, id: 'call', argumentsDelta: 'x' },
} },
]],
] as const
const unnamed = [
chunk(0, 'tool-call-delta', 'a'),
chunk(1, 'tool-call-delta', 'b'),
chunk(2, 'tool-call-delta', 'c'),
]
const unnamedV0 = { ...artifact(unnamed), header: { ...artifact(unnamed).header, version: 0 } }
const unnamedEncoded = releasedV0SessionFormatCodec.encodeArtifact(unnamedV0, { packChunks: true })
expect(unnamedEncoded.rows[0]?.['data']).not.toHaveProperty('name')
expect(releasedV0SessionFormatCodec.decodeArtifact(unnamedEncoded.header, unnamedEncoded.rows).events).toEqual(unnamed)
})
it('keeps short or non-continuing chunk runs unpacked', () => {
const events = [
chunk(0, 'text-delta', 'a'),
chunk(1, 'text-delta', 'b', { index: 1 }),
chunk(2, 'text-delta', 'c', { turn: 2 }),
chunk(3, 'text-delta', 'd', { step: 1 }),
chunk(4, 'tool-call-delta', 'a'),
chunk(5, 'tool-call-delta', 'b', { name: 'read' }),
chunk(6, 'tool-call-delta', 'c', { name: 'read' }),
]
const v0 = { ...artifact(events), header: { ...artifact(events).header, version: 0 } }
const encoded = releasedV0SessionFormatCodec.encodeArtifact(v0, { packChunks: true })
expect(encoded.rows).toEqual(events)
for (const [row, expected] of cases) {
const decoder = releasedV1SessionFormatCodec.createDecoder(fullHeader, 'strict')
const item = decodeRow(decoder, row)[0]
if (item === undefined || !('runType' in item)) throw new Error('expected a packed Assistant run')
expect([...(item as SessionFormatEventRun).expand()]).toEqual(expected)
}
})
it.each([
@@ -118,7 +150,7 @@ describe('released v0/v1 physical codecs', () => {
['tool name', { type: 'tool-call-chunks', seq0: 0, time0: 1, data: { turn: 1, step: 0, index: 0, id: 'id', name: 1, dt: [], args: ['a'] } }],
['unsafe time sum', { type: 'text-chunks', seq0: 0, time0: Number.MAX_SAFE_INTEGER, data: { turn: 1, step: 0, index: 0, dt: [1], texts: ['a', 'b'] } }],
])('refuses malformed packed row: %s', (_name, row) => {
expect(() => releasedV1SessionFormatCodec.decodeArtifact(fullHeader, [row])).toThrow()
expect(() => decodeArtifact(releasedV1SessionFormatCodec, fullHeader, [row])).toThrow()
})
it.each([
@@ -135,39 +167,83 @@ describe('released v0/v1 physical codecs', () => {
id: 'u', role: 'user', content: [{ type: 'text', text: 'x' }], source: { kind: 'user' },
}, sourceEventSeqs, surfaceOp: 'append' },
]
expect(() => releasedV1SessionFormatCodec.decodeArtifact(
expect(() => decodeArtifact(releasedV1SessionFormatCodec,
{ type: 'session', version: 1, id: 'codec', createdAt: 1, delegationDepth: 0 },
rows,
)).toThrow()
})
it('contains non-SessionFormatError row failures during recoverable scans', () => {
const bad = new Proxy({}, { ownKeys: () => { throw new Error('proxy failure') } })
const recovered = releasedV1SessionFormatCodec.decodeRecoverableArtifact(
const bad = new Proxy({}, { get: () => { throw new Error('proxy failure') } })
const decoder = releasedV1SessionFormatCodec.createDecoder(
{ type: 'session', version: 1, id: 'codec', createdAt: 1, delegationDepth: 0 },
[bad],
'recoverable',
)
expect(recovered).toMatchObject({ events: [] })
expect(decodeRow(decoder, bad)).toEqual([])
expect(() => decodeRow(decoder, {
type: 'turn/end', seq: 0, time: 2, data: { turn: 1, reason: { kind: 'completed' } },
})).toThrow('released Session row 0 is malformed')
})
it('streams the same row-atomic recoverable prefix', () => {
const create = (header: unknown) => releasedV1SessionFormatCodec.createDecoder(header, 'recoverable')
const header = { type: 'session', version: 1, id: 'codec', createdAt: 1, delegationDepth: 0 }
const malformed = create(header)
expect(decodeRow(malformed, null)).toEqual([])
const malformedProxy = create(header)
const proxy = new Proxy({}, { get: () => { throw new Error('proxy failure') } })
expect(decodeRow(malformedProxy, proxy)).toEqual([])
expect(decodeRow(malformedProxy, { type: 'step/start', seq: 0, time: 1, data: { turn: 1, step: 1 } })).toEqual([])
expect(() => decodeRow(malformedProxy, {
type: 'turn/end', seq: 0, time: 2, data: { turn: 1, reason: { kind: 'completed' } },
})).toThrow('released Session row 0 is malformed')
const gap = create(header)
expect(decodeRow(gap, { type: 'step/start', seq: 1, time: 1, data: { turn: 1, step: 1 } })).toEqual([])
const terminalGap = create(header)
expect(() => decodeRow(terminalGap, {
type: 'turn/end', seq: 1, time: 2, data: { turn: 1, reason: { kind: 'completed' } },
})).toThrow(/seq gap/)
const seeded = create({ ...header, seedLength: 1 })
expect(() => seeded.finish(new DecodedItemCollector())).toThrow(/inheritedEventCount exceeds/)
const packed = create(header)
const runs = decodeRow(packed, {
type: 'text-chunks', seq0: 0, time0: 1,
data: { turn: 1, step: 1, index: 0, dt: [1], texts: ['a', 'b'] },
})
expect(runs).toHaveLength(1)
expect([...((runs[0] as SessionFormatEventRun).expand())]).toHaveLength(2)
const provenance = create(header)
decodeRow(provenance, { type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } })
const item = decodeRow(provenance, {
type: 'user/message', seq: 1, time: 2,
data: { id: 'm', role: 'user', content: [], source: { kind: 'user' } },
sourceEventSeqs: [0], surfaceOp: 'append',
})[0]
expect(item !== undefined && !('runType' in item) ? item.sourceEventSeqs : undefined).toEqual([0])
})
it('ignores decodable non-terminal rows after the first recoverable issue', () => {
const header = { type: 'session', version: 1, id: 'codec', createdAt: 1, delegationDepth: 0 }
const recovered = releasedV1SessionFormatCodec.decodeRecoverableArtifact(header, [
const recovered = decodeArtifact(releasedV1SessionFormatCodec, header, [
{ type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
{ type: 'turn/start', seq: 4, time: 2, data: { turn: 2 } },
{ type: 'step/start', seq: 1, time: 3, data: { turn: 1, step: 0 } },
])
], 'recoverable')
expect(recovered).toMatchObject({ events: [{ seq: 0 }] })
})
it('rejects strict gaps and a recoverable gap row that itself closes a turn', () => {
const currentHeader = { type: 'session', version: 1, id: 'codec', createdAt: 1, delegationDepth: 0 }
expect(() => releasedV1SessionFormatCodec.decodeArtifact(currentHeader, [
expect(() => decodeArtifact(releasedV1SessionFormatCodec, currentHeader, [
{ type: 'turn/start', seq: 1, time: 1, data: { turn: 1 } },
])).toThrow(/seq gap/)
expect(() => releasedV1SessionFormatCodec.decodeRecoverableArtifact(currentHeader, [
expect(() => decodeArtifact(releasedV1SessionFormatCodec, currentHeader, [
{ type: 'turn/end', seq: 1, time: 1, data: { turn: 1, reason: { kind: 'completed' } } },
])).toThrow(/seq gap/)
], 'recoverable')).toThrow(/seq gap/)
})
it('refuses overlapping ranges after a valid first range', () => {
@@ -182,74 +258,6 @@ describe('released v0/v1 physical codecs', () => {
data: { id: 'u', role: 'user', content: [textBlock], source: { kind: 'user' } },
},
]
expect(() => releasedV1SessionFormatCodec.decodeArtifact(header, rows)).toThrow(/strictly increasing/)
})
it('keeps non-consecutive provenance scalar and leaves invalid v0 chunks unpacked', () => {
const events = [
{ type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
{ type: 'step/start', seq: 1, time: 2, data: { turn: 1, step: 1 } },
{ type: 'step/end', seq: 2, time: 3, data: { turn: 1, step: 1 } },
{
type: 'user/message', seq: 3, time: 4, surfaceOp: 'append', sourceEventSeqs: [0, 2],
data: { id: 'u', role: 'user', content: [textBlock], source: { kind: 'user' } },
},
] as SessionFormatEvent[]
expect(releasedV1SessionFormatCodec.encodeArtifact(artifact(events), { packChunks: false }).rows[3])
.toEqual(events[3])
const invalidChunk = { type: 'assistant/chunk', seq: 0, time: 1, data: { turn: 1, step: 0, chunk: null } }
const v0 = {
header: { version: 0, id: 'codec', createdAt: 1, isSeeded: false, delegationDepth: 0 },
inheritedEventCount: 0,
events: [invalidChunk],
} as unknown as SessionFormatArtifact
expect(releasedV0SessionFormatCodec.encodeArtifact(v0, { packChunks: true }).rows).toEqual([invalidChunk])
const causal = {
...v0,
events: [
{ type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
{ type: 'user/message', seq: 1, time: 2, surfaceOp: 'append', data: {
id: 'one', role: 'user', content: [textBlock], source: { kind: 'user' },
} },
{ type: 'user/message', seq: 2, time: 3, surfaceOp: 'append', data: {
id: 'two', role: 'user', content: [textBlock], source: { kind: 'user' },
} },
{ type: 'user/message', seq: 3, time: 4, surfaceOp: 'append', sourceEventSeqs: [2, 0], data: {
id: 'three', role: 'user', content: [textBlock], source: { kind: 'user' },
} },
],
} as unknown as SessionFormatArtifact
expect(releasedV0SessionFormatCodec.encodeArtifact(causal, { packChunks: false }).rows[3]?.['sourceEventSeqs'])
.toEqual([2, 0])
for (const badData of [
null,
{ turn: 1, step: 1, chunk: null },
{ turn: 1, step: 1, chunk: { type: 'other', index: 0 } },
{ turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 1 } },
{ turn: 1, step: 1, chunk: { type: 'tool-call-delta', index: 0, id: 1, argumentsDelta: 'x' } },
]) {
const bad = { ...v0, events: [{ type: 'assistant/chunk', seq: 0, time: 1, data: badData }] } as unknown as SessionFormatArtifact
expect(releasedV0SessionFormatCodec.encodeArtifact(bad, { packChunks: true }).rows).toHaveLength(1)
}
const farTimes = [
chunk(0, 'text-delta', 'a', { time: Number.MIN_SAFE_INTEGER }),
chunk(1, 'text-delta', 'b', { time: Number.MAX_SAFE_INTEGER }),
chunk(2, 'text-delta', 'c', { time: Number.MAX_SAFE_INTEGER }),
]
const far = { ...v0, events: farTimes } as unknown as SessionFormatArtifact
expect(releasedV0SessionFormatCodec.encodeArtifact(far, { packChunks: true }).rows).toHaveLength(3)
const ignorable = [
{ ...chunk(0, 'text-delta', 'a'), ignorable: true },
{ ...chunk(1, 'text-delta', 'b'), ignorable: true },
{ ...chunk(2, 'text-delta', 'c'), ignorable: true },
] as SessionFormatEvent[]
const ignorableV0 = { ...v0, events: ignorable } as unknown as SessionFormatArtifact
expect(releasedV0SessionFormatCodec.encodeArtifact(ignorableV0, { packChunks: true }).rows)
.toEqual(ignorable)
expect(() => decodeArtifact(releasedV1SessionFormatCodec, header, rows)).toThrow(/strictly increasing/)
})
})
@@ -1,9 +1,9 @@
import { describe, expect, it } from 'vitest'
import { SessionFormatUnsupportedMigrationError } from '@deepseek-ai/dsh-session-format'
import {
releasedV0SessionFormatCodec,
sessionFormatV0ToV1,
} from '../src/index.ts'
import { restoreV0ToV1 } from '../src/testing/restore.ts'
const header = {
type: 'session',
@@ -14,7 +14,7 @@ const header = {
} as const
function migrate(rows: readonly unknown[]) {
return sessionFormatV0ToV1.migrate(releasedV0SessionFormatCodec.decodeArtifact(header, rows))
return restoreV0ToV1(header, rows)
}
describe('released v0 legacy normalization', () => {
@@ -1,11 +1,33 @@
import { describe, expect, it } from 'vitest'
import { SessionFormatEventCollector } from '@deepseek-ai/dsh-session-format'
import type {
SessionFormatEvent,
SessionFormatEventRun,
} from '@deepseek-ai/dsh-session-format'
import {
RELEASED_V0_EVENT_TYPES,
releasedV0SessionFormatCodec,
releasedV1SessionFormatCodec,
restoreReleasedV1Artifact,
sessionFormatV0ToV1,
} from '../src/index.ts'
import { restoreV0ToV1, restoreV1 } from '../src/testing/restore.ts'
function createMigrationStage(id: string) {
const sourceHeader = { version: 0, id, createdAt: 1, isSeeded: false, delegationDepth: 0 }
const stage = sessionFormatV0ToV1.createStage({
sourceHeader,
targetHeader: sessionFormatV0ToV1.migrateHeader(sourceHeader),
sourceInheritedEventCount: 0,
sourceKind: 'decoded',
})
return {
transform(event: SessionFormatEvent): SessionFormatEvent[] {
const context = new SessionFormatEventCollector()
stage.transformEvent(event, context)
return context.values
},
}
}
describe('released Session format v0 to v1', () => {
it('changes only the version of a canonical decoded artifact', () => {
@@ -27,19 +49,71 @@ describe('released Session format v0 to v1', () => {
data: { turn: 1, step: 1, index: 0, dt: [1, 1], texts: ['a', 'b', 'c'] },
},
]
const source = releasedV0SessionFormatCodec.decodeArtifact(header, rows)
const migrated = sessionFormatV0ToV1.migrate(source)
const migrated = restoreV0ToV1(header, rows)
expect(migrated).toEqual({
...source,
header: { ...source.header, version: 1 },
header: {
version: 1, id: 'identity', createdAt: 1, cwd: '/work', isSeeded: false, delegationDepth: 0,
},
inheritedEventCount: 0,
events: [
rows[0],
rows[1],
{ type: 'assistant/chunk', seq: 2, time: 4, data: {
turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'a' },
} },
{ type: 'assistant/chunk', seq: 3, time: 5, data: {
turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'b' },
} },
{ type: 'assistant/chunk', seq: 4, time: 6, data: {
turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'c' },
} },
],
})
sessionFormatV0ToV1.validateTarget(migrated)
expect(releasedV1SessionFormatCodec.encodeArtifact(migrated, { packChunks: true })).toEqual({
header: { ...header, version: 1 },
rows,
})
it('expands an unrecognized compact run through the identity stage', () => {
const sourceHeader = {
version: 0, id: 'generic-run', createdAt: 1, isSeeded: false, delegationDepth: 0,
} as const
const stage = sessionFormatV0ToV1.createStage({
sourceHeader,
targetHeader: sessionFormatV0ToV1.migrateHeader(sourceHeader),
sourceInheritedEventCount: 0,
sourceKind: 'transformed',
})
const source = {
type: 'feedback/record', seq: 0, time: 2, data: { text: 'retained' },
} as const
const run: SessionFormatEventRun = {
runType: 'test-run', firstSeq: 0, eventCount: 1, *expand() { yield source },
}
const output = new SessionFormatEventCollector()
stage.transformRun(run, output)
expect(output.values).toEqual([source])
})
it('accepts an inherited delivery marker for its source generation', () => {
const sourceHeader = {
version: 0, id: 'child', createdAt: 1, parentSession: 'parent', isSeeded: true, delegationDepth: 0,
} as const
const stage = sessionFormatV0ToV1.createStage({
sourceHeader,
targetHeader: sessionFormatV0ToV1.migrateHeader(sourceHeader),
sourceInheritedEventCount: 2,
sourceKind: 'decoded',
})
const marker = {
type: 'session-log-deepseek/delivery-accepted', seq: 1, time: 2,
data: { sessionId: 'parent', throughSeq: 0 },
} as const
const output = new SessionFormatEventCollector()
stage.transformEvent(marker, output)
expect(output.values).toEqual([marker])
})
it('recovers only the complete row prefix and refuses a later committing turn end', () => {
@@ -61,13 +135,13 @@ describe('released Session format v0 to v1', () => {
data: { turn: 2, step: 0, index: 0, dt: [1], texts: ['x', 'y'] },
}
expect(releasedV0SessionFormatCodec.decodeRecoverableArtifact(header, [...prefix, badRow]))
.toEqual(releasedV0SessionFormatCodec.decodeArtifact(header, prefix))
expect(() => releasedV0SessionFormatCodec.decodeRecoverableArtifact(header, [
expect(restoreV0ToV1(header, [...prefix, badRow], 'recoverable'))
.toEqual(restoreV0ToV1(header, prefix))
expect(() => restoreV0ToV1(header, [
...prefix,
badRow,
{ type: 'turn/end', seq: 2, time: 6, data: { turn: 2, reason: { kind: 'interrupted' } } },
])).toThrow(/seq gap/)
], 'recoverable')).toThrow(/seq gap/)
})
it('requires canonical delegation depth and decodes provenance without mutating source rows', () => {
@@ -100,15 +174,15 @@ describe('released Session format v0 to v1', () => {
provenanceRow,
]
const decoded = releasedV0SessionFormatCodec.decodeArtifact(header, rows)
const migrated = restoreV0ToV1(header, rows)
expect(() => releasedV0SessionFormatCodec.decodeArtifact(incompleteHeader, rows)).toThrow(/delegationDepth/)
expect(decoded.header.delegationDepth).toBe(0)
expect(decoded.events[3]?.sourceEventSeqs).toEqual([0, 1, 2])
expect(() => restoreV0ToV1(incompleteHeader, rows)).toThrow(/delegationDepth/)
expect(migrated.header.delegationDepth).toBe(0)
expect(migrated.events[3]?.sourceEventSeqs).toEqual([0, 1, 2])
expect(provenanceRow.sourceEventSeqs).toEqual([[0, 2]])
const migrated = sessionFormatV0ToV1.migrate(decoded)
expect(releasedV1SessionFormatCodec.encodeArtifact(migrated, { packChunks: false }).header)
.toEqual({ ...header, version: 1 })
expect(migrated.header).toEqual({
version: 1, id: 'old', createdAt: 1, isSeeded: false, delegationDepth: 0,
})
})
it('refuses v1-only generation fields in v0 and accepts them in v1', () => {
@@ -122,10 +196,91 @@ describe('released Session format v0 to v1', () => {
const v0 = { type: 'session', version: 0, id: 'delivery', createdAt: 1, delegationDepth: 0 }
const v1 = { ...v0, version: 1 }
expect(() => sessionFormatV0ToV1.migrate(
releasedV0SessionFormatCodec.decodeArtifact(v0, [prefix, event]),
)).toThrow(/unexpected member "sessionFormatVersion"/)
expect(releasedV1SessionFormatCodec.decodeArtifact(v1, [prefix, event]).events).toEqual([prefix, event])
expect(() => restoreV0ToV1(v0, [prefix, event])).toThrow(/unexpected member "sessionFormatVersion"/)
expect(restoreV1(v1, [prefix, event]).events).toEqual([prefix, event])
})
it('does not synthesize over an explicitly invalid retry id', () => {
const header = { type: 'session', version: 0, id: 'retry', createdAt: 1, delegationDepth: 0 }
const rows = [
{ type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
{ type: 'step/start', seq: 1, time: 2, data: { turn: 1, step: 1 } },
{ type: 'request/header', seq: 2, time: 3, data: {
header: { config: { provider: 'p', model: 'm' } }, reason: 'initial',
} },
{ type: 'llm/retry', seq: 3, time: 4, data: {
retryId: null, turn: 1, step: 1, provider: 'p', mode: 'normal', policyKey: 'k',
retry: 1, maxRetries: 1, delayMs: 0, failure: { message: 'retry', code: 'SERVER' },
} },
]
expect(() => restoreV0ToV1(header, rows)).toThrow(/retryId/)
const transformer = createMigrationStage('retry')
const retry = (seq: number, attempt: number) => ({
type: 'llm/retry', seq, time: seq + 1,
data: {
turn: 1, step: 1, provider: 'p', mode: 'normal', policyKey: 'k', retry: attempt,
maxRetries: 2, delayMs: 0, failure: { message: 'retry', code: 'SERVER' },
},
})
expect(transformer.transform(retry(0, 1))[0]?.data).toMatchObject({ retryId: 'legacy-retry:retry:0' })
expect(transformer.transform(retry(1, 2))[0]?.data).toMatchObject({ retryId: 'legacy-retry:retry:0' })
})
it('normalizes legacy compaction names and assigns one deterministic id to the group', () => {
const transformer = createMigrationStage('compact')
const rows = [
{ type: 'compaction/start', seq: 0, time: 1, data: { turn: null } },
{
type: 'compaction/summary', seq: 1, time: 2,
data: {
summary: [{ type: 'text', text: 'summary' }], shadowedRange: { start: 0, end: 0 },
shadowedSeqs: [0], shadowedTokenCount: 0, provider: 'p', model: 'm',
},
},
{
type: 'compaction/prune', seq: 2, time: 3,
data: { shadowedRange: { start: 0, end: 0 }, shadowedSeqs: [0], shadowedTokenCount: 1 },
},
{
type: 'user/message', seq: 3, time: 4,
data: {
id: 'checkpoint', role: 'user', content: [{ type: 'text', text: 'summary' }],
source: { kind: 'plugin', plugin: 'compact' },
},
sourceEventSeqs: [0, 1], surfaceOp: { op: 'replace', start: 0, end: 0 },
},
{ type: 'compaction/end', seq: 4, time: 5, data: { turn: null } },
]
const migrated = rows.map(row => transformer.transform(row)[0])
expect(migrated.map(event => event?.type === 'user/message'
? (event.data as { source: { compactionId: string } }).source.compactionId
: (event?.data as { compactionId: string }).compactionId))
.toEqual([
'legacy-compaction:compact:0',
'legacy-compaction:compact:0',
undefined,
'legacy-compaction:compact:0',
'legacy-compaction:compact:0',
])
const historical = rows.map(row => ({
...row,
type: row.type.replace(/^compaction\//, 'compact/'),
}))
const historicalTransformer = createMigrationStage('compact')
expect(historical.map(row => historicalTransformer.transform(row as SessionFormatEvent)[0])).toEqual(migrated)
const reset = createMigrationStage('compact')
expect(() => reset.transform({
...(rows[0] as SessionFormatEvent), data: { turn: null, compactionId: null },
})).toThrow(/compactionId/)
reset.transform(rows[0] as never)
expect(reset.transform({ type: 'feedback/record', seq: 1, time: 2, data: { text: 'kept' } })[0]?.type)
.toBe('feedback/record')
reset.transform({ type: 'session/end-seed', seq: 2, time: 3, data: {} })
expect(() => reset.transform({ ...(rows[1] as SessionFormatEvent), seq: 3 })).toThrow(/compactionId/)
})
it('preserves a complete canonical multi-owner log except for header.version', () => {
@@ -209,9 +364,14 @@ describe('released Session format v0 to v1', () => {
},
{ type: 'turn/end', seq: 19, time: 20, data: { turn: 1, reason: { kind: 'completed' } } },
]
const source = releasedV0SessionFormatCodec.decodeArtifact(physicalHeader, rows)
const migrated = sessionFormatV0ToV1.migrate(source)
expect(migrated).toEqual({ ...source, header: { ...source.header, version: 1 } })
const migrated = restoreV0ToV1(physicalHeader, rows)
expect(migrated).toEqual({
header: {
version: 1, id: 'full-identity', createdAt: 1, cwd: '/work', isSeeded: false, delegationDepth: 0,
},
inheritedEventCount: 0,
events: rows,
})
})
it('keeps the v1 physical codec vocabulary-neutral for current growth and a future source freeze', () => {
@@ -219,10 +379,17 @@ describe('released Session format v0 to v1', () => {
type: 'session', version: 1, id: 'ordinary-growth', createdAt: 1, delegationDepth: 0,
}
const ordinary = { type: 'ordinary/post-v1', seq: 0, time: 1, data: { required: true } }
const decoded = releasedV1SessionFormatCodec.decodeArtifact(physicalHeader, [ordinary])
const decoder = releasedV1SessionFormatCodec.createDecoder(physicalHeader, 'strict')
const events = new SessionFormatEventCollector()
decoder.decodeRow(ordinary, events)
const decoded = {
header: decoder.header,
inheritedEventCount: decoder.finish(events),
events: events.values,
}
expect(decoded.events).toEqual([ordinary])
expect(() => { sessionFormatV0ToV1.validateTarget(decoded) }).toThrow(/unknown required event/)
expect(RELEASED_V0_EVENT_TYPES).not.toContain(ordinary.type)
const generatedCurrentTypes = new Set([...RELEASED_V0_EVENT_TYPES, ordinary.type])
expect(() => restoreReleasedV1Artifact(decoded, generatedCurrentTypes)).not.toThrow()
@@ -230,10 +397,19 @@ describe('released Session format v0 to v1', () => {
const frozenFutureV1SourceTypes = new Set(generatedCurrentTypes)
expect(() => restoreReleasedV1Artifact(decoded, frozenFutureV1SourceTypes)).not.toThrow()
const extendedKnownPayload = releasedV1SessionFormatCodec.decodeArtifact(physicalHeader, [{
const extendedDecoder = releasedV1SessionFormatCodec.createDecoder(physicalHeader, 'strict')
const extendedEvents = new SessionFormatEventCollector()
extendedDecoder.decodeRow({
type: 'turn/start', seq: 0, time: 1, data: { turn: 1, postReleaseMember: true },
}, extendedEvents)
const extendedKnownPayload = {
header: extendedDecoder.header,
inheritedEventCount: extendedDecoder.finish(extendedEvents),
events: extendedEvents.values,
}
expect(extendedKnownPayload.events).toEqual([{
type: 'turn/start', seq: 0, time: 1, data: { turn: 1, postReleaseMember: true },
}])
expect(() => { sessionFormatV0ToV1.validateTarget(extendedKnownPayload) }).toThrow(/unexpected member/)
expect(() => restoreReleasedV1Artifact(extendedKnownPayload, generatedCurrentTypes)).not.toThrow()
})
})
@@ -1,11 +1,9 @@
import { describe, expect, it } from 'vitest'
import { SessionFormatUnsupportedMigrationError } from '@deepseek-ai/dsh-session-format'
import {
releasedV0SessionFormatCodec,
releasedV1SessionFormatCodec,
assertReleasedV1Artifact,
sessionFormatV0ToV1,
assertReleasedArtifactRelationships,
} from '../src/index.ts'
import { restoreV0ToV1, restoreV1 } from '../src/testing/restore.ts'
const header = {
type: 'session', version: 1, id: 'relationships', createdAt: 1, delegationDepth: 0,
@@ -15,9 +13,7 @@ const user = (id: string, source: object = { kind: 'user' }) => ({
})
function decode(rows: readonly unknown[], physicalHeader: unknown = header) {
const artifact = releasedV1SessionFormatCodec.decodeArtifact(physicalHeader, rows)
assertReleasedV1Artifact(artifact)
return artifact
return restoreV1(physicalHeader, rows)
}
describe('released v1 whole-artifact relationships', () => {
@@ -299,6 +295,7 @@ describe('released v1 whole-artifact relationships', () => {
})
expect(() => decode([...prefix, retry({ maxRetries: undefined })])).toThrow()
expect(() => decode([...prefix, retry({ retry: 2, maxRetries: 1 })])).toThrow()
expect(() => decode([...prefix, retry({ turn: 2 })])).toThrow(/current turn/)
expect(() => decode([...prefix, retry({ provider: 'q' })])).toThrow(/provider/)
expect(() => decode([...prefix, retry({ failure: { message: 'x', code: 'X', status: 99 } })])).toThrow(/status/)
expect(() => decode([...prefix, retry({ delayMs: -0.5 })])).toThrow(/non-negative/)
@@ -309,6 +306,25 @@ describe('released v1 whole-artifact relationships', () => {
.toHaveLength(4)
})
it('accepts a legacy retry scheduled immediately after its step closes', () => {
const rows = [
{ type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
{ type: 'step/start', seq: 1, time: 2, data: { turn: 1, step: 1 } },
{ type: 'request/header', seq: 2, time: 3, data: { header: { config: { provider: 'p', model: 'm' } }, reason: 'initial' } },
{ type: 'step/end', seq: 3, time: 4, data: { turn: 1, step: 1 } },
{
type: 'llm/retry', seq: 4, time: 5,
data: {
retryId: 'r', turn: 1, step: 1, provider: 'p', mode: 'normal', policyKey: 'k', retry: 1,
maxRetries: 2, delayMs: 1, failure: { message: 'retry', code: 'SERVER' },
},
},
{ type: 'turn/end', seq: 5, time: 6, data: { turn: 1, reason: { kind: 'completed' } } },
]
expect(decode(rows).events).toHaveLength(rows.length)
})
it('enforces command pairing and authoritative source-event rules', () => {
expect(() => decode([{
type: 'command/done', seq: 0, time: 1, data: { commandId: 'c', kind: 'success' },
@@ -366,7 +382,9 @@ describe('released v1 whole-artifact relationships', () => {
}
const decoded = decode([...prefix, inertV0])
expect(decoded.events[1]).toEqual(inertV0)
expect(() => { sessionFormatV0ToV1.validateTarget(decoded) }).not.toThrow()
expect(() => {
assertReleasedArtifactRelationships(decoded, { legacyInterruptedTurnRestart: true })
}).not.toThrow()
})
it('validates versioned subagent descriptors by source/current policy', () => {
@@ -376,7 +394,7 @@ describe('released v1 whole-artifact relationships', () => {
}
expect(decode([future]).events).toEqual([future])
const v0Header = { ...header, version: 0 }
expect(() => sessionFormatV0ToV1.migrate(releasedV0SessionFormatCodec.decodeArtifact(v0Header, [future])))
expect(() => restoreV0ToV1(v0Header, [future]))
.toThrow(SessionFormatUnsupportedMigrationError)
})
@@ -465,10 +483,30 @@ describe('released v1 whole-artifact relationships', () => {
data: { sessionId: 'wrong', throughSeq: 0 },
},
]
expect(() => sessionFormatV0ToV1.migrate(releasedV0SessionFormatCodec.decodeArtifact(v0, rows)))
expect(() => restoreV0ToV1(v0, rows))
.toThrow(/wrong Session/)
})
it('accepts the released interrupted-turn restart sequence during v0 migration', () => {
const v0 = { ...header, version: 0 }
const rows = [
{ type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
{ type: 'step/start', seq: 1, time: 2, data: { turn: 1, step: 1 } },
{ type: 'step/end', seq: 2, time: 3, data: { turn: 1, step: 1 } },
{
type: 'agent/inbox/spliced', seq: 3, time: 4,
data: { target: 'next-turn', start: 0, inserted: [user('queued')] },
},
{ type: 'turn/start', seq: 4, time: 5, data: { turn: 2 } },
{ type: 'turn/end', seq: 5, time: 6, data: { turn: 2, reason: { kind: 'completed' } } },
]
expect(restoreV0ToV1(v0, rows).events).toEqual(rows)
expect(() => restoreV0ToV1(v0, rows.map(candidate => candidate.seq === 3
? { ...candidate, data: { ...candidate.data, inserted: [] } }
: candidate))).toThrow(/does not open expected turn/)
})
it('accepts recoverable open tails for later interrupted-turn repair', () => {
const v0 = { ...header, version: 0 }
const rows = [
@@ -500,8 +538,7 @@ describe('released v1 whole-artifact relationships', () => {
},
{ type: 'compaction/start', seq: 7, time: 8, data: { compactionId: 'c', turn: 1 } },
]
const recovered = releasedV0SessionFormatCodec.decodeRecoverableArtifact(v0, rows)
expect(sessionFormatV0ToV1.migrate(recovered).events).toHaveLength(rows.length)
expect(restoreV0ToV1(v0, rows, 'recoverable').events).toHaveLength(rows.length)
})
it('refuses invalid core openings, request placement, and replacement placement', () => {
@@ -4,12 +4,16 @@ import { KNOWN_SESSION_EVENT_TYPES } from '@deepseek-ai/dsh-session'
import {
RELEASED_V0_EVENT_TYPES,
RELEASED_V0_EVENT_DISPOSITIONS,
assertReleasedV1Artifact,
releasedV0SessionFormatCodec,
releasedV1SessionFormatCodec,
sessionFormatV0ToV1,
} from '../src/index.ts'
import { assertReleasedEventPayload } from '../src/validation.ts'
import { assertReleasedEventPayload, assertReleasedSurfaceMetadata } from '../src/validation.ts'
import { restoreV0ToV1, restoreV1 } from '../src/testing/restore.ts'
import {
assertNormalizedReleasedV0Artifact,
assertReleasedV0SourceArtifact,
assertReleasedV1Artifact,
assertReleasedV1MigrationSource,
assertReleasedV1PhysicalArtifact,
} from '../src/testing/validation.ts'
const v0Header = {
type: 'session', version: 0, id: 'validation', createdAt: 1, delegationDepth: 0,
@@ -290,6 +294,15 @@ describe('released event and payload inventory', () => {
}
})
it('refuses opaque members that are not lossless JSON without snapshotting them', () => {
const data = {
...(validPayloads['tool/result'] as Record<string, unknown>),
meta: { value: undefined },
}
expect(() => { assertPayload('tool/result', data as unknown as SessionFormatJsonValue) })
.toThrow(/opaque meta is not lossless JSON/)
})
it.each([
['user/message content block', 'user/message', {
...userMessage,
@@ -312,10 +325,58 @@ describe('released event and payload inventory', () => {
it('refuses unknown v0 events even when the envelope marks them ignorable', () => {
const row = { type: 'plugin/unknown', seq: 0, time: 1, data: {}, ignorable: true }
expect(() => releasedV0SessionFormatCodec.decodeArtifact(v0Header, [row]))
expect(() => restoreV0ToV1(v0Header, [row]))
.toThrow(/unknown historical event.*refuses.*ignorable/)
})
it('exercises each released test validation policy', () => {
const v0 = {
header: { version: 0, id: 'validation', createdAt: 1, isSeeded: false, delegationDepth: 0 },
inheritedEventCount: 0,
events: [],
} as const
const v1 = { ...v0, header: { ...v0.header, version: 1 } } as const
const forwardCompatible = {
...v1,
events: [{ type: 'plugin/future', seq: 0, time: 1, data: {}, ignorable: true }],
} as const
expect(() => { assertReleasedV0SourceArtifact(v0) }).not.toThrow()
expect(() => { assertNormalizedReleasedV0Artifact({
...v0,
events: [{ type: 'feedback/record', seq: 0, time: 1, data: { text: 'retained' } }],
}) }).not.toThrow()
expect(() => { assertReleasedV1MigrationSource(v1) }).not.toThrow()
expect(() => { assertReleasedV1MigrationSource(forwardCompatible) }).not.toThrow()
expect(() => { assertReleasedV1PhysicalArtifact(v1) }).not.toThrow()
expect(() => { assertReleasedV0SourceArtifact({
...v0,
events: [{
type: 'steering/message', seq: 0, time: 1, surfaceOp: 'append',
data: { turn: 1, content: [], source: { kind: 'user' } },
}],
}) }).not.toThrow()
expect(() => { assertReleasedV0SourceArtifact({
...v0,
events: [{ type: 'compact/start', seq: 0, time: 1, data: { turn: null } }],
}) }).not.toThrow()
expect(() => { assertReleasedV0SourceArtifact({
...v0,
events: [{ type: 'plugin/unknown', seq: 0, time: 1, data: {}, ignorable: true }],
}) }).toThrow(/unknown historical event/)
})
it('permits empty Assistant provenance only under the released-v1 policy', () => {
const assistant = {
type: 'assistant/message', seq: 1, time: 2, data: {},
sourceEventSeqs: [], surfaceOp: 'append',
}
expect(() => { assertReleasedSurfaceMetadata(assistant, 1, assistant.type, 'allow-empty-assistant') })
.not.toThrow()
expect(() => { assertReleasedSurfaceMetadata(assistant, 1, assistant.type, 'forbid-assistant') })
.toThrow(/obsolete chunk provenance/)
})
it('keeps capturedFormatVersion v1-only inside session-reference sources', () => {
const data = {
id: 'reference', role: 'user', content: [textBlock],
@@ -329,9 +390,9 @@ describe('released event and payload inventory', () => {
},
}
const event = { type: 'user/message', seq: 0, time: 1, data, surfaceOp: 'append' }
expect(() => sessionFormatV0ToV1.migrate(releasedV0SessionFormatCodec.decodeArtifact(v0Header, [event])))
expect(() => restoreV0ToV1(v0Header, [event]))
.toThrow(/capturedFormatVersion/)
expect(releasedV1SessionFormatCodec.decodeArtifact(v1Header, [event]).events).toEqual([event])
expect(restoreV1(v1Header, [event]).events).toEqual([event])
})
it('accepts every released nested union variant and optional member', () => {
@@ -489,6 +550,30 @@ describe('released event and payload inventory', () => {
}
})
it('validates legacy round-zero goal mutation messages', () => {
const change = {
kind: 'goal/change', version: 1, operation: 'clear',
cleared: { id: 'goal', revision: 2 }, clearedAt: 3,
}
const content = [{
type: 'text',
text: `<goal_state>${JSON.stringify({ cleared: change.cleared, clearedAt: change.clearedAt })}</goal_state>`,
}]
const message = {
id: 'legacy-goal', role: 'user', content,
source: { kind: 'goal', goalId: 'goal', revision: 2, round: 0, change },
}
expect(() => { assertPayload('user/message', message) }).not.toThrow()
expect(() => { assertPayload('user/message', {
...message, source: { ...message.source, round: 1 },
}) }).toThrow(/round must be 0/)
expect(() => { assertPayload('user/message', {
...message, source: { ...message.source, goalId: 'other' },
}) }).toThrow(/does not match/)
expect(() => { assertPayload('user/message', { ...message, content: [textBlock] }) })
.toThrow(/content does not match/)
})
it('refuses malformed logical headers, cuts, event envelopes, and surface metadata', () => {
const base = v1Artifact('turn/start', { turn: 1 })
const invalidHeaders = [
@@ -1,40 +1,42 @@
import {
SessionFormatError,
sessionFormatCount,
snapshotSessionFormatArtifact,
sessionFormatSafeInteger,
snapshotSessionFormatJson,
} from '@deepseek-ai/dsh-session-format'
import type {
EncodedSessionFormatArtifact,
SessionFormatArtifact,
SessionFormatArtifactDecoder,
SessionFormatCodec,
SessionFormatCurrentEncoder,
SessionFormatEvent,
SessionFormatHeader,
SessionFormatJsonObject,
SessionFormatJsonValue,
SessionFormatRecovery,
} from '@deepseek-ai/dsh-session-format'
import { assertReleasedV2Header, assertReleasedV2PhysicalArtifact } from './validation.ts'
import { assertReleasedV2Header } from './validation.ts'
const HEADER_REQUIRED = ['type', 'version', 'id', 'createdAt', 'isSeeded', 'delegationDepth'] as const
const HEADER_OPTIONAL = ['cwd', 'parentSession', 'origin', 'agentPreset'] as const
const EVENT_REQUIRED = ['type', 'seq', 'time', 'data'] as const
const EVENT_OPTIONAL = ['ignorable', 'sourceEventSeqs', 'surfaceOp'] as const
const EVENT_KEYS: ReadonlySet<string> = new Set([...EVENT_REQUIRED, ...EVENT_OPTIONAL])
/** Frozen physical JSON codec for released v2. */
export const releasedV2SessionFormatCodec = Object.freeze({
version: 2,
decodeHeader(value: unknown) {
return decodePhysicalHeader(value)
},
decodeArtifact(headerValue: unknown, rowValues: readonly unknown[]) {
return decodeArtifact(headerValue, rowValues, false)
createDecoder(headerValue: unknown, recovery: SessionFormatRecovery) {
return createDecoder(headerValue, recovery)
},
decodeRecoverableArtifact(headerValue: unknown, rowValues: readonly unknown[]) {
return decodeArtifact(headerValue, rowValues, true)
encodeHeader(header: SessionFormatHeader, inheritedEventCount: number) {
return encodeHeader(header, inheritedEventCount)
},
encodeArtifact(artifact: SessionFormatArtifact) {
return encodeArtifact(artifact)
encodeEvent(event: SessionFormatEvent) {
return encodeProvenance(event)
},
} satisfies SessionFormatCodec & {
encodeArtifact(artifact: SessionFormatArtifact): EncodedSessionFormatArtifact
})
} satisfies SessionFormatCodec & SessionFormatCurrentEncoder)
function decodePhysicalHeader(value: unknown): SessionFormatHeader {
const snapshot = snapshotSessionFormatJson(value, 'released v2 physical header')
@@ -70,78 +72,96 @@ function decodePhysicalHeader(value: unknown): SessionFormatHeader {
return header
}
function decodeArtifact(
function createDecoder(
headerValue: unknown,
rowValues: readonly unknown[],
recoverable: boolean,
): SessionFormatArtifact {
recovery: SessionFormatRecovery,
): SessionFormatArtifactDecoder {
const header = decodePhysicalHeader(headerValue)
const events: SessionFormatEvent[] = []
let rowIndex = 0
let eventCount = 0
let inheritedEventCount: number | undefined
let issue: SessionFormatError | undefined
for (const [rowIndex, value] of rowValues.entries()) {
let event: SessionFormatEvent
try {
event = decodeEvent(value, rowIndex)
} catch (error: unknown) {
const current = error instanceof SessionFormatError
? error
: new SessionFormatError(`released v2 row ${rowIndex} is malformed`, { cause: error })
if (!recoverable) throw current
issue ??= current
continue
}
if (issue !== undefined) {
if (event.type === 'turn/end') throw issue
continue
}
if (event.seq !== events.length) {
const gap = new SessionFormatError(
`released v2 row ${rowIndex} has seq gap (expected ${events.length}, got ${event.seq})`,
)
if (!recoverable) throw gap
issue = gap
if (event.type === 'turn/end') throw issue
continue
}
events.push(event)
return {
header,
decodeRow(value, context) {
const currentRow = rowIndex
rowIndex += 1
let event: SessionFormatEvent
try {
event = decodeEvent(value, currentRow)
} catch (error: unknown) {
const current = error instanceof SessionFormatError
? error
: new SessionFormatError(`released v2 row ${currentRow} is malformed`, { cause: error })
if (recovery === 'strict') throw current
issue ??= current
return
}
if (issue !== undefined) {
if (event.type === 'turn/end') throw issue
return
}
if (event.seq !== eventCount) {
const gap = new SessionFormatError(
`released v2 row ${currentRow} has seq gap (expected ${eventCount}, got ${event.seq})`,
)
if (recovery === 'strict') throw gap
issue = gap
if (event.type === 'turn/end') throw issue
return
}
eventCount += 1
if (event.type === 'session/end-seed') {
const data = jsonRecord(event.data, `session/end-seed ${event.seq} data`)
if (data['inherited'] === true) inheritedEventCount = event.seq
}
context.emitEvent(event)
},
finish(_context) {
if (header.isSeeded && inheritedEventCount === undefined) {
throw new SessionFormatError('released v2 seeded Session lacks an inherited end-seed marker')
}
if (!header.isSeeded && inheritedEventCount !== undefined) {
throw new SessionFormatError('released v2 unseeded Session contains an inherited end-seed marker')
}
return inheritedEventCount ?? 0
},
}
const inheritedEventCount = deriveInheritedEventCount(header, events)
const artifact = snapshotSessionFormatArtifact({ header, inheritedEventCount, events }, 'released v2 artifact')
assertReleasedV2PhysicalArtifact(artifact)
return artifact
}
function decodeEvent(value: unknown, rowIndex: number): SessionFormatEvent {
const snapshot = snapshotSessionFormatJson(value, `released v2 row ${rowIndex}`)
const record = jsonRecord(snapshot, `released v2 row ${rowIndex}`)
const record = jsonRecord(value as SessionFormatJsonValue, `released v2 row ${rowIndex}`)
const missing = EVENT_REQUIRED.find(key => !Object.hasOwn(record, key))
if (missing !== undefined) throw new SessionFormatError(`released v2 row ${rowIndex} lacks required field ${missing}`)
const unexpected = Object.keys(record).find(key => !EVENT_KEYS.has(key))
if (unexpected !== undefined) {
throw new SessionFormatError(`released v2 row ${rowIndex} has unexpected field ${unexpected}`)
}
if (typeof record['type'] !== 'string') {
throw new SessionFormatError(`released v2 row ${rowIndex} type must be a string`)
}
sessionFormatSafeInteger(record['time'], `released v2 row ${rowIndex} time`)
if (record['ignorable'] !== undefined && record['ignorable'] !== true) {
throw new SessionFormatError(`released v2 row ${rowIndex} ignorable must be true when present`)
}
if (record['sourceEventSeqs'] === undefined) return record as unknown as SessionFormatEvent
const seq = sessionFormatCount(record['seq'], `released v2 row ${rowIndex} seq`)
return snapshotSessionFormatJson({
return {
...record,
sourceEventSeqs: decodeSeqRanges(record['sourceEventSeqs'], seq),
}, `released v2 row ${rowIndex} provenance`) as SessionFormatEvent
} as unknown as SessionFormatEvent
}
function deriveInheritedEventCount(header: SessionFormatHeader, events: readonly SessionFormatEvent[]): number {
let cut: number | undefined
for (const event of events) {
if (event.type !== 'session/end-seed') continue
const data = jsonRecord(event.data, `session/end-seed ${event.seq} data`)
if (data['inherited'] === true) cut = event.seq
function encodeHeader(
header: SessionFormatHeader,
inheritedEventCount: number,
): SessionFormatJsonObject {
assertReleasedV2Header(header)
const cut = sessionFormatCount(inheritedEventCount, 'format v2 inherited event count')
if (!header.isSeeded && cut !== 0) {
throw new SessionFormatError('unseeded format v2 Session has inherited events')
}
if (header.isSeeded && cut === undefined) {
throw new SessionFormatError('released v2 seeded Session lacks an inherited end-seed marker')
}
if (!header.isSeeded && cut !== undefined) {
throw new SessionFormatError('released v2 unseeded Session contains an inherited end-seed marker')
}
return cut ?? 0
}
function encodeArtifact(artifact: SessionFormatArtifact): EncodedSessionFormatArtifact {
assertReleasedV2PhysicalArtifact(artifact)
const header = artifact.header
const physicalHeader = snapshotSessionFormatJson({
return {
type: 'session',
version: 2,
id: header.id,
@@ -152,17 +172,15 @@ function encodeArtifact(artifact: SessionFormatArtifact): EncodedSessionFormatAr
...(header.origin === undefined ? {} : { origin: header.origin }),
delegationDepth: header.delegationDepth,
...(header.agentPreset === undefined ? {} : { agentPreset: header.agentPreset }),
}, 'released v2 encoded header') as SessionFormatJsonObject
const rows = Object.freeze(artifact.events.map(event => encodeProvenance(event)))
return Object.freeze({ header: physicalHeader, rows })
}
}
function encodeProvenance(event: SessionFormatEvent): SessionFormatJsonObject {
if (event.sourceEventSeqs === undefined) return event
return snapshotSessionFormatJson({
return {
...event,
sourceEventSeqs: encodeSeqRanges(event.sourceEventSeqs as readonly number[]),
}, `released v2 event ${event.seq} provenance`) as SessionFormatJsonObject
}
}
function decodeSeqRanges(value: SessionFormatJsonValue, maxEntries: number): readonly number[] {
@@ -4,4 +4,4 @@ export { releasedV1SessionFormatCodec } from '@deepseek-ai/dsh-session-format-v0
export * from './codec.ts'
export * from './dispositions.ts'
export * from './migration.ts'
export * from './validation.ts'
export { assertReleasedV2Header, restoreReleasedV2Artifact } from './validation.ts'
@@ -1,33 +1,41 @@
import { AssistantStreamAccumulator } from '@deepseek-ai/dsh-llm'
import type { AssistantStreamRecord } from '@deepseek-ai/dsh-llm'
import {
SessionFormatUnsupportedMigrationError,
defineSessionFormatMigration,
snapshotSessionFormatArtifact,
} from '@deepseek-ai/dsh-session-format'
import type {
SessionFormatArtifact,
SessionFormatEvent,
SessionFormatEventRun,
SessionFormatJsonObject,
SessionFormatJsonValue,
SessionFormatHeader,
SessionFormatMigrationContext,
SessionFormatMigrationStage,
SessionFormatMigrationStageInput,
} from '@deepseek-ai/dsh-session-format'
import {
RELEASED_V0_EVENT_DISPOSITIONS,
assertReleasedV1Artifact,
assertReleasedEventPayload,
assertReleasedV1Header,
isReleasedAssistantChunkRun,
} from '@deepseek-ai/dsh-session-format-v0-to-v1'
import { assertReleasedV2Artifact, assertReleasedV2Header } from './validation.ts'
import { assertReleasedV2Header } from './validation.ts'
const CHUNK_EVENT_REQUIRED = ['type', 'seq', 'time', 'data'] as const
const CHUNK_EVENT_OPTIONAL = ['ignorable', 'sourceEventSeqs', 'surfaceOp'] as const
const CHUNK_EVENT_KEYS: ReadonlySet<string> = new Set([...CHUNK_EVENT_REQUIRED, ...CHUNK_EVENT_OPTIONAL])
interface AttemptGroup {
readonly turn: number
readonly step: number
readonly chunks: SessionFormatEvent[]
readonly spans: Array<{ firstSeq: number; eventCount: number }>
readonly stream: Array<{ record: AssistantStreamRecord; lastTime: number }>
accumulator?: AssistantStreamAccumulator
chunkCount: number
lastChunkSeq?: number
lastChunkTime?: number
terminal: boolean
messageSeq?: number
}
interface StagedEvent {
readonly origin: number
readonly event: SessionFormatEvent
}
/** Adjacent migration that embeds released-v1 top-level Assistant chunks into v2 attempt events. */
@@ -39,164 +47,501 @@ export const sessionFormatV1ToV2 = defineSessionFormatMigration({
assertReleasedV1Header(header)
return { ...header, version: 2 }
},
migrate(source) {
assertReleasedV1Artifact(source)
const unknown = source.events.find(event => RELEASED_V0_EVENT_DISPOSITIONS[event.type] === undefined)
if (unknown !== undefined) {
throw refusal(`format v1 contains unknown event type ${JSON.stringify(unknown.type)} at seq ${unknown.seq}`)
}
const groups = collectAttemptGroups(source.events)
const groupByChunk = new Map<number, AttemptGroup>()
const groupByMessage = new Map<number, AttemptGroup>()
for (const group of groups) {
for (const chunk of group.chunks) groupByChunk.set(chunk.seq, group)
if (group.messageSeq !== undefined) groupByMessage.set(group.messageSeq, group)
}
const staged: StagedEvent[] = []
const oldToNew = new Map<number, number>()
for (const sourceEvent of source.events) {
const group = groupByChunk.get(sourceEvent.seq)
if (group !== undefined) {
if (group.messageSeq === undefined && sourceEvent.seq === group.chunks.at(-1)?.seq) {
stage(staged, oldToNew, sourceEvent.seq, attemptEvent(group))
}
continue
}
const messageGroup = groupByMessage.get(sourceEvent.seq)
if (messageGroup !== undefined) {
stage(staged, oldToNew, sourceEvent.seq, messageEvent(sourceEvent, messageGroup))
continue
}
const event = source.header.isSeeded
&& sourceEvent.seq === source.inheritedEventCount
&& sourceEvent.type === 'session/end-seed'
? { ...sourceEvent, data: { inherited: true } }
: sourceEvent
stage(staged, oldToNew, sourceEvent.seq, event)
}
const inheritedEventCount = remapInheritedCut(source, groups, staged)
if (source.header.isSeeded && source.events[source.inheritedEventCount]?.type !== 'session/end-seed') {
const next = source.events[source.inheritedEventCount]
const previous = source.events[source.inheritedEventCount - 1]
staged.splice(inheritedEventCount, 0, {
origin: -1,
event: {
type: 'session/end-seed',
seq: inheritedEventCount,
time: next?.time ?? previous?.time ?? source.header.createdAt,
data: { inherited: true },
},
})
oldToNew.clear()
for (const [seq, candidate] of staged.entries()) {
if (candidate.origin >= 0) oldToNew.set(candidate.origin, seq)
}
}
for (const group of groups) {
for (const chunk of group.chunks) oldToNew.delete(chunk.seq)
}
const target = snapshotSessionFormatArtifact({
header: { ...source.header, version: 2 },
inheritedEventCount,
events: staged.map(({ event }, seq) => remapReferences(event, seq, oldToNew)),
}, 'released v1-to-v2 target')
assertReleasedV2Artifact(target)
return target
createStage(input) {
return input.sourceKind === 'decoded'
? new DecodedReleasedV1ToV2Stage(input)
: new TransformedReleasedV1ToV2Stage(input)
},
validateTarget: assertReleasedV2Artifact,
validateTargetHeader: assertReleasedV2Header,
})
function collectAttemptGroups(events: readonly SessionFormatEvent[]): readonly AttemptGroup[] {
const groups: AttemptGroup[] = []
const current = new Map<string, AttemptGroup>()
for (const event of events) {
if (event.type === 'assistant/chunk') {
const data = record(event.data)
const turn = coordinate(data['turn'])
const step = coordinate(data['step'])
const key = `${turn}:${step}`
let group = current.get(key)
if (group === undefined || group.terminal) {
group = { turn, step, chunks: [], terminal: false }
groups.push(group)
current.set(key, group)
}
group.chunks.push(event)
const chunk = record(data['chunk'])
if (chunk['type'] === 'finish') group.terminal = true
continue
class TransformedReleasedV1ToV2Stage implements SessionFormatMigrationStage {
private readonly state: ReleasedV1ToV2State
constructor(input: SessionFormatMigrationStageInput) {
assertReleasedV1Header(input.sourceHeader)
this.state = {
sourceHeader: input.sourceHeader,
sourceCut: input.sourceInheritedEventCount,
mapping: new Map(),
legacyTurns: legacyTurnState(),
pending: undefined,
targetSeq: 0,
targetCut: input.sourceHeader.isSeeded ? undefined : 0,
lastTime: input.sourceHeader.createdAt,
}
if (event.type !== 'assistant/message') {
closeAttemptAtBoundary(event, current)
continue
}
const data = record(event.data)
const turn = coordinate(data['turn'])
const step = coordinate(data['step'])
const sources = event.sourceEventSeqs
if (!Array.isArray(sources)) {
const unclaimed = groups.some(candidate => candidate.messageSeq === undefined
&& candidate.turn === turn
&& candidate.step === step)
if (unclaimed) throw refusal(`assistant/message ${event.seq} does not cite its complete v1 chunk attempt`)
groups.push({ turn, step, chunks: [], terminal: true, messageSeq: event.seq })
continue
}
if (sources.length === 0) {
// Released v1 uses an explicit empty list to state that this message
// owns no preceding chunks; an absent list cannot make that claim.
groups.push({ turn, step, chunks: [], terminal: true, messageSeq: event.seq })
continue
}
const group = groups.find(candidate => candidate.messageSeq === undefined
&& candidate.turn === turn
&& candidate.step === step
&& sameNumbers(candidate.chunks.map(chunk => chunk.seq), sources))
if (group === undefined) {
throw refusal(`assistant/message ${event.seq} chunk provenance is not one complete ordered attempt`)
}
group.messageSeq = event.seq
group.terminal = true
}
return groups
transformEvent(
event: SessionFormatEvent,
context: SessionFormatMigrationContext,
): void {
transformReleasedEvent(this.state, event, context)
}
transformRun(
run: SessionFormatEventRun,
context: SessionFormatMigrationContext,
): void {
transformReleasedRun(this.state, run, context)
}
finish(context: SessionFormatMigrationContext): number {
return finishMigration(this.state, context)
}
}
function closeAttemptAtBoundary(
event: SessionFormatEvent,
current: ReadonlyMap<string, AttemptGroup>,
): void {
if (event.type === 'turn/end') {
const data = record(event.data)
const turn = coordinate(data['turn'])
for (const group of current.values()) {
if (group.turn === turn) group.terminal = true
class DecodedReleasedV1ToV2Stage extends TransformedReleasedV1ToV2Stage {
override transformEvent(
event: SessionFormatEvent,
context: SessionFormatMigrationContext,
): void {
if (event.type !== 'assistant/chunk'
&& RELEASED_V0_EVENT_DISPOSITIONS[event.type] !== undefined) {
assertReleasedEventPayload(event, 1)
}
super.transformEvent(event, context)
}
}
interface StreamingAttempt {
readonly group: AttemptGroup
readonly afterLastChunk: SessionFormatEvent[]
}
interface ReleasedV1ToV2State {
readonly sourceHeader: SessionFormatHeader
readonly sourceCut: number
readonly mapping: Map<number, number>
readonly legacyTurns: LegacyTurnState
pending: StreamingAttempt | undefined
targetSeq: number
targetCut: number | undefined
lastTime: number
}
function transformReleasedEvent(
state: ReleasedV1ToV2State,
event: SessionFormatEvent,
context: SessionFormatMigrationContext,
): void {
if (event.type === 'assistant/chunk') assertChunkEnvelope(event)
if (RELEASED_V0_EVENT_DISPOSITIONS[event.type] === undefined) {
throw refusal(`format v1 contains unknown event type ${JSON.stringify(event.type)} at seq ${event.seq}`)
}
const interrupted = legacyInterruptedTurn(state.legacyTurns, event)
if (event.type === 'turn/start' && state.legacyTurns.openTurn !== null && interrupted === undefined) {
throw refusal(`turn/start ${JSON.stringify(record(event.data)['turn'])} does not close the prior turn`)
}
assertSourceDeliveryMarker(state, event)
observeLegacyTurn(state.legacyTurns, event)
state.lastTime = event.time
if (interrupted !== undefined) {
finishAttempt(state, context)
emitGenerated(state, event.seq, interrupted, context)
}
const legacyGoal = splitLegacyGoalChange(event)
if (legacyGoal !== undefined) {
emitGenerated(state, event.seq, legacyGoal.change, context)
emitSource(state, legacyGoal.message, context)
return
}
if (event.type !== 'step/end'
&& event.type !== 'llm/retry'
&& event.type !== 'llm/retry-started') return
if (event.type === 'assistant/chunk') {
transformChunk(state, event, context)
return
}
if (event.type === 'assistant/message') {
transformMessage(state, event, context)
return
}
if (closesAttempt(event)) {
finishAttempt(state, context)
emitSource(state, event, context)
return
}
if (state.pending !== undefined) {
state.pending.afterLastChunk.push(event)
return
}
emitSource(state, event, context)
}
function assertChunkEnvelope(event: SessionFormatEvent): void {
const unexpected = Object.keys(event).find(key => !CHUNK_EVENT_KEYS.has(key))
if (unexpected !== undefined) throw refusal(`assistant/chunk ${event.seq} has unexpected member ${unexpected}`)
const missing = CHUNK_EVENT_REQUIRED.find(key => !Object.hasOwn(event, key))
if (missing !== undefined) throw refusal(`assistant/chunk ${event.seq} lacks required member ${missing}`)
if (event.ignorable !== undefined && event.ignorable !== true) {
throw refusal(`assistant/chunk ${event.seq} ignorable must be true when present`)
}
}
function assertSourceDeliveryMarker(state: ReleasedV1ToV2State, event: SessionFormatEvent): void {
if (event.type !== 'session-log-deepseek/delivery-accepted') return
const data = record(event.data)
const inherited = state.sourceHeader.parentSession !== undefined && event.seq < state.sourceCut
if (data['sessionFormatVersion'] === 1 && !inherited && data['sessionId'] !== state.sourceHeader.id) {
throw refusal('current-generation delivery marker names the wrong Session')
}
}
function transformReleasedRun(
state: ReleasedV1ToV2State,
run: SessionFormatEventRun,
context: SessionFormatMigrationContext,
): void {
if (!isReleasedAssistantChunkRun(run)) {
for (const event of run.expand()) transformReleasedEvent(state, event, context)
return
}
state.legacyTurns.previous = undefined
state.lastTime = run.lastTime
if (state.pending !== undefined
&& (state.pending.group.terminal
|| state.pending.group.turn !== run.turn
|| state.pending.group.step !== run.step)) {
finishAttempt(state, context)
} else if (state.pending !== undefined) {
flushBuffered(state, state.pending, context)
}
state.pending ??= { group: attemptGroup(run.turn, run.step), afterLastChunk: [] }
assertAttemptRange(state, run.firstSeq, run.lastSeq)
flushAccumulator(state.pending.group)
appendStreamRecord(
state.pending.group,
run.stream as unknown as AssistantStreamRecord,
run.lastTime,
)
recordChunkSpan(state.pending.group, run.firstSeq, run.eventCount, run.lastTime)
}
function finishMigration(
state: ReleasedV1ToV2State,
context: SessionFormatMigrationContext,
): number {
finishAttempt(state, context)
if (state.sourceHeader.isSeeded && state.targetCut === undefined) {
state.targetCut = state.targetSeq
context.emitEvent({
type: 'session/end-seed',
seq: state.targetSeq,
time: state.lastTime,
data: { inherited: true },
})
state.targetSeq += 1
}
return state.targetCut as number
}
function transformChunk(
state: ReleasedV1ToV2State,
event: SessionFormatEvent,
context: SessionFormatMigrationContext,
): void {
const data = record(event.data)
const turn = coordinate(data['turn'])
const step = coordinate(data['step'])
const group = current.get(`${turn}:${step}`)
if (group !== undefined) group.terminal = true
const chunk = record(data['chunk'])
if (state.pending !== undefined
&& (state.pending.group.terminal
|| state.pending.group.turn !== turn
|| state.pending.group.step !== step)) {
finishAttempt(state, context)
} else if (state.pending !== undefined) {
flushBuffered(state, state.pending, context)
}
state.pending ??= { group: attemptGroup(turn, step), afterLastChunk: [] }
assertAttemptCut(state, state.pending.group, event.seq)
state.pending.group.accumulator ??= new AssistantStreamAccumulator()
state.pending.group.accumulator.push({
time: event.time,
chunk: data['chunk'] as Parameters<AssistantStreamAccumulator['push']>[0]['chunk'],
})
recordChunkSpan(state.pending.group, event.seq, 1, event.time)
if (chunk['type'] === 'finish') state.pending.group.terminal = true
}
function transformMessage(
state: ReleasedV1ToV2State,
event: SessionFormatEvent,
context: SessionFormatMigrationContext,
): void {
const data = record(event.data)
const turn = coordinate(data['turn'])
const step = coordinate(data['step'])
const sources = event.sourceEventSeqs
const pending = state.pending
if (pending !== undefined && (pending.group.turn !== turn || pending.group.step !== step)) {
finishAttempt(state, context)
emitSource(state, messageEvent(event, attemptGroup(turn, step)), context)
return
}
if (!Array.isArray(sources)) {
if (pending !== undefined) {
throw refusal(`assistant/message ${event.seq} does not cite its complete v1 chunk attempt`)
}
emitSource(state, messageEvent(event, attemptGroup(turn, step)), context)
return
}
if (sources.length === 0) {
finishAttempt(state, context)
emitSource(state, messageEvent(event, attemptGroup(turn, step)), context)
return
}
if (pending === undefined
|| !matchesChunkSources(pending.group, sources)) {
throw refusal(`assistant/message ${event.seq} chunk provenance is not one complete ordered attempt`)
}
assertAttemptCut(state, pending.group, event.seq)
pending.group.terminal = true
flushBuffered(state, pending, context)
emitSource(state, messageEvent(event, pending.group), context)
state.pending = undefined
}
function finishAttempt(
state: ReleasedV1ToV2State,
context: SessionFormatMigrationContext,
): void {
const pending = state.pending
if (pending === undefined) return
emitGenerated(state, pending.group.lastChunkSeq as number, attemptEvent(pending.group), context)
flushBuffered(state, pending, context)
state.pending = undefined
}
function flushBuffered(
state: ReleasedV1ToV2State,
pending: StreamingAttempt,
context: SessionFormatMigrationContext,
): void {
for (const event of pending.afterLastChunk) emitSource(state, event, context)
pending.afterLastChunk.length = 0
}
function emitSource(
state: ReleasedV1ToV2State,
event: SessionFormatEvent,
context: SessionFormatMigrationContext,
): void {
let source = event
if (state.sourceHeader.isSeeded
&& event.seq === state.sourceCut
&& event.type === 'session/end-seed') {
source = { ...event, data: { inherited: true } }
}
ensureTargetCut(state, event.seq, event.time, source.type, context)
state.mapping.set(event.seq, state.targetSeq)
context.emitEvent(remapReferences(source, state.targetSeq, state.mapping))
state.targetSeq += 1
}
function emitGenerated(
state: ReleasedV1ToV2State,
origin: number,
event: SessionFormatEvent,
context: SessionFormatMigrationContext,
): void {
ensureTargetCut(state, origin, event.time, event.type, context)
context.emitEvent(remapReferences(event, state.targetSeq, state.mapping))
state.targetSeq += 1
}
function ensureTargetCut(
state: ReleasedV1ToV2State,
origin: number,
time: number,
type: string,
context: SessionFormatMigrationContext,
): void {
if (!state.sourceHeader.isSeeded || state.targetCut !== undefined || origin < state.sourceCut) return
state.targetCut = state.targetSeq
if (origin === state.sourceCut && type === 'session/end-seed') return
context.emitEvent({
type: 'session/end-seed',
seq: state.targetSeq,
time,
data: { inherited: true },
})
state.targetSeq += 1
}
function assertAttemptCut(state: ReleasedV1ToV2State, group: AttemptGroup, member: number): void {
const first = group.spans[0]?.firstSeq ?? member
if ((first < state.sourceCut) !== (member < state.sourceCut)) {
throw refusal(`inherited Session cut ${state.sourceCut} splits one Assistant attempt`)
}
}
function assertAttemptRange(state: ReleasedV1ToV2State, first: number, last: number): void {
if ((first < state.sourceCut) !== (last < state.sourceCut)) {
throw refusal(`inherited Session cut ${state.sourceCut} splits one Assistant attempt`)
}
}
interface LegacyTurnState {
openTurn: number | null
openStep: number | null
previous: SessionFormatEvent | undefined
}
function legacyTurnState(): LegacyTurnState {
return { openTurn: null, openStep: null, previous: undefined }
}
function legacyInterruptedTurn(
state: LegacyTurnState,
event: SessionFormatEvent,
): SessionFormatEvent | undefined {
if (event.type !== 'turn/start' || state.openTurn === null || state.openStep !== null
|| coordinate(record(event.data)['turn']) !== state.openTurn + 1
|| state.previous?.type !== 'agent/inbox/spliced') return undefined
const splice = record(state.previous.data)
if (splice['target'] !== 'next-turn' || !Array.isArray(splice['inserted']) || splice['inserted'].length === 0) {
return undefined
}
return {
type: 'turn/end',
seq: event.seq,
time: event.time,
data: { turn: state.openTurn, reason: { kind: 'interrupted' } },
}
}
function observeLegacyTurn(state: LegacyTurnState, event: SessionFormatEvent): void {
const data = record(event.data)
if (event.type === 'turn/start') {
state.openTurn = coordinate(data['turn'])
state.openStep = null
} else if (event.type === 'turn/end') {
state.openTurn = null
state.openStep = null
} else if (event.type === 'step/start') {
state.openStep = coordinate(data['step'])
} else if (event.type === 'step/end') {
state.openStep = null
}
state.previous = event
}
function splitLegacyGoalChange(event: SessionFormatEvent): {
readonly change: SessionFormatEvent
readonly message: SessionFormatEvent
} | undefined {
if (event.type !== 'user/message') return undefined
const data = record(event.data)
const source = record(data['source'])
if (source['kind'] !== 'goal' || source['change'] === undefined) return undefined
return {
change: {
type: 'goal/change',
seq: event.seq,
time: event.time,
data: source['change'],
},
message: {
...event,
data: { ...data, source: { kind: 'plugin', plugin: 'goal' } },
},
}
}
function closesAttempt(event: SessionFormatEvent): boolean {
return event.type === 'turn/end'
|| event.type === 'step/end'
|| event.type === 'llm/retry'
|| event.type === 'llm/retry-started'
}
function attemptGroup(turn: number, step: number): AttemptGroup {
return { turn, step, spans: [], stream: [], chunkCount: 0, terminal: false }
}
function recordChunkSpan(group: AttemptGroup, firstSeq: number, eventCount: number, lastTime: number): void {
const previous = group.spans.at(-1)
if (previous !== undefined && previous.firstSeq + previous.eventCount === firstSeq) {
previous.eventCount += eventCount
} else {
group.spans.push({ firstSeq, eventCount })
}
group.chunkCount += eventCount
group.lastChunkSeq = firstSeq + eventCount - 1
group.lastChunkTime = lastTime
}
function matchesChunkSources(group: AttemptGroup, sources: readonly unknown[]): boolean {
if (sources.length !== group.chunkCount) return false
let index = 0
for (const span of group.spans) {
for (let offset = 0; offset < span.eventCount; offset += 1) {
if (sources[index] !== span.firstSeq + offset) return false
index += 1
}
}
return true
}
function recordLastTime(record: AssistantStreamRecord): number {
if (record.type === 'chunk') return record.time
return record.dt.reduce((time, gap) => time + gap, record.time0)
}
function mutableRecord(record: AssistantStreamRecord): AssistantStreamRecord {
if (record.type === 'chunk') return record
if (record.type === 'tool-call-chunks') {
return { ...record, dt: [...record.dt], args: [...record.args] }
}
return { ...record, dt: [...record.dt], texts: [...record.texts] }
}
function appendStreamRecord(
group: AttemptGroup,
source: AssistantStreamRecord,
lastTime: number,
owned = true,
): void {
const previous = group.stream.at(-1)
if (previous === undefined || source.type === 'chunk' || previous.record.type !== source.type) {
group.stream.push({ record: owned ? source : mutableRecord(source), lastTime })
return
}
const gap = source.time0 - previous.lastTime
if (previous.record.index !== source.index || !Number.isSafeInteger(gap)) {
group.stream.push({ record: owned ? source : mutableRecord(source), lastTime })
return
}
if (source.type === 'tool-call-chunks') {
const target = previous.record as Extract<AssistantStreamRecord, { readonly type: 'tool-call-chunks' }>
if (target.id !== source.id || target.name !== source.name) {
group.stream.push({ record: owned ? source : mutableRecord(source), lastTime })
return
}
;(target.dt as number[]).push(gap)
for (const value of source.dt) (target.dt as number[]).push(value)
for (const value of source.args) (target.args as string[]).push(value)
} else {
const target = previous.record as Extract<AssistantStreamRecord, {
readonly type: 'text-chunks' | 'reasoning-chunks'
}>
;(target.dt as number[]).push(gap)
for (const value of source.dt) (target.dt as number[]).push(value)
for (const value of source.texts) (target.texts as string[]).push(value)
}
previous.lastTime = lastTime
}
function flushAccumulator(group: AttemptGroup): void {
const accumulator = group.accumulator
if (accumulator === undefined) return
for (const record of accumulator.snapshot()) {
appendStreamRecord(group, record, recordLastTime(record), false)
}
delete group.accumulator
}
function streamOf(group: AttemptGroup) {
const accumulator = new AssistantStreamAccumulator()
for (const event of group.chunks) {
const data = record(event.data)
accumulator.push({
time: event.time,
chunk: data['chunk'] as Parameters<AssistantStreamAccumulator['push']>[0]['chunk'],
})
}
return accumulator.snapshot() as unknown as SessionFormatJsonValue
flushAccumulator(group)
return group.stream.map(({ record }) => record) as unknown as SessionFormatJsonValue
}
function messageEvent(source: SessionFormatEvent, group: AttemptGroup): SessionFormatEvent {
@@ -209,42 +554,14 @@ function messageEvent(source: SessionFormatEvent, group: AttemptGroup): SessionF
}
function attemptEvent(group: AttemptGroup): SessionFormatEvent {
const last = group.chunks.at(-1) as SessionFormatEvent
return {
type: 'assistant/attempt',
seq: last.seq,
time: last.time,
seq: group.lastChunkSeq as number,
time: group.lastChunkTime as number,
data: { turn: group.turn, step: group.step, stream: streamOf(group) },
}
}
function stage(
staged: StagedEvent[],
oldToNew: Map<number, number>,
origin: number,
event: SessionFormatEvent,
): void {
oldToNew.set(origin, staged.length)
staged.push({ origin, event })
}
function remapInheritedCut(
source: SessionFormatArtifact,
groups: readonly AttemptGroup[],
staged: readonly StagedEvent[],
): number {
const cut = source.inheritedEventCount
for (const group of groups) {
const members = group.messageSeq === undefined
? group.chunks.map(chunk => chunk.seq)
: [...group.chunks.map(chunk => chunk.seq), group.messageSeq]
const before = members.some(seq => seq < cut)
const after = members.some(seq => seq >= cut)
if (before && after) throw refusal(`inherited Session cut ${cut} splits one Assistant attempt`)
}
return staged.filter(candidate => candidate.origin < cut).length
}
function remapReferences(
source: SessionFormatEvent,
targetSeq: number,
@@ -360,7 +677,6 @@ function mapOne(
return mapped
}
/* assertReleasedV1Artifact validates these payload coordinates before migration. */
function record(value: SessionFormatJsonValue | undefined): SessionFormatJsonObject {
return value as SessionFormatJsonObject
}
@@ -373,10 +689,6 @@ function coordinate(value: SessionFormatJsonValue | undefined): number {
return value as number
}
function sameNumbers(left: readonly number[], right: readonly number[]): boolean {
return left.length === right.length && left.every((value, index) => value === right[index])
}
function refusal(message: string): SessionFormatUnsupportedMigrationError {
return new SessionFormatUnsupportedMigrationError(message)
}
@@ -0,0 +1,105 @@
import { BlockAssembler, expandAssistantStream } from '@deepseek-ai/dsh-llm'
import { deepEqualJson } from '@deepseek-ai/dsh-util-values'
import {
SessionFormatError,
SessionFormatUnsupportedMigrationError,
sessionFormatCount,
snapshotSessionFormatJson,
} from '@deepseek-ai/dsh-session-format'
import type {
SessionFormatArtifact,
SessionFormatEvent,
SessionFormatJsonValue,
} from '@deepseek-ai/dsh-session-format'
import {
assertReleasedPayloadSemantics,
assertReleasedSurfaceMetadata,
} from '@deepseek-ai/dsh-session-format-v0-to-v1'
import { RELEASED_V2_EVENT_DISPOSITIONS, RELEASED_V2_EVENT_TYPES } from '../dispositions.ts'
import {
assertReleasedV2Keys,
assertReleasedV2PhysicalArtifact,
releasedV2Record,
restoreReleasedV2Artifact,
} from '../validation.ts'
const RELEASED_V2_EVENT_TYPE_SET = new Set(RELEASED_V2_EVENT_TYPES)
const SURFACE_TYPES = new Set(['user/message', 'assistant/message', 'tool/result'])
/**
* Validate the frozen released-v2 writer image in tests.
* @param artifact - released-v2 logical artifact.
*/
export function assertReleasedV2Artifact(artifact: SessionFormatArtifact): void {
assertReleasedV2PhysicalArtifact(artifact)
for (const [index, event] of artifact.events.entries()) {
const disposition = RELEASED_V2_EVENT_DISPOSITIONS[event.type]
if (disposition === undefined) {
throw new SessionFormatUnsupportedMigrationError(
`format v2 contains unknown event type ${JSON.stringify(event.type)} at seq ${index}`,
)
}
if (SURFACE_TYPES.has(event.type)) {
assertReleasedSurfaceMetadata(event, index, event.type, 'forbid-assistant')
}
assertPayload(event, disposition)
}
restoreReleasedV2Artifact(artifact, RELEASED_V2_EVENT_TYPE_SET)
}
export { assertReleasedV2PhysicalArtifact } from '../validation.ts'
function assertPayload(
event: SessionFormatEvent,
disposition: (typeof RELEASED_V2_EVENT_DISPOSITIONS)[string],
): void {
const data = releasedV2Record(event.data, `${event.type} ${event.seq} data`)
assertReleasedV2Keys(data, disposition.required, disposition.optional, `${event.type} ${event.seq} data`)
for (const key of disposition.opaque) {
if (Object.hasOwn(data, key)) snapshotSessionFormatJson(data[key], `${event.type} ${event.seq} opaque ${key}`)
}
if (event.type === 'assistant/attempt' || event.type === 'assistant/message') {
const turn = sessionFormatCount(data['turn'], `${event.type} ${event.seq} turn`)
const step = sessionFormatCount(data['step'], `${event.type} ${event.seq} step`)
const assembler = new BlockAssembler()
let timed: ReturnType<typeof expandAssistantStream>
try {
timed = expandAssistantStream(data['stream'] as never)
for (const member of timed) {
assertReleasedPayloadSemantics({
type: 'assistant/chunk',
seq: event.seq,
time: member.time,
data: { turn, step, chunk: member.chunk } as unknown as SessionFormatJsonValue,
}, 2)
assembler.push(member.chunk)
}
} catch (error: unknown) {
throw new SessionFormatError(`${event.type} ${event.seq} has an invalid embedded stream`, { cause: error })
}
if (event.type === 'assistant/attempt') return
assertReleasedPayloadSemantics(event, 2)
if (timed.length > 0) {
const message = releasedV2Record(data['message'], `assistant/message ${event.seq} message`)
const content = data['interrupted'] === true ? assembler.interruptedBlocks() : assembler.blocks()
if (!deepEqualJson(message['content'], content)) {
throw new SessionFormatError(`assistant/message ${event.seq} message content disagrees with its embedded stream`)
}
if (!deepEqualJson(data['usage'], assembler.usage)) {
throw new SessionFormatError(`assistant/message ${event.seq} usage disagrees with its embedded stream`)
}
const source = releasedV2Record(message['source'], `assistant/message ${event.seq} source`)
if (!deepEqualJson(source['replayState'], assembler.replayState)) {
throw new SessionFormatError(`assistant/message ${event.seq} replay state disagrees with its embedded stream`)
}
}
return
}
if (event.type === 'session/end-seed') {
if (data['inherited'] !== undefined && data['inherited'] !== true) {
throw new SessionFormatError(`session/end-seed ${event.seq} inherited must be true when present`)
}
return
}
assertReleasedPayloadSemantics(event, 2)
}
@@ -1,26 +1,18 @@
import { isAbsolute } from 'node:path'
import { BlockAssembler, expandAssistantStream } from '@deepseek-ai/dsh-llm'
import { deepEqualJson } from '@deepseek-ai/dsh-util-values'
import {
SessionFormatError,
SessionFormatUnsupportedMigrationError,
sessionFormatCount,
sessionFormatSafeInteger,
snapshotSessionFormatJson,
} from '@deepseek-ai/dsh-session-format'
import type {
SessionFormatArtifact,
SessionFormatEvent,
SessionFormatHeader,
SessionFormatJsonObject,
SessionFormatJsonValue,
} from '@deepseek-ai/dsh-session-format'
import {
assertReleasedArtifactRelationships,
assertReleasedPayloadSemantics,
assertReleasedSurfaceMetadata,
} from '@deepseek-ai/dsh-session-format-v0-to-v1'
import { RELEASED_V2_EVENT_DISPOSITIONS, RELEASED_V2_EVENT_TYPES } from './dispositions.ts'
import { assertReleasedArtifactRelationships } from '@deepseek-ai/dsh-session-format-v0-to-v1'
import { RELEASED_V2_EVENT_DISPOSITIONS } from './dispositions.ts'
const HEADER_REQUIRED = ['version', 'id', 'createdAt', 'isSeeded', 'delegationDepth'] as const
const HEADER_OPTIONAL = ['cwd', 'parentSession', 'origin', 'agentPreset'] as const
@@ -28,7 +20,6 @@ const EVENT_REQUIRED = ['type', 'seq', 'time', 'data'] as const
const SURFACE_TYPES = new Set(['user/message', 'assistant/message', 'tool/result'])
const SURFACE_OPTIONAL = ['ignorable', 'sourceEventSeqs', 'surfaceOp'] as const
const LOG_OPTIONAL = ['ignorable'] as const
const RELEASED_V2_EVENT_TYPE_SET = new Set(RELEASED_V2_EVENT_TYPES)
const RELEASED_V2_RELATIONSHIP_EXTENSIONS = {
stepEvents: new Set(['assistant/attempt']),
preservedSourceTitleRequestText: true,
@@ -40,8 +31,8 @@ const RELEASED_V2_RELATIONSHIP_EXTENSIONS = {
* @throws {SessionFormatError} when the header is not an exact released-v2 value.
*/
export function assertReleasedV2Header(header: SessionFormatHeader): void {
const record = jsonRecord(header, 'format v2 header')
exactKeys(record, HEADER_REQUIRED, HEADER_OPTIONAL, 'format v2 header')
const record = releasedV2Record(header, 'format v2 header')
assertReleasedV2Keys(record, HEADER_REQUIRED, HEADER_OPTIONAL, 'format v2 header')
if (record['version'] !== 2) throw new SessionFormatError('expected format v2 header')
if (typeof record['id'] !== 'string') throw new SessionFormatError('format v2 header id must be a string')
sessionFormatCount(record['createdAt'], 'format v2 header createdAt')
@@ -60,28 +51,9 @@ export function assertReleasedV2Header(header: SessionFormatHeader): void {
}
}
/**
* Validate the exact logical image emitted by the released v2 writer.
* @param artifact - complete decoded released-v2 Session artifact.
* @throws {SessionFormatError} when an envelope, payload, relationship, or inherited cut is invalid.
* @throws {SessionFormatUnsupportedMigrationError} when the artifact contains an unknown event type.
*/
export function assertReleasedV2Artifact(artifact: SessionFormatArtifact): void {
validateReleasedV2Artifact(artifact, 'target', RELEASED_V2_EVENT_TYPE_SET)
}
/**
* Validate only the released-v2 physical header, event envelopes, and inherited cut.
* Event vocabulary and payload semantics belong to target or installed-current restoration.
* @param artifact - complete physical-codec output.
*/
export function assertReleasedV2PhysicalArtifact(artifact: SessionFormatArtifact): void {
validateReleasedV2Artifact(artifact, 'physical')
}
function validateReleasedV2Artifact(
artifact: SessionFormatArtifact,
mode: 'target' | 'current' | 'physical',
mode: 'current' | 'physical',
knownEventTypes?: ReadonlySet<string>,
): void {
assertReleasedV2Header(artifact.header)
@@ -90,15 +62,13 @@ function validateReleasedV2Artifact(
if (!artifact.header.isSeeded && cut !== 0) throw new SessionFormatError('unseeded format v2 Session has inherited events')
let lastInheritedMarker: number | undefined
for (const [index, event] of artifact.events.entries()) {
const record = jsonRecord(event, `format v2 event ${index}`)
const record = releasedV2Record(event, `format v2 event ${index}`)
const type = record['type']
if (typeof type !== 'string') throw new SessionFormatError(`format v2 event ${index} type must be a string`)
const disposition = RELEASED_V2_EVENT_DISPOSITIONS[type]
const installed = knownEventTypes?.has(type) === true
const ignorableUnknown = disposition === undefined
&& mode === 'current'
&& record['ignorable'] === true
if (mode !== 'physical' && disposition === undefined && !installed && !ignorableUnknown) {
const ignorableUnknown = disposition === undefined && record['ignorable'] === true
if (mode === 'current' && disposition === undefined && !installed && !ignorableUnknown) {
throw new SessionFormatUnsupportedMigrationError(
`format v2 contains unknown event type ${JSON.stringify(type)} at seq ${index}`,
)
@@ -107,16 +77,14 @@ function validateReleasedV2Artifact(
const optional = mode === 'physical' || disposition === undefined
? SURFACE_OPTIONAL
: surface ? SURFACE_OPTIONAL : LOG_OPTIONAL
exactKeys(record, EVENT_REQUIRED, optional, `format v2 event ${index}`)
assertReleasedV2Keys(record, EVENT_REQUIRED, optional, `format v2 event ${index}`)
if (record['seq'] !== index) throw new SessionFormatError(`format v2 event ${index} is not dense`)
sessionFormatSafeInteger(record['time'], `format v2 event ${index} time`)
if (record['ignorable'] !== undefined && record['ignorable'] !== true) {
throw new SessionFormatError(`format v2 event ${index} ignorable must be true when present`)
}
if (mode === 'target' && surface) assertReleasedSurfaceMetadata(record, index, type, 'forbid-assistant')
if (mode === 'target' && disposition !== undefined) assertPayload(event, disposition)
if (type === 'session/end-seed') {
const data = jsonRecord(event.data, `session/end-seed ${index} data`)
const data = releasedV2Record(event.data, `session/end-seed ${index} data`)
if (data['inherited'] === true) lastInheritedMarker = index
}
}
@@ -126,74 +94,35 @@ function validateReleasedV2Artifact(
if (!artifact.header.isSeeded && lastInheritedMarker !== undefined) {
throw new SessionFormatError('format v2 unseeded Session contains an inherited end-seed marker')
}
if (mode === 'target') {
if (mode === 'current') {
assertReleasedArtifactRelationships(artifact, RELEASED_V2_RELATIONSHIP_EXTENSIONS)
}
}
function assertPayload(
event: SessionFormatEvent,
disposition: (typeof RELEASED_V2_EVENT_DISPOSITIONS)[string],
): void {
const data = jsonRecord(event.data, `${event.type} ${event.seq} data`)
exactKeys(data, disposition.required, disposition.optional, `${event.type} ${event.seq} data`)
for (const key of disposition.opaque) {
if (Object.hasOwn(data, key)) snapshotSessionFormatJson(data[key], `${event.type} ${event.seq} opaque ${key}`)
}
if (event.type === 'assistant/attempt' || event.type === 'assistant/message') {
const turn = sessionFormatCount(data['turn'], `${event.type} ${event.seq} turn`)
const step = sessionFormatCount(data['step'], `${event.type} ${event.seq} step`)
const assembler = new BlockAssembler()
let timed: ReturnType<typeof expandAssistantStream>
try {
timed = expandAssistantStream(data['stream'] as never)
for (const member of timed) {
assertReleasedPayloadSemantics({
type: 'assistant/chunk',
seq: event.seq,
time: member.time,
data: { turn, step, chunk: member.chunk } as unknown as SessionFormatJsonValue,
}, 2)
assembler.push(member.chunk)
}
} catch (error: unknown) {
throw new SessionFormatError(`${event.type} ${event.seq} has an invalid embedded stream`, { cause: error })
}
if (event.type === 'assistant/attempt') return
assertReleasedPayloadSemantics(event, 2)
if (timed.length > 0) {
const message = jsonRecord(data['message'], `assistant/message ${event.seq} message`)
const content = data['interrupted'] === true ? assembler.interruptedBlocks() : assembler.blocks()
if (!deepEqualJson(message['content'], content)) {
throw new SessionFormatError(`assistant/message ${event.seq} message content disagrees with its embedded stream`)
}
if (!deepEqualJson(data['usage'], assembler.usage)) {
throw new SessionFormatError(`assistant/message ${event.seq} usage disagrees with its embedded stream`)
}
const source = jsonRecord(message['source'], `assistant/message ${event.seq} source`)
if (!deepEqualJson(source['replayState'], assembler.replayState)) {
throw new SessionFormatError(`assistant/message ${event.seq} replay state disagrees with its embedded stream`)
}
}
return
}
if (event.type === 'session/end-seed') {
if (data['inherited'] !== undefined && data['inherited'] !== true) {
throw new SessionFormatError(`session/end-seed ${event.seq} inherited must be true when present`)
}
return
}
assertReleasedPayloadSemantics(event, 2)
}
function jsonRecord(value: SessionFormatJsonValue | undefined, label: string): SessionFormatJsonObject {
/**
* Require one released-v2 value to be a JSON object.
* @param value - value to narrow.
* @param label - diagnostic subject.
* @returns the narrowed object.
*/
export function releasedV2Record(
value: SessionFormatJsonValue | undefined,
label: string,
): SessionFormatJsonObject {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new SessionFormatError(`${label} must be an object`)
}
return value as SessionFormatJsonObject
}
function exactKeys(
/**
* Require one released-v2 object to contain exactly the admitted keys.
* @param value - object to inspect.
* @param required - keys that must be present.
* @param optional - additional keys that may be present.
* @param label - diagnostic subject.
*/
export function assertReleasedV2Keys(
value: SessionFormatJsonObject,
required: readonly string[],
optional: readonly string[],
@@ -219,3 +148,11 @@ export function restoreReleasedV2Artifact(
validateReleasedV2Artifact(artifact, 'current', knownEventTypes)
return artifact
}
/**
* Validate the released-v2 physical envelope without interpreting event vocabulary.
* @param artifact - released-v2 physical artifact.
*/
export function assertReleasedV2PhysicalArtifact(artifact: SessionFormatArtifact): void {
validateReleasedV2Artifact(artifact, 'physical')
}
@@ -1,8 +1,11 @@
import { describe, expect, it } from 'vitest'
import { SessionFormatEventCollector } from '@deepseek-ai/dsh-session-format'
import type {
SessionFormatArtifact,
SessionFormatArtifactDecoder,
SessionFormatEvent,
SessionFormatJsonObject,
SessionFormatRecovery,
} from '@deepseek-ai/dsh-session-format'
import { decodeSeqRanges as decodeCurrentSeqRanges } from '@deepseek-ai/dsh-session'
import { releasedV2SessionFormatCodec } from '@deepseek-ai/dsh-session-format-v1-to-v2'
@@ -21,6 +24,10 @@ const fullPhysicalHeader = {
const textBlock = { type: 'text', text: 'text' } as const
function throwUnknown(value: unknown): never {
throw value
}
function feedback(seq: number): SessionFormatEvent {
return { type: 'feedback/record', seq, time: seq + 1, data: { text: `feedback-${seq}` } }
}
@@ -50,6 +57,27 @@ function artifact(
}
}
function decodeV2(
header: unknown,
rows: readonly unknown[],
recovery: SessionFormatRecovery = 'strict',
): SessionFormatArtifact {
const decoder: SessionFormatArtifactDecoder = releasedV2SessionFormatCodec.createDecoder(header, recovery)
const context = new SessionFormatEventCollector()
for (const row of rows) decoder.decodeRow(row, context)
return { header: decoder.header, inheritedEventCount: decoder.finish(context), events: context.values }
}
function encodeV2(source: SessionFormatArtifact): {
readonly header: SessionFormatJsonObject
readonly rows: readonly SessionFormatJsonObject[]
} {
return {
header: releasedV2SessionFormatCodec.encodeHeader(source.header, source.inheritedEventCount),
rows: source.events.map(event => releasedV2SessionFormatCodec.encodeEvent(event)),
}
}
describe('releasedV2SessionFormatCodec headers', () => {
it('round-trips the minimal and complete optional header images', () => {
expect(releasedV2SessionFormatCodec.version).toBe(2)
@@ -68,7 +96,7 @@ describe('releasedV2SessionFormatCodec headers', () => {
agentPreset: 'default',
})
const encodedMinimal = releasedV2SessionFormatCodec.encodeArtifact(artifact([]))
const encodedMinimal = encodeV2(artifact([]))
expect(encodedMinimal).toStrictEqual({ header: minimalPhysicalHeader, rows: [] })
const complete = artifact([], {
header: {
@@ -83,8 +111,12 @@ describe('releasedV2SessionFormatCodec headers', () => {
agentPreset: 'default',
},
})
expect(releasedV2SessionFormatCodec.encodeArtifact(complete).header)
expect(encodeV2(complete).header)
.toStrictEqual(fullPhysicalHeader)
expect(() => releasedV2SessionFormatCodec.encodeHeader(
artifact([]).header,
1,
)).toThrow(/unseeded.*inherited events/)
})
it.each([
@@ -117,14 +149,14 @@ describe('releasedV2SessionFormatCodec rows', () => {
userMessage(2, [0, 1]),
userMessage(3, [0, 1, 2]),
])
const encoded = releasedV2SessionFormatCodec.encodeArtifact(source)
const encoded = encodeV2(source)
expect(encoded.rows.map(row => row['sourceEventSeqs'])).toStrictEqual([
undefined,
[0],
[0, 1],
[[0, 2]],
])
expect(releasedV2SessionFormatCodec.decodeArtifact(encoded.header, encoded.rows)).toStrictEqual(source)
expect(decodeV2(encoded.header, encoded.rows)).toStrictEqual(source)
})
it('keeps non-monotonic provenance scalar-only for the current backend reader', () => {
@@ -134,12 +166,12 @@ describe('releasedV2SessionFormatCodec rows', () => {
userMessage(6, sourceEventSeqs),
])
const encoded = releasedV2SessionFormatCodec.encodeArtifact(source)
const encoded = encodeV2(source)
const stored = encoded.rows[6]?.['sourceEventSeqs']
expect(stored).toStrictEqual(sourceEventSeqs)
expect(decodeCurrentSeqRanges(stored, 6)).toStrictEqual(sourceEventSeqs)
expect(releasedV2SessionFormatCodec.decodeArtifact(encoded.header, encoded.rows)).toStrictEqual(source)
expect(decodeV2(encoded.header, encoded.rows)).toStrictEqual(source)
})
it('keeps the v2 physical codec vocabulary-neutral for current growth and a future source freeze', () => {
@@ -149,9 +181,9 @@ describe('releasedV2SessionFormatCodec rows', () => {
{ type: 'turn/start', seq: 2, time: 3, data: { turn: 1, postReleaseMember: true } },
])
const encoded = releasedV2SessionFormatCodec.encodeArtifact(source)
const encoded = encodeV2(source)
expect(releasedV2SessionFormatCodec.decodeArtifact(encoded.header, encoded.rows)).toStrictEqual(source)
expect(decodeV2(encoded.header, encoded.rows)).toStrictEqual(source)
})
it('expands mixed stored ranges and preserves non-provenance rows', () => {
@@ -159,10 +191,10 @@ describe('releasedV2SessionFormatCodec rows', () => {
...userMessage(5),
sourceEventSeqs: [[0, 2], 4],
}]
const decoded = releasedV2SessionFormatCodec.decodeArtifact(minimalPhysicalHeader, rows)
const decoded = decodeV2(minimalPhysicalHeader, rows)
expect(decoded.events[0]).toStrictEqual(feedback(0))
expect(decoded.events[5]?.sourceEventSeqs).toStrictEqual([0, 1, 2, 4])
const descending = releasedV2SessionFormatCodec.decodeArtifact(minimalPhysicalHeader, [
const descending = decodeV2(minimalPhysicalHeader, [
feedback(0), feedback(1), { ...userMessage(2), sourceEventSeqs: [1, 0] },
])
expect(descending.events[2]?.sourceEventSeqs).toStrictEqual([1, 0])
@@ -185,42 +217,61 @@ describe('releasedV2SessionFormatCodec rows', () => {
const rows = [feedback(0), feedback(1), feedback(2), feedback(3), {
...userMessage(4), sourceEventSeqs,
}]
expect(() => releasedV2SessionFormatCodec.decodeArtifact(minimalPhysicalHeader, rows)).toThrow(message)
expect(() => decodeV2(minimalPhysicalHeader, rows)).toThrow(message)
})
it('contains ordinary and non-SessionFormatError row failures in a recoverable tail', () => {
const explosive = new Proxy({}, { ownKeys: () => { throw new Error('proxy failure') } })
expect(releasedV2SessionFormatCodec.decodeRecoverableArtifact(
const explosive = new Proxy(feedback(1), { ownKeys: () => { throw new Error('proxy failure') } })
const primitiveFailure = new Proxy(feedback(1), {
ownKeys: () => throwUnknown('primitive proxy failure'),
})
expect(decodeV2(
minimalPhysicalHeader,
[feedback(0), explosive],
'recoverable',
).events).toStrictEqual([feedback(0)])
expect(releasedV2SessionFormatCodec.decodeRecoverableArtifact(
expect(decodeV2(
minimalPhysicalHeader,
[feedback(0), null],
'recoverable',
).events).toStrictEqual([feedback(0)])
expect(releasedV2SessionFormatCodec.decodeRecoverableArtifact(
expect(decodeV2(
minimalPhysicalHeader,
[feedback(0), primitiveFailure],
'recoverable',
).events).toStrictEqual([feedback(0)])
expect(decodeV2(
minimalPhysicalHeader,
[null, [], feedback(0)],
'recoverable',
).events).toStrictEqual([])
})
it('refuses malformed strict rows, strict gaps, and terminal recoverable tails', () => {
expect(() => releasedV2SessionFormatCodec.decodeArtifact(minimalPhysicalHeader, [null]))
expect(() => decodeV2(minimalPhysicalHeader, [null]))
.toThrow(/row 0/)
expect(() => releasedV2SessionFormatCodec.decodeArtifact(minimalPhysicalHeader, [feedback(1)]))
expect(() => decodeV2(minimalPhysicalHeader, [feedback(1)]))
.toThrow(/seq gap/)
expect(() => releasedV2SessionFormatCodec.decodeRecoverableArtifact(minimalPhysicalHeader, [{
expect(() => decodeV2(minimalPhysicalHeader, [{
type: 'turn/end', seq: 1, time: 1, data: { turn: 1, reason: { kind: 'completed' } },
}])).toThrow(/seq gap/)
expect(() => releasedV2SessionFormatCodec.decodeRecoverableArtifact(minimalPhysicalHeader, [null, {
}], 'recoverable')).toThrow(/seq gap/)
expect(() => decodeV2(minimalPhysicalHeader, [null, {
type: 'turn/end', seq: 0, time: 1, data: { turn: 1, reason: { kind: 'completed' } },
}])).toThrow(/row 0/)
}], 'recoverable')).toThrow(/row 0/)
expect(() => decodeV2(minimalPhysicalHeader, [{ ...feedback(0), extra: true }]))
.toThrow(/unexpected field extra/)
expect(() => decodeV2(minimalPhysicalHeader, [{ type: 'feedback/record', seq: 0, time: 1 }]))
.toThrow(/lacks required field data/)
expect(() => decodeV2(minimalPhysicalHeader, [{ ...feedback(0), type: 1 }]))
.toThrow(/type must be a string/)
expect(() => decodeV2(minimalPhysicalHeader, [{ ...feedback(0), ignorable: false }]))
.toThrow(/ignorable must be true/)
})
it('keeps the prefix before a recoverable non-terminal seq gap', () => {
expect(releasedV2SessionFormatCodec.decodeRecoverableArtifact(minimalPhysicalHeader, [
expect(decodeV2(minimalPhysicalHeader, [
feedback(0), feedback(3), feedback(1),
]).events).toStrictEqual([feedback(0)])
], 'recoverable').events).toStrictEqual([feedback(0)])
})
it('derives the last tagged seed marker and rejects lineage disagreements', () => {
@@ -230,13 +281,13 @@ describe('releasedV2SessionFormatCodec rows', () => {
{ type: 'session/end-seed', seq: 1, time: 2, data: {} },
{ type: 'session/end-seed', seq: 2, time: 3, data: { inherited: true } },
]
expect(releasedV2SessionFormatCodec.decodeArtifact(seededHeader, markers).inheritedEventCount).toBe(2)
expect(releasedV2SessionFormatCodec.decodeArtifact(seededHeader, [markers[0]]).inheritedEventCount).toBe(0)
expect(() => releasedV2SessionFormatCodec.decodeArtifact(seededHeader, []))
expect(decodeV2(seededHeader, markers).inheritedEventCount).toBe(2)
expect(decodeV2(seededHeader, [markers[0]]).inheritedEventCount).toBe(0)
expect(() => decodeV2(seededHeader, []))
.toThrow(/lacks an inherited end-seed marker/)
expect(() => releasedV2SessionFormatCodec.decodeArtifact(minimalPhysicalHeader, [markers[0]]))
expect(() => decodeV2(minimalPhysicalHeader, [markers[0]]))
.toThrow(/unseeded Session contains an inherited end-seed marker/)
expect(() => releasedV2SessionFormatCodec.decodeArtifact(seededHeader, [{
expect(() => decodeV2(seededHeader, [{
type: 'session/end-seed', seq: 0, time: 1, data: null,
}])).toThrow(/must be an object/)
})
@@ -1,6 +1,23 @@
import { describe, expect, it } from 'vitest'
import { sessionFormatV1ToV2 } from '@deepseek-ai/dsh-session-format-v1-to-v2'
import type { SessionFormatArtifact, SessionFormatEvent } from '@deepseek-ai/dsh-session-format'
import {
assertReleasedV2Header,
releasedV2SessionFormatCodec,
sessionFormatV1ToV2,
} from '@deepseek-ai/dsh-session-format-v1-to-v2'
import { assertReleasedV2Artifact } from '../src/testing/validation.ts'
import {
releasedV0SessionFormatCodec,
releasedV1SessionFormatCodec,
sessionFormatV0ToV1,
} from '@deepseek-ai/dsh-session-format-v0-to-v1'
import type {
SessionFormatArtifact,
SessionFormatEvent,
SessionFormatEventRun,
SessionFormatHeader,
SessionFormatJsonObject,
} from '@deepseek-ai/dsh-session-format'
import { createSessionFormatCatalog, SessionFormatEventCollector } from '@deepseek-ai/dsh-session-format'
const message = {
id: 'assistant-1',
@@ -20,6 +37,94 @@ function event(type: string, seq: number, time: number, data: SessionFormatEvent
return { type, seq, time, data }
}
const catalog = createSessionFormatCatalog({
currentVersion: 2,
codecs: [releasedV0SessionFormatCodec, releasedV1SessionFormatCodec, releasedV2SessionFormatCodec],
currentEncoder: releasedV2SessionFormatCodec,
migrations: [sessionFormatV0ToV1, sessionFormatV1ToV2],
restoreCurrent(artifact) {
assertReleasedV2Artifact(artifact)
return artifact
},
restoreTransformedCurrent(artifact) {
assertReleasedV2Artifact(artifact)
return artifact
},
restoreCurrentHeader(header) {
assertReleasedV2Header(header)
return header
},
})
function physicalV1Header(header: SessionFormatHeader, inheritedEventCount: number) {
return {
type: 'session',
version: 1,
id: header.id,
createdAt: header.createdAt,
...(header.cwd === undefined ? {} : { cwd: header.cwd }),
...(header.parentSession === undefined ? {} : { parentSession: header.parentSession }),
...(header.isSeeded ? { seedLength: inheritedEventCount } : {}),
...(header.origin === undefined ? {} : { origin: header.origin }),
delegationDepth: header.delegationDepth,
...(header.agentPreset === undefined ? {} : { agentPreset: header.agentPreset }),
}
}
function migrateV1ToV2(source: SessionFormatArtifact): SessionFormatArtifact {
const restore = catalog.createRestore(
physicalV1Header(source.header, source.inheritedEventCount),
{ recovery: 'strict', validation: 'current' },
)
for (const row of source.events) restore.decodeRow(row)
return restore.finish()
}
function stageHarness(options: {
readonly id?: string
readonly sourceCut?: number
readonly seeded?: boolean
readonly sourceKind?: 'decoded' | 'transformed'
} = {}) {
const sourceHeader: SessionFormatHeader = {
version: 1,
id: options.id ?? 'stage',
createdAt: 1,
...(options.seeded === true ? { parentSession: 'parent' } : {}),
isSeeded: options.seeded === true,
delegationDepth: 0,
}
const stage = sessionFormatV1ToV2.createStage({
sourceHeader,
targetHeader: sessionFormatV1ToV2.migrateHeader(sourceHeader),
sourceInheritedEventCount: options.sourceCut ?? 0,
sourceKind: options.sourceKind ?? 'decoded',
})
return { stage, output: new SessionFormatEventCollector() }
}
function packedRun(options: {
readonly firstSeq: number
readonly eventCount?: number
readonly turn?: number
readonly step?: number
readonly lastTime: number
readonly stream: SessionFormatJsonObject
}): SessionFormatEventRun {
const eventCount = options.eventCount ?? 1
return {
runType: 'released-assistant-chunks',
firstSeq: options.firstSeq,
eventCount,
turn: options.turn ?? 1,
step: options.step ?? 1,
lastSeq: options.firstSeq + eventCount - 1,
lastTime: options.lastTime,
stream: options.stream,
*expand() {},
} as SessionFormatEventRun
}
describe('sessionFormatV1ToV2', () => {
it('migrates one exact released-v1 header without reading events', () => {
const header = {
@@ -33,6 +138,346 @@ describe('sessionFormatV1ToV2', () => {
expect(() => sessionFormatV1ToV2.migrateHeader({ ...header, version: 0 })).toThrow(/v1 header/)
})
it('validates a directly decoded v1 source before transforming it', () => {
const header = {
version: 1, id: 'source-validation', createdAt: 1, isSeeded: false, delegationDepth: 0,
}
expect(() => migrateV1ToV2({
header,
inheritedEventCount: 0,
events: [event('external/post-v1', 0, 1, null)],
})).toThrow(/unknown event type/)
expect(() => migrateV1ToV2({
header,
inheritedEventCount: 0,
events: [{
type: 'turn/start', seq: 0, time: 1, data: { turn: 1, postReleaseMember: true },
}],
})).toThrow(/unexpected member/)
})
it.each([
['unexpected member', { ...event('assistant/chunk', 0, 1, {}), extra: true }, /unexpected member extra/],
['missing member', { type: 'assistant/chunk', seq: 0, time: 1 }, /lacks required member data/],
['invalid ignorable marker', { ...event('assistant/chunk', 0, 1, {}), ignorable: false }, /ignorable must be true/],
])('refuses an Assistant chunk envelope with an %s', (_name, candidate, expected) => {
const { stage, output } = stageHarness({ sourceKind: 'transformed' })
expect(() => { stage.transformEvent(candidate as SessionFormatEvent, output) }).toThrow(expected)
})
it('checks own-generation delivery markers but accepts inherited markers', () => {
const marker = (sessionId: string): SessionFormatEvent => event(
'session-log-deepseek/delivery-accepted',
1,
2,
{ sessionId, throughSeq: 0, sessionFormatVersion: 1 },
)
const own = stageHarness({ id: 'child' })
expect(() => { own.stage.transformEvent(marker('other'), own.output) }).toThrow(/wrong Session/)
const inherited = stageHarness({ id: 'child', seeded: true, sourceCut: 2 })
inherited.stage.transformEvent(marker('parent'), inherited.output)
expect(inherited.output.values).toEqual([{ ...marker('parent'), seq: 0 }])
})
it('expands generic runs and refuses a packed run split by the inherited cut', () => {
const generic = stageHarness({ sourceKind: 'transformed' })
const retained = event('feedback/record', 0, 1, { text: 'retained' })
generic.stage.transformRun({
runType: 'test-run', firstSeq: 0, eventCount: 1, *expand() { yield retained },
}, generic.output)
expect(generic.output.values).toEqual([retained])
const split = stageHarness({ seeded: true, sourceCut: 1 })
expect(() => { split.stage.transformRun(packedRun({
firstSeq: 0,
eventCount: 2,
lastTime: 2,
stream: { type: 'text-chunks', time0: 1, index: 0, dt: [1], texts: ['a', 'b'] },
}), split.output) }).toThrow(/splits one Assistant attempt/)
})
it('coalesces adjacent packed text and tool-call runs without expanding them', () => {
const text = stageHarness()
text.stage.transformRun(packedRun({
firstSeq: 0, lastTime: 1,
stream: { type: 'text-chunks', time0: 1, index: 0, dt: [], texts: ['a'] },
}), text.output)
text.stage.transformRun(packedRun({
firstSeq: 1, eventCount: 2, lastTime: 4,
stream: { type: 'text-chunks', time0: 3, index: 0, dt: [1], texts: ['b', 'c'] },
}), text.output)
text.stage.finish(text.output)
expect(text.output.values[0]?.data).toMatchObject({
stream: [{ type: 'text-chunks', time0: 1, index: 0, dt: [2, 1], texts: ['a', 'b', 'c'] }],
})
const tool = stageHarness()
tool.stage.transformEvent(event('assistant/chunk', 0, 1, {
turn: 1, step: 1,
chunk: { type: 'tool-call-delta', index: 0, id: 'call', name: 'read', argumentsDelta: '{' },
}), tool.output)
tool.stage.transformRun(packedRun({
firstSeq: 1, eventCount: 2, lastTime: 3,
stream: {
type: 'tool-call-chunks', time0: 2, index: 0,
id: 'call', name: 'read', dt: [1], args: ['}', '!'],
},
}), tool.output)
tool.stage.finish(tool.output)
expect(tool.output.values[0]?.data).toMatchObject({
stream: [{
type: 'tool-call-chunks', time0: 1, index: 0,
id: 'call', name: 'read', dt: [1, 1], args: ['{', '}', '!'],
}],
})
})
it('keeps incompatible packed records and attempt groups separate', () => {
const mismatched = [
{ index: 1, id: 'call', name: 'read', time0: 2 },
{ index: 0, id: 'other', name: 'read', time0: 2 },
{ index: 0, id: 'call', name: 'write', time0: 2 },
{ index: 0, id: 'call', name: 'read', time0: Number.MAX_SAFE_INTEGER },
] as const
for (const [index, next] of mismatched.entries()) {
const current = stageHarness()
current.stage.transformRun(packedRun({
firstSeq: 0,
lastTime: index === mismatched.length - 1 ? Number.MIN_SAFE_INTEGER : 1,
stream: {
type: 'tool-call-chunks', time0: index === mismatched.length - 1 ? Number.MIN_SAFE_INTEGER : 1,
index: 0, id: 'call', name: 'read', dt: [], args: ['a'],
},
}), current.output)
current.stage.transformRun(packedRun({
firstSeq: 1,
lastTime: next.time0,
stream: {
type: 'tool-call-chunks', time0: next.time0,
index: next.index, id: next.id, name: next.name, dt: [], args: ['b'],
},
}), current.output)
current.stage.finish(current.output)
expect((current.output.values[0]?.data as { stream: unknown[] }).stream).toHaveLength(2)
}
const groups = stageHarness()
groups.stage.transformRun(packedRun({
firstSeq: 0, lastTime: 1,
stream: { type: 'text-chunks', time0: 1, index: 0, dt: [], texts: ['a'] },
}), groups.output)
groups.stage.transformRun(packedRun({
firstSeq: 1, step: 2, lastTime: 2,
stream: { type: 'text-chunks', time0: 2, index: 0, dt: [], texts: ['b'] },
}), groups.output)
groups.stage.finish(groups.output)
expect(groups.output.values).toHaveLength(2)
})
it('copies incompatible accumulator records behind an owned packed prefix', () => {
const text = stageHarness()
text.stage.transformRun(packedRun({
firstSeq: 0, lastTime: 1,
stream: { type: 'text-chunks', time0: 1, index: 0, dt: [], texts: ['packed'] },
}), text.output)
text.stage.transformEvent(event('assistant/chunk', 1, 2, {
turn: 1, step: 1, chunk: { type: 'text-delta', index: 1, text: 'a' },
}), text.output)
text.stage.transformEvent(event('assistant/chunk', 2, 3, {
turn: 1, step: 1, chunk: { type: 'text-delta', index: 1, text: 'b' },
}), text.output)
text.stage.transformRun(packedRun({
firstSeq: 3, lastTime: 4,
stream: { type: 'reasoning-chunks', time0: 4, index: 0, dt: [], texts: ['flush'] },
}), text.output)
text.stage.finish(text.output)
expect((text.output.values[0]?.data as { stream: unknown[] }).stream).toHaveLength(3)
const tool = stageHarness()
tool.stage.transformRun(packedRun({
firstSeq: 0, lastTime: 1,
stream: { type: 'tool-call-chunks', time0: 1, index: 0, id: 'call', dt: [], args: ['a'] },
}), tool.output)
tool.stage.transformEvent(event('assistant/chunk', 1, 2, {
turn: 1, step: 1,
chunk: { type: 'tool-call-delta', index: 0, id: 'other', argumentsDelta: 'b' },
}), tool.output)
tool.stage.transformRun(packedRun({
firstSeq: 2, lastTime: 3,
stream: { type: 'text-chunks', time0: 3, index: 0, dt: [], texts: ['flush'] },
}), tool.output)
tool.stage.finish(tool.output)
expect((tool.output.values[0]?.data as { stream: unknown[] }).stream).toHaveLength(3)
})
it('separates a terminal packed successor and a message from another attempt', () => {
const terminal = stageHarness()
terminal.stage.transformEvent(event('assistant/chunk', 0, 1, {
turn: 1, step: 1, chunk: { type: 'finish', reason: { kind: 'stop' } },
}), terminal.output)
terminal.stage.transformRun(packedRun({
firstSeq: 1, lastTime: 2,
stream: { type: 'text-chunks', time0: 2, index: 0, dt: [], texts: ['next'] },
}), terminal.output)
terminal.stage.finish(terminal.output)
expect(terminal.output.values).toHaveLength(2)
const messageMismatch = stageHarness()
messageMismatch.stage.transformEvent(event('assistant/chunk', 0, 1, {
turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'partial' },
}), messageMismatch.output)
messageMismatch.stage.transformEvent({
...event('assistant/message', 1, 2, { turn: 2, step: 1, message }),
surfaceOp: 'append',
}, messageMismatch.output)
expect(messageMismatch.output.values).toHaveLength(2)
})
it('settles incremental attempts and rejects ambiguous streamed input', () => {
const header = {
version: 1, id: 'streaming', createdAt: 1, isSeeded: false, delegationDepth: 0,
}
const chunk = (seq: number, value: string | undefined, turn = 1): SessionFormatEvent => event(
'assistant/chunk',
seq,
seq + 1,
{
turn,
step: 1,
chunk: value === undefined
? { type: 'finish', reason: { kind: 'stop' } }
: { type: 'text-delta', index: 0, text: value },
},
)
const assistant = (
seq: number,
turn: number,
sourceEventSeqs?: readonly number[],
): SessionFormatEvent => ({
...event('assistant/message', seq, seq + 1, { turn, step: 1, message }),
...(sourceEventSeqs === undefined ? {} : { sourceEventSeqs }),
surfaceOp: 'append',
})
const restarted = migrateV1ToV2({
header,
inheritedEventCount: 0,
events: [
event('turn/start', 0, 1, { turn: 1 }),
event('step/start', 1, 2, { turn: 1, step: 1 }),
chunk(2, 'first'),
chunk(3, undefined),
chunk(4, 'second'),
event('step/end', 5, 6, { turn: 1, step: 1 }),
event('turn/end', 6, 7, { turn: 1, reason: { kind: 'completed' } }),
],
})
expect(restarted.events.filter(candidate => candidate.type === 'assistant/attempt')).toHaveLength(2)
expect(() => migrateV1ToV2({
header,
inheritedEventCount: 0,
events: [chunk(0, 'partial'), assistant(1, 1)],
})).toThrow(/does not cite/)
expect(() => migrateV1ToV2({
header,
inheritedEventCount: 0,
events: [chunk(0, 'partial'), assistant(1, 1, [1])],
})).toThrow(/complete ordered attempt/)
expect(() => migrateV1ToV2({
header,
inheritedEventCount: 0,
events: [event('external/unknown', 0, 1, null)],
})).toThrow(/unknown event type/)
const seededHeader = { ...header, parentSession: 'parent', isSeeded: true }
expect(migrateV1ToV2({ header: seededHeader, inheritedEventCount: 0, events: [] }).events).toEqual([{
type: 'session/end-seed', seq: 0, time: 1, data: { inherited: true },
}])
expect(migrateV1ToV2({
header: seededHeader,
inheritedEventCount: 0,
events: [event('turn/start', 0, 2, { turn: 1 })],
}).events[0]).toEqual({
type: 'session/end-seed', seq: 0, time: 2, data: { inherited: true },
})
expect(() => migrateV1ToV2({
header: seededHeader,
inheritedEventCount: 1,
events: [chunk(0, 'partial'), assistant(1, 1, [0])],
})).toThrow(/splits one Assistant attempt/)
})
it('splits a legacy goal mutation from its preserved model-visible message', () => {
const change = {
kind: 'goal/change', version: 1, operation: 'create',
goal: { id: 'goal-1', revision: 1, objective: 'finish', phase: 'active', maxGoalRounds: 2 },
roundsStarted: 0, createdAt: 1, updatedAt: 1,
}
const payload = {
goal: change.goal,
roundsStarted: change.roundsStarted,
createdAt: change.createdAt,
updatedAt: change.updatedAt,
}
const source: SessionFormatArtifact = {
header: { version: 1, id: 'legacy-goal', createdAt: 1, isSeeded: false, delegationDepth: 0 },
inheritedEventCount: 0,
events: [
{
...event('user/message', 0, 1, {
id: 'goal-message', role: 'user',
content: [{ type: 'text', text: `<goal_state>${JSON.stringify(payload)}</goal_state>` }],
source: { kind: 'goal', goalId: 'goal-1', revision: 1, round: 0, change },
}),
surfaceOp: 'append',
},
],
}
expect(migrateV1ToV2(source).events).toMatchObject([
{ type: 'goal/change', seq: 0, data: change },
{ type: 'user/message', seq: 1, data: { source: { kind: 'plugin', plugin: 'goal' } } },
])
})
it('closes the prior turn for the bounded legacy next-turn resume pattern', () => {
const source: SessionFormatArtifact = {
header: { version: 1, id: 'legacy-turn', createdAt: 1, isSeeded: false, delegationDepth: 0 },
inheritedEventCount: 0,
events: [
event('turn/start', 0, 1, { turn: 1 }),
event('step/start', 1, 2, { turn: 1, step: 1 }),
event('step/end', 2, 3, { turn: 1, step: 1 }),
event('agent/inbox/spliced', 3, 4, {
target: 'next-turn', start: 0, inserted: [userMessage],
}),
event('turn/start', 4, 5, { turn: 2 }),
event('turn/end', 5, 6, { turn: 2, reason: { kind: 'completed' } }),
],
}
const migrated = migrateV1ToV2(source)
expect(migrated.events.map(candidate => candidate.type)).toEqual([
'turn/start', 'step/start', 'step/end', 'agent/inbox/spliced',
'turn/end', 'turn/start', 'turn/end',
])
expect(migrated.events[4]).toMatchObject({
data: { turn: 1, reason: { kind: 'interrupted' } },
})
const invalid = {
...source,
events: source.events.map(candidate => candidate.seq === 3
? event('agent/inbox/spliced', 3, 4, { target: 'next-turn', start: 0, inserted: [] })
: candidate),
}
expect(() => migrateV1ToV2(invalid)).toThrow(/turn\/start 2/)
})
it('embeds an interleaved successful stream and densely remaps survivors', () => {
const source: SessionFormatArtifact = {
header: {
@@ -73,7 +518,7 @@ describe('sessionFormatV1ToV2', () => {
],
}
expect(sessionFormatV1ToV2.migrate(source)).toStrictEqual({
expect(migrateV1ToV2(source)).toStrictEqual({
header: { ...source.header, version: 2 },
inheritedEventCount: 0,
events: [
@@ -133,7 +578,7 @@ describe('sessionFormatV1ToV2', () => {
],
}
expect(sessionFormatV1ToV2.migrate(source)).toStrictEqual({
expect(migrateV1ToV2(source)).toStrictEqual({
header: { ...source.header, version: 2 },
inheritedEventCount: 0,
events: [
@@ -203,7 +648,7 @@ describe('sessionFormatV1ToV2', () => {
],
}
const migrated = sessionFormatV1ToV2.migrate(source)
const migrated = migrateV1ToV2(source)
expect(migrated.events.filter(event => event.type.startsWith('assistant/'))).toStrictEqual([
event('assistant/attempt', 3, 110, {
turn: 1,
@@ -260,7 +705,7 @@ describe('sessionFormatV1ToV2', () => {
],
}
const migrated = sessionFormatV1ToV2.migrate(source)
const migrated = migrateV1ToV2(source)
expect(migrated.inheritedEventCount).toBe(5)
expect(migrated.events[5]).toStrictEqual({
type: 'session/end-seed',
@@ -284,7 +729,7 @@ describe('sessionFormatV1ToV2', () => {
events: [],
}
expect(sessionFormatV1ToV2.migrate(source)).toStrictEqual({
expect(migrateV1ToV2(source)).toStrictEqual({
header: { ...source.header, version: 2 },
inheritedEventCount: 0,
events: [{
@@ -305,7 +750,7 @@ describe('sessionFormatV1ToV2', () => {
inheritedEventCount: 1,
events: [event('feedback/record', 0, 9, { text: 'inherited' })],
}
expect(sessionFormatV1ToV2.migrate(source)).toMatchObject({
expect(migrateV1ToV2(source)).toMatchObject({
inheritedEventCount: 1,
events: [
{ type: 'feedback/record', seq: 0 },
@@ -351,7 +796,7 @@ describe('sessionFormatV1ToV2', () => {
],
}
expect(() => sessionFormatV1ToV2.migrate(source)).toThrow(/message content disagrees with its embedded stream/)
expect(() => migrateV1ToV2(source)).toThrow(/message content disagrees with its embedded stream/)
})
it('refuses an undeclared reference into the last consumed chunk of a failed attempt', () => {
@@ -391,7 +836,7 @@ describe('sessionFormatV1ToV2', () => {
],
}
expect(() => sessionFormatV1ToV2.migrate(source)).toThrow(
expect(() => migrateV1ToV2(source)).toThrow(
/command\/done 7 sourceEventSeq targets consumed assistant\/chunk 3/,
)
})
@@ -412,7 +857,7 @@ describe('sessionFormatV1ToV2', () => {
}],
}
expect(() => sessionFormatV1ToV2.migrate(source)).toThrow(
expect(() => migrateV1ToV2(source)).toThrow(
/format v1 contains unknown event type "external\/info" at seq 0/,
)
})
@@ -438,7 +883,7 @@ describe('sessionFormatV1ToV2', () => {
event('turn/end', 4, 5, { turn: 1, reason: { kind: 'completed' } }),
],
}
const migrated = sessionFormatV1ToV2.migrate(source)
const migrated = migrateV1ToV2(source)
expect(migrated.events[2]).toMatchObject({
type: 'assistant/message', data: { stream: [] }, surfaceOp: 'append',
})
@@ -470,9 +915,51 @@ describe('sessionFormatV1ToV2', () => {
event('turn/end', 6, 7, { turn: 1, reason: { kind: 'completed' } }),
],
})
expect(() => sessionFormatV1ToV2.migrate(build(undefined))).toThrow(/does not cite/)
expect(() => sessionFormatV1ToV2.migrate(build([2]))).toThrow(/complete ordered attempt/)
expect(() => sessionFormatV1ToV2.migrate(build([3, 2]))).toThrow(/complete ordered attempt/)
expect(() => migrateV1ToV2(build(undefined))).toThrow(/does not cite/)
expect(() => migrateV1ToV2(build([2]))).toThrow(/complete ordered attempt/)
expect(() => migrateV1ToV2(build([3, 2]))).toThrow(/complete ordered attempt/)
})
it.each([
['a message after its attempt step has closed', [
event('turn/start', 0, 1, { turn: 1 }),
event('step/start', 1, 2, { turn: 1, step: 1 }),
event('assistant/chunk', 2, 3, {
turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'hello' },
}),
event('assistant/chunk', 3, 4, {
turn: 1, step: 1, chunk: { type: 'finish', reason: { kind: 'stop' } },
}),
event('step/end', 4, 5, { turn: 1, step: 1 }),
{ ...event('assistant/message', 5, 6, { turn: 1, step: 1, message }), surfaceOp: 'append' },
event('turn/end', 6, 7, { turn: 1, reason: { kind: 'completed' } }),
]],
['a message that cites a different pending attempt', [
event('turn/start', 0, 1, { turn: 1 }),
event('step/start', 1, 2, { turn: 1, step: 1 }),
event('assistant/chunk', 2, 3, {
turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'hello' },
}),
event('assistant/chunk', 3, 4, {
turn: 1, step: 1, chunk: { type: 'finish', reason: { kind: 'stop' } },
}),
{
...event('assistant/message', 4, 5, { turn: 1, step: 2, message }),
sourceEventSeqs: [2, 3],
surfaceOp: 'append',
},
event('step/end', 5, 6, { turn: 1, step: 1 }),
event('turn/end', 6, 7, { turn: 1, reason: { kind: 'completed' } }),
]],
] as const)('rejects %s during complete target validation', (_name, events) => {
expect(() => migrateV1ToV2({
header: {
version: 1, id: 'v1-final-validation', createdAt: 1,
isSeeded: false, delegationDepth: 0,
},
inheritedEventCount: 0,
events,
})).toThrow(/does not match an open turn and step/)
})
it('refuses a lineage cut between members of one interleaved stream', () => {
@@ -501,7 +988,7 @@ describe('sessionFormatV1ToV2', () => {
event('turn/end', 7, 8, { turn: 1, reason: { kind: 'completed' } }),
],
}
expect(() => sessionFormatV1ToV2.migrate(source)).toThrow(/cut 3 splits one Assistant attempt/)
expect(() => migrateV1ToV2(source)).toThrow(/cut 3 splits one Assistant attempt/)
})
it('refuses a lineage cut between a complete stream and its committed message', () => {
@@ -530,7 +1017,7 @@ describe('sessionFormatV1ToV2', () => {
event('turn/end', 7, 8, { turn: 1, reason: { kind: 'completed' } }),
],
}
expect(() => sessionFormatV1ToV2.migrate(source)).toThrow(/cut 4 splits one Assistant attempt/)
expect(() => migrateV1ToV2(source)).toThrow(/cut 4 splits one Assistant attempt/)
})
it('keeps referenced-session generation provenance frozen', () => {
@@ -565,7 +1052,7 @@ describe('sessionFormatV1ToV2', () => {
events: [{ ...event('user/message', 0, 1, reference), surfaceOp: 'append' }],
}
const migrated = sessionFormatV1ToV2.migrate(source)
const migrated = migrateV1ToV2(source)
expect(migrated.events[0]?.data).toStrictEqual(reference)
})
@@ -634,7 +1121,7 @@ describe('sessionFormatV1ToV2', () => {
event('turn/end', 15, 16, { turn: 1, reason: { kind: 'completed' } }),
],
}
const migrated = sessionFormatV1ToV2.migrate(source)
const migrated = migrateV1ToV2(source)
expect(migrated.events.find(event => event.type === 'compaction/prune')?.data).toMatchObject({
shadowedRange: { start: 2, end: 2 }, shadowedSeqs: [2],
})
@@ -687,7 +1174,7 @@ describe('sessionFormatV1ToV2', () => {
],
}
const migrated = sessionFormatV1ToV2.migrate(source)
const migrated = migrateV1ToV2(source)
const titleRequest = migrated.events.find(event => event.type === 'session/title-llm-request')
expect(titleRequest?.data).toMatchObject({
@@ -728,7 +1215,7 @@ describe('sessionFormatV1ToV2', () => {
event('turn/end', 10, 11, { turn: 2, reason: { kind: 'completed' } }),
],
}
expect(sessionFormatV1ToV2.migrate(source).events[3]?.data).toMatchObject({
expect(migrateV1ToV2(source).events[3]?.data).toMatchObject({
shadowedRange: { start: 1, end: 1 }, shadowedSeqs: [1],
})
})
@@ -758,7 +1245,7 @@ describe('sessionFormatV1ToV2', () => {
event('turn/end', 9, 10, { turn: 2, reason: { kind: 'error', error: failure } }),
],
}
expect(sessionFormatV1ToV2.migrate(source).events.filter(event => event.type === 'assistant/attempt'))
expect(migrateV1ToV2(source).events.filter(event => event.type === 'assistant/attempt'))
.toHaveLength(2)
})
})
@@ -8,10 +8,10 @@ import type {
} from '@deepseek-ai/dsh-session-format'
import {
RELEASED_V2_EVENT_TYPES,
assertReleasedV2Artifact,
assertReleasedV2Header,
restoreReleasedV2Artifact,
} from '@deepseek-ai/dsh-session-format-v1-to-v2'
import { assertReleasedV2Artifact } from '../src/testing/validation.ts'
const textBlock = { type: 'text', text: 'hello' } as const
const usage = { inputTokens: 3, outputTokens: 2 } as const
@@ -208,6 +208,22 @@ describe('released v2 event envelopes and payloads', () => {
expect(() => { assertReleasedV2Artifact(value) }).toThrow(message)
})
it.each([
['inherited count beyond events', artifact([], { inheritedEventCount: 1 }), /exceeds its events/],
['unseeded inherited count', artifact([
event('feedback/record', 0, { text: 'x' }),
], { inheritedEventCount: 1 }), /unseeded.*inherited events/],
['non-string type', artifact([
{ type: 1, seq: 0, time: 1, data: {} } as unknown as SessionFormatEvent,
]), /type must be a string/],
['non-dense seq', artifact([event('feedback/record', 1, { text: 'x' })]), /not dense/],
['false ignorable marker', artifact([
event('feedback/record', 0, { text: 'x' }, { ignorable: false }),
]), /ignorable must be true/],
])('rejects production artifact drift before payload restoration: %s', (_name, value, message) => {
expect(() => { restoreReleasedV2Artifact(value, new Set(RELEASED_V2_EVENT_TYPES)) }).toThrow(message)
})
it('validates present and absent opaque payload members before relationship checks', () => {
for (const includeMeta of [false, true]) {
const value = artifact([
@@ -298,6 +314,16 @@ describe('released v2 seed and surface relationships', () => {
expect(() => { assertReleasedV2Artifact(value) }).toThrow(message)
})
it('rejects production seed lineage disagreement', () => {
const known = new Set(RELEASED_V2_EVENT_TYPES)
expect(() => { restoreReleasedV2Artifact(artifact([], {
header: { version: 2, id: 'seeded', createdAt: 1, isSeeded: true, delegationDepth: 0 },
}), known) }).toThrow(/seeded header disagrees/)
expect(() => { restoreReleasedV2Artifact(artifact([
event('session/end-seed', 0, { inherited: true }),
]), known) }).toThrow(/unseeded.*inherited end-seed/)
})
it('accepts append and exact replacement surface operations', () => {
expect(() => { assertReleasedV2Artifact(artifact([
event('user/message', 0, userData('one'), { surfaceOp: 'append' }),
+65 -81
View File
@@ -48,15 +48,10 @@ interface ParsedSessionFixture {
readonly createdAt: number
readonly inheritedEventCount: SessionLogOffsetType
readonly events: SessionEvent[]
readonly artifact: ReturnType<typeof sessionFormatCatalog.migrate>
readonly artifact: ReturnType<ReturnType<typeof sessionFormatCatalog.createRestore>['finish']>
readonly sourceHeader: Readonly<Record<string, unknown>>
}
interface FixtureJsonLine {
readonly lineNumber: number
readonly value: Record<string, unknown>
}
/**
* One recorded model call. `throw` may replay prefix chunks before failing;
* `hang` models cancellation. Derived chunk entries come from ordinary model
@@ -210,7 +205,13 @@ export function parseSessionLog(text: string): SessionEvent[] {
/** Parse, complete, decode, and migrate one projected snapshot artifact without writing its source. */
function parseSessionFixture(text: string): ParsedSessionFixture {
const parsed: FixtureJsonLine[] = []
let headerLineNumber: number | undefined
let sourceHeader: Record<string, unknown> | undefined
let restore: ReturnType<typeof sessionFormatCatalog.createRestore> | undefined
const rowLines: number[] = []
const eventLines: number[] = []
let bodyKind: 'complete' | 'projected' | undefined
let nextSeq = 0
for (const [index, line] of text.split(/\r?\n/).entries()) {
if (line.trim().length === 0) continue
let value: unknown
@@ -222,19 +223,22 @@ function parseSessionFixture(text: string): ParsedSessionFixture {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`session snapshot line ${index + 1} must be a JSON object`)
}
parsed.push({ lineNumber: index + 1, value: value as Record<string, unknown> })
}
const headerLine = parsed[0]
if (headerLine === undefined) throw new Error('session snapshot must start with a session header')
const header = normalizeProjectedHeader(headerLine.value)
const rows: Record<string, unknown>[] = []
const rowLines: number[] = []
const eventLines: number[] = []
let bodyKind: 'complete' | 'projected' | undefined
let nextSeq = 0
for (const source of parsed.slice(1)) {
const record = normalizeProjectedRow(source.value)
const lineNumber = index + 1
const recordValue = value as Record<string, unknown>
if (restore === undefined) {
headerLineNumber = lineNumber
sourceHeader = recordValue
try {
restore = sessionFormatCatalog.createRestore(normalizeProjectedHeader(recordValue), {
recovery: 'strict',
validation: 'current',
})
} catch (error: unknown) {
throw fixtureFormatError(error, lineNumber, [], [])
}
continue
}
const record = normalizeProjectedRow(recordValue)
const packed = PACKED_CHUNK_ROW_TYPES.has(record.type as string)
const seqKey = packed ? 'seq0' : 'seq'
const timeKey = packed ? 'time0' : 'time'
@@ -242,12 +246,12 @@ function parseSessionFixture(text: string): ParsedSessionFixture {
const hasTime = Object.hasOwn(record, timeKey)
if (hasSeq !== hasTime) {
throw new Error(
`session snapshot line ${source.lineNumber} must contain both ${seqKey} and ${timeKey}, or neither`,
`session snapshot line ${lineNumber} must contain both ${seqKey} and ${timeKey}, or neither`,
)
}
const currentKind = hasSeq ? 'complete' : 'projected'
if (bodyKind !== undefined && currentKind !== bodyKind) {
throw new Error(`session snapshot line ${source.lineNumber} cannot mix projected and complete body rows`)
throw new Error(`session snapshot line ${lineNumber} cannot mix projected and complete body rows`)
}
bodyKind = currentKind
if (currentKind === 'projected') {
@@ -255,36 +259,28 @@ function parseSessionFixture(text: string): ParsedSessionFixture {
record[timeKey] = 0
}
const cardinality = physicalRowCardinality(record)
rows.push(record)
rowLines.push(source.lineNumber)
eventLines.push(...Array.from({ length: cardinality }, () => source.lineNumber))
rowLines.push(lineNumber)
eventLines.push(...Array.from({ length: cardinality }, () => lineNumber))
nextSeq += cardinality
try {
restore.decodeRow(record)
} catch (error: unknown) {
throw fixtureFormatError(error, headerLineNumber as number, rowLines, eventLines, rowLines.length - 1)
}
}
let decoded: ReturnType<typeof sessionFormatCatalog.decodeArtifact>
try {
decoded = sessionFormatCatalog.decodeArtifact(header, rows)
} catch (error: unknown) {
const physicalRow = locateUnlabelledPhysicalFailure(error, header, rows)
throw fixtureFormatError(
error,
headerLine.lineNumber,
rowLines,
eventLines,
physicalRow,
)
if (restore === undefined || sourceHeader === undefined || headerLineNumber === undefined) {
throw new Error('session snapshot must start with a session header')
}
try {
const current = sessionFormatCatalog.migrate(decoded)
return parsedSessionFixture(current, headerLine.value)
return parsedSessionFixture(restore.finish(), sourceHeader)
} catch (error: unknown) {
throw fixtureFormatError(error, headerLine.lineNumber, rowLines, eventLines)
throw fixtureFormatError(error, headerLineNumber, rowLines, eventLines)
}
}
/** Materialize the common replay view from a migrated artifact. */
function parsedSessionFixture(
artifact: ReturnType<typeof sessionFormatCatalog.decodeArtifact>,
artifact: ParsedSessionFixture['artifact'],
sourceHeader: Readonly<Record<string, unknown>>,
): ParsedSessionFixture {
return {
@@ -410,17 +406,16 @@ function migrateWrappedEventGroup(
time: 0,
data: { text: `comparison prefix ${String(seq)}` },
}))
const migrated = sessionFormatCatalog.migrate({
header: {
version: 1,
id: first.sessionId,
createdAt: 0,
isSeeded: false,
delegationDepth: 0,
},
inheritedEventCount: 0,
events: [...prefix, ...entries.map(entry => normalizeProjectedRow(entry.event))] as never,
}).events.slice(prefix.length) as readonly Readonly<Record<string, unknown>>[]
const restore = sessionFormatCatalog.createRestore({
type: 'session',
version: 1,
id: first.sessionId,
createdAt: 0,
delegationDepth: 0,
}, { recovery: 'strict', validation: 'current' })
for (const event of prefix) restore.decodeRow(event)
for (const entry of entries) restore.decodeRow(normalizeProjectedRow(entry.event))
const migrated = restore.finish().events.slice(prefix.length) as readonly Readonly<Record<string, unknown>>[]
const assigned = new Map<number, Readonly<Record<string, unknown>>[]>()
let migratedIndex = 0
@@ -498,11 +493,18 @@ export function prepareSessionEventNotificationsForComparison(text: string): str
/** Encode one migrated fixture while retaining a projected cwd token. */
function encodeCurrentSessionSnapshotFixture(text: string, parsed: ParsedSessionFixture): string {
const encoded = sessionFormatCatalog.encodeCurrent(parsed.artifact)
const header = { ...encoded.header }
const header = {
...sessionFormatCatalog.encodeCurrentHeader(
parsed.artifact.header,
parsed.artifact.inheritedEventCount,
),
}
const sourceCwd = parsed.sourceHeader['cwd']
if (typeof sourceCwd === 'string' && /^\{\{cwd\}\}(?:\/|$)/.test(sourceCwd)) header['cwd'] = sourceCwd
const output = [header, ...encoded.rows].map(record => JSON.stringify(record)).join('\n')
const output = [
JSON.stringify(header),
...parsed.artifact.events.map(event => JSON.stringify(sessionFormatCatalog.encodeCurrentEvent(event))),
].join('\n')
return text.endsWith('\n') ? `${output}\n` : output
}
@@ -564,13 +566,15 @@ function fixtureFormatError(
const locationDetail = error instanceof Error && error.cause instanceof Error
? error.cause.message
: detail
const row = /released (?:Session|(?:text|reasoning|tool-call)-chunks) row (\d+)/.exec(locationDetail)
const storedRow = /^released Session row (\d+)/.exec(locationDetail)
const event = /Session event (\d+)/.exec(locationDetail)
?? / at seq (\d+)/.exec(locationDetail)
?? /^[^ ]+ (\d+) /.exec(locationDetail)
const line = physicalRow === undefined && row === null
? event === null ? headerLine : eventLines[Number(event[1])] ?? headerLine
: rowLines[physicalRow ?? Number(row?.[1])] ?? headerLine
?? /inherited Session cut (\d+)/.exec(locationDetail)
let line: number
if (physicalRow !== undefined) line = rowLines[physicalRow] as number
else if (storedRow !== null) line = rowLines[Number(storedRow[1])] ?? headerLine
else if (event === null) line = headerLine
else line = eventLines[Number(event[1])] ?? headerLine
const message = `session snapshot line ${line}: ${detail}`
if (error instanceof SessionFormatUnsupportedMigrationError) {
return new SessionFormatUnsupportedMigrationError(message, { cause: error })
@@ -578,26 +582,6 @@ function fixtureFormatError(
return new Error(message, { cause: error })
}
/** Locate range-decoder failures whose frozen diagnostic predates physical-row labels. */
function locateUnlabelledPhysicalFailure(
error: unknown,
header: Readonly<Record<string, unknown>>,
rows: readonly Readonly<Record<string, unknown>>[],
): number | undefined {
const detail = error instanceof Error ? error.message : String(error)
if (!detail.startsWith('sourceEventSeqs ')) return undefined
const diagnosticHeader = Object.hasOwn(header, 'seedLength') ? { ...header, seedLength: 0 } : header
for (let index = 0; index < rows.length; index += 1) {
try {
sessionFormatCatalog.decodeArtifact(diagnosticHeader, rows.slice(0, index + 1))
} catch (candidate: unknown) {
const candidateDetail = candidate instanceof Error ? candidate.message : String(candidate)
if (candidateDetail === detail) return index
}
}
return undefined
}
/**
* Read replay identity, ordering, and fork-seed facts from the JSONL header.
*
@@ -235,7 +235,7 @@ describe('Session format package parity', () => {
})
describe('fixture format diagnostics', () => {
it('attaches the header line to a non-Error catalog failure', async () => {
it('attaches the header line to a restore-construction failure', async () => {
vi.resetModules()
vi.doMock('@deepseek-ai/dsh-session-format-catalog', async (importOriginal) => {
const actual = await importOriginal<typeof import('@deepseek-ai/dsh-session-format-catalog')>()
@@ -243,7 +243,7 @@ describe('fixture format diagnostics', () => {
...actual,
sessionFormatCatalog: {
...actual.sessionFormatCatalog,
decodeArtifact(): never {
createRestore(): never {
const failure: unknown = 'decoder exploded'
throw failure
},
@@ -261,6 +261,37 @@ describe('fixture format diagnostics', () => {
}
})
it('attaches the header line to a restore-finalization failure', async () => {
vi.resetModules()
vi.doMock('@deepseek-ai/dsh-session-format-catalog', async (importOriginal) => {
const actual = await importOriginal<typeof import('@deepseek-ai/dsh-session-format-catalog')>()
return {
...actual,
sessionFormatCatalog: {
...actual.sessionFormatCatalog,
createRestore() {
return {
header: { version: 2, id: 'fixture', createdAt: 0, isSeeded: false, delegationDepth: 0 },
decodeRow() {},
finish(): never {
throw new Error('Session event 99 restore finalization failed')
},
}
},
},
}
})
try {
const replay = await import('../src/index.ts')
expect(() => replay.parseSessionLog(sessionJsonl([])))
.toThrow('session snapshot line 1: Session event 99 restore finalization failed')
} finally {
vi.doUnmock('@deepseek-ai/dsh-session-format-catalog')
vi.resetModules()
}
})
it('falls back to the header when a source-range diagnostic has no matching physical prefix', async () => {
vi.resetModules()
vi.doMock('@deepseek-ai/dsh-session-format-catalog', async (importOriginal) => {
@@ -269,8 +300,14 @@ describe('fixture format diagnostics', () => {
...actual,
sessionFormatCatalog: {
...actual.sessionFormatCatalog,
decodeArtifact(): never {
throw new Error('sourceEventSeqs synthetic unmatched failure')
createRestore() {
return {
header: { version: 2, id: 'fixture', createdAt: 0, isSeeded: false, delegationDepth: 0 },
decodeRow() {},
finish(): never {
throw new Error('sourceEventSeqs synthetic unmatched failure')
},
}
},
},
}
@@ -286,22 +323,24 @@ describe('fixture format diagnostics', () => {
}
})
it('maps a matching non-Error source-range prefix failure to its physical row', async () => {
it('maps a non-Error row failure to its physical row', async () => {
vi.resetModules()
vi.doMock('@deepseek-ai/dsh-session-format-catalog', async (importOriginal) => {
const actual = await importOriginal<typeof import('@deepseek-ai/dsh-session-format-catalog')>()
let callCount = 0
let row = 0
return {
...actual,
sessionFormatCatalog: {
...actual.sessionFormatCatalog,
decodeArtifact(): never {
callCount += 1
if (callCount === 1) throw new Error('sourceEventSeqs synthetic prefix failure')
const failure: unknown = callCount === 2
? 'sourceEventSeqs different prefix failure'
: 'sourceEventSeqs synthetic prefix failure'
throw failure
createRestore() {
return {
header: { version: 2, id: 'fixture', createdAt: 0, isSeeded: false, delegationDepth: 0 },
decodeRow(): void {
row += 1
if (row === 2) throw 'row decoder exploded'
},
finish(): never { throw new Error('unexpected finish') },
}
},
},
}
@@ -314,14 +353,21 @@ describe('fixture format diagnostics', () => {
]
expect(() => replay.parseSessionLog(sessionJsonl(events)))
.toThrow('session snapshot line 3: sourceEventSeqs synthetic prefix failure')
.toThrow('session snapshot line 3: row decoder exploded')
} finally {
vi.doUnmock('@deepseek-ai/dsh-session-format-catalog')
vi.resetModules()
}
})
it('falls back to the header for an out-of-range physical-row diagnostic', async () => {
it.each([
['physical row', 'released Session row 0 is malformed', 2],
['logical event', 'Session event 0 is malformed', 2],
['event seq', 'format v1 contains an invalid event at seq 0', 2],
['inherited cut', 'inherited Session cut 0 splits one Assistant attempt', 2],
['out-of-range physical row', 'released Session row 99 is malformed', 1],
['out-of-range logical event', 'Session event 99 is malformed', 1],
])('maps a %s finalization diagnostic to its source line', async (_label, message, line) => {
vi.resetModules()
vi.doMock('@deepseek-ai/dsh-session-format-catalog', async (importOriginal) => {
const actual = await importOriginal<typeof import('@deepseek-ai/dsh-session-format-catalog')>()
@@ -329,8 +375,12 @@ describe('fixture format diagnostics', () => {
...actual,
sessionFormatCatalog: {
...actual.sessionFormatCatalog,
decodeArtifact(): never {
throw new Error('released Session row 99 is malformed')
createRestore() {
return {
header: { version: 2, id: 'fixture', createdAt: 0, isSeeded: false, delegationDepth: 0 },
decodeRow() {},
finish(): never { throw new Error(message) },
}
},
},
}
@@ -340,33 +390,7 @@ describe('fixture format diagnostics', () => {
const event: SessionEvent = { type: 'turn/start', seq: SessionSeq(0), time: 0, data: { turn: 1 } }
expect(() => replay.parseSessionLog(sessionJsonl([event])))
.toThrow('session snapshot line 1: released Session row 99 is malformed')
} finally {
vi.doUnmock('@deepseek-ai/dsh-session-format-catalog')
vi.resetModules()
}
})
it('falls back to the header for an out-of-range logical-event diagnostic', async () => {
vi.resetModules()
vi.doMock('@deepseek-ai/dsh-session-format-catalog', async (importOriginal) => {
const actual = await importOriginal<typeof import('@deepseek-ai/dsh-session-format-catalog')>()
return {
...actual,
sessionFormatCatalog: {
...actual.sessionFormatCatalog,
migrate(): never {
throw new Error('Session event 99 is malformed')
},
},
}
})
try {
const replay = await import('../src/index.ts')
const event: SessionEvent = { type: 'turn/start', seq: SessionSeq(0), time: 0, data: { turn: 1 } }
expect(() => replay.parseSessionLog(sessionJsonl([event])))
.toThrow('session snapshot line 1: Session event 99 is malformed')
.toThrow(`session snapshot line ${line}: ${message}`)
} finally {
vi.doUnmock('@deepseek-ai/dsh-session-format-catalog')
vi.resetModules()
+2 -1
View File
@@ -103,8 +103,9 @@ describe('session format catalog generator', () => {
expect(declarations.map(item => [item.from, item.to])).toEqual([[0, 1], [1, 2]])
expect(output).toContain("from '@deepseek-ai/dsh-session-format-v0-to-v1'")
expect(output).toContain('currentVersion: 2')
expect(output).toContain('encodeCurrentArtifact: artifact => releasedV2SessionFormatCodec.encodeArtifact(artifact)')
expect(output).toContain('currentEncoder: releasedV2SessionFormatCodec')
expect(output).toContain('restoreReleasedV2Artifact(artifact, KNOWN_SESSION_EVENT_TYPES)')
expect(output).toContain('restoreTransformedCurrent(artifact)')
expect(output).toContain('assertReleasedV2Header(header)')
expect(output).toContain('validateInstalledCurrentSessionHeader(header)')
expect(output).toContain("from '@deepseek-ai/dsh-session'")
+4 -1
View File
@@ -191,13 +191,16 @@ export function renderSessionFormatCatalog(
'export const sessionFormatCatalog = createSessionFormatCatalog({',
` currentVersion: ${currentVersion},`,
` codecs: [${codecs.join(', ')}],`,
` encodeCurrentArtifact: artifact => ${currentCodec}.encodeArtifact(artifact),`,
` currentEncoder: ${currentCodec},`,
` migrations: [${declarations.map(item => item.migration).join(', ')}],`,
' restoreCurrent(artifact) {',
` const restored = ${restorer}(artifact, KNOWN_SESSION_EVENT_TYPES)`,
' validateInstalledCurrentSessionArtifact(restored)',
' return restored',
' },',
' restoreTransformedCurrent(artifact) {',
` return ${restorer}(artifact, KNOWN_SESSION_EVENT_TYPES)`,
' },',
' restoreCurrentHeader(header) {',
` ${headerValidator}(header)`,
' validateInstalledCurrentSessionHeader(header)',
+88 -6
View File
@@ -58,6 +58,51 @@ function validationHeader(value: unknown): unknown {
return header
}
function validationRow(source: Readonly<Record<string, unknown>>): Record<string, unknown> {
if (source.type !== 'request/header') return { ...source }
const data = source.data
if (data === null || typeof data !== 'object' || Array.isArray(data)) return { ...source }
const header = (data as Record<string, unknown>).header
if (header === null || typeof header !== 'object' || Array.isArray(header)) return { ...source }
if ((header as Record<string, unknown>).tools !== '{{tools}}') return { ...source }
return {
...source,
data: {
...data,
header: { ...header, tools: [] },
},
}
}
function restoreRequestHeaderTokens(
events: readonly SessionEvent[],
rows: readonly Readonly<Record<string, unknown>>[],
): SessionEvent[] {
const sources = rows.filter(row => row.type === 'request/header')
let sourceIndex = 0
return events.map((event) => {
if (event.type !== 'request/header') return event
const source = sources[sourceIndex]
sourceIndex += 1
const sourceData = source?.data
const sourceHeader = sourceData !== null && typeof sourceData === 'object' && !Array.isArray(sourceData)
? (sourceData as Record<string, unknown>).header
: undefined
if (sourceHeader === null || typeof sourceHeader !== 'object' || Array.isArray(sourceHeader)
|| (sourceHeader as Record<string, unknown>).tools !== '{{tools}}') return event
return {
...event,
data: {
...event.data,
header: {
...(event.data as unknown as { header: Record<string, unknown> }).header,
tools: '{{tools}}',
},
},
} as SessionEvent
})
}
function renderFixture(headerLine: string, events: readonly SessionEvent[]): string {
return [
headerLine,
@@ -95,6 +140,7 @@ function parseFixtureObjectLine(line: string, lineNumber: number): Record<string
function parseFixtureRows(content: string, headerValue: unknown): SessionEvent[] {
const rows: Record<string, unknown>[] = []
const rowLines: number[] = []
const eventLines: number[] = []
let nextSeq: SessionLogOffsetType = SessionLogOffset(0)
let headerSkipped = false
for (const [index, line] of content.split(/\r?\n/).entries()) {
@@ -113,7 +159,9 @@ function parseFixtureRows(content: string, headerValue: unknown): SessionEvent[]
if (!Object.hasOwn(record, timeKey)) record[timeKey] = 0
rows.push(record)
rowLines.push(index + 1)
nextSeq = SessionLogOffset(nextSeq + projectedRowCardinality(record))
const cardinality = projectedRowCardinality(record)
for (let offset = 0; offset < cardinality; offset += 1) eventLines.push(index + 1)
nextSeq = SessionLogOffset(nextSeq + cardinality)
}
// Versionless protocol fixtures and current projected snapshots use scalar
// event rows. Current snapshots may contain owner-restored scrub tokens such
@@ -143,18 +191,52 @@ function parseFixtureRows(content: string, headerValue: unknown): SessionEvent[]
}
})
}
let restore: ReturnType<typeof sessionFormatCatalog.createRestore>
try {
return [
...sessionFormatCatalog.decodeArtifact(validationHeader(headerValue), rows).events,
] as unknown as SessionEvent[]
restore = sessionFormatCatalog.createRestore(validationHeader(headerValue), {
recovery: 'strict',
validation: 'current',
})
} catch (error) {
const detail = error instanceof Error ? error.message : String(error)
const storedRow = /\brow (\d+)\b/.exec(detail)
const line = storedRow === null ? 1 : rowLines[Number(storedRow[1])] ?? 1
throw new Error(`session snapshot line 1: ${detail}`, { cause: error })
}
for (const [index, row] of rows.entries()) {
try {
restore.decodeRow(validationRow(row))
} catch (error) {
const detail = error instanceof Error ? error.message : String(error)
throw new Error(`session snapshot line ${rowLines[index] ?? 1}: ${detail}`, { cause: error })
}
}
try {
return restoreRequestHeaderTokens(
[...restore.finish().events] as unknown as SessionEvent[],
rows,
)
} catch (error) {
const detail = error instanceof Error ? error.message : String(error)
const line = fixtureDiagnosticLine(error, rowLines, eventLines)
throw new Error(`session snapshot line ${line}: ${detail}`, { cause: error })
}
}
function fixtureDiagnosticLine(
error: unknown,
rowLines: readonly number[],
eventLines: readonly number[],
): number {
const detail = error instanceof Error && error.cause instanceof Error
? error.cause.message
: error instanceof Error ? error.message : String(error)
const physicalRow = /^released Session row (\d+)/.exec(detail)
if (physicalRow !== null) return rowLines[Number(physicalRow[1])] ?? 1
const event = /Session event (\d+)/.exec(detail)
?? / at seq (\d+)/.exec(detail)
?? /inherited Session cut (\d+)/.exec(detail)
return event === null ? 1 : eventLines[Number(event[1])] ?? 1
}
function withoutEnvelope(events: readonly SessionEvent[]): Array<Omit<SessionEvent, 'seq' | 'time'>> {
return events.map((event) => {
const { seq: _seq, time: _time, ...projected } = event