codec.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import {
  2. SessionFormatError,
  3. sessionFormatCount,
  4. sessionFormatSafeInteger,
  5. snapshotSessionFormatJson,
  6. } from '@deepseek-ai/dsh-session-format'
  7. import type {
  8. SessionFormatArtifactDecoder,
  9. SessionFormatCodec,
  10. SessionFormatCurrentEncoder,
  11. SessionFormatEvent,
  12. SessionFormatHeader,
  13. SessionFormatJsonObject,
  14. SessionFormatJsonValue,
  15. SessionFormatRecovery,
  16. } from '@deepseek-ai/dsh-session-format'
  17. import { assertReleasedV2Header } from './validation.ts'
  18. const HEADER_REQUIRED = ['type', 'version', 'id', 'createdAt', 'isSeeded', 'delegationDepth'] as const
  19. const HEADER_OPTIONAL = ['cwd', 'parentSession', 'origin', 'agentPreset'] as const
  20. const EVENT_REQUIRED = ['type', 'seq', 'time', 'data'] as const
  21. const EVENT_OPTIONAL = ['ignorable', 'sourceEventSeqs', 'surfaceOp'] as const
  22. const EVENT_KEYS: ReadonlySet<string> = new Set([...EVENT_REQUIRED, ...EVENT_OPTIONAL])
  23. /** Frozen physical JSON codec for released v2. */
  24. export const releasedV2SessionFormatCodec = Object.freeze({
  25. version: 2,
  26. decodeHeader(value: unknown) {
  27. return decodePhysicalHeader(value)
  28. },
  29. createDecoder(headerValue: unknown, recovery: SessionFormatRecovery) {
  30. return createDecoder(headerValue, recovery)
  31. },
  32. encodeHeader(header: SessionFormatHeader, inheritedEventCount: number) {
  33. return encodeHeader(header, inheritedEventCount)
  34. },
  35. encodeEvent(event: SessionFormatEvent) {
  36. return encodeProvenance(event)
  37. },
  38. } satisfies SessionFormatCodec & SessionFormatCurrentEncoder)
  39. function decodePhysicalHeader(value: unknown): SessionFormatHeader {
  40. const snapshot = snapshotSessionFormatJson(value, 'released v2 physical header')
  41. const record = jsonRecord(snapshot, 'released v2 physical header')
  42. exactKeys(record, HEADER_REQUIRED, HEADER_OPTIONAL, 'released v2 physical header')
  43. if (record['type'] !== 'session' || record['version'] !== 2) {
  44. throw new SessionFormatError('expected released v2 physical Session header')
  45. }
  46. if (typeof record['id'] !== 'string') throw new SessionFormatError('released v2 header id must be a string')
  47. const createdAt = sessionFormatCount(record['createdAt'], 'released v2 header createdAt')
  48. const delegationDepth = sessionFormatCount(record['delegationDepth'], 'released v2 header delegationDepth')
  49. if (typeof record['isSeeded'] !== 'boolean') throw new SessionFormatError('released v2 header isSeeded must be boolean')
  50. for (const key of ['cwd', 'parentSession', 'agentPreset'] as const) {
  51. if (record[key] !== undefined && typeof record[key] !== 'string') {
  52. throw new SessionFormatError(`released v2 header ${key} must be a string`)
  53. }
  54. }
  55. if (record['origin'] !== undefined && record['origin'] !== 'subagent') {
  56. throw new SessionFormatError('released v2 header origin must be "subagent"')
  57. }
  58. const header = snapshotSessionFormatJson({
  59. version: 2,
  60. id: record['id'],
  61. createdAt,
  62. ...(record['cwd'] === undefined ? {} : { cwd: record['cwd'] }),
  63. ...(record['parentSession'] === undefined ? {} : { parentSession: record['parentSession'] }),
  64. isSeeded: record['isSeeded'],
  65. ...(record['origin'] === undefined ? {} : { origin: record['origin'] }),
  66. delegationDepth,
  67. ...(record['agentPreset'] === undefined ? {} : { agentPreset: record['agentPreset'] }),
  68. }, 'released v2 logical header') as SessionFormatHeader
  69. assertReleasedV2Header(header)
  70. return header
  71. }
  72. function createDecoder(
  73. headerValue: unknown,
  74. recovery: SessionFormatRecovery,
  75. ): SessionFormatArtifactDecoder {
  76. const header = decodePhysicalHeader(headerValue)
  77. let rowIndex = 0
  78. let eventCount = 0
  79. let inheritedEventCount: number | undefined
  80. let issue: SessionFormatError | undefined
  81. return {
  82. header,
  83. decodeRow(value, context) {
  84. const currentRow = rowIndex
  85. rowIndex += 1
  86. let event: SessionFormatEvent
  87. try {
  88. event = decodeEvent(value, currentRow)
  89. } catch (error: unknown) {
  90. const current = error instanceof SessionFormatError
  91. ? error
  92. : new SessionFormatError(`released v2 row ${currentRow} is malformed`, { cause: error })
  93. if (recovery === 'strict') throw current
  94. issue ??= current
  95. return
  96. }
  97. if (issue !== undefined) {
  98. if (event.type === 'turn/end') throw issue
  99. return
  100. }
  101. if (event.seq !== eventCount) {
  102. const gap = new SessionFormatError(
  103. `released v2 row ${currentRow} has seq gap (expected ${eventCount}, got ${event.seq})`,
  104. )
  105. if (recovery === 'strict') throw gap
  106. issue = gap
  107. if (event.type === 'turn/end') throw issue
  108. return
  109. }
  110. eventCount += 1
  111. if (event.type === 'session/end-seed') {
  112. const data = jsonRecord(event.data, `session/end-seed ${event.seq} data`)
  113. if (data['inherited'] === true) inheritedEventCount = event.seq
  114. }
  115. context.emitEvent(event)
  116. },
  117. finish(_context) {
  118. if (header.isSeeded && inheritedEventCount === undefined) {
  119. throw new SessionFormatError('released v2 seeded Session lacks an inherited end-seed marker')
  120. }
  121. if (!header.isSeeded && inheritedEventCount !== undefined) {
  122. throw new SessionFormatError('released v2 unseeded Session contains an inherited end-seed marker')
  123. }
  124. return inheritedEventCount ?? 0
  125. },
  126. }
  127. }
  128. function decodeEvent(value: unknown, rowIndex: number): SessionFormatEvent {
  129. const record = jsonRecord(value as SessionFormatJsonValue, `released v2 row ${rowIndex}`)
  130. const missing = EVENT_REQUIRED.find(key => !Object.hasOwn(record, key))
  131. if (missing !== undefined) throw new SessionFormatError(`released v2 row ${rowIndex} lacks required field ${missing}`)
  132. const unexpected = Object.keys(record).find(key => !EVENT_KEYS.has(key))
  133. if (unexpected !== undefined) {
  134. throw new SessionFormatError(`released v2 row ${rowIndex} has unexpected field ${unexpected}`)
  135. }
  136. if (typeof record['type'] !== 'string') {
  137. throw new SessionFormatError(`released v2 row ${rowIndex} type must be a string`)
  138. }
  139. sessionFormatSafeInteger(record['time'], `released v2 row ${rowIndex} time`)
  140. if (record['ignorable'] !== undefined && record['ignorable'] !== true) {
  141. throw new SessionFormatError(`released v2 row ${rowIndex} ignorable must be true when present`)
  142. }
  143. if (record['sourceEventSeqs'] === undefined) return record as unknown as SessionFormatEvent
  144. const seq = sessionFormatCount(record['seq'], `released v2 row ${rowIndex} seq`)
  145. return {
  146. ...record,
  147. sourceEventSeqs: decodeSeqRanges(record['sourceEventSeqs'], seq),
  148. } as unknown as SessionFormatEvent
  149. }
  150. function encodeHeader(
  151. header: SessionFormatHeader,
  152. inheritedEventCount: number,
  153. ): SessionFormatJsonObject {
  154. assertReleasedV2Header(header)
  155. const cut = sessionFormatCount(inheritedEventCount, 'format v2 inherited event count')
  156. if (!header.isSeeded && cut !== 0) {
  157. throw new SessionFormatError('unseeded format v2 Session has inherited events')
  158. }
  159. return {
  160. type: 'session',
  161. version: 2,
  162. id: header.id,
  163. createdAt: header.createdAt,
  164. ...(header.cwd === undefined ? {} : { cwd: header.cwd }),
  165. ...(header.parentSession === undefined ? {} : { parentSession: header.parentSession }),
  166. isSeeded: header.isSeeded,
  167. ...(header.origin === undefined ? {} : { origin: header.origin }),
  168. delegationDepth: header.delegationDepth,
  169. ...(header.agentPreset === undefined ? {} : { agentPreset: header.agentPreset }),
  170. }
  171. }
  172. function encodeProvenance(event: SessionFormatEvent): SessionFormatJsonObject {
  173. if (event.sourceEventSeqs === undefined) return event
  174. return {
  175. ...event,
  176. sourceEventSeqs: encodeSeqRanges(event.sourceEventSeqs as readonly number[]),
  177. }
  178. }
  179. function decodeSeqRanges(value: SessionFormatJsonValue, maxEntries: number): readonly number[] {
  180. if (!Array.isArray(value)) throw new SessionFormatError('sourceEventSeqs must be an array')
  181. const output: number[] = []
  182. let hasRange = false
  183. for (const entry of value) {
  184. if (!Array.isArray(entry)) {
  185. output.push(sessionFormatCount(entry, 'sourceEventSeqs member'))
  186. continue
  187. }
  188. if (entry.length !== 2) throw new SessionFormatError('sourceEventSeqs range must be a [start, end] pair')
  189. const start = sessionFormatCount(entry[0], 'sourceEventSeqs range start')
  190. const end = sessionFormatCount(entry[1], 'sourceEventSeqs range end')
  191. if (start > end || end >= maxEntries || end - start + 1 > maxEntries - output.length) {
  192. throw new SessionFormatError('sourceEventSeqs range exceeds its event seq')
  193. }
  194. for (let current = start; current <= end; current += 1) output.push(current)
  195. hasRange = true
  196. }
  197. const seen = new Set<number>()
  198. for (const source of output) {
  199. if (source >= maxEntries || seen.has(source)) {
  200. throw new SessionFormatError('sourceEventSeqs ranges must contain unique earlier seqs')
  201. }
  202. seen.add(source)
  203. }
  204. if (hasRange && output.some((source, index) => index > 0 && source <= (output[index - 1] as number))) {
  205. throw new SessionFormatError('sourceEventSeqs ranges must be strictly increasing')
  206. }
  207. return output
  208. }
  209. function encodeSeqRanges(values: readonly number[]): readonly SessionFormatJsonValue[] {
  210. if (values.some((value, index) => index > 0 && value <= (values[index - 1] as number))) return [...values]
  211. const output: SessionFormatJsonValue[] = []
  212. for (let index = 0; index < values.length;) {
  213. const start = values[index] as number
  214. let end = start
  215. while (index + 1 < values.length && values[index + 1] === end + 1) {
  216. index += 1
  217. end += 1
  218. }
  219. output.push(end - start >= 2 ? [start, end] : start)
  220. if (end - start === 1) output.push(end)
  221. index += 1
  222. }
  223. return output
  224. }
  225. function jsonRecord(value: SessionFormatJsonValue | undefined, label: string): SessionFormatJsonObject {
  226. if (typeof value !== 'object' || value === null || Array.isArray(value)) {
  227. throw new SessionFormatError(`${label} must be an object`)
  228. }
  229. return value as SessionFormatJsonObject
  230. }
  231. function exactKeys(
  232. record: SessionFormatJsonObject,
  233. required: readonly string[],
  234. optional: readonly string[],
  235. label: string,
  236. ): void {
  237. const allowed = new Set([...required, ...optional])
  238. const missing = required.find(key => !Object.hasOwn(record, key))
  239. if (missing !== undefined) throw new SessionFormatError(`${label} lacks ${missing}`)
  240. const unexpected = Object.keys(record).find(key => !allowed.has(key))
  241. if (unexpected !== undefined) throw new SessionFormatError(`${label} has unexpected field ${unexpected}`)
  242. }