dsh-lcx-codex 0.4.2 → 0.4.3-pre.2

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.
Files changed (47) hide show
  1. package/README.md +116 -202
  2. package/cordis.patch.yml +3 -20
  3. package/lib/client.js +273 -146
  4. package/lib/compact-v2.js +218 -199
  5. package/lib/dsh-compat.js +227 -100
  6. package/lib/dsh-responses.js +445 -277
  7. package/lib/index.js +851 -757
  8. package/lib/json-store.js +50 -31
  9. package/lib/native-checkpoint.js +520 -194
  10. package/lib/responses-request.js +106 -121
  11. package/lib/responses-stream.js +972 -443
  12. package/lib/route.js +229 -355
  13. package/lib/service-mutex.js +73 -64
  14. package/lib/token-budget.js +176 -108
  15. package/lib/transport.js +277 -67
  16. package/lib/types/client/index.d.ts +6 -0
  17. package/lib/types/compact-v2.d.ts +104 -0
  18. package/lib/types/dsh-compat.d.ts +78 -0
  19. package/lib/types/dsh-responses.d.ts +82 -0
  20. package/lib/types/index.d.ts +83 -0
  21. package/lib/types/json-store.d.ts +10 -0
  22. package/lib/types/native-checkpoint.d.ts +213 -0
  23. package/lib/types/responses-request.d.ts +58 -0
  24. package/lib/types/responses-stream.d.ts +51 -0
  25. package/lib/types/route.d.ts +132 -0
  26. package/lib/types/service-mutex.d.ts +14 -0
  27. package/lib/types/token-budget.d.ts +50 -0
  28. package/lib/types/transport.d.ts +20 -0
  29. package/lib/types/web-run-output.d.ts +29 -0
  30. package/lib/types/web-search-alpha.d.ts +286 -0
  31. package/lib/types/web-search-capability.d.ts +26 -0
  32. package/lib/types/web-search-hosted.d.ts +246 -0
  33. package/lib/types/web-search-ref-store.d.ts +22 -0
  34. package/lib/web-run-output.js +167 -18
  35. package/lib/web-search-alpha.js +865 -163
  36. package/lib/web-search-capability.js +55 -65
  37. package/lib/web-search-hosted.js +210 -32
  38. package/lib/web-search-ref-store.js +63 -59
  39. package/package.json +79 -27
  40. package/ARCHITECTURE.md +0 -117
  41. package/CHANGELOG.md +0 -224
  42. package/README_EN.md +0 -277
  43. package/assets/dsh-lcx-codex-banner.jpg +0 -0
  44. package/lib/legacy-v3.js +0 -20
  45. package/lib/responses-replay.js +0 -68
  46. package/scripts/probe-alpha.mjs +0 -43
  47. package/scripts/validate-dsh-schema.mjs +0 -31
@@ -1,33 +1,9 @@
1
1
  // @ts-check
2
-
3
- import { baseURLFingerprint, routeCompatible } from './route.js'
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 */
2
+ import { isCompactCheckpointSource } from "@deepseek-ai/dsh-compaction";
3
+ import { SessionSeq, } from "@deepseek-ai/dsh-session";
4
+ import { baseURLFingerprint, routeCompatible } from "./route.js";
5
+ import { persistNativeImageReferences, responseInputItems } from "./dsh-responses.js";
6
+ import { estimateBudgetItem, modelVisibleBudgetView, portableBudgetError, portableTokenCeiling, } from "./token-budget.js";
31
7
  /**
32
8
  * @typedef {object} NativeCheckpointBase
33
9
  * @property {string} compactionId
@@ -44,14 +20,13 @@ import { estimateBudgetItem, portableBudgetError, portableTokenCeiling } from '.
44
20
  * @property {number} [createdAt]
45
21
  */
46
22
  /** @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 */
23
+ /** @typedef {NativeCheckpointV5} NativeCheckpointBlock */
49
24
  /**
50
25
  * Minimal validated candidate shape. DSH rawOutput is unknown until the existing
51
26
  * version, field, count, and compaction validation below has completed.
52
27
  * @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
28
+ * @property {typeof NATIVE_BLOCK_TYPE} type
29
+ * @property {typeof NATIVE_BLOCK_VERSION} version
55
30
  * @property {unknown} [compactionId]
56
31
  * @property {unknown} [nativeOutput]
57
32
  * @property {unknown} [provider]
@@ -63,261 +38,612 @@ import { estimateBudgetItem, portableBudgetError, portableTokenCeiling } from '.
63
38
  * @property {unknown} [retainedAssistantCount]
64
39
  */
65
40
  /** @typedef {{ compaction: CompactionItem }} NativeCompactionResult */
66
- /** @typedef {{ session: NativeSession, route: RouteIdentity, result: NativeCompactionResult, input?: unknown[], ephemeralPreludeItemCount?: number, imageMap?: unknown, retentionOptions?: RetentionOptions }} CreateCheckpointOptions */
41
+ /** @typedef {{ session: Session, route: RouteIdentity, result: NativeCompactionResult, input?: unknown[], ephemeralPreludeItemCount?: number, imageMap?: unknown, retentionOptions?: RetentionOptions }} CreateCheckpointOptions */
67
42
  /** @typedef {Error & { code?: string }} LcxError */
