dsh-lcx-codex 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,70 @@
1
+ // @ts-check
2
+
1
3
  import { baseURLFingerprint, routeCompatible } from './route.js'
2
4
  import { persistNativeImageReferences } from './dsh-responses.js'
5
+ import { estimateBudgetItem, portableBudgetError, portableTokenCeiling } from './token-budget.js'
6
+
7
+ /** @typedef {Record<string, unknown>} UnknownRecord */
8
+ /** @typedef {{ type: 'compaction', encrypted_content: string }} CompactionItem */
9
+ /** @typedef {{ role: 'developer' | 'user' | 'system', content?: string | unknown[] }} RetainedClientItem */
10
+ /** @typedef {{ type: string } | RetainedClientItem} NativeOutputItem */
11
+ /** @typedef {{ provider: string, model: string, baseURL: string, sessionId: string }} RouteIdentity */
12
+ /** @typedef {{ type?: string, text?: string, id?: unknown, name?: unknown, toolCallId?: unknown, content?: DshContentBlock[] }} DshContentBlock */
13
+ /** @typedef {{ kind?: string, plugin?: string, compactionId?: string }} DshMessageSource */
14
+ /** @typedef {{ role?: string, content?: DshContentBlock[], source?: DshMessageSource }} DshMessage */
15
+ /**
16
+ * @typedef {DshMessage & {
17
+ * compactionId?: string,
18
+ * rawOutput?: unknown[],
19
+ * shadowedSeqs?: number[],
20
+ * message?: DshMessage
21
+ * }} SessionEventData
22
+ */
23
+ /** @typedef {{ type: string, seq?: number, data?: SessionEventData }} SessionEvent */
24
+ /** @typedef {{ type: 'compaction/summary', seq?: number, data: SessionEventData & { compactionId: string, rawOutput?: unknown[], shadowedSeqs?: number[] } }} CompactionSummaryEvent */
25
+ /** @typedef {{ id?: string, events?: SessionEvent[], deriveEventMessage?: (event: SessionEvent) => DshMessage | null | undefined }} NativeSession */
26
+ /** @typedef {{ tokenBudget?: number, assistantTokenReserve?: number, assistantPerMessageTokenCap?: number }} RetentionOptions */
27
+ /** @typedef {{ item: unknown, index: number }} IndexedItem */
28
+ /** @typedef {{ index: number, item: unknown, tokens: number }} SelectedItem */
29
+ /** @typedef {{ selected: SelectedItem[], used: number }} Selection */
30
+ /** @typedef {{ items: unknown[], clientCount: number, assistantCount: number, estimatedTokens: number, clientEstimatedTokens: number, assistantEstimatedTokens: number }} RetentionPlan */
31
+ /**
32
+ * @typedef {object} NativeCheckpointBase
33
+ * @property {string} compactionId
34
+ * @property {string} provider
35
+ * @property {string} model
36
+ * @property {string} baseURLFingerprint
37
+ * @property {string} sourceSessionId
38
+ * @property {NativeOutputItem[]} nativeOutput
39
+ * @property {CompactionItem} [nativeCompaction]
40
+ * @property {number} [retainedInputCount]
41
+ * @property {number} [retainedClientCount]
42
+ * @property {number} [retainedAssistantCount]
43
+ * @property {number} [retainedEstimatedTokens]
44
+ * @property {number} [createdAt]
45
+ */
46
+ /** @typedef {NativeCheckpointBase & { type: 'lcx-native-compaction-v5', version: 5, retentionPolicy?: 'conversation-fidelity-v1' }} NativeCheckpointV5 */
47
+ /** @typedef {NativeCheckpointBase & { type: 'lcx-native-compaction-v4', version: 4 }} NativeCheckpointV4 */
48
+ /** @typedef {NativeCheckpointV5 | NativeCheckpointV4} NativeCheckpointBlock */
49
+ /**
50
+ * Minimal validated candidate shape. DSH rawOutput is unknown until the existing
51
+ * version, field, count, and compaction validation below has completed.
52
+ * @typedef {object} NativeCheckpointCandidate
53
+ * @property {typeof NATIVE_BLOCK_TYPE | typeof LEGACY_V4_BLOCK_TYPE} type
54
+ * @property {typeof NATIVE_BLOCK_VERSION | typeof LEGACY_V4_BLOCK_VERSION} version
55
+ * @property {unknown} [compactionId]
56
+ * @property {unknown} [nativeOutput]
57
+ * @property {unknown} [provider]
58
+ * @property {unknown} [model]
59
+ * @property {unknown} [baseURLFingerprint]
60
+ * @property {unknown} [sourceSessionId]
61
+ * @property {unknown} [retainedInputCount]
62
+ * @property {unknown} [retainedClientCount]
63
+ * @property {unknown} [retainedAssistantCount]
64
+ */
65
+ /** @typedef {{ compaction: CompactionItem }} NativeCompactionResult */
66
+ /** @typedef {{ session: NativeSession, route: RouteIdentity, result: NativeCompactionResult, input?: unknown[], imageMap?: unknown, retentionOptions?: RetentionOptions }} CreateCheckpointOptions */
67
+ /** @typedef {Error & { code?: string }} LcxError */
3
68
 
