/** * Fixed physical-record compression for SQLite. Schema-owned functions * encode logical events and decode tagged rows before persistence consumers * observe them. * @module @deepseek-ai/dsh-session-persistence-sqlite/compression */ import { readFileSync } from 'node:fs' import { TextDecoder } from 'node:util' import { constants, zstdCompressSync, zstdDecompressSync } from 'node:zlib' import type { SessionEvent, SurfaceEventType } from '@deepseek-ai/dsh-session' import { decodeSerializedChunkRow, type ChunkRow, MAX_PACKED_DATA_BYTES, type StorageRecord, } from './codec.ts' import type { EventRow } from './schema.ts' /** One physical row ready for SQLite parameter binding. */ export interface BoundRecord { readonly seq: number readonly type: string readonly time: number readonly data: string | Uint8Array readonly sourceEventSeqs: Uint8Array | null readonly surfaceOp: string | null readonly isPacked: 0 | 1 } const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true }) const ZSTD_COMPRESSION_LEVEL = 3 const DELTA_TAG = 0 const RUN_TAG = 1 const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER) const MAX_ZIGZAG_INTEGER = MAX_SAFE_INTEGER * 2n /** * Schema-19 raw-content zstd dictionary for independently decodable data rows. * Its exact bytes are part of the physical format; changing the resource * requires a schema-version bump. */ const ZSTD_DICTIONARY = readFileSync(new URL('../resources/zstd-dictionary.bin', import.meta.url)) /** Compress options shared by every data-column frame. */ const DATA_ZSTD_OPTIONS = { dictionary: ZSTD_DICTIONARY, params: { [constants.ZSTD_c_compressionLevel]: ZSTD_COMPRESSION_LEVEL }, } as const const CHUNK_TAGS = ['text-chunks', 'reasoning-chunks', 'tool-call-chunks'] as const type ChunkTag = typeof CHUNK_TAGS[number] function isChunkTag(value: string): value is ChunkTag { return (CHUNK_TAGS as readonly string[]).includes(value) } /** * Decode one physical SQLite row into its complete logical event span. * @param row - detached SQLite event row. * @returns every logical event represented by the row. */ export function decodeRow(row: EventRow): SessionEvent[] { if (row.is_packed === 0) return [decodeScalarRow(row)] if (!isChunkTag(row.type)) { throw new Error(`malformed ${row.type} storage row: packed discriminator requires a chunk tag`) } if (row.source_event_seqs !== null || row.surface_op !== null) { throw new Error(`malformed ${row.type} storage row: packed surface fields must be null`) } return decodeSerializedChunkRow( row.type, row.seq, row.time, decodeData(row.data, MAX_PACKED_DATA_BYTES), ) } /** * Convert a storage record to SQLite column values. * @param record - scalar event or packed chunk record. * @returns column values for one physical insert. */ export function bindRecord(record: StorageRecord): BoundRecord { if (isChunkRow(record)) { return { seq: record.seq0, type: record.type, time: record.time0, data: encodeData(JSON.stringify(record.data)), sourceEventSeqs: null, surfaceOp: null, isPacked: 1, } } const event = record const surface = event as SessionEvent return { seq: event.seq, type: event.type, time: event.time, data: encodeData(JSON.stringify(event.data)), sourceEventSeqs: surface.sourceEventSeqs === undefined ? null : encodeSourceEventSeqs(surface.sourceEventSeqs), surfaceOp: surface.surfaceOp === undefined ? null : JSON.stringify(surface.surfaceOp), isPacked: 0, } } function encodeData(serialized: string): string | Uint8Array { const bytes = Buffer.from(serialized) const compressed = zstdCompressSync(bytes, DATA_ZSTD_OPTIONS) return compressed.length < bytes.length ? compressed : serialized } function decodeData(value: string | Uint8Array, maxOutputLength?: number): string { if (typeof value === 'string') return value const decoded = maxOutputLength === undefined ? zstdDecompressSync(value, { dictionary: ZSTD_DICTIONARY }) : zstdDecompressSync(value, { dictionary: ZSTD_DICTIONARY, maxOutputLength }) return UTF8_DECODER.decode(decoded) } function encodeSourceEventSeqs(values: readonly number[]): Uint8Array { if (values.length === 0) return new Uint8Array() const deltas = [DELTA_TAG] let previous = 0n for (let index = 0; index < values.length; index += 1) { const value = values[index] as number if (!Number.isSafeInteger(value) || value < 0) { throw new TypeError('sourceEventSeqs must contain non-negative safe integers') } const current = BigInt(value) const encoded = index === 0 ? current : current >= previous ? (current - previous) * 2n : ((previous - current) * 2n) - 1n appendVarint(deltas, encoded) previous = current } if (!isStrictlyIncreasing(values)) return Uint8Array.from(deltas) const runs = [RUN_TAG] let start = values[0] as number let end = start for (let index = 1; index < values.length; index += 1) { const value = values[index] as number if (value === end + 1) { end = value continue } appendVarint(runs, BigInt(start)) appendVarint(runs, BigInt(end - start + 1)) start = value end = start } appendVarint(runs, BigInt(start)) appendVarint(runs, BigInt(end - start + 1)) return Uint8Array.from(runs.length < deltas.length ? runs : deltas) } function isStrictlyIncreasing(values: readonly number[]): boolean { return values.every((value, index) => index === 0 || value > (values[index - 1] as number)) } function appendVarint(bytes: number[], value: bigint): void { let remaining = value while (remaining >= 0x80n) { bytes.push(Number(remaining & 0x7fn) | 0x80) remaining >>= 7n } bytes.push(Number(remaining)) } function decodeSourceEventSeqs(bytes: Uint8Array, maxEntries: number): number[] { if (bytes.length === 0) return [] if (bytes.length === 1) { throw new Error('malformed source_event_seqs storage value: truncated tagged payload') } switch (bytes[0]) { case DELTA_TAG: return decodeDeltaVarints(bytes, 1) case RUN_TAG: return decodeRunVarints(bytes, 1, maxEntries) default: throw new Error('malformed source_event_seqs storage value: unknown encoding tag') } } function decodeDeltaVarints(bytes: Uint8Array, offset: number): number[] { const values: number[] = [] let previous = 0n while (offset < bytes.length) { const first = values.length === 0 const decoded = readVarint(bytes, offset, first ? MAX_SAFE_INTEGER : MAX_ZIGZAG_INTEGER) offset = decoded.offset const delta = first ? decoded.value : (decoded.value & 1n) === 0n ? decoded.value / 2n : -((decoded.value + 1n) / 2n) const value = first ? delta : previous + delta if (value < 0n || value > MAX_SAFE_INTEGER) { throw new Error('malformed source_event_seqs storage value: decoded seq is out of range') } values.push(Number(value)) previous = value } return values } function decodeRunVarints(bytes: Uint8Array, offset: number, maxEntries: number): number[] { const values: number[] = [] let previousEnd = -1 while (offset < bytes.length) { const start = readVarint(bytes, offset, MAX_SAFE_INTEGER) const count = readVarint(bytes, start.offset, MAX_SAFE_INTEGER) offset = count.offset const first = Number(start.value) const length = Number(count.value) if (length < 1) { throw new Error('malformed source_event_seqs storage value: run count must be positive') } if (first <= previousEnd || !Number.isSafeInteger(first + length - 1)) { throw new Error('malformed source_event_seqs storage value: runs must ascend within safe integers') } if (length > maxEntries - values.length) { throw new Error('malformed source_event_seqs storage value: run exceeds its event sequence') } for (let index = 0; index < length; index += 1) values.push(first + index) previousEnd = first + length - 1 } return values } function readVarint( bytes: Uint8Array, offset: number, limit: bigint, ): { readonly value: bigint; readonly offset: number } { let value = 0n let shift = 0n while (offset < bytes.length) { const byte = bytes[offset] as number offset += 1 value |= BigInt(byte & 0x7f) << shift if ((byte & 0x80) === 0) { if (shift > 0n && (byte & 0x7f) === 0) { throw new Error('malformed source_event_seqs storage value: non-canonical varint') } if (value > limit) { throw new Error('malformed source_event_seqs storage value: varint is out of range') } return { value, offset } } shift += 7n if (shift > 56n) { throw new Error('malformed source_event_seqs storage value: varint is out of range') } } throw new Error('malformed source_event_seqs storage value: truncated varint') } function isChunkRow(record: StorageRecord): record is ChunkRow { return isChunkTag(record.type) && 'seq0' in record && !('seq' in record) } function decodeScalarRow(row: EventRow): SessionEvent { const surfaceFields = { ...row.source_event_seqs === null ? {} : { sourceEventSeqs: decodeSourceEventSeqs(row.source_event_seqs, row.seq) }, ...row.surface_op === null ? {} : { surfaceOp: JSON.parse(row.surface_op) as SessionEvent['surfaceOp'] }, } return { type: row.type as SessionEvent['type'], seq: row.seq, time: row.time, data: JSON.parse(decodeData(row.data)) as SessionEvent['data'], ...surfaceFields, } as SessionEvent } /** * Validate and flatten physical rows into their logical prefix. A malformed * row or logical gap is committed corruption when a later valid turn end * exists; otherwise it starts a removable physical tail. * @param rows - physical rows ordered by their first logical sequence. * @param base - logical sequence expected from the first selected row. * @returns the contiguous logical prefix and optional physical deletion base. */ export function scanRows( rows: readonly EventRow[], base = 0, ): { preserved: SessionEvent[]; tornFrom?: number } { let lastTurnEndRow = -1 for (let index = rows.length - 1; index >= 0; index -= 1) { try { if (decodeRow(rows[index] as EventRow).some(event => event.type === 'turn/end')) { lastTurnEndRow = index break } } catch { // A malformed row cannot prove that an earlier physical prefix committed. } } const preserved: SessionEvent[] = [] let expected = base for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) { const physical = rows[rowIndex] as EventRow let logicalEvents: SessionEvent[] | undefined try { logicalEvents = decodeRow(physical) } catch { // The committed-prefix rule below owns whether this invalid row is fatal or repairable. } if (logicalEvents === undefined) { if (rowIndex <= lastTurnEndRow) { throw new Error(`corrupt session log: invalid committed physical row at seq ${physical.seq}`) } return { preserved, tornFrom: physical.seq } } let contiguous = true for (const event of logicalEvents) { if (event.seq !== expected) { contiguous = false break } expected += 1 } if (!contiguous) { if (rowIndex <= lastTurnEndRow) { throw new Error(`corrupt session log: invalid committed physical row at seq ${physical.seq}`) } return { preserved, tornFrom: physical.seq } } preserved.push(...logicalEvents) } return { preserved } }