68
-
69
- export const NATIVE_BLOCK_TYPE = 'lcx-native-compaction-v5'
70
- export const NATIVE_BLOCK_VERSION = 5
71
- export const LEGACY_V4_BLOCK_TYPE = 'lcx-native-compaction-v4'
72
- export const LEGACY_V4_BLOCK_VERSION = 4
73
- const LEGACY_V3_PATTERN = /\[dsh-lcx-codex-v3-checkpoint:([0-9a-f-]{36})\]/iu
74
-
75
- export const RETAINED_MESSAGE_TOKEN_BUDGET = 64_000
76
- export const ASSISTANT_RETENTION_TOKEN_RESERVE = 24_000
77
- export const ASSISTANT_RETENTION_PER_MESSAGE_TOKEN_CAP = 3_000
78
-
43
+ export const NATIVE_BLOCK_TYPE = "lcx-native-compaction-v5";
44
+ export const NATIVE_BLOCK_VERSION = 5;
45
+ export const RETAINED_MESSAGE_TOKEN_BUDGET = 64_000;
46
+ export const ASSISTANT_RETENTION_TOKEN_RESERVE = 24_000;
47
+ export const ASSISTANT_RETENTION_PER_MESSAGE_TOKEN_CAP = 3_000;
79
48
  /**
80
49
  * @param {unknown} value
81
50
  * @returns {value is UnknownRecord}
82
51
  */
83
- function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) }
84
- /** @param {DshMessage | null | undefined} message */
85
- function textOf(message) { return (message?.content ?? []).filter((b) => b?.type === 'text').map((b) => b.text).join('') }
52
+ function isObject(value) {
53
+ return value !== null && typeof value === "object" && !Array.isArray(value);
54
+ }
55
+ /** @param {Message | null | undefined} message */
56
+ function textOf(message) {
57
+ return (message?.content ?? [])
58
+ .filter((block) => block.type === "text")
59
+ .map((block) => block.text)
60
+ .join("");
61
+ }
86
62
  /** @param {unknown} item */
87
- function estimatedItemTokens(item) { return estimateBudgetItem(item) }
63
+ function estimatedItemTokens(item) {
64
+ return estimateBudgetItem(item);
65
+ }
88
66
  /**
89
67
  * @param {unknown} item
90
68
  * @returns {item is UnknownRecord}
91
69
  */
92
- function isRetainedClientItem(item) { return isObject(item) && (item.type === undefined || item.type === 'message') && ['user', 'developer', 'system'].includes(String(item.role ?? '')) }
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)) }
70
+ function isRetainedClientItem(item) {
71
+ if (!isObject(item) || (item.type !== undefined && item.type !== "message"))
72
+ return false;
73
+ return (item.role === "user" || item.role === "developer" || item.role === "system");
74
+ }
75
+ function assistantTextParts(item) {
76
+ if (!isObject(item) ||
77
+ item.type !== "message" ||
78
+ item.role !== "assistant" ||
79
+ !Array.isArray(item.content))
80
+ return [];
81
+ return item.content.filter((part) => isObject(part) &&
82
+ part.type === "output_text" &&
83
+ typeof part.text === "string" &&
84
+ part.text.length > 0);
85
+ }
98
86
  /** @param {unknown} item */
99
- function isRetainedAssistantItem(item) { return assistantTextParts(item).length > 0 }
87
+ function isRetainedAssistantItem(item) {
88
+ return assistantTextParts(item).length > 0;
89
+ }
100
90
  /** @param {unknown} item */
101
- function assistantText(item) { return assistantTextParts(item).map((part) => part.text).join('') }
91
+ function assistantText(item) {
92
+ return assistantTextParts(item)
93
+ .map((part) => part.text)
94
+ .join("");
95
+ }
102
96
  /** @param {unknown} item */
103
- function retainedAssistantPhase(item) { const phase = isObject(item) ? item.phase : undefined; return phase === 'commentary' || phase === 'final_answer' ? phase : undefined }
104
-
97
+ function retainedAssistantPhase(item) {
98
+ const phase = isObject(item) ? item.phase : undefined;
99
+ return phase === "commentary" || phase === "final_answer" ? phase : undefined;
100
+ }
101
+ /** @param {unknown} value */
102
+ function containsBudgetImage(value) {
103
+ if (Array.isArray(value))
104
+ return value.some(containsBudgetImage);
105
+ if (!isObject(value))
106
+ return false;
107
+ return value.image === true || Object.values(value).some(containsBudgetImage);
108
+ }
109
+ /** @param {unknown} item */
110
+ function isImageBudgetItem(item) {
111
+ return containsBudgetImage(modelVisibleBudgetView(item));
112
+ }
105
113
  /**
106
114
  * @param {unknown} item
107
115
  * @param {number} [maxTokens]
108
116
  * @returns {UnknownRecord | undefined}
109
117
  */