4
69
  export const NATIVE_BLOCK_TYPE = 'lcx-native-compaction-v5'
5
70
  export const NATIVE_BLOCK_VERSION = 5
@@ -11,37 +76,89 @@ export const RETAINED_MESSAGE_TOKEN_BUDGET = 64_000
11
76
  export const ASSISTANT_RETENTION_TOKEN_RESERVE = 24_000
12
77
  export const ASSISTANT_RETENTION_PER_MESSAGE_TOKEN_CAP = 3_000
13
78
 
79
+ /**
80
+ * @param {unknown} value
81
+ * @returns {value is UnknownRecord}
82
+ */
14
83
  function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) }
84
+ /** @param {DshMessage | null | undefined} message */
15
85
  function textOf(message) { return (message?.content ?? []).filter((b) => b?.type === 'text').map((b) => b.text).join('') }
16
- function estimatedItemTokens(item) { try { return Math.max(1, Math.ceil(JSON.stringify(item).length / 4)) } catch { return RETAINED_MESSAGE_TOKEN_BUDGET + 1 } }
86
+ /** @param {unknown} item */
87
+ function estimatedItemTokens(item) { return estimateBudgetItem(item) }
88
+ /**
89
+ * @param {unknown} item
90
+ * @returns {item is UnknownRecord}
91
+ */
17
92
  function isRetainedClientItem(item) { return isObject(item) && (item.type === undefined || item.type === 'message') && ['user', 'developer', 'system'].includes(String(item.role ?? '')) }
18
- function assistantTextParts(item) { if (!isObject(item) || item.type !== 'message' || item.role !== 'assistant' || !Array.isArray(item.content)) return []; return item.content.filter((part) => part?.type === 'output_text' && typeof part.text === 'string' && part.text.length > 0) }
93
+ /**
94
+ * @param {unknown} item
95
+ * @returns {{ type: 'output_text', text: string }[]}
96
+ */
97
+ function assistantTextParts(item) { if (!isObject(item) || item.type !== 'message' || item.role !== 'assistant' || !Array.isArray(item.content)) return []; return /** @type {{ type: 'output_text', text: string }[]} */ (item.content.filter((part) => /** @type {UnknownRecord | undefined} */ (part)?.type === 'output_text' && typeof /** @type {UnknownRecord} */ (part).text === 'string' && /** @type {string} */ (/** @type {UnknownRecord} */ (part).text).length > 0)) }
98
+ /** @param {unknown} item */
19
99
  function isRetainedAssistantItem(item) { return assistantTextParts(item).length > 0 }
100
+ /** @param {unknown} item */
20
101
  function assistantText(item) { return assistantTextParts(item).map((part) => part.text).join('') }
21
102
 
103
+ /**
104
+ * @param {unknown} item
105
+ * @param {number} [maxTokens]
106
+ * @returns {UnknownRecord | undefined}
107
+ */
22
108
  function truncateVisibleAssistantItem(item, maxTokens = ASSISTANT_RETENTION_PER_MESSAGE_TOKEN_CAP) {
23
109
  const text = assistantText(item); if (!text) return undefined
24
- const maxChars = Math.max(256, maxTokens * 4); let retained = text
25
- if (text.length > maxChars) { const marker = '\n…[LCX retained answer truncated]…\n'; const available = Math.max(0, maxChars - marker.length); const head = Math.floor(available * 0.72); const tail = available - head; retained = `${text.slice(0, head)}${marker}${text.slice(Math.max(head, text.length - tail))}` }
26
- return { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: retained }] }
110
+ /** @param {string} retained */
111
+ const candidate = (retained) => ({ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: retained }] })
112
+ const full = candidate(text)
113
+ if ((estimatedItemTokens(full) ?? Infinity) <= maxTokens) return full
114
+ const marker = '\n…[LCX retained answer truncated]…\n'
115
+ /** @param {number} count */
116
+ const shortened = (count) => {
117
+ const available = Math.max(0, count - marker.length)
118
+ const head = Math.floor(available * 0.72); const tail = available - head
119
+ return candidate(`${text.slice(0, head)}${marker}${text.slice(Math.max(head, text.length - tail))}`)
120
+ }
121
+ let low = 0; let high = text.length; let best
122
+ while (low <= high) {
123
+ const count = Math.floor((low + high) / 2); const next = shortened(count)
124
+ if ((estimatedItemTokens(next) ?? Infinity) <= maxTokens) { best = next; low = count + 1 } else high = count - 1
125
+ }
126
+ return best
27
127
  }
28
128
 
129
+ /**
130
+ * @param {IndexedItem[]} candidates
131
+ * @param {number} budget
132
+ * @param {(value: IndexedItem) => unknown} [transform]
133
+ * @returns {Selection}
134
+ */
29
135
  function selectNewest(candidates, budget, transform = (value) => structuredClone(value.item)) {
136
+ /** @type {SelectedItem[]} */
30
137
  const selected = []; let used = 0
31
- for (let index = candidates.length - 1; index >= 0; index -= 1) { const candidate = candidates[index]; const item = transform(candidate); if (!item) continue; const tokens = estimatedItemTokens(item); if (tokens > budget || used + tokens > budget) continue; selected.push({ index: candidate.index, item, tokens }); used += tokens }
138
+ for (let index = candidates.length - 1; index >= 0; index -= 1) { const candidate = candidates[index]; const item = transform(candidate); if (!item) continue; const tokens = estimatedItemTokens(item); if (tokens === undefined || tokens > budget || used + tokens > budget) continue; selected.push({ index: candidate.index, item, tokens }); used += tokens }
32
139
  return { selected, used }
33
140
  }
34
141
 
142
+ /**
143
+ * @param {unknown[] | null | undefined} input
144
+ * @param {RetentionOptions} [options]
145
+ * @returns {unknown[]}
146
+ */
35
147
  export function retainedCompactionInput(input, options = {}) {
36
- const budget = Number.isSafeInteger(options.tokenBudget) && options.tokenBudget > 0 ? options.tokenBudget : RETAINED_MESSAGE_TOKEN_BUDGET
148
+ const budget = Number.isSafeInteger(options.tokenBudget) && /** @type {number} */ (options.tokenBudget) > 0 ? /** @type {number} */ (options.tokenBudget) : RETAINED_MESSAGE_TOKEN_BUDGET
37
149
  const candidates = (input ?? []).map((item, index) => ({ item, index })).filter(({ item }) => isRetainedClientItem(item))
38
150
  return selectNewest(candidates, budget).selected.sort((a, b) => a.index - b.index).map(({ item }) => item)
39
151
  }
40
152
 
153
+ /**
154
+ * @param {unknown[] | null | undefined} input
155
+ * @param {RetentionOptions} [options]
156
+ * @returns {RetentionPlan}
157
+ */
41
158
  export function retainedConversationPlan(input, options = {}) {
42
- const totalBudget = Number.isSafeInteger(options.tokenBudget) && options.tokenBudget > 0 ? options.tokenBudget : RETAINED_MESSAGE_TOKEN_BUDGET
43
- const assistantReserve = Math.min(totalBudget, Number.isSafeInteger(options.assistantTokenReserve) && options.assistantTokenReserve >= 0 ? options.assistantTokenReserve : ASSISTANT_RETENTION_TOKEN_RESERVE)
44
- const perMessageCap = Number.isSafeInteger(options.assistantPerMessageTokenCap) && options.assistantPerMessageTokenCap > 0 ? options.assistantPerMessageTokenCap : ASSISTANT_RETENTION_PER_MESSAGE_TOKEN_CAP
159
+ const totalBudget = Number.isSafeInteger(options.tokenBudget) && /** @type {number} */ (options.tokenBudget) > 0 ? /** @type {number} */ (options.tokenBudget) : RETAINED_MESSAGE_TOKEN_BUDGET
160
+ const assistantReserve = Math.min(totalBudget, Number.isSafeInteger(options.assistantTokenReserve) && /** @type {number} */ (options.assistantTokenReserve) >= 0 ? /** @type {number} */ (options.assistantTokenReserve) : ASSISTANT_RETENTION_TOKEN_RESERVE)
161
+ const perMessageCap = Number.isSafeInteger(options.assistantPerMessageTokenCap) && /** @type {number} */ (options.assistantPerMessageTokenCap) > 0 ? /** @type {number} */ (options.assistantPerMessageTokenCap) : ASSISTANT_RETENTION_PER_MESSAGE_TOKEN_CAP
45
162
  const indexed = (input ?? []).map((item, index) => ({ item, index }))
46
163
  const assistant = selectNewest(indexed.filter(({ item }) => isRetainedAssistantItem(item)), assistantReserve, ({ item }) => truncateVisibleAssistantItem(item, perMessageCap))
47
164
  const remaining = Math.max(0, totalBudget - assistant.used)
@@ -49,45 +166,147 @@ export function retainedConversationPlan(input, options = {}) {
49
166
  const selected = [...client.selected, ...assistant.selected].sort((a, b) => a.index - b.index)
50
167
  return { items: selected.map(({ item }) => item), clientCount: client.selected.length, assistantCount: assistant.selected.length, estimatedTokens: client.used + assistant.used, clientEstimatedTokens: client.used, assistantEstimatedTokens: assistant.used }
51
168
  }
169
+ /**
170
+ * @param {unknown[] | null | undefined} input
171
+ * @param {RetentionOptions} [options]
172
+ */
52
173
  export function retainedConversationInput(input, options = {}) { return retainedConversationPlan(input, options).items }
174
+ /** @param {unknown[] | null | undefined} items */
53
175
  export function hasRetainedCompactionInput(items) { return (items ?? []).some((item) => isRetainedClientItem(item) || isRetainedAssistantItem(item)) }
176
+ /** @param {DshMessage | null | undefined} message */
54
177
  export function compactCheckpointId(message) { const source = message?.source; return source?.kind === 'plugin' && source?.plugin === 'compact' && typeof source.compactionId === 'string' && source.compactionId ? source.compactionId : undefined }
178
+ /** @param {DshMessage | null | undefined} message */
55
179
  export function legacyCheckpointId(message) { return textOf(message).match(LEGACY_V3_PATTERN)?.[1]?.toLowerCase() }
56
- export function activeCompactionId(session) { if (!session?.events) return undefined; const ended = new Set(); for (let index = session.events.length - 1; index >= 0; index -= 1) { const event = session.events[index]; if (event.type === 'compaction/end' && event.data?.compactionId) ended.add(event.data.compactionId); if (event.type === 'compaction/start' && event.data?.compactionId && !ended.has(event.data.compactionId)) return event.data.compactionId } return undefined }
180
+ /** @param {NativeSession | null | undefined} session */
181
+ export function activeCompactionId(session) { if (!session?.events) return undefined; /** @type {Set<string>} */ const ended = new Set(); for (let index = session.events.length - 1; index >= 0; index -= 1) { const event = session.events[index]; if (event.type === 'compaction/end' && event.data?.compactionId) ended.add(event.data.compactionId); if (event.type === 'compaction/start' && event.data?.compactionId && !ended.has(event.data.compactionId)) return event.data.compactionId } return undefined }
57
182
 
183
+ /**
184
+ * @param {CreateCheckpointOptions} options
185
+ * @returns {NativeCheckpointV5}
186
+ */
58
187
  export function createNativeCheckpointBlock({ session, route, result, input = [], imageMap, retentionOptions = {} }) {
59
188
  const compactionId = activeCompactionId(session)
60
- if (!compactionId) { const error = new Error('Native compaction could not correlate the active DSH compaction transaction'); error.code = 'LCX_COMPACTION_ID_UNAVAILABLE'; throw error }
189
+ if (!compactionId) {
190
+ /** @type {LcxError} */
191
+ const error = new Error('Native compaction could not correlate the active DSH compaction transaction')
192
+ error.code = 'LCX_COMPACTION_ID_UNAVAILABLE'
193
+ throw error
194
+ }
61
195
  const retention = retainedConversationPlan(input, retentionOptions)
196
+ /** @type {NativeOutputItem[]} */
62
197
  const nativeOutput = persistNativeImageReferences([...retention.items, structuredClone(result.compaction)], imageMap)
63
198
  return { type: NATIVE_BLOCK_TYPE, version: NATIVE_BLOCK_VERSION, retentionPolicy: 'conversation-fidelity-v1', compactionId, provider: route.provider, model: route.model, baseURLFingerprint: baseURLFingerprint(route.baseURL), sourceSessionId: route.sessionId, nativeOutput, nativeCompaction: structuredClone(result.compaction), retainedInputCount: retention.items.length, retainedClientCount: retention.clientCount, retainedAssistantCount: retention.assistantCount, retainedEstimatedTokens: retention.estimatedTokens, createdAt: Date.now() }
64
199
  }
65
200
 
201
+ /**
202
+ * @param {NativeCheckpointBlock} block
203
+ * @param {unknown} usage
204
+ * @returns {UnknownRecord[]}
205
+ */
66
206
  export function nativeCheckpointChunks(block, usage) {
67
207
  const text = 'LCX Native V2 checkpoint saved in the DSH session log.'
68
208
  return [{ type: 'block-start', index: 0, blockType: 'text' }, { type: 'text-delta', index: 0, text }, { type: 'block-end', index: 0, block: { type: 'text', text } }, { type: 'block-start', index: 1, blockType: NATIVE_BLOCK_TYPE }, { type: 'block-end', index: 1, block: structuredClone(block) }, ...(usage ? [{ type: 'usage', usage: structuredClone(usage) }] : []), { type: 'finish', reason: { kind: 'stop' } }]
69
209
  }
70
210
 
71
- export function compactionSummaryEvent(session, compactionId) { if (!session?.events || !compactionId) return undefined; for (let index = session.events.length - 1; index >= 0; index -= 1) { const event = session.events[index]; if (event.type === 'compaction/summary' && event.data?.compactionId === compactionId) return event } return undefined }
72
- function nativeBlockFromSummary(event) { const raw = event?.data?.rawOutput ?? []; return raw.find((value) => value?.type === NATIVE_BLOCK_TYPE && value?.version === NATIVE_BLOCK_VERSION) ?? raw.find((value) => value?.type === LEGACY_V4_BLOCK_TYPE && value?.version === LEGACY_V4_BLOCK_VERSION) }
73
- function validateCount(value) { return value === undefined || (Number.isSafeInteger(value) && value >= 0) }
211
+ /**
212
+ * @param {NativeSession | null | undefined} session
213
+ * @param {string | null | undefined} compactionId
214
+ * @returns {CompactionSummaryEvent | undefined}
215
+ */
216
+ export function compactionSummaryEvent(session, compactionId) { if (!session?.events || !compactionId) return undefined; for (let index = session.events.length - 1; index >= 0; index -= 1) { const event = session.events[index]; if (event.type === 'compaction/summary' && event.data?.compactionId === compactionId) return /** @type {CompactionSummaryEvent} */ (event) } return undefined }
217
+ /**
218
+ * rawOutput elements remain untrusted until stateFromSummaryEvent validates them.
219
+ * @param {CompactionSummaryEvent | null | undefined} event
220
+ * @returns {NativeCheckpointCandidate | undefined}
221
+ */
222
+ function nativeBlockFromSummary(event) { const raw = event?.data?.rawOutput ?? []; return /** @type {NativeCheckpointCandidate | undefined} */ (raw.find((value) => /** @type {UnknownRecord | undefined} */ (value)?.type === NATIVE_BLOCK_TYPE && /** @type {UnknownRecord | undefined} */ (value)?.version === NATIVE_BLOCK_VERSION) ?? raw.find((value) => /** @type {UnknownRecord | undefined} */ (value)?.type === LEGACY_V4_BLOCK_TYPE && /** @type {UnknownRecord | undefined} */ (value)?.version === LEGACY_V4_BLOCK_VERSION)) }
223
+ /** @param {unknown} value */
224
+ function validateCount(value) { return value === undefined || (Number.isSafeInteger(value) && /** @type {number} */ (value) >= 0) }
225
+ /**
226
+ * @param {CompactionSummaryEvent | null | undefined} event
227
+ * @returns {NativeCheckpointBlock | undefined}
228
+ */
74
229
  export function stateFromSummaryEvent(event) {
75
230
  if (event?.type !== 'compaction/summary') return undefined
76
231
  const block = nativeBlockFromSummary(event); if (!isObject(block)) return undefined
77
232
  if (block.compactionId !== event.data.compactionId || !Array.isArray(block.nativeOutput) || block.nativeOutput.length === 0) return undefined
78
233
  if (typeof block.provider !== 'string' || typeof block.model !== 'string' || typeof block.baseURLFingerprint !== 'string' || typeof block.sourceSessionId !== 'string') return undefined
79
234
  if (!validateCount(block.retainedInputCount) || !validateCount(block.retainedClientCount) || !validateCount(block.retainedAssistantCount)) return undefined
80
- const compactions = block.nativeOutput.filter((item) => item?.type === 'compaction'); if (compactions.length !== 1 || typeof compactions[0]?.encrypted_content !== 'string') return undefined
81
- const compactionIndex = block.nativeOutput.findIndex((item) => item?.type === 'compaction'); const prefix = compactionIndex >= 0 ? block.nativeOutput.slice(0, compactionIndex) : []
235
+ const compactions = block.nativeOutput.filter((item) => /** @type {UnknownRecord | undefined} */ (item)?.type === 'compaction'); if (compactions.length !== 1 || typeof /** @type {UnknownRecord | undefined} */ (compactions[0])?.encrypted_content !== 'string') return undefined
236
+ const compactionIndex = block.nativeOutput.findIndex((item) => /** @type {UnknownRecord | undefined} */ (item)?.type === 'compaction'); const prefix = compactionIndex >= 0 ? block.nativeOutput.slice(0, compactionIndex) : []
82
237
  if (block.version === NATIVE_BLOCK_VERSION) { const clients = prefix.filter(isRetainedClientItem).length; const assistants = prefix.filter(isRetainedAssistantItem).length; const total = clients + assistants; if (block.retainedInputCount !== undefined && block.retainedInputCount !== total) return undefined; if (block.retainedClientCount !== undefined && block.retainedClientCount !== clients) return undefined; if (block.retainedAssistantCount !== undefined && block.retainedAssistantCount !== assistants) return undefined } else { const clients = prefix.filter(isRetainedClientItem).length; if (block.retainedInputCount !== undefined && block.retainedInputCount !== clients) return undefined }
83
- return structuredClone(block)
238
+ return /** @type {NativeCheckpointBlock} */ (structuredClone(block))
84
239
  }
240
+ /**
241
+ * @param {NativeSession} session
242
+ * @param {DshMessage} message
243
+ */
85
244
  export function checkpointStateForMessage(session, message) { const id = compactCheckpointId(message); return id ? stateFromSummaryEvent(compactionSummaryEvent(session, id)) : undefined }
245
+ /**
246
+ * @param {NativeCheckpointBlock} state
247
+ * @param {RouteIdentity} route
248
+ * @param {unknown} ctx
249
+ */
86
250
  export function stateRouteCompatible(state, route, ctx) { return routeCompatible(state, route, ctx) }
87
- function eventMessage(session, seq) { const events = session?.events ?? []; const event = events[seq]?.seq === seq ? events[seq] : events.find((candidate) => candidate?.seq === seq); if (!event) return undefined; if (typeof session.deriveEventMessage === 'function') return session.deriveEventMessage(event) ?? undefined; if (event.type === 'user/message') return event.data; if (event.type === 'assistant/message') return event.data?.message; if (event.type === 'tool/result') return event.data?.message; return undefined }
88
- function estimateChars(message) { try { return JSON.stringify(message).length } catch { return 0 } }
251
+ /**
252
+ * @param {NativeSession | null | undefined} session
253
+ * @param {number} seq
254
+ * @returns {DshMessage | undefined}
255
+ */
256
+ function eventMessage(session, seq) { const events = session?.events ?? []; const event = events[seq]?.seq === seq ? events[seq] : events.find((candidate) => candidate?.seq === seq); if (!event) return undefined; if (typeof /** @type {NativeSession} */ (session).deriveEventMessage === 'function') return /** @type {(event: SessionEvent) => DshMessage | null | undefined} */ (/** @type {NativeSession} */ (session).deriveEventMessage)(event) ?? undefined; if (event.type === 'user/message') return event.data; if (event.type === 'assistant/message') return event.data?.message; if (event.type === 'tool/result') return event.data?.message; return undefined }
257
+ /** @param {DshMessage} message */
258
+ function estimateChars(message) { try { const encoded = JSON.stringify(message); return typeof encoded === 'string' ? encoded.length : undefined } catch { return undefined } }
259
+ /**
260
+ * @param {NativeSession} session
261
+ * @param {string} compactionId
262
+ * @param {Set<string>} visited
263
+ * @param {number} depth
264
+ * @returns {DshMessage[]}
265
+ */
89
266
  function expandedCheckpointMessages(session, compactionId, visited, depth) { if (depth > 16 || visited.has(compactionId)) return []; visited.add(compactionId); const summary = compactionSummaryEvent(session, compactionId); if (!summary) return []; const result = []; for (const seq of summary.data?.shadowedSeqs ?? []) { const message = eventMessage(session, seq); if (!message) continue; const nested = compactCheckpointId(message); if (nested) result.push(...expandedCheckpointMessages(session, nested, visited, depth + 1)); else result.push(structuredClone(message)) } return result }
90
- export function shadowedMessagesForCheckpoint(session, compactionId) { return expandedCheckpointMessages(session, compactionId, new Set(), 0) }
267
+ /**
268
+ * @param {NativeSession} session
269
+ * @param {string} compactionId
270
+ * @returns {DshMessage[]}
271
+ */
272
+ export function shadowedMessagesForCheckpoint(session, compactionId) { return expandedCheckpointMessages(session, compactionId, /** @type {Set<string>} */ (new Set()), 0) }
273
+ /**
274
+ * @param {DshMessage[]} messages
275
+ * @returns {DshMessage[][]}
276
+ */
91
277
  function groupMessages(messages) { const groups = []; for (let index = 0; index < messages.length; index += 1) { const message = messages[index]; const toolCalls = message?.role === 'assistant' ? (message.content ?? []).filter((b) => b?.type === 'tool-call').map((b) => String(b.id)) : []; if (toolCalls.length === 0) { groups.push([message]); continue } const group = [message]; const pending = new Set(toolCalls); let cursor = index + 1; while (cursor < messages.length && pending.size > 0) { const next = messages[cursor]; const results = next?.role === 'user' ? (next.content ?? []).filter((b) => b?.type === 'tool-result').map((b) => String(b.toolCallId)) : []; if (results.length === 0) break; group.push(next); for (const id of results) pending.delete(id); cursor += 1 } if (pending.size === 0) index = cursor - 1; groups.push(group) } return groups }
92
- export function portableMessagesForCheckpoint(session, compactionId, options = {}) { const maxChars = Number.isSafeInteger(options.maxChars) && options.maxChars > 0 ? options.maxChars : 80_000; const expanded = shadowedMessagesForCheckpoint(session, compactionId); if (expanded.length === 0) return []; const groups = groupMessages(expanded); const kept = []; let chars = 0; for (let index = groups.length - 1; index >= 0; index -= 1) { const group = groups[index]; const size = group.reduce((sum, message) => sum + estimateChars(message), 0); if (kept.length > 0 && chars + size > maxChars) break; kept.unshift(...group); chars += size } return kept }
278
+ /**
279
+ * @param {NativeSession} session
280
+ * @param {string} compactionId
281
+ * @param {{ maxChars?: number }} [options]
282
+ * @returns {DshMessage[]}
283
+ */
284
+ export function portableMessagesForCheckpoint(session, compactionId, options = {}) {
285
+ const maxChars = Number.isSafeInteger(options.maxChars) && /** @type {number} */ (options.maxChars) > 0 ? /** @type {number} */ (options.maxChars) : 80_000
286
+ const maxTokens = portableTokenCeiling(maxChars) ?? 1
287
+ const expanded = shadowedMessagesForCheckpoint(session, compactionId); if (expanded.length === 0) return []
288
+ const groups = groupMessages(expanded); const kept = []; let chars = 0; let tokens = 0
289
+ for (let index = groups.length - 1; index >= 0; index -= 1) {
290
+ const group = groups[index]
291
+ let groupChars = 0; let groupTokens = 0; let budgetable = true
292
+ for (const message of group) {
293
+ const messageChars = estimateChars(message); const messageTokens = estimatedItemTokens(message)
294
+ if (messageChars === undefined || messageTokens === undefined) { budgetable = false; break }
295
+ groupChars += messageChars; groupTokens += messageTokens
296
+ }
297
+ const exceeds = !budgetable || chars + groupChars > maxChars || tokens + groupTokens > maxTokens
298
+ if (exceeds) {
299
+ if (kept.length === 0) throw portableBudgetError('Portable checkpoint newest message group exceeds the configured budget')
300
+ break
301
+ }
302
+ kept.unshift(...group); chars += groupChars; tokens += groupTokens
303
+ }
304
+ return kept
305
+ }
306
+ /**
307
+ * @param {DshMessage[]} messages
308
+ * @param {NativeSession} session
309
+ * @param {{ maxChars?: number }} [options]
310
+ * @returns {DshMessage[]}
311
+ */
93
312
  export function rewriteCheckpointsPortable(messages, session, options = {}) { const rewritten = []; let changed = false; for (const message of messages ?? []) { const id = compactCheckpointId(message); if (!id || !checkpointStateForMessage(session, message)) { rewritten.push(message); continue } const portable = portableMessagesForCheckpoint(session, id, options); if (portable.length === 0) { rewritten.push(message); continue } rewritten.push(...portable); changed = true } return changed ? rewritten : messages }