feat(session): add identity V2-to-V3 migration and writer skeleton
This commit is contained in:
1 parent
81dcacbb44
commit
f7a6221158
34 files changed
+681
-66
No files matched your search
@@ -83,7 +83,7 @@ export type OptionalSessionSeq = SessionSeq | null
|
||||
* immutable prior-generation, and current fast-path rules are recorded in
|
||||
* `.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md`.
|
||||
*/
|
||||
export const SESSION_FORMAT_VERSION = 2
|
||||
export const SESSION_FORMAT_VERSION = 3
|
||||
|
||||
/**
|
||||
* Immutable validated storage metadata, kept outside the conversation event log.
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/session/session-format-catalog/README.md
|
||||
README.md: 28dc1ac5b7c4711e8bba964f611fd48f7cfed298
|
||||
README.zh.md: fe48e3466adaab61772ef51ddb14a6d3be7bddae
|
||||
README.md: 1ca40f230e604b77b88044d4fef54bf8b9ec6d32
|
||||
README.zh.md: 198fef8dd944578c5a078ffb409d31459da8523a
|
||||
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
## Summary
|
||||
|
||||
`dsh-session-format-catalog` gives persistence one deterministic Session format reader without consulting mounted plugins. It assembles the frozen v0, v1, and v2 codecs with the adjacent v0-to-v1 and v1-to-v2 edges, checks the complete gap-free chain at module initialization, and exposes physical dispatch, header-only classification, single-pass row restoration, and current record encoding through `sessionFormatCatalog`.
|
||||
`dsh-session-format-catalog` gives persistence one deterministic Session format reader without consulting mounted plugins. It assembles codecs and adjacent edges from released v0 through current v3, checks the complete gap-free chain at module initialization, and exposes physical dispatch, header-only classification, single-pass row restoration, and current record encoding through `sessionFormatCatalog`.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -66,6 +66,7 @@ The catalog contains all supported historical readers directly. A profile cannot
|
||||
- [Migration machinery](../session-format/README.md) — catalog construction and dispatch behavior.
|
||||
- [Released v0 to v1 edge](../session-format-v0-to-v1/README.md) — codec and validator ownership.
|
||||
- [Released v1 to v2 edge](../session-format-v1-to-v2/README.md) — Assistant stream embedding and cardinality-changing reference remapping.
|
||||
- [Released v2 to v3 edge](../session-format-v2-to-v3/README.md) — identity event restoration.
|
||||
- [JSONL persistence](../session-persistence-jsonl/README.md) — immutable generation naming and exclusive publication.
|
||||
|
||||
-----
|
||||
|
||||
@@ -9,7 +9,7 @@ kind: "package-library"
|
||||
|
||||
## 概述
|
||||
|
||||
`dsh-session-format-catalog` 为持久化提供一个确定性的 Session 格式读取器,且无需查询已挂载插件。它把冻结的 v0、v1 与 v2 编解码器和相邻的 v0 到 v1、v1 到 v2 迁移边装配起来,在模块初始化时校验完整且无缺口的迁移链,并通过 `sessionFormatCatalog` 暴露物理分派、仅 header 分类、单遍行还原和当前格式逐记录编码。
|
||||
`dsh-session-format-catalog` 为持久化提供一个确定性的 Session 格式读取器,且无需查询已挂载插件。它装配从已发布 v0 到当前 v3 的编解码器与相邻迁移边,在模块初始化时校验完整且无缺口的迁移链,并通过 `sessionFormatCatalog` 暴露物理分派、仅 header 分类、单遍行还原和当前格式逐记录编码。
|
||||
|
||||
## 目录
|
||||
|
||||
@@ -66,6 +66,7 @@ Production 历史读取使用 `{ recovery: 'recoverable', validation: 'transform
|
||||
- [迁移机制](../session-format/README.zh.md)——目录构造与分派行为。
|
||||
- [已发布 v0 到 v1 迁移边](../session-format-v0-to-v1/README.zh.md)——编解码器与校验器所有权。
|
||||
- [已发布 v1 到 v2 迁移边](../session-format-v1-to-v2/README.zh.md)——Assistant stream 嵌入与基数变化引用重映射。
|
||||
- [已发布 v2 到 v3 迁移边](../session-format-v2-to-v3/README.zh.md)——恒等事件还原。
|
||||
- [JSONL 持久化](../session-persistence-jsonl/README.zh.md)——不可变 generation 命名与排他发布。
|
||||
|
||||
-----
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
"dependencies": {
|
||||
"@deepseek-ai/dsh-session-format": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-format-v0-to-v1": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-format-v1-to-v2": "workspace:^"
|
||||
"@deepseek-ai/dsh-session-format-v1-to-v2": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-format-v2-to-v3": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
|
||||
@@ -7,24 +7,25 @@ import { KNOWN_SESSION_EVENT_TYPES } from '@deepseek-ai/dsh-session'
|
||||
import { createSessionFormatCatalog } from '@deepseek-ai/dsh-session-format'
|
||||
import { validateInstalledCurrentSessionArtifact, validateInstalledCurrentSessionHeader } from './current.ts'
|
||||
import { releasedV0SessionFormatCodec, releasedV1SessionFormatCodec, sessionFormatV0ToV1 } from '@deepseek-ai/dsh-session-format-v0-to-v1'
|
||||
import { assertReleasedV2Header, releasedV2SessionFormatCodec, restoreReleasedV2Artifact, sessionFormatV1ToV2 } from '@deepseek-ai/dsh-session-format-v1-to-v2'
|
||||
import { releasedV2SessionFormatCodec, sessionFormatV1ToV2 } from '@deepseek-ai/dsh-session-format-v1-to-v2'
|
||||
import { assertReleasedV3Header, releasedV3SessionFormatCodec, restoreReleasedV3Artifact, sessionFormatV2ToV3 } from '@deepseek-ai/dsh-session-format-v2-to-v3'
|
||||
|
||||
/** Physical codec dispatch and complete adjacent chain, independent of mounted plugins. */
|
||||
export const sessionFormatCatalog = createSessionFormatCatalog({
|
||||
currentVersion: 2,
|
||||
codecs: [releasedV0SessionFormatCodec, releasedV1SessionFormatCodec, releasedV2SessionFormatCodec],
|
||||
currentEncoder: releasedV2SessionFormatCodec,
|
||||
migrations: [sessionFormatV0ToV1, sessionFormatV1ToV2],
|
||||
currentVersion: 3,
|
||||
codecs: [releasedV0SessionFormatCodec, releasedV1SessionFormatCodec, releasedV2SessionFormatCodec, releasedV3SessionFormatCodec],
|
||||
currentEncoder: releasedV3SessionFormatCodec,
|
||||
migrations: [sessionFormatV0ToV1, sessionFormatV1ToV2, sessionFormatV2ToV3],
|
||||
restoreCurrent(artifact) {
|
||||
const restored = restoreReleasedV2Artifact(artifact, KNOWN_SESSION_EVENT_TYPES)
|
||||
const restored = restoreReleasedV3Artifact(artifact, KNOWN_SESSION_EVENT_TYPES)
|
||||
validateInstalledCurrentSessionArtifact(restored)
|
||||
return restored
|
||||
},
|
||||
restoreTransformedCurrent(artifact) {
|
||||
return restoreReleasedV2Artifact(artifact, KNOWN_SESSION_EVENT_TYPES)
|
||||
return restoreReleasedV3Artifact(artifact, KNOWN_SESSION_EVENT_TYPES)
|
||||
},
|
||||
restoreCurrentHeader(header) {
|
||||
assertReleasedV2Header(header)
|
||||
assertReleasedV3Header(header)
|
||||
validateInstalledCurrentSessionHeader(header)
|
||||
return header
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { sessionFormatCatalog } from '../src/index.ts'
|
||||
|
||||
describe('first-party Session format catalog', () => {
|
||||
it('statically owns the complete adjacent v0 to v2 chain', () => {
|
||||
it('statically owns the complete adjacent v0 to v3 chain', () => {
|
||||
const header = {
|
||||
type: 'session',
|
||||
version: 0,
|
||||
@@ -12,13 +12,13 @@ describe('first-party Session format catalog', () => {
|
||||
delegationDepth: 0,
|
||||
}
|
||||
|
||||
expect(sessionFormatCatalog.currentVersion).toBe(2)
|
||||
expect(sessionFormatCatalog.currentVersion).toBe(3)
|
||||
expect(sessionFormatCatalog.readHeader(header)).toEqual({
|
||||
status: 'migration-required',
|
||||
storedVersion: 0,
|
||||
targetVersion: 2,
|
||||
targetVersion: 3,
|
||||
header: {
|
||||
version: 2,
|
||||
version: 3,
|
||||
id: 'catalog',
|
||||
createdAt: 1,
|
||||
isSeeded: true,
|
||||
@@ -32,13 +32,13 @@ describe('first-party Session format catalog', () => {
|
||||
})
|
||||
restore.decodeRow({ type: 'turn/start', seq: 0, time: 2, data: { turn: 1 } })
|
||||
expect(restore.finish()).toMatchObject({
|
||||
header: { version: 2, id: 'catalog' },
|
||||
header: { version: 3, id: 'catalog' },
|
||||
})
|
||||
})
|
||||
|
||||
it('restores the installed current vocabulary without freezing ordinary payload additions', () => {
|
||||
const header = {
|
||||
type: 'session', version: 2, id: 'current-growth', createdAt: 1, isSeeded: false, delegationDepth: 0,
|
||||
type: 'session', version: 3, id: 'current-growth', createdAt: 1, isSeeded: false, delegationDepth: 0,
|
||||
}
|
||||
const restore = (rows: readonly unknown[]) => {
|
||||
const current = sessionFormatCatalog.createRestore(header, {
|
||||
@@ -66,6 +66,42 @@ describe('first-party Session format catalog', () => {
|
||||
}])
|
||||
})
|
||||
|
||||
it.each([0, 1])('restores v%i empty and non-empty inherited prefixes through every adjacent edge', (version) => {
|
||||
for (const seedLength of [0, 1]) {
|
||||
const sourceHeader = {
|
||||
type: 'session', version, id: 'seed-chain', createdAt: 1,
|
||||
parentSession: 'parent', seedLength, delegationDepth: 0,
|
||||
}
|
||||
const restore = sessionFormatCatalog.createRestore(sourceHeader, { recovery: 'strict', validation: 'current' })
|
||||
if (seedLength > 0) {
|
||||
restore.decodeRow({ type: 'feedback/record', seq: 0, time: 1, data: { text: 'inherited' } })
|
||||
}
|
||||
const artifact = restore.finish()
|
||||
expect(artifact.header.version).toBe(3)
|
||||
expect(artifact.inheritedEventCount).toBe(seedLength)
|
||||
expect(artifact.events.at(-1)).toEqual({
|
||||
type: 'session/end-seed', seq: seedLength, time: 1, data: { inherited: true },
|
||||
})
|
||||
expect(sourceHeader.version).toBe(version)
|
||||
}
|
||||
})
|
||||
|
||||
it.each([false, true])('preserves decoded v2 events and the inherited cut (seeded=%s)', (isSeeded) => {
|
||||
const header = { type: 'session', version: 2, id: 'v2-identity', createdAt: 1, isSeeded, delegationDepth: 0 }
|
||||
const rows = [
|
||||
{ type: 'external/event', seq: 0, time: 1, data: { extra: ['unchanged'] }, ignorable: true },
|
||||
...(isSeeded ? [{ type: 'session/end-seed', seq: 1, time: 2, data: { inherited: true } }] : []),
|
||||
]
|
||||
const before = JSON.stringify({ header, rows })
|
||||
const restore = sessionFormatCatalog.createRestore(header, { recovery: 'strict', validation: 'current' })
|
||||
for (const row of rows) restore.decodeRow(row)
|
||||
expect(restore.finish()).toEqual({
|
||||
header: { version: 3, id: 'v2-identity', createdAt: 1, isSeeded, delegationDepth: 0 },
|
||||
inheritedEventCount: isSeeded ? 1 : 0, events: rows,
|
||||
})
|
||||
expect(JSON.stringify({ header, rows })).toBe(before)
|
||||
})
|
||||
|
||||
it('validates complete relationships after streaming migration', () => {
|
||||
const stream = sessionFormatCatalog.createRestore({
|
||||
type: 'session', version: 1, id: 'invalid-stream', createdAt: 1, delegationDepth: 0,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '../src/current.ts'
|
||||
|
||||
const currentHeader: SessionFormatHeader = {
|
||||
version: 2,
|
||||
version: 3,
|
||||
id: 'installed-current',
|
||||
createdAt: 1,
|
||||
isSeeded: false,
|
||||
@@ -16,14 +16,14 @@ const currentHeader: SessionFormatHeader = {
|
||||
describe('installed current Session restoration', () => {
|
||||
it('rejects version skew before entering current Session validation', () => {
|
||||
expect(() => { validateInstalledCurrentSessionHeader({ ...currentHeader, version: 0 }) })
|
||||
.toThrow(/installed Session format is v2, got v0/)
|
||||
.toThrow(/installed Session format is v3, got v0/)
|
||||
const artifact: SessionFormatArtifact = {
|
||||
header: { ...currentHeader, version: 0 },
|
||||
inheritedEventCount: 0,
|
||||
events: [],
|
||||
}
|
||||
expect(() => { validateInstalledCurrentSessionArtifact(artifact) })
|
||||
.toThrow(/installed Session format is v2, got v0/)
|
||||
.toThrow(/installed Session format is v3, got v0/)
|
||||
})
|
||||
|
||||
it('accepts only current request-header reasons and the true starts-series marker', () => {
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../session-format-v1-to-v2"
|
||||
},
|
||||
{
|
||||
"path": "../session-format-v2-to-v3"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -42,7 +42,7 @@ class ReleasedV0ToV1Stage implements SessionFormatMigrationStage {
|
||||
|
||||
constructor(private readonly input: SessionFormatMigrationStageInput) {
|
||||
assertHeaderVersion(input.sourceHeader, 0)
|
||||
this.headerInheritedEventCount = input.sourceInheritedEventCount
|
||||
this.headerInheritedEventCount = sessionFormatCount(input.sourceInheritedEventCount, 'format v0 inherited event count')
|
||||
}
|
||||
|
||||
transformEvent(
|
||||
@@ -66,7 +66,7 @@ class ReleasedV0ToV1Stage implements SessionFormatMigrationStage {
|
||||
}
|
||||
|
||||
finish(_context: SessionFormatMigrationContext): number {
|
||||
return this.input.sourceInheritedEventCount
|
||||
return this.headerInheritedEventCount
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ function assertSourceDeliveryMarker(
|
||||
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
|
||||
&& event.seq < sessionFormatCount(input.sourceInheritedEventCount, 'format v0 inherited event count')
|
||||
if (acceptedVersion === 0 && !inherited && data['sessionId'] !== input.sourceHeader.id) {
|
||||
throw new SessionFormatError('current-generation delivery marker names the wrong Session')
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { AssistantStreamRecord } from '@deepseek-ai/dsh-llm'
|
||||
import {
|
||||
SessionFormatUnsupportedMigrationError,
|
||||
defineSessionFormatMigration,
|
||||
sessionFormatCount,
|
||||
} from '@deepseek-ai/dsh-session-format'
|
||||
import type {
|
||||
SessionFormatEvent,
|
||||
@@ -62,7 +63,7 @@ class TransformedReleasedV1ToV2Stage implements SessionFormatMigrationStage {
|
||||
assertReleasedV1Header(input.sourceHeader)
|
||||
this.state = {
|
||||
sourceHeader: input.sourceHeader,
|
||||
sourceCut: input.sourceInheritedEventCount,
|
||||
sourceCut: sessionFormatCount(input.sourceInheritedEventCount, 'format v1 inherited event count'),
|
||||
mapping: new Map(),
|
||||
legacyTurns: legacyTurnState(),
|
||||
pending: undefined,
|
||||
|
||||
@@ -55,6 +55,7 @@ function validateReleasedV2Artifact(
|
||||
artifact: SessionFormatArtifact,
|
||||
mode: 'current' | 'physical',
|
||||
knownEventTypes?: ReadonlySet<string>,
|
||||
relationshipHeaderVersion: number = artifact.header.version,
|
||||
): void {
|
||||
assertReleasedV2Header(artifact.header)
|
||||
const cut = sessionFormatCount(artifact.inheritedEventCount, 'format v2 inherited event count')
|
||||
@@ -95,7 +96,9 @@ function validateReleasedV2Artifact(
|
||||
throw new SessionFormatError('format v2 unseeded Session contains an inherited end-seed marker')
|
||||
}
|
||||
if (mode === 'current') {
|
||||
assertReleasedArtifactRelationships(artifact, RELEASED_V2_RELATIONSHIP_EXTENSIONS)
|
||||
assertReleasedArtifactRelationships({
|
||||
...artifact, header: { ...artifact.header, version: relationshipHeaderVersion },
|
||||
}, RELEASED_V2_RELATIONSHIP_EXTENSIONS)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,13 +142,15 @@ export function assertReleasedV2Keys(
|
||||
* Restore and validate one decoded released-v2 artifact.
|
||||
* @param artifact - detached vocabulary-restored artifact.
|
||||
* @param knownEventTypes - event types understood by the installed current Session package.
|
||||
* @param relationshipHeaderVersion - logical generation whose version-sensitive relationships are checked.
|
||||
* @returns the same validated artifact.
|
||||
*/
|
||||
export function restoreReleasedV2Artifact(
|
||||
artifact: SessionFormatArtifact,
|
||||
knownEventTypes: ReadonlySet<string>,
|
||||
relationshipHeaderVersion: number = artifact.header.version,
|
||||
): SessionFormatArtifact {
|
||||
validateReleasedV2Artifact(artifact, 'current', knownEventTypes)
|
||||
validateReleasedV2Artifact(artifact, 'current', knownEventTypes, relationshipHeaderVersion)
|
||||
return artifact
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/session/session-format-v2-to-v3/README.md
|
||||
README.md: e13c20e55cab2dd043318e8d3a6feaaa3583f3a1
|
||||
README.zh.md: 10cf1162d9be4b6a3e9feb455d74b6e845bf817d
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
description: "Restore released-v2 Session logs as v3 without changing their events."
|
||||
kind: "package-library"
|
||||
---
|
||||
|
||||
# @deepseek-ai/dsh-session-format-v2-to-v3
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
## Summary
|
||||
|
||||
This library restores released-v2 Session records as v3 while preserving event payloads, sequence references, timestamps, ordering, and inherited prefixes. Persistence consumes it through the static Session format catalog. It does not publish or modify durable files.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Use this package](#use-this-package)
|
||||
- [Understand the implementation](#understand-the-implementation)
|
||||
- [Further Exploration](#further-exploration)
|
||||
- [Model Experience](#model-experience)
|
||||
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
||||
- [Dev Note](#dev-note)
|
||||
|
||||
-----
|
||||
|
||||
<a id="use-this-package"></a>
|
||||
## Use this package
|
||||
|
||||
### When to use it
|
||||
|
||||
Use the [catalog](../session-format-catalog/README.md) for restoration. Direct imports serve catalog assembly and tests; this library is not mounted in a Cordis composition.
|
||||
|
||||
### Entry point
|
||||
|
||||
```text
|
||||
const targetHeader = sessionFormatV2ToV3.migrateHeader(sourceHeader)
|
||||
```
|
||||
|
||||
The header version becomes 3; all other header fields remain unchanged. The stage forwards events and compact runs synchronously. Scalar inherited end-seed markers determine the exact cut at EOF. V3 record encoding and validation reuse the frozen released-v2 implementation without modifying it. Unknown required events remain refusals; installed event types and ignorable unknown events retain their admission rules.
|
||||
|
||||
-----
|
||||
|
||||
<a id="understand-the-implementation"></a>
|
||||
## Understand the implementation
|
||||
|
||||
<details>
|
||||
<summary>Implementation internals — click to expand</summary>
|
||||
|
||||
The [stage](src/migration.ts) tracks the inherited cut and source delivery ownership without changing event values. The [codec](src/codec.ts) changes physical header versions and shares v2 record encoding. The [validator](src/validation.ts) checks the v3 version before applying released-v2 event validation. No runtime invariant companion is published because this library owns no independently observable runtime registrations or state replicas.
|
||||
|
||||
</details>
|
||||
|
||||
-----
|
||||
|
||||
<a id="further-exploration"></a>
|
||||
## Further Exploration
|
||||
|
||||
- [Released v1 to v2](../session-format-v1-to-v2/README.md) — frozen source codec and event validation.
|
||||
- [Session format protocol](../session-format/README.md) — adjacent streaming stages.
|
||||
|
||||
-----
|
||||
|
||||
<a id="model-experience"></a>
|
||||
## Model Experience
|
||||
|
||||
### Historical restoration
|
||||
|
||||
#### What the model sees
|
||||
|
||||
`sessionFormatV2ToV3` preserves every model-visible event and its payload.
|
||||
|
||||
#### Token effect
|
||||
|
||||
No model-visible content is added or removed.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
The model-message prefix remains unchanged.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- **No file publication** — persistence owns immutable successor publication; this package never overwrites released generations.
|
||||
- **Identity conversion only** — the stage introduces no structural event transformations.
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### Dev Note
|
||||
|
||||
<details>
|
||||
<summary>Working context for maintainers — click to expand</summary>
|
||||
|
||||
None.
|
||||
|
||||
</details>
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
description: "将已发布的 v2 Session 日志恢复为 v3,保留所有事件。"
|
||||
kind: "package-library"
|
||||
---
|
||||
|
||||
# @deepseek-ai/dsh-session-format-v2-to-v3
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
## 概述
|
||||
|
||||
本库将已发布的 v2 Session 记录恢复为 v3,保留事件载荷、序列引用、时间戳、顺序和继承前缀。持久化通过静态 Session 格式目录使用本库。本库不发布或修改持久化文件。
|
||||
|
||||
## 目录
|
||||
|
||||
- [使用本包](#use-this-package)
|
||||
- [理解实现](#understand-the-implementation)
|
||||
- [深入探索](#further-exploration)
|
||||
- [模型体验](#model-experience)
|
||||
- [已知限制与后续工作](#known-limitations-and-deferred-work)
|
||||
- [开发备注](#dev-note)
|
||||
|
||||
-----
|
||||
|
||||
<a id="use-this-package"></a>
|
||||
## 使用本包
|
||||
|
||||
### 使用场景
|
||||
|
||||
恢复时请使用[目录](../session-format-catalog/README.zh.md)。直接导入用于目录组装和测试;本库不挂载到 Cordis 组合中。
|
||||
|
||||
### 入口
|
||||
|
||||
```text
|
||||
const targetHeader = sessionFormatV2ToV3.migrateHeader(sourceHeader)
|
||||
```
|
||||
|
||||
头部版本变为 3,其余头部字段保持不变。阶段同步转发事件和紧凑事件段。标量继承 end-seed 标记在 EOF 确定精确切点。V3 记录编码和校验复用冻结的已发布 v2 实现,不修改该实现。未知必需事件仍被拒绝;已安装事件类型和可忽略的未知事件保留其准入规则。
|
||||
|
||||
-----
|
||||
|
||||
<a id="understand-the-implementation"></a>
|
||||
## 理解实现
|
||||
|
||||
<details>
|
||||
<summary>实现细节 — 点击展开</summary>
|
||||
|
||||
[阶段](src/migration.ts)跟踪继承切点和源投递归属,不改变事件值。[编解码器](src/codec.ts)改变物理头部版本并共享 v2 记录编码。[校验器](src/validation.ts)先检查 v3 版本,再执行已发布 v2 事件校验。本库不拥有可独立观察的运行时注册或状态副本,因此不发布运行时不变量伴随入口。
|
||||
|
||||
</details>
|
||||
|
||||
-----
|
||||
|
||||
<a id="further-exploration"></a>
|
||||
## 深入探索
|
||||
|
||||
- [已发布 v1 到 v2](../session-format-v1-to-v2/README.zh.md) — 冻结的源编解码器和事件校验。
|
||||
- [Session 格式协议](../session-format/README.zh.md) — 相邻流式阶段。
|
||||
|
||||
-----
|
||||
|
||||
<a id="model-experience"></a>
|
||||
## 模型体验
|
||||
|
||||
### 历史日志恢复
|
||||
|
||||
#### 模型看到什么
|
||||
|
||||
`sessionFormatV2ToV3` 保留每个模型可见事件及其载荷。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
不添加或删除模型可见内容。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
模型消息前缀保持不变。
|
||||
|
||||
## 已知限制与后续工作
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- **不发布文件** — 持久化负责不可变后继代的发布;本包绝不覆盖已发布代。
|
||||
- **仅恒等转换** — 阶段不引入结构性事件转换。
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### 开发备注
|
||||
|
||||
<details>
|
||||
<summary>维护者工作上下文 — 点击展开</summary>
|
||||
|
||||
无。
|
||||
|
||||
</details>
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-session-format-v2-to-v3",
|
||||
"description": "Released-v2 Session codec and identity streaming migration to v3",
|
||||
"version": "0.1.3-alpha.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/session/session-format-v2-to-v3"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dsh": {
|
||||
"sessionFormatMigration": {
|
||||
"from": 2,
|
||||
"to": 3,
|
||||
"export": ".",
|
||||
"migration": "sessionFormatV2ToV3",
|
||||
"sourceCodec": "releasedV2SessionFormatCodec",
|
||||
"targetCodec": "releasedV3SessionFormatCodec",
|
||||
"targetHeaderValidator": "assertReleasedV3Header",
|
||||
"targetRestorer": "restoreReleasedV3Artifact"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@deepseek-ai/dsh-session-format": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-format-v1-to-v2": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/** V3 record encoding shares the unchanged released-v2 physical event language. */
|
||||
|
||||
import { SessionFormatError, isSessionFormatJsonObject, snapshotSessionFormatJson } from '@deepseek-ai/dsh-session-format'
|
||||
import type {
|
||||
SessionFormatCodec,
|
||||
SessionFormatCurrentEncoder,
|
||||
SessionFormatHeader,
|
||||
} from '@deepseek-ai/dsh-session-format'
|
||||
import { releasedV2SessionFormatCodec } from '@deepseek-ai/dsh-session-format-v1-to-v2'
|
||||
import { assertReleasedV3Header } from './validation.ts'
|
||||
|
||||
/** Physical v3 codec: only the header version differs from released v2. */
|
||||
export const releasedV3SessionFormatCodec = Object.freeze({
|
||||
version: 3,
|
||||
decodeHeader(value: unknown) {
|
||||
return { ...releasedV2SessionFormatCodec.decodeHeader(v2PhysicalHeader(value)), version: 3 }
|
||||
},
|
||||
createDecoder(value, recovery) {
|
||||
const decoder = releasedV2SessionFormatCodec.createDecoder(v2PhysicalHeader(value), recovery)
|
||||
return { ...decoder, header: { ...decoder.header, version: 3 } }
|
||||
},
|
||||
encodeHeader(header, inheritedEventCount) {
|
||||
assertReleasedV3Header(header)
|
||||
return {
|
||||
...releasedV2SessionFormatCodec.encodeHeader({ ...header, version: 2 }, inheritedEventCount),
|
||||
version: 3,
|
||||
}
|
||||
},
|
||||
encodeEvent: releasedV2SessionFormatCodec.encodeEvent,
|
||||
} satisfies SessionFormatCodec & SessionFormatCurrentEncoder)
|
||||
|
||||
function v2PhysicalHeader(value: unknown): SessionFormatHeader {
|
||||
const header = snapshotSessionFormatJson(value, 'format v3 physical header')
|
||||
if (!isSessionFormatJsonObject(header) || header['version'] !== 3) {
|
||||
throw new SessionFormatError('expected format v3 physical Session header')
|
||||
}
|
||||
return { ...header, version: 2 } as SessionFormatHeader
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/** Released-v2 physical codec and identity streaming migration into v3. */
|
||||
|
||||
export { releasedV2SessionFormatCodec } from '@deepseek-ai/dsh-session-format-v1-to-v2'
|
||||
export * from './codec.ts'
|
||||
export * from './migration.ts'
|
||||
export * from './validation.ts'
|
||||
@@ -0,0 +1,70 @@
|
||||
/** Identity adjacent stage; events and compact runs retain their values and ordering. */
|
||||
|
||||
import { SessionFormatError, defineSessionFormatMigration, isSessionFormatJsonObject, sessionFormatCount } from '@deepseek-ai/dsh-session-format'
|
||||
import type {
|
||||
SessionFormatEvent,
|
||||
SessionFormatEventRun,
|
||||
SessionFormatMigrationContext,
|
||||
SessionFormatMigrationStage,
|
||||
SessionFormatMigrationStageInput,
|
||||
} from '@deepseek-ai/dsh-session-format'
|
||||
import { assertReleasedV2Header } from '@deepseek-ai/dsh-session-format-v1-to-v2'
|
||||
import { assertReleasedV3Header } from './validation.ts'
|
||||
|
||||
/** Adjacent identity migration from released v2 to v3. */
|
||||
export const sessionFormatV2ToV3 = defineSessionFormatMigration({
|
||||
name: '@deepseek-ai/dsh-session-format-v2-to-v3',
|
||||
fromVersion: 2,
|
||||
toVersion: 3,
|
||||
migrateHeader(header) {
|
||||
assertReleasedV2Header(header)
|
||||
return { ...header, version: 3 }
|
||||
},
|
||||
createStage(input) {
|
||||
return new ReleasedV2ToV3Stage(input)
|
||||
},
|
||||
validateTargetHeader: assertReleasedV3Header,
|
||||
})
|
||||
|
||||
class ReleasedV2ToV3Stage implements SessionFormatMigrationStage {
|
||||
private inheritedEventCount: number | undefined
|
||||
private lastForeignDeliverySeq: number | undefined
|
||||
|
||||
constructor(private readonly input: SessionFormatMigrationStageInput) {
|
||||
this.inheritedEventCount = input.sourceHeader.isSeeded ? undefined : 0
|
||||
}
|
||||
|
||||
transformEvent(event: SessionFormatEvent, context: SessionFormatMigrationContext): void {
|
||||
if (event.type === 'session/end-seed'
|
||||
&& isSessionFormatJsonObject(event.data)
|
||||
&& event.data['inherited'] === true) {
|
||||
this.inheritedEventCount = event.seq
|
||||
}
|
||||
if (event.type === 'session-log-deepseek/delivery-accepted'
|
||||
&& isSessionFormatJsonObject(event.data)
|
||||
&& event.data['sessionFormatVersion'] === 2
|
||||
&& event.data['sessionId'] !== this.input.sourceHeader.id) {
|
||||
this.lastForeignDeliverySeq = event.seq
|
||||
}
|
||||
context.emitEvent(event)
|
||||
}
|
||||
|
||||
transformRun(run: SessionFormatEventRun, context: SessionFormatMigrationContext): void {
|
||||
context.emitRun(run)
|
||||
}
|
||||
|
||||
finish(_context: SessionFormatMigrationContext): number {
|
||||
const cut = sessionFormatCount(this.inheritedEventCount, 'format v2 inherited end-seed marker')
|
||||
if (this.input.sourceInheritedEventCount !== undefined && this.input.sourceInheritedEventCount !== cut) {
|
||||
throw new SessionFormatError('format v2 inherited end-seed marker disagrees with its source cut')
|
||||
}
|
||||
if (!this.input.sourceHeader.isSeeded && cut !== 0) {
|
||||
throw new SessionFormatError('format v2 unseeded Session contains an inherited end-seed marker')
|
||||
}
|
||||
if (this.lastForeignDeliverySeq !== undefined
|
||||
&& (this.input.sourceHeader.parentSession === undefined || this.lastForeignDeliverySeq >= cut)) {
|
||||
throw new SessionFormatError('current-generation delivery marker names the wrong Session')
|
||||
}
|
||||
return cut
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/** V3 validation preserves the released-v2 event model. */
|
||||
|
||||
import { SessionFormatError } from '@deepseek-ai/dsh-session-format'
|
||||
import type { SessionFormatArtifact, SessionFormatHeader } from '@deepseek-ai/dsh-session-format'
|
||||
import { assertReleasedV2Header, restoreReleasedV2Artifact } from '@deepseek-ai/dsh-session-format-v1-to-v2'
|
||||
|
||||
/**
|
||||
* Validate v3 logical metadata with the released-v2 fields.
|
||||
* @param header - decoded v3 Session header.
|
||||
*/
|
||||
export function assertReleasedV3Header(header: SessionFormatHeader): void {
|
||||
if (header.version !== 3) throw new SessionFormatError('expected format v3 header')
|
||||
assertReleasedV2Header({ ...header, version: 2 })
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate v3 event admission, relationships, and inherited cut without copying events.
|
||||
* @param artifact - detached v3 artifact.
|
||||
* @param knownEventTypes - event types understood by the installed Session package.
|
||||
* @returns the same validated artifact.
|
||||
*/
|
||||
export function restoreReleasedV3Artifact(
|
||||
artifact: SessionFormatArtifact,
|
||||
knownEventTypes: ReadonlySet<string>,
|
||||
): SessionFormatArtifact {
|
||||
assertReleasedV3Header(artifact.header)
|
||||
restoreReleasedV2Artifact({ ...artifact, header: { ...artifact.header, version: 2 } }, knownEventTypes, 3)
|
||||
return artifact
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { SessionFormatEventCollector } from '@deepseek-ai/dsh-session-format'
|
||||
import type { SessionFormatEvent, SessionFormatHeader } from '@deepseek-ai/dsh-session-format'
|
||||
import {
|
||||
assertReleasedV3Header,
|
||||
releasedV2SessionFormatCodec,
|
||||
releasedV3SessionFormatCodec,
|
||||
restoreReleasedV3Artifact,
|
||||
sessionFormatV2ToV3,
|
||||
} from '../src/index.ts'
|
||||
|
||||
const header: SessionFormatHeader = {
|
||||
version: 2, id: 'identity', createdAt: 1, isSeeded: false, delegationDepth: 0,
|
||||
}
|
||||
|
||||
describe('v2 to v3 identity migration', () => {
|
||||
it('changes only the header version and forwards events and runs without expansion', () => {
|
||||
const target = sessionFormatV2ToV3.migrateHeader(header)
|
||||
expect(target).toEqual({ ...header, version: 3 })
|
||||
expect(header.version).toBe(2)
|
||||
const stage = sessionFormatV2ToV3.createStage({
|
||||
sourceHeader: header, targetHeader: target, sourceInheritedEventCount: 0, sourceKind: 'decoded',
|
||||
})
|
||||
const event: SessionFormatEvent = {
|
||||
type: 'external/event', seq: 5, time: 12, data: { nested: ['payload'] },
|
||||
ignorable: true, sourceEventSeqs: [1, 2], surfaceOp: { replace: [3] },
|
||||
}
|
||||
const run = { runType: 'opaque', firstSeq: 6, eventCount: 2, expand: vi.fn() }
|
||||
const context = { emitEvent: vi.fn(), emitRun: vi.fn() }
|
||||
stage.transformEvent(event, context)
|
||||
stage.transformRun(run, context)
|
||||
expect(context.emitEvent.mock.calls[0]?.[0]).toBe(event)
|
||||
expect(context.emitRun.mock.calls[0]?.[0]).toBe(run)
|
||||
expect(run.expand).not.toHaveBeenCalled()
|
||||
expect(stage.finish(context)).toBe(0)
|
||||
})
|
||||
|
||||
it('derives the inherited cut from scalar end-seed markers', () => {
|
||||
const seeded = { ...header, isSeeded: true }
|
||||
const stage = sessionFormatV2ToV3.createStage({
|
||||
sourceHeader: seeded, targetHeader: { ...seeded, version: 3 },
|
||||
sourceInheritedEventCount: undefined, sourceKind: 'transformed',
|
||||
})
|
||||
const context = new SessionFormatEventCollector()
|
||||
for (const data of [null, false, [], {}, { inherited: true }]) {
|
||||
stage.transformEvent({ type: 'session/end-seed', seq: 4, time: 1, data }, context)
|
||||
}
|
||||
expect(stage.finish(context)).toBe(4)
|
||||
})
|
||||
|
||||
it('rejects missing, mismatched, and unseeded inherited cuts', () => {
|
||||
const context = new SessionFormatEventCollector()
|
||||
for (const [isSeeded, sourceCut, marker] of [[true, undefined, undefined], [true, 2, 1], [false, undefined, 1]] as const) {
|
||||
const source = { ...header, isSeeded }
|
||||
const stage = sessionFormatV2ToV3.createStage({
|
||||
sourceHeader: source, targetHeader: { ...source, version: 3 },
|
||||
sourceInheritedEventCount: sourceCut, sourceKind: 'decoded',
|
||||
})
|
||||
if (marker !== undefined) stage.transformEvent({
|
||||
type: 'session/end-seed', seq: marker, time: 1, data: { inherited: true },
|
||||
}, context)
|
||||
expect(() => stage.finish(context)).toThrow(/inherited/)
|
||||
}
|
||||
})
|
||||
|
||||
it('round-trips v3 records with identical v2 provenance encoding', () => {
|
||||
const current = { ...header, version: 3 }
|
||||
const event = { type: 'external/event', seq: 3, time: 1, data: null, sourceEventSeqs: [0, 1, 2] }
|
||||
const physical = releasedV3SessionFormatCodec.encodeHeader(current, 0)
|
||||
expect(physical).toEqual({ ...releasedV2SessionFormatCodec.encodeHeader(header, 0), version: 3 })
|
||||
expect(releasedV3SessionFormatCodec.decodeHeader(physical)).toEqual(current)
|
||||
const row = releasedV3SessionFormatCodec.encodeEvent(event)
|
||||
expect(row).toEqual(releasedV2SessionFormatCodec.encodeEvent(event))
|
||||
const decoder = releasedV3SessionFormatCodec.createDecoder(physical, 'strict')
|
||||
const context = new SessionFormatEventCollector()
|
||||
const first = { type: 'external/event', seq: 0, time: 1, data: null }
|
||||
decoder.decodeRow(first, context)
|
||||
expect(decoder.header).toEqual(current)
|
||||
expect(decoder.finish(context)).toBe(0)
|
||||
expect(context.values).toEqual([first])
|
||||
})
|
||||
|
||||
it.each([null, [], false, { version: 2 }])('rejects a non-v3 physical header %j', (value) => {
|
||||
expect(() => releasedV3SessionFormatCodec.decodeHeader(value)).toThrow(/format v3 physical/)
|
||||
})
|
||||
|
||||
it.each([false, true])('validates V2 delivery ownership before promotion (inherited=%s)', (inherited) => {
|
||||
const source = { ...header, isSeeded: inherited, ...(inherited ? { parentSession: 'parent' } : {}) }
|
||||
const stage = sessionFormatV2ToV3.createStage({
|
||||
sourceHeader: source, targetHeader: { ...source, version: 3 },
|
||||
sourceInheritedEventCount: undefined, sourceKind: 'decoded',
|
||||
})
|
||||
const context = new SessionFormatEventCollector()
|
||||
stage.transformEvent({ type: 'session-log-deepseek/delivery-accepted', seq: 0, time: 1,
|
||||
data: { sessionId: 'parent', sessionFormatVersion: 2 } }, context)
|
||||
if (inherited) stage.transformEvent({ type: 'session/end-seed', seq: 1, time: 1, data: { inherited: true } }, context)
|
||||
if (inherited) {
|
||||
expect(stage.finish(context)).toBe(1)
|
||||
stage.transformEvent({ type: 'session-log-deepseek/delivery-accepted', seq: 2, time: 1,
|
||||
data: { sessionId: 'parent', sessionFormatVersion: 2 } }, context)
|
||||
}
|
||||
expect(() => stage.finish(context)).toThrow(/wrong Session/)
|
||||
})
|
||||
|
||||
it('checks native v3 delivery ownership without reinterpreting historical markers', () => {
|
||||
const artifact = (version: number) => ({
|
||||
header: { ...header, version: 3 }, inheritedEventCount: 0, events: [{
|
||||
type: 'session-log-deepseek/delivery-accepted', seq: 0, time: 1,
|
||||
data: { sessionId: 'other-session', throughSeq: 0, sessionFormatVersion: version },
|
||||
}],
|
||||
})
|
||||
expect(() => restoreReleasedV3Artifact(artifact(3), new Set())).toThrow(/wrong Session/)
|
||||
expect(restoreReleasedV3Artifact(artifact(2), new Set()).events).toEqual(artifact(2).events)
|
||||
})
|
||||
|
||||
it('validates v3 metadata and event admission without mutating the artifact', () => {
|
||||
expect(() => assertReleasedV3Header(header)).toThrow(/format v3 header/)
|
||||
expect(() => assertReleasedV3Header({ ...header, version: 3, cwd: 'relative' })).toThrow(/absolute/)
|
||||
const artifact = { header: { ...header, version: 3 }, inheritedEventCount: 0, events: [
|
||||
{ type: 'external/event', seq: 0, time: 1, data: null, ignorable: true },
|
||||
] }
|
||||
expect(restoreReleasedV3Artifact(artifact, new Set())).toBe(artifact)
|
||||
expect(artifact.header.version).toBe(3)
|
||||
expect(() => restoreReleasedV3Artifact({ ...artifact, events: [
|
||||
{ type: 'external/required', seq: 0, time: 1, data: null },
|
||||
] }, new Set())).toThrow(/unknown event type/)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../vendor/cordis"
|
||||
},
|
||||
{
|
||||
"path": "../session-format"
|
||||
},
|
||||
{
|
||||
"path": "../session-format-v1-to-v2"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'tsdown'
|
||||
|
||||
/** Build the frozen adjacent-migration library. */
|
||||
export default defineConfig({
|
||||
entry: ['lib/types/index.js'],
|
||||
outDir: 'lib',
|
||||
format: ['esm'],
|
||||
platform: 'node',
|
||||
target: 'es2024',
|
||||
fixedExtension: false,
|
||||
dts: false,
|
||||
clean: false,
|
||||
})
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/session/session-format/README.md
|
||||
README.md: d44a7091bc5840afcb5d1e73f594284c3d5c8206
|
||||
README.zh.md: f4b6acd8e0ba3c28a1b5088fc577e1b4289ca9a0
|
||||
README.md: 040d3383864d008b9df62378063a11223c1373be
|
||||
README.zh.md: 7c57123ac7315cfc15e7a20f6834e8ae48fad3ec
|
||||
@@ -55,7 +55,7 @@ The recoverable decoder returns the accepted logical prefix. A codec may drop on
|
||||
<details>
|
||||
<summary>Implementation internals — click to expand</summary>
|
||||
|
||||
The chain validates unique gap-free ordering at construction. The catalog composes one row decoder with stateful adjacent event transformers, retains only their bounded state and the final current events, and performs target validation at `finish()`; only the caller decides whether and how to publish that result.
|
||||
The chain validates unique gap-free ordering at construction. A source cut may be unknown until EOF; stages that require a header cut reject its absence, while marker-based stages derive it from emitted events. Each stage returns its exact target cut at finish, and any predeclared cut must agree. The catalog composes one row decoder with stateful adjacent event transformers, retains only their bounded state and the final current events, and performs target validation at `finish()`; only the caller decides whether and how to publish that result.
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
|
||||
@@ -55,7 +55,7 @@ const eventRecords = current.events.map(catalog.encodeCurrentEvent)
|
||||
<details>
|
||||
<summary>实现细节——点击展开</summary>
|
||||
|
||||
迁移链在构造时校验唯一且无缺口的顺序。Catalog 把一个行 decoder 与有状态的相邻事件 transformer 组合起来,只保留其有界状态与最终当前事件,并在 `finish()` 时执行目标校验;只有调用方决定是否发布该结果以及如何发布。
|
||||
迁移链在构造时校验唯一且无缺口的顺序。源切点可以在 EOF 前保持未知;依赖头部切点的 Stage 拒绝缺失值,而基于标记的 Stage 从已发出的事件推导切点。每个 Stage 在完成时返回精确的目标切点,且必须与预声明切点一致。Catalog 把一个行 decoder 与有状态的相邻事件 transformer 组合起来,只保留其有界状态与最终当前事件,并在 `finish()` 时执行目标校验;只有调用方决定是否发布该结果以及如何发布。
|
||||
|
||||
| 文件 | 职责 |
|
||||
|---|---|
|
||||
|
||||
@@ -139,7 +139,7 @@ export function createSessionFormatCatalog(options: SessionFormatCatalogOptions)
|
||||
const collector = new SessionFormatEventCollector()
|
||||
const migration = chain.createStream(
|
||||
decoder.header,
|
||||
requiredHistoricalCut(storedVersion, sourceCut),
|
||||
sourceCut,
|
||||
collector,
|
||||
)
|
||||
return new MigratingSessionFormatRestore(
|
||||
@@ -279,13 +279,6 @@ function identityArtifact(artifact: SessionFormatArtifact): SessionFormatArtifac
|
||||
return artifact
|
||||
}
|
||||
|
||||
function requiredHistoricalCut(version: number, cut: number | undefined): number {
|
||||
if (cut === undefined) {
|
||||
throw new SessionFormatError(`format v${version} decoder must expose its inherited cut before migration`)
|
||||
}
|
||||
return cut
|
||||
}
|
||||
|
||||
function malformed(targetVersion: number, error: unknown, storedVersion?: number): SessionFormatHeaderReadResult {
|
||||
return Object.freeze({
|
||||
status: 'malformed',
|
||||
|
||||
@@ -88,11 +88,13 @@ class CompiledSessionFormatChain implements SessionFormatChain {
|
||||
|
||||
createStream(
|
||||
sourceHeader: SessionFormatHeader,
|
||||
sourceCut: number,
|
||||
sourceCut: number | undefined,
|
||||
output: SessionFormatMigrationContext,
|
||||
): SessionFormatMigrationStream {
|
||||
let header = sourceHeader
|
||||
const validatedSourceCut = sessionFormatCount(sourceCut, 'Session inherited event count')
|
||||
const validatedSourceCut = sourceCut === undefined
|
||||
? undefined
|
||||
: sessionFormatCount(sourceCut, 'Session inherited event count')
|
||||
let inheritedEventCount = validatedSourceCut
|
||||
const stages: Array<{
|
||||
readonly migration: SessionFormatMigration
|
||||
@@ -114,13 +116,7 @@ class CompiledSessionFormatChain implements SessionFormatChain {
|
||||
}
|
||||
header = targetHeader
|
||||
stages.push({ migration, stage })
|
||||
if (index + 1 < plan.length) {
|
||||
const targetCut = stage.headerInheritedEventCount
|
||||
if (targetCut === undefined) {
|
||||
throw new SessionFormatError(`${migration.name} must expose its inherited cut before the next migration`)
|
||||
}
|
||||
inheritedEventCount = targetCut
|
||||
}
|
||||
inheritedEventCount = stage.headerInheritedEventCount
|
||||
}
|
||||
return new CompiledSessionFormatMigrationStream(
|
||||
header,
|
||||
@@ -215,7 +211,7 @@ class CompiledSessionFormatMigrationStream implements SessionFormatMigrationStre
|
||||
|
||||
constructor(
|
||||
readonly header: SessionFormatHeader,
|
||||
private readonly sourceInheritedEventCount: number,
|
||||
private readonly sourceInheritedEventCount: number | undefined,
|
||||
entries: readonly CompiledMigrationStage[],
|
||||
output: SessionFormatMigrationContext,
|
||||
) {
|
||||
@@ -241,7 +237,7 @@ class CompiledSessionFormatMigrationStream implements SessionFormatMigrationStre
|
||||
finish(): number {
|
||||
let inheritedEventCount = this.sourceInheritedEventCount
|
||||
for (const stage of this.stages) inheritedEventCount = stage.finish()
|
||||
return inheritedEventCount
|
||||
return sessionFormatCount(inheritedEventCount, 'finished Session inherited event count')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ export interface SessionFormatMigrationStageInput {
|
||||
readonly sourceHeader: SessionFormatHeader
|
||||
/** Validated target metadata produced by this edge's header migration. */
|
||||
readonly targetHeader: SessionFormatHeader
|
||||
/** Exact inherited prefix length in source coordinates. */
|
||||
readonly sourceInheritedEventCount: number
|
||||
/** Inherited prefix length in source coordinates, or undefined until body decoding completes. */
|
||||
readonly sourceInheritedEventCount: number | undefined
|
||||
/** Whether this edge consumes physical decode output or a prior migration's validated output. */
|
||||
readonly sourceKind: 'decoded' | 'transformed'
|
||||
}
|
||||
@@ -80,7 +80,7 @@ export interface SessionFormatChain {
|
||||
/** Compile the complete migration stage chain for one decoded source artifact. */
|
||||
createStream(
|
||||
header: SessionFormatHeader,
|
||||
inheritedEventCount: number,
|
||||
inheritedEventCount: number | undefined,
|
||||
context: SessionFormatMigrationContext,
|
||||
): SessionFormatMigrationStream
|
||||
/** Convert only a supported header to the current logical representation. */
|
||||
|
||||
@@ -38,9 +38,9 @@ function codec(version: number, inheritedEventCount = 0): SessionFormatCodec & S
|
||||
}
|
||||
}
|
||||
|
||||
function identityMigrationStage(inheritedEventCount: number) {
|
||||
function identityMigrationStage(inheritedEventCount: number | undefined) {
|
||||
return {
|
||||
headerInheritedEventCount: inheritedEventCount,
|
||||
...(inheritedEventCount === undefined ? {} : { headerInheritedEventCount: inheritedEventCount }),
|
||||
transformEvent(
|
||||
event: SessionFormatEvent,
|
||||
context: SessionFormatMigrationContext,
|
||||
@@ -53,7 +53,7 @@ function identityMigrationStage(inheritedEventCount: number) {
|
||||
) {
|
||||
context.emitRun(run)
|
||||
},
|
||||
finish: () => inheritedEventCount,
|
||||
finish: () => inheritedEventCount ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,10 +336,10 @@ describe('Session format catalog', () => {
|
||||
}
|
||||
},
|
||||
}
|
||||
expect(() => catalog(edge(), 1, deferredCut).createRestore(
|
||||
expect(catalog(edge(), 1, deferredCut).createRestore(
|
||||
oldHeader,
|
||||
{ recovery: 'strict', validation: 'current' },
|
||||
)).toThrow(/must expose its inherited cut/)
|
||||
).finish().inheritedEventCount).toBe(0)
|
||||
})
|
||||
|
||||
it('expands an unhandled compact run without an intermediate array', () => {
|
||||
|
||||
@@ -33,9 +33,9 @@ function throwUnknown(value: unknown): never {
|
||||
throw value
|
||||
}
|
||||
|
||||
function identityStage(inheritedEventCount: number) {
|
||||
function identityStage(inheritedEventCount: number | undefined) {
|
||||
return {
|
||||
headerInheritedEventCount: inheritedEventCount,
|
||||
...(inheritedEventCount === undefined ? {} : { headerInheritedEventCount: inheritedEventCount }),
|
||||
transformEvent(
|
||||
value: SessionFormatEvent,
|
||||
context: SessionFormatMigrationContext,
|
||||
@@ -48,7 +48,7 @@ function identityStage(inheritedEventCount: number) {
|
||||
) {
|
||||
context.emitRun(run)
|
||||
},
|
||||
finish: () => inheritedEventCount,
|
||||
finish: () => inheritedEventCount ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ describe('Session format chain', () => {
|
||||
})).toThrow(/does not lead/)
|
||||
})
|
||||
|
||||
it('requires every intermediate stage to expose its inherited cut before the next edge', () => {
|
||||
it('composes stages whose inherited cut is derived from the body', () => {
|
||||
const first = migration({
|
||||
createStage: () => ({
|
||||
transformEvent: (value, context) => { context.emitEvent(value) },
|
||||
@@ -155,11 +155,11 @@ describe('Session format chain', () => {
|
||||
restoreCurrentHeader: header => header,
|
||||
})
|
||||
|
||||
expect(() => current.createStream(
|
||||
expect(current.createStream(
|
||||
{ ...currentHeader, version: 0 },
|
||||
0,
|
||||
discard,
|
||||
)).toThrow(/expose its inherited cut/)
|
||||
).finish()).toBe(0)
|
||||
|
||||
const createSecondStage = vi.fn(({ sourceKind }: SessionFormatMigrationStageInput) => {
|
||||
expect(sourceKind).toBe('transformed')
|
||||
@@ -213,6 +213,15 @@ describe('Session format chain', () => {
|
||||
expect(finishRefusal).toBeInstanceOf(SessionFormatUnsupportedMigrationError)
|
||||
expect(finishRefusal.cause).toBe(finishFailure)
|
||||
|
||||
const eventFailure = new Error('event relationship is invalid')
|
||||
const eventFailing = chain(migration({
|
||||
createStage: () => ({
|
||||
...identityStage(0),
|
||||
transformEvent: () => { throw eventFailure },
|
||||
}),
|
||||
})).createStream(source, 0, discard)
|
||||
expect(() => eventFailing.emitEvent(event)).toThrow(/event relationship is invalid/)
|
||||
|
||||
const runFailure = new Error('run relationship is invalid')
|
||||
const runFailing = chain(migration({
|
||||
createStage: () => ({
|
||||
|
||||
Generated
+16
@@ -7278,6 +7278,9 @@ importers:
|
||||
'@deepseek-ai/dsh-session-format-v1-to-v2':
|
||||
specifier: workspace:^
|
||||
version: link:../session-format-v1-to-v2
|
||||
'@deepseek-ai/dsh-session-format-v2-to-v3':
|
||||
specifier: workspace:^
|
||||
version: link:../session-format-v2-to-v3
|
||||
devDependencies:
|
||||
'@deepseek-ai/cordis':
|
||||
specifier: workspace:^
|
||||
@@ -7324,6 +7327,19 @@ importers:
|
||||
specifier: workspace:^
|
||||
version: link:../../core/session
|
||||
|
||||
packages/session/session-format-v2-to-v3:
|
||||
dependencies:
|
||||
'@deepseek-ai/dsh-session-format':
|
||||
specifier: workspace:^
|
||||
version: link:../session-format
|
||||
'@deepseek-ai/dsh-session-format-v1-to-v2':
|
||||
specifier: workspace:^
|
||||
version: link:../session-format-v1-to-v2
|
||||
devDependencies:
|
||||
'@deepseek-ai/cordis':
|
||||
specifier: workspace:^
|
||||
version: link:../../../vendor/cordis
|
||||
|
||||
packages/session/session-log-deepseek:
|
||||
dependencies:
|
||||
'@deepseek-ai/dsh-brand':
|
||||
|
||||
@@ -334,6 +334,7 @@
|
||||
"@deepseek-ai/dsh-session-format-catalog": ["./packages/session/session-format-catalog/src"],
|
||||
"@deepseek-ai/dsh-session-format-v0-to-v1": ["./packages/session/session-format-v0-to-v1/src"],
|
||||
"@deepseek-ai/dsh-session-format-v1-to-v2": ["./packages/session/session-format-v1-to-v2/src"],
|
||||
"@deepseek-ai/dsh-session-format-v2-to-v3": ["./packages/session/session-format-v2-to-v3/src"],
|
||||
"@deepseek-ai/dsh-session-log-deepseek": ["./packages/session/session-log-deepseek/src"],
|
||||
"@deepseek-ai/dsh-session-log-deepseek/invariant": ["./packages/session/session-log-deepseek/src/invariant.ts"],
|
||||
"@deepseek-ai/dsh-session-log-export": ["./packages/session-query/session-log-export/src"],
|
||||
|
||||
@@ -167,6 +167,7 @@
|
||||
{ "path": "./packages/session/session-format" },
|
||||
{ "path": "./packages/session/session-format-v0-to-v1" },
|
||||
{ "path": "./packages/session/session-format-v1-to-v2" },
|
||||
{ "path": "./packages/session/session-format-v2-to-v3" },
|
||||
{ "path": "./packages/session/session-format-catalog" },
|
||||
{ "path": "./packages/session/session-checkpoint-policy" },
|
||||
{ "path": "./packages/session/session-log-deepseek" },
|
||||
|
||||
Reference in new issue
Block a user