110
118
  function truncateVisibleAssistantItem(item, maxTokens = ASSISTANT_RETENTION_PER_MESSAGE_TOKEN_CAP) {
111
- const text = assistantText(item); if (!text) return undefined
112
- const phase = retainedAssistantPhase(item)
113
- const providerId = isObject(item) && typeof item.id === 'string' && item.id ? item.id : undefined
114
- /** @param {string} retained @param {boolean} preserveProviderId */
115
- const candidate = (retained, preserveProviderId) => ({
116
- type: 'message', role: 'assistant', content: [{ type: 'output_text', text: retained }],
117
- ...(phase ? { phase } : {}),
118
- ...(preserveProviderId && providerId ? { id: providerId } : {}),
119
- })
120
- const full = candidate(text, true)
121
- if ((estimatedItemTokens(full) ?? Infinity) <= maxTokens) return full
122
- const marker = '\n…[LCX retained answer truncated]…\n'
123
- /** @param {number} count */
124
- const shortened = (count) => {
125
- const available = Math.max(0, count - marker.length)
126
- const head = Math.floor(available * 0.72); const tail = available - head
127
- return candidate(`${text.slice(0, head)}${marker}${text.slice(Math.max(head, text.length - tail))}`, false)
128
- }
129
- let low = 0; let high = text.length; let best
130
- while (low <= high) {
131
- const count = Math.floor((low + high) / 2); const next = shortened(count)
132
- if ((estimatedItemTokens(next) ?? Infinity) <= maxTokens) { best = next; low = count + 1 } else high = count - 1
133
- }
134
- return best
135
- }
136
-
119
+ const text = assistantText(item);
120
+ if (!text)
121
+ return undefined;
122
+ const phase = retainedAssistantPhase(item);
123
+ const providerId = isObject(item) && typeof item.id === "string" && item.id
124
+ ? item.id
125
+ : undefined;
126
+ /** @param {string} retained @param {boolean} preserveProviderId */
127
+ const candidate = (retained, preserveProviderId) => ({
128
+ type: "message",
129
+ role: "assistant",
130
+ content: [{ type: "output_text", text: retained }],
131
+ ...(phase ? { phase } : {}),
132
+ ...(preserveProviderId && providerId ? { id: providerId } : {}),
133
+ });
134
+ const full = candidate(text, true);
135
+ if ((estimatedItemTokens(full) ?? Infinity) <= maxTokens)
136
+ return full;
137
+ const marker = "\n…[LCX retained answer truncated]…\n";
138
+ /** @param {number} count */
139
+ const shortened = (count) => {
140
+ const available = Math.max(0, count - marker.length);
141
+ const head = Math.floor(available * 0.72);
142
+ const tail = available - head;
143
+ return candidate(`${text.slice(0, head)}${marker}${text.slice(Math.max(head, text.length - tail))}`, false);
144
+ };
145
+ let low = 0;
146
+ let high = text.length;
147
+ let best;
148
+ while (low <= high) {
149
+ const count = Math.floor((low + high) / 2);
150
+ const next = shortened(count);
151
+ if ((estimatedItemTokens(next) ?? Infinity) <= maxTokens) {
152
+ best = next;
153
+ low = count + 1;
154
+ }
155
+ else
156
+ high = count - 1;
157
+ }
158
+ return best;
159
+ }
137
160
  /**
138
161
  * @param {IndexedItem[]} candidates
139
162
  * @param {number} budget
140
163
  * @param {(value: IndexedItem) => unknown} [transform]
164
+ * @param {boolean} [stopOnImageOverflow]
141
165
  * @returns {Selection}
142
166
  */
143
- function selectNewest(candidates, budget, transform = (value) => structuredClone(value.item)) {
144
- /** @type {SelectedItem[]} */
145
- const selected = []; let used = 0
146
- 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 }
147
- return { selected, used }
167
+ function selectNewest(candidates, budget, transform = (value) => structuredClone(value.item), stopOnImageOverflow = false) {
168
+ /** @type {SelectedItem[]} */
169
+ const selected = [];
170
+ let used = 0;
171
+ for (let index = candidates.length - 1; index >= 0; index -= 1) {
172
+ const candidate = candidates[index];
173
+ const item = transform(candidate);
174
+ if (!item)
175
+ continue;
176
+ const tokens = estimatedItemTokens(item);
177
+ if (tokens === undefined || tokens > budget || used + tokens > budget) {
178
+ if (stopOnImageOverflow &&
179
+ tokens !== undefined &&
180
+ isImageBudgetItem(item))
181
+ break;
182
+ continue;
183
+ }
184
+ selected.push({ index: candidate.index, item, tokens });
185
+ used += tokens;
186
+ }
187
+ return { selected, used };
148
188
  }
149
-
150
189
  /**
151
190
  * @param {unknown[] | null | undefined} input
152
191
  * @param {RetentionOptions} [options]
153
192
  * @returns {unknown[]}
154
193
  */
155
194
  export function retainedCompactionInput(input, options = {}) {
156
- const budget = Number.isSafeInteger(options.tokenBudget) && /** @type {number} */ (options.tokenBudget) > 0 ? /** @type {number} */ (options.tokenBudget) : RETAINED_MESSAGE_TOKEN_BUDGET
157
- const candidates = (input ?? []).map((item, index) => ({ item, index })).filter(({ item }) => isRetainedClientItem(item))
158
- return selectNewest(candidates, budget).selected.sort((a, b) => a.index - b.index).map(({ item }) => item)
195
+ const configuredBudget = options.tokenBudget;
196
+ const budget = typeof configuredBudget === "number" &&
197
+ Number.isSafeInteger(configuredBudget) &&
198
+ configuredBudget > 0
199
+ ? configuredBudget
200
+ : RETAINED_MESSAGE_TOKEN_BUDGET;
201
+ const candidates = (input ?? [])
202
+ .map((item, index) => ({ item, index }))
203
+ .filter(({ item }) => isRetainedClientItem(item));
204
+ return selectNewest(candidates, budget)
205
+ .selected.sort((a, b) => a.index - b.index)
206
+ .map(({ item }) => item);
159
207
  }
160
-
161
208
  /**
162
209
  * @param {unknown[] | null | undefined} input
163
210
  * @param {RetentionOptions} [options]
164
211
  * @returns {RetentionPlan}
165
212
  */
166
213
  export function retainedConversationPlan(input, options = {}) {
167
- const totalBudget = Number.isSafeInteger(options.tokenBudget) && /** @type {number} */ (options.tokenBudget) > 0 ? /** @type {number} */ (options.tokenBudget) : RETAINED_MESSAGE_TOKEN_BUDGET
168
- const assistantReserve = Math.min(totalBudget, Number.isSafeInteger(options.assistantTokenReserve) && /** @type {number} */ (options.assistantTokenReserve) >= 0 ? /** @type {number} */ (options.assistantTokenReserve) : ASSISTANT_RETENTION_TOKEN_RESERVE)
169
- const perMessageCap = Number.isSafeInteger(options.assistantPerMessageTokenCap) && /** @type {number} */ (options.assistantPerMessageTokenCap) > 0 ? /** @type {number} */ (options.assistantPerMessageTokenCap) : ASSISTANT_RETENTION_PER_MESSAGE_TOKEN_CAP
170
- const indexed = (input ?? []).map((item, index) => ({ item, index }))
171
- const assistant = selectNewest(indexed.filter(({ item }) => isRetainedAssistantItem(item)), assistantReserve, ({ item }) => truncateVisibleAssistantItem(item, perMessageCap))
172
- const remaining = Math.max(0, totalBudget - assistant.used)
173
- const client = selectNewest(indexed.filter(({ item }) => isRetainedClientItem(item)), remaining)
174
- const selected = [...client.selected, ...assistant.selected].sort((a, b) => a.index - b.index)
175
- 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 }
214
+ const configuredBudget = options.tokenBudget;
215
+ const totalBudget = typeof configuredBudget === "number" &&
216
+ Number.isSafeInteger(configuredBudget) &&
217
+ configuredBudget > 0
218
+ ? configuredBudget
219
+ : RETAINED_MESSAGE_TOKEN_BUDGET;
220
+ const configuredReserve = options.assistantTokenReserve;
221
+ const assistantReserve = Math.min(totalBudget, typeof configuredReserve === "number" &&
222
+ Number.isSafeInteger(configuredReserve) &&
223
+ configuredReserve >= 0
224
+ ? configuredReserve
225
+ : ASSISTANT_RETENTION_TOKEN_RESERVE);
226
+ const configuredPerMessageCap = options.assistantPerMessageTokenCap;
227
+ const perMessageCap = typeof configuredPerMessageCap === "number" &&
228
+ Number.isSafeInteger(configuredPerMessageCap) &&
229
+ configuredPerMessageCap > 0
230
+ ? configuredPerMessageCap
231
+ : ASSISTANT_RETENTION_PER_MESSAGE_TOKEN_CAP;
232
+ const indexed = (input ?? []).map((item, index) => ({ item, index }));
233
+ const assistant = selectNewest(indexed.filter(({ item }) => isRetainedAssistantItem(item)), assistantReserve, ({ item }) => truncateVisibleAssistantItem(item, perMessageCap));
234
+ const remaining = Math.max(0, totalBudget - assistant.used);
235
+ const client = selectNewest(indexed.filter(({ item }) => isRetainedClientItem(item)), remaining, undefined, true);
236
+ const selected = [...client.selected, ...assistant.selected].sort((a, b) => a.index - b.index);
237
+ return {
238
+ items: selected.map(({ item }) => item),
239
+ clientCount: client.selected.length,
240
+ assistantCount: assistant.selected.length,
241
+ estimatedTokens: client.used + assistant.used,
242
+ clientEstimatedTokens: client.used,
243
+ assistantEstimatedTokens: assistant.used,
244
+ };
176
245
  }
177
246
  /**
178
247
  * @param {unknown[] | null | undefined} input
179
248
  * @param {RetentionOptions} [options]
180
249
  */
181
- export function retainedConversationInput(input, options = {}) { return retainedConversationPlan(input, options).items }
250
+ export function retainedConversationInput(input, options = {}) {
251
+ return retainedConversationPlan(input, options).items;
252
+ }
182
253
  /** @param {unknown[] | null | undefined} items */
183
- export function hasRetainedCompactionInput(items) { return (items ?? []).some((item) => isRetainedClientItem(item) || isRetainedAssistantItem(item)) }
184
- /** @param {DshMessage | null | undefined} message */
185
- export function compactCheckpointId(message) { const source = message?.source; return source?.kind === 'plugin' && source?.plugin === 'compact' && typeof source.compactionId === 'string' && source.compactionId ? source.compactionId : undefined }
186
- /** @param {DshMessage | null | undefined} message */
187
- export function legacyCheckpointId(message) { return textOf(message).match(LEGACY_V3_PATTERN)?.[1]?.toLowerCase() }
188
- /** @param {NativeSession | null | undefined} session */
189
- 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 }
190
-
254
+ export function hasRetainedCompactionInput(items) {
255
+ return (items ?? []).some((item) => isRetainedClientItem(item) || isRetainedAssistantItem(item));
256
+ }
257
+ /** @param {Message | null | undefined} message */
258
+ function hasCompactionId(source) {
259
+ return (isCompactCheckpointSource(source) &&
260
+ "compactionId" in source &&
261
+ typeof source.compactionId === "string" &&
262
+ source.compactionId.length > 0);
263
+ }
264
+ export function compactCheckpointId(message) {
265
+ const source = message?.source;
266
+ return source && hasCompactionId(source) ? source.compactionId : undefined;
267
+ }
268
+ /** Reject reserved checkpoint markers without interpreting unsupported formats or opening sidecars. */
269
+ export function assertSupportedCheckpointMessage(message) {
270
+ for (const block of message.content) {
271
+ if (block.type.startsWith("lcx-native-compaction-") && block.type !== NATIVE_BLOCK_TYPE ||
272
+ block.type === "text" && /\[dsh-lcx-codex-[^\]\r\n]*checkpoint:/iu.test(block.text))
273
+ throw Object.assign(new Error("Unsupported LCX checkpoint format; start a new session"), {
274
+ code: "LCX_CHECKPOINT_UNSUPPORTED",
275
+ });
276
+ }
277
+ }
278
+ /** @param {Session | null | undefined} session */
279
+ export function activeCompactionId(session) {
280
+ if (!session)
281
+ return undefined;
282
+ /** @type {Set<string>} */ const ended = new Set();
283
+ const events = session.snapshotEvents();
284
+ for (let index = events.length - 1; index >= 0; index -= 1) {
285
+ const event = events[index];
286
+ if (!event)
287
+ continue;
288
+ if (event.type === "compaction/end")
289
+ ended.add(String(event.data.compactionId));
290
+ if (event.type === "compaction/start" &&
291
+ !ended.has(String(event.data.compactionId)))
292
+ return String(event.data.compactionId);
293
+ }
294
+ return undefined;
295
+ }
191
296
  /**
192
297
  * @param {CreateCheckpointOptions} options
193
298
  * @returns {NativeCheckpointV5}
194
299
  */
195
- export function createNativeCheckpointBlock({ session, route, result, input = [], ephemeralPreludeItemCount = 0, imageMap, retentionOptions = {} }) {
196
- const compactionId = activeCompactionId(session)
197
- if (!compactionId) {
198
- /** @type {LcxError} */
199
- const error = new Error('Native compaction could not correlate the active DSH compaction transaction')
200
- error.code = 'LCX_COMPACTION_ID_UNAVAILABLE'
201
- throw error
202
- }
203
- if (!Number.isSafeInteger(ephemeralPreludeItemCount) || ephemeralPreludeItemCount < 0 || ephemeralPreludeItemCount > input.length) {
204
- throw Object.assign(new Error('Native compaction prelude provenance is invalid'), { code: 'LCX_COMPACTION_PRELUDE_PROVENANCE_INVALID' })
205
- }
206
- const retention = retainedConversationPlan(input.slice(ephemeralPreludeItemCount), retentionOptions)
207
- /** @type {NativeOutputItem[]} */
208
- const nativeOutput = persistNativeImageReferences([...retention.items, structuredClone(result.compaction)], imageMap)
209
- 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() }
210
- }
211
-
300
+ export function createNativeCheckpointBlock({ session, route, result, input = [], ephemeralPreludeItemCount = 0, imageMap, retentionOptions = {}, }) {
301
+ const compactionId = activeCompactionId(session);
302
+ if (!compactionId) {
303
+ const error = new Error("Native compaction could not correlate the active DSH compaction transaction");
304
+ error.code = "LCX_COMPACTION_ID_UNAVAILABLE";
305
+ throw error;
306
+ }
307
+ if (!Number.isSafeInteger(ephemeralPreludeItemCount) ||
308
+ ephemeralPreludeItemCount < 0 ||
309
+ ephemeralPreludeItemCount > input.length) {
310
+ throw Object.assign(new Error("Native compaction prelude provenance is invalid"), { code: "LCX_COMPACTION_PRELUDE_PROVENANCE_INVALID" });
311
+ }
312
+ const retention = retainedConversationPlan(input.slice(ephemeralPreludeItemCount), retentionOptions);
313
+ /** @type {NativeOutputItem[]} */
314
+ const nativeOutput = persistNativeImageReferences([...retention.items, structuredClone(result.compaction)], imageMap ?? new Map());
315
+ return {
316
+ type: NATIVE_BLOCK_TYPE,
317
+ version: NATIVE_BLOCK_VERSION,
318
+ retentionPolicy: "conversation-fidelity-v1",
319
+ compactionId,
320
+ provider: route.provider,
321
+ model: route.model,
322
+ baseURLFingerprint: baseURLFingerprint(route.baseURL),
323
+ sourceSessionId: route.sessionId,
324
+ nativeOutput,
325
+ nativeCompaction: structuredClone(result.compaction),
326
+ retainedInputCount: retention.items.length,
327
+ retainedClientCount: retention.clientCount,
328
+ retainedAssistantCount: retention.assistantCount,
329
+ retainedEstimatedTokens: retention.estimatedTokens,
330
+ createdAt: Date.now(),
331
+ };
332
+ }
212
333
  /**
213
334
  * @param {NativeCheckpointBlock} block
214
335
  * @param {unknown} usage
215
336
  * @returns {UnknownRecord[]}
216
337
  */
217
338
  export function nativeCheckpointChunks(block, usage) {
218
- const text = 'LCX Native V2 checkpoint saved in the DSH session log.'
219
- 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' } }]
339
+ const text = "LCX Native V2 checkpoint saved in the DSH session log.";
340
+ return [
341
+ { type: "block-start", index: 0, blockType: "text" },
342
+ { type: "text-delta", index: 0, text },
343
+ { type: "block-end", index: 0, block: { type: "text", text } },
344
+ { type: "block-start", index: 1, blockType: block.type },
345
+ { type: "block-end", index: 1, block: structuredClone(block) },
346
+ ...(usage === undefined ? [] : [{ type: "usage", usage }]),
347
+ { type: "finish", reason: { kind: "stop" } },
348
+ ];
220
349
  }
221
-
222
350
  /**
223
- * @param {NativeSession | null | undefined} session
351
+ * @param {Session | null | undefined} session
224
352
  * @param {string | null | undefined} compactionId
225
353
  * @returns {CompactionSummaryEvent | undefined}
226
354
  */
227
- 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 }
355
+ export function compactionSummaryEvent(session, compactionId) {
356
+ if (!session || !compactionId)
357
+ return undefined;
358
+ const events = session.snapshotEvents();
359
+ for (let index = events.length - 1; index >= 0; index -= 1) {
360
+ const event = events[index];
361
+ if (event?.type === "compaction/summary" &&
362
+ String(event.data.compactionId) === compactionId)
363
+ return event;
364
+ }
365
+ return undefined;
366
+ }
228
367
  /**
229
368
  * rawOutput elements remain untrusted until stateFromSummaryEvent validates them.
230
369
  * @param {CompactionSummaryEvent | null | undefined} event
231
370
  * @returns {NativeCheckpointCandidate | undefined}
232
371
  */
233
- 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)) }
372
+ function isNativeCheckpointCandidate(value) {
373
+ if (!isObject(value))
374
+ return false;
375
+ return (value.type === NATIVE_BLOCK_TYPE && value.version === NATIVE_BLOCK_VERSION);
376
+ }
377
+ function nativeBlockFromSummary(event) {
378
+ const raw = event?.data.rawOutput;
379
+ if (!raw)
380
+ return undefined;
381
+ for (const value of raw)
382
+ if (isNativeCheckpointCandidate(value))
383
+ return value;
384
+ return undefined;
385
+ }
234
386
  /** @param {unknown} value */
235
- function validateCount(value) { return value === undefined || (Number.isSafeInteger(value) && /** @type {number} */ (value) >= 0) }
387
+ function validateCount(value) {
388
+ return (value === undefined ||
389
+ (typeof value === "number" && Number.isSafeInteger(value) && value >= 0));
390
+ }
236
391
  /**
237
392
  * @param {CompactionSummaryEvent | null | undefined} event
238
393
  * @returns {NativeCheckpointBlock | undefined}
239
394
  */
240
395
  export function stateFromSummaryEvent(event) {
241
- if (event?.type !== 'compaction/summary') return undefined
242
- const block = nativeBlockFromSummary(event); if (!isObject(block)) return undefined
243
- if (block.compactionId !== event.data.compactionId || !Array.isArray(block.nativeOutput) || block.nativeOutput.length === 0) return undefined
244
- if (typeof block.provider !== 'string' || typeof block.model !== 'string' || typeof block.baseURLFingerprint !== 'string' || typeof block.sourceSessionId !== 'string') return undefined
245
- if (!validateCount(block.retainedInputCount) || !validateCount(block.retainedClientCount) || !validateCount(block.retainedAssistantCount)) return undefined
246
- 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
247
- const compactionIndex = block.nativeOutput.findIndex((item) => /** @type {UnknownRecord | undefined} */ (item)?.type === 'compaction'); const prefix = compactionIndex >= 0 ? block.nativeOutput.slice(0, compactionIndex) : []
248
- 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 }
249
- return /** @type {NativeCheckpointBlock} */ (structuredClone(block))
396
+ if (!event)
397
+ return undefined;
398
+ const block = nativeBlockFromSummary(event);
399
+ if (!block)
400
+ return undefined;
401
+ if (block.compactionId !== event.data.compactionId ||
402
+ !Array.isArray(block.nativeOutput) ||
403
+ block.nativeOutput.length === 0)
404
+ return undefined;
405
+ if (typeof block.provider !== "string" ||
406
+ typeof block.model !== "string" ||
407
+ typeof block.baseURLFingerprint !== "string" ||
408
+ typeof block.sourceSessionId !== "string")
409
+ return undefined;
410
+ if (!validateCount(block.retainedInputCount) ||
411
+ !validateCount(block.retainedClientCount) ||
412
+ !validateCount(block.retainedAssistantCount))
413
+ return undefined;
414
+ try {
415
+ responseInputItems(block.nativeOutput);
416
+ }
417
+ catch {
418
+ return undefined;
419
+ }
420
+ const compactions = block.nativeOutput.filter((item) => isObject(item) &&
421
+ item.type === "compaction" &&
422
+ typeof item.encrypted_content === "string");
423
+ if (compactions.length !== 1 || compactions[0].encrypted_content.trim().length === 0)
424
+ return undefined;
425
+ const compactionIndex = block.nativeOutput.findIndex((item) => isObject(item) && item.type === "compaction");
426
+ const prefix = compactionIndex >= 0 ? block.nativeOutput.slice(0, compactionIndex) : [];
427
+ {
428
+ const clients = prefix.filter(isRetainedClientItem).length;
429
+ const assistants = prefix.filter(isRetainedAssistantItem).length;
430
+ const total = clients + assistants;
431
+ if (block.retainedInputCount !== undefined &&
432
+ block.retainedInputCount !== total)
433
+ return undefined;
434
+ if (block.retainedClientCount !== undefined &&
435
+ block.retainedClientCount !== clients)
436
+ return undefined;
437
+ if (block.retainedAssistantCount !== undefined &&
438
+ block.retainedAssistantCount !== assistants)
439
+ return undefined;
440
+ }
441
+ const nativeCompaction = compactions[0];
442
+ if (!nativeCompaction)
443
+ return undefined;
444
+ const base = {
445
+ compactionId: String(block.compactionId),
446
+ provider: block.provider,
447
+ model: block.model,
448
+ baseURLFingerprint: block.baseURLFingerprint,
449
+ sourceSessionId: block.sourceSessionId,
450
+ nativeOutput: structuredClone(block.nativeOutput),
451
+ nativeCompaction: structuredClone(nativeCompaction),
452
+ ...(block.retainedInputCount === undefined
453
+ ? {}
454
+ : { retainedInputCount: block.retainedInputCount }),
455
+ ...(block.retainedClientCount === undefined
456
+ ? {}
457
+ : { retainedClientCount: block.retainedClientCount }),
458
+ ...(block.retainedAssistantCount === undefined
459
+ ? {}
460
+ : { retainedAssistantCount: block.retainedAssistantCount }),
461
+ };
462
+ return { ...base, type: NATIVE_BLOCK_TYPE, version: NATIVE_BLOCK_VERSION };
250
463
  }
251
464
  /**
252
- * @param {NativeSession} session
253
- * @param {DshMessage} message
465
+ * @param {Session} session
466
+ * @param {Message} message
254
467
  */
255
- export function checkpointStateForMessage(session, message) { const id = compactCheckpointId(message); return id ? stateFromSummaryEvent(compactionSummaryEvent(session, id)) : undefined }
468
+ export function checkpointStateForMessage(session, message) {
469
+ assertSupportedCheckpointMessage(message);
470
+ const id = compactCheckpointId(message);
471
+ if (!id)
472
+ return undefined;
473
+ const event = compactionSummaryEvent(session, id);
474
+ const state = stateFromSummaryEvent(event);
475
+ const hasNativeBlock = event?.data.rawOutput?.some(value => isObject(value) && typeof value.type === "string" && value.type.startsWith("lcx-native-compaction-"));
476
+ if (hasNativeBlock && !state)
477
+ throw Object.assign(new Error("Unsupported or invalid LCX checkpoint; start a new session"), { code: "LCX_CHECKPOINT_UNSUPPORTED" });
478
+ return state;
479
+ }
256
480
  /**
257
481
  * @param {NativeCheckpointBlock} state
258
482
  * @param {RouteIdentity} route
259
483
  * @param {unknown} ctx
260
484
  */
261
- export function stateRouteCompatible(state, route, ctx) { return routeCompatible(state, route, ctx) }
485
+ export function stateRouteCompatible(state, route, ctx) {
486
+ return routeCompatible(state, route, ctx);
487
+ }
262
488
  /**
263
- * @param {NativeSession | null | undefined} session
489
+ * @param {Session} session
264
490
  * @param {number} seq
265
- * @returns {DshMessage | undefined}
491
+ * @returns {Message | undefined}
266
492
  */
267
- 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 }
268
- /** @param {DshMessage} message */
269
- function estimateChars(message) { try { const encoded = JSON.stringify(message); return typeof encoded === 'string' ? encoded.length : undefined } catch { return undefined } }
493
+ function eventMessage(session, seq) {
494
+ if (!Number.isSafeInteger(seq) || seq < 0)
495
+ return undefined;
496
+ const event = session.eventAt(SessionSeq(seq));
497
+ return event ? (session.deriveEventMessage(event) ?? undefined) : undefined;
498
+ }
499
+ /** @param {Message} message */
500
+ function estimateChars(message) {
501
+ try {
502
+ const encoded = JSON.stringify(message);
503
+ return typeof encoded === "string" ? encoded.length : undefined;
504
+ }
505
+ catch {
506
+ return undefined;
507
+ }
508
+ }
270
509
  /**
271
- * @param {NativeSession} session
510
+ * @param {Session} session
272
511
  * @param {string} compactionId
273
- * @param {Set<string>} visited
274
- * @param {number} depth
275
- * @returns {DshMessage[]}
512
+ * @returns {Message[]}
276
513
  */
277
- 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 }
514
+ function expandedCheckpointMessages(session, compactionId) {
515
+ const summaries = new Map();
516
+ for (const event of session.snapshotEvents())
517
+ if (event.type === "compaction/summary")
518
+ summaries.set(String(event.data.compactionId), event);
519
+ const active = new Set();
520
+ const stack = [];
521
+ const enter = (id) => {
522
+ const summary = summaries.get(id);
523
+ if (active.has(id) || !summary)
524
+ throw Object.assign(new Error("Portable checkpoint history is cyclic or incomplete"), { code: "LCX_CHECKPOINT_UNSUPPORTED" });
525
+ active.add(id);
526
+ stack.push({ id, seqs: summary.data.shadowedSeqs, index: 0 });
527
+ };
528
+ // Walk the DSH event graph iteratively so repeated compaction has no depth cutoff.
529
+ const result = [];
530
+ enter(compactionId);
531
+ while (stack.length) {
532
+ const frame = stack[stack.length - 1];
533
+ if (frame.index === frame.seqs.length) {
534
+ active.delete(frame.id);
535
+ stack.pop();
536
+ continue;
537
+ }
538
+ const message = eventMessage(session, frame.seqs[frame.index++]);
539
+ if (!message)
540
+ continue;
541
+ const nested = compactCheckpointId(message);
542
+ if (nested)
543
+ enter(nested);
544
+ else
545
+ result.push(structuredClone(message));
546
+ }
547
+ return result;
548
+ }
278
549
  /**
279
- * @param {NativeSession} session
550
+ * @param {Session} session
280
551
  * @param {string} compactionId
281
- * @returns {DshMessage[]}
552
+ * @returns {Message[]}
282
553
  */
283
- export function shadowedMessagesForCheckpoint(session, compactionId) { return expandedCheckpointMessages(session, compactionId, /** @type {Set<string>} */ (new Set()), 0) }
554
+ export function shadowedMessagesForCheckpoint(session, compactionId) {
555
+ return expandedCheckpointMessages(session, compactionId);
556
+ }
284
557
  /**
285
- * @param {DshMessage[]} messages
286
- * @returns {DshMessage[][]}
558
+ * @param {readonly Message[]} messages
559
+ * @returns {Message[][]}
287
560
  */
288
- 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 }
561
+ function groupMessages(messages) {
562
+ /** @type {Message[][]} */
563
+ const groups = [];
564
+ for (let index = 0; index < messages.length; index += 1) {
565
+ const message = messages[index];
566
+ const toolCalls = message.role === "assistant"
567
+ ? message.content
568
+ .filter((block) => block.type === "tool-call")
569
+ .map((block) => String(block.id))
570
+ : [];
571
+ if (toolCalls.length === 0) {
572
+ groups.push([message]);
573
+ continue;
574
+ }
575
+ const group = [message];
576
+ const pending = new Set(toolCalls);
577
+ let cursor = index + 1;
578
+ while (cursor < messages.length && pending.size > 0) {
579
+ const next = messages[cursor];
580
+ if (!next)
581
+ break;
582
+ const results = next.role === "user"
583
+ ? next.content
584
+ .filter((block) => block.type === "tool-result")
585
+ .map((block) => String(block.toolCallId))
586
+ : [];
587
+ if (results.length === 0)
588
+ break;
589
+ group.push(next);
590
+ for (const id of results)
591
+ pending.delete(id);
592
+ cursor += 1;
593
+ }
594
+ if (pending.size === 0)
595
+ index = cursor - 1;
596
+ groups.push(group);
597
+ }
598
+ return groups;
599
+ }
289
600
  /**
290
- * @param {NativeSession} session
601
+ * @param {Session} session
291
602
  * @param {string} compactionId
292
603
  * @param {{ maxChars?: number }} [options]
293
- * @returns {DshMessage[]}
604
+ * @returns {Message[]}
294
605
  */
295
606
  export function portableMessagesForCheckpoint(session, compactionId, options = {}) {
296
- const maxChars = Number.isSafeInteger(options.maxChars) && /** @type {number} */ (options.maxChars) > 0 ? /** @type {number} */ (options.maxChars) : 80_000
297
- const maxTokens = portableTokenCeiling(maxChars) ?? 1
298
- const expanded = shadowedMessagesForCheckpoint(session, compactionId); if (expanded.length === 0) return []
299
- const groups = groupMessages(expanded); const kept = []; let chars = 0; let tokens = 0
300
- for (let index = groups.length - 1; index >= 0; index -= 1) {
301
- const group = groups[index]
302
- let groupChars = 0; let groupTokens = 0; let budgetable = true
303
- for (const message of group) {
304
- const messageChars = estimateChars(message); const messageTokens = estimatedItemTokens(message)
305
- if (messageChars === undefined || messageTokens === undefined) { budgetable = false; break }
306
- groupChars += messageChars; groupTokens += messageTokens
307
- }
308
- const exceeds = !budgetable || chars + groupChars > maxChars || tokens + groupTokens > maxTokens
309
- if (exceeds) {
310
- if (kept.length === 0) throw portableBudgetError('Portable checkpoint newest message group exceeds the configured budget')
311
- break
607
+ const configuredMaxChars = options.maxChars;
608
+ const maxChars = typeof configuredMaxChars === "number" &&
609
+ Number.isSafeInteger(configuredMaxChars) &&
610
+ configuredMaxChars > 0
611
+ ? configuredMaxChars
612
+ : 80_000;
613
+ const maxTokens = portableTokenCeiling(maxChars) ?? 1;
614
+ const expanded = shadowedMessagesForCheckpoint(session, compactionId);
615
+ if (expanded.length === 0)
616
+ return [];
617
+ const groups = groupMessages(expanded);
618
+ const kept = [];
619
+ let chars = 0;
620
+ let tokens = 0;
621
+ for (let index = groups.length - 1; index >= 0; index -= 1) {
622
+ const group = groups[index];
623
+ let groupChars = 0;
624
+ let groupTokens = 0;
625
+ let budgetable = true;
626
+ for (const message of group) {
627
+ const messageChars = estimateChars(message);
628
+ const messageTokens = estimatedItemTokens(message);
629
+ if (messageChars === undefined || messageTokens === undefined) {
630
+ budgetable = false;
631
+ break;
632
+ }
633
+ groupChars += messageChars;
634
+ groupTokens += messageTokens;
635
+ }
636
+ const exceeds = !budgetable ||
637
+ chars + groupChars > maxChars ||
638
+ tokens + groupTokens > maxTokens;
639
+ if (exceeds) {
640
+ if (kept.length === 0)
641
+ throw portableBudgetError("Portable checkpoint newest message group exceeds the configured budget");
642
+ break;
643
+ }
644
+ kept.unshift(...group);
645
+ chars += groupChars;
646
+ tokens += groupTokens;
312
647
  }
313
- kept.unshift(...group); chars += groupChars; tokens += groupTokens
314
- }
315
- return kept
648
+ return kept;
316
649
  }
317
- /**
318
- * @param {DshMessage[]} messages
319
- * @param {NativeSession} session
320
- * @param {{ maxChars?: number }} [options]
321
- * @returns {DshMessage[]}
322
- */
323
- 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 }