@narumitw/pi-codex-compact 0.51.3 → 0.53.0

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.
package/src/checkpoint.ts CHANGED
@@ -1,282 +1,329 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
3
+ import type { Api } from "@earendil-works/pi-ai";
3
4
  import type { CompactionEntry, SessionEntry } from "@earendil-works/pi-coding-agent";
5
+ import type { RemoteCompactionProtocol, ResponsesCompactionProfile } from "./model-api.js";
4
6
  import { type JsonObject, validateCompactionItem } from "./protocol.js";
5
7
 
6
8
  export const CHECKPOINT_KIND = "pi-codex-remote-compaction";
7
- export const CHECKPOINT_VERSION = 1;
9
+ export const CHECKPOINT_VERSION = 3;
8
10
  export const REPLACEMENT_TOKEN_BUDGET = 64_000;
9
11
  export const REPLACEMENT_BYTE_BUDGET = 8 * 1024 * 1024;
10
12
  const MAX_MEDIA_ITEM_BYTES = 2 * 1024 * 1024;
13
+ const MAX_CHECKPOINT_DETAILS_BYTES = 10 * 1024 * 1024;
14
+ const MAX_CHECKPOINT_ID_LENGTH = 128;
11
15
  const MAX_PROVIDER_ID_LENGTH = 256;
16
+ const MAX_API_ID_LENGTH = 256;
17
+ const MAX_MODEL_ID_LENGTH = 512;
18
+ const MAX_KEPT_FINGERPRINTS = 100_000;
12
19
 
13
20
  export interface CodexCheckpointDetails {
14
- kind: typeof CHECKPOINT_KIND;
15
- version: typeof CHECKPOINT_VERSION;
16
- checkpointId: string;
17
- provider: string;
18
- api: "openai-codex-responses";
19
- modelId: string;
20
- protocol: "remote-compaction-v2";
21
- replacementHistory: JsonObject[];
22
- keptMessageFingerprints: string[];
23
- createdAt: string;
21
+ kind: typeof CHECKPOINT_KIND;
22
+ version: typeof CHECKPOINT_VERSION;
23
+ checkpointId: string;
24
+ provider: string;
25
+ api: Api;
26
+ profile: ResponsesCompactionProfile;
27
+ modelId: string;
28
+ protocol: RemoteCompactionProtocol;
29
+ replacementHistory: JsonObject[];
30
+ keptMessageFingerprints: string[];
31
+ createdAt: string;
24
32
  }
25
33
 
26
34
  function isObject(value: unknown): value is JsonObject {
27
- return typeof value === "object" && value !== null && !Array.isArray(value);
35
+ return typeof value === "object" && value !== null && !Array.isArray(value);
28
36
  }
29
37
 
30
38
  function stableValue(value: unknown): unknown {
31
- if (Array.isArray(value)) return value.map(stableValue);
32
- if (!isObject(value)) return value;
33
- return Object.fromEntries(
34
- Object.entries(value)
35
- .sort(([left], [right]) => left.localeCompare(right))
36
- .map(([key, child]) => [key, stableValue(child)]),
37
- );
39
+ if (Array.isArray(value)) return value.map(stableValue);
40
+ if (!isObject(value)) return value;
41
+ return Object.fromEntries(
42
+ Object.entries(value)
43
+ .sort(([left], [right]) => left.localeCompare(right))
44
+ .map(([key, child]) => [key, stableValue(child)]),
45
+ );
38
46
  }
39
47
 
40
48
  function serializedBytes(value: unknown): number {
41
- return Buffer.byteLength(JSON.stringify(value), "utf8");
49
+ return Buffer.byteLength(JSON.stringify(value), "utf8");
42
50
  }
43
51
 
44
52
  export function fingerprintMessage(message: AgentMessage): string {
45
- return createHash("sha256")
46
- .update(JSON.stringify(stableValue(message)))
47
- .digest("hex");
53
+ return createHash("sha256")
54
+ .update(JSON.stringify(stableValue(message)))
55
+ .digest("hex");
48
56
  }
49
57
 
50
58
  export function checkpointMarker(checkpointId: string): string {
51
- return [
52
- `[PI_CODEX_REMOTE_CHECKPOINT:${checkpointId}]`,
53
- "Opaque checkpoint injection failed. Do not infer missing history; tell the user to re-enable",
54
- "@narumitw/pi-codex-compact with a model using the openai-codex-responses API.",
55
- ].join(" ");
59
+ return [
60
+ `[PI_CODEX_REMOTE_CHECKPOINT:${checkpointId}]`,
61
+ "Opaque checkpoint injection failed. Do not infer missing history; tell the user to re-enable",
62
+ "@narumitw/pi-codex-compact with the same model and Responses API.",
63
+ ].join(" ");
56
64
  }
57
65
 
58
66
  export function fallbackSummary(checkpointId: string): string {
59
- return [
60
- `OpenAI Codex Remote Compaction V2 checkpoint ${checkpointId} stores the older history opaquely.`,
61
- "Full replay requires @narumitw/pi-codex-compact and the same model through a compatible Codex Responses provider.",
62
- "Without them, only Pi's retained recent messages remain available.",
63
- ].join(" ");
67
+ return [
68
+ `Responses compaction checkpoint ${checkpointId} stores the older history opaquely.`,
69
+ "Full replay requires @narumitw/pi-codex-compact and the same model through a compatible Responses provider.",
70
+ "Without them, only Pi's retained recent messages remain available.",
71
+ ].join(" ");
64
72
  }
65
73
 
66
74
  function markerMessage(checkpointId: string, timestamp: number): AgentMessage {
67
- return {
68
- role: "user",
69
- content: [{ type: "text", text: checkpointMarker(checkpointId) }],
70
- timestamp,
71
- };
75
+ return {
76
+ role: "user",
77
+ content: [{ type: "text", text: checkpointMarker(checkpointId) }],
78
+ timestamp,
79
+ };
72
80
  }
73
81
 
74
82
  export function parseCheckpointDetails(value: unknown): CodexCheckpointDetails | undefined {
75
- if (!isObject(value)) return undefined;
76
- if (
77
- value.kind !== CHECKPOINT_KIND ||
78
- value.version !== CHECKPOINT_VERSION ||
79
- typeof value.checkpointId !== "string" ||
80
- value.checkpointId.length < 8 ||
81
- typeof value.provider !== "string" ||
82
- value.provider.length === 0 ||
83
- value.provider.length > MAX_PROVIDER_ID_LENGTH ||
84
- value.api !== "openai-codex-responses" ||
85
- typeof value.modelId !== "string" ||
86
- value.protocol !== "remote-compaction-v2" ||
87
- !Array.isArray(value.replacementHistory) ||
88
- !Array.isArray(value.keptMessageFingerprints) ||
89
- typeof value.createdAt !== "string"
90
- ) {
91
- return undefined;
92
- }
93
- if (
94
- value.replacementHistory.length === 0 ||
95
- !value.replacementHistory.every(isObject) ||
96
- !value.keptMessageFingerprints.every(
97
- (fingerprint) => typeof fingerprint === "string" && /^[a-f0-9]{64}$/.test(fingerprint),
98
- ) ||
99
- serializedBytes(value.replacementHistory) > REPLACEMENT_BYTE_BUDGET
100
- ) {
101
- return undefined;
102
- }
103
- const last = value.replacementHistory.at(-1);
104
- try {
105
- validateCompactionItem(last);
106
- } catch {
107
- return undefined;
108
- }
109
- return structuredClone(value) as unknown as CodexCheckpointDetails;
83
+ if (!isObject(value)) return undefined;
84
+ try {
85
+ if (serializedBytes(value) > MAX_CHECKPOINT_DETAILS_BYTES) return undefined;
86
+ } catch {
87
+ return undefined;
88
+ }
89
+ const isVersionOne =
90
+ value.version === 1 && value.api === "openai-codex-responses" && value.protocol === "remote-compaction-v2";
91
+ const isVersionTwo =
92
+ value.version === 2 &&
93
+ (value.api === "openai-codex-responses" ||
94
+ value.api === "openai-responses" ||
95
+ value.api === "azure-openai-responses") &&
96
+ (value.protocol === "remote-v2" || value.protocol === "responses-compact");
97
+ const isVersionThree =
98
+ value.version === CHECKPOINT_VERSION &&
99
+ typeof value.api === "string" &&
100
+ value.api.length > 0 &&
101
+ value.api.length <= MAX_API_ID_LENGTH &&
102
+ (value.profile === "codex-responses-v1" || value.profile === "openai-responses-v1") &&
103
+ (value.protocol === "remote-v2" || value.protocol === "responses-compact");
104
+ if (
105
+ value.kind !== CHECKPOINT_KIND ||
106
+ (!isVersionOne && !isVersionTwo && !isVersionThree) ||
107
+ typeof value.checkpointId !== "string" ||
108
+ value.checkpointId.length < 8 ||
109
+ value.checkpointId.length > MAX_CHECKPOINT_ID_LENGTH ||
110
+ typeof value.provider !== "string" ||
111
+ value.provider.length === 0 ||
112
+ value.provider.length > MAX_PROVIDER_ID_LENGTH ||
113
+ typeof value.modelId !== "string" ||
114
+ value.modelId.length === 0 ||
115
+ value.modelId.length > MAX_MODEL_ID_LENGTH ||
116
+ !Array.isArray(value.replacementHistory) ||
117
+ !Array.isArray(value.keptMessageFingerprints) ||
118
+ value.keptMessageFingerprints.length > MAX_KEPT_FINGERPRINTS ||
119
+ typeof value.createdAt !== "string" ||
120
+ value.createdAt.length > 64
121
+ ) {
122
+ return undefined;
123
+ }
124
+ const api = value.api as Api;
125
+ const profile =
126
+ api === "openai-codex-responses"
127
+ ? "codex-responses-v1"
128
+ : api === "openai-responses" || api === "azure-openai-responses"
129
+ ? "openai-responses-v1"
130
+ : value.profile;
131
+ if (
132
+ (profile !== "codex-responses-v1" && profile !== "openai-responses-v1") ||
133
+ (api !== "openai-codex-responses" &&
134
+ api !== "openai-responses" &&
135
+ api !== "azure-openai-responses" &&
136
+ profile !== "codex-responses-v1") ||
137
+ (isVersionThree && value.profile !== profile)
138
+ ) {
139
+ return undefined;
140
+ }
141
+ if (
142
+ value.replacementHistory.length === 0 ||
143
+ !value.replacementHistory.every(isObject) ||
144
+ !value.keptMessageFingerprints.every(
145
+ (fingerprint) => typeof fingerprint === "string" && /^[a-f0-9]{64}$/.test(fingerprint),
146
+ ) ||
147
+ serializedBytes(value.replacementHistory) > REPLACEMENT_BYTE_BUDGET
148
+ ) {
149
+ return undefined;
150
+ }
151
+ const last = value.replacementHistory.at(-1);
152
+ try {
153
+ validateCompactionItem(last);
154
+ } catch {
155
+ return undefined;
156
+ }
157
+ return {
158
+ kind: CHECKPOINT_KIND,
159
+ version: CHECKPOINT_VERSION,
160
+ checkpointId: value.checkpointId,
161
+ provider: value.provider,
162
+ api,
163
+ profile,
164
+ modelId: value.modelId,
165
+ protocol: isVersionOne ? "remote-v2" : (value.protocol as RemoteCompactionProtocol),
166
+ replacementHistory: structuredClone(value.replacementHistory),
167
+ keptMessageFingerprints: [...value.keptMessageFingerprints],
168
+ createdAt: value.createdAt,
169
+ };
110
170
  }
111
171
 
112
172
  export function latestCheckpoint(entries: readonly SessionEntry[]):
113
- | {
114
- entry: CompactionEntry<CodexCheckpointDetails>;
115
- details: CodexCheckpointDetails;
116
- }
117
- | undefined {
118
- for (let index = entries.length - 1; index >= 0; index--) {
119
- const entry = entries[index];
120
- if (entry.type !== "compaction") continue;
121
- const details = parseCheckpointDetails(entry.details);
122
- return details
123
- ? { entry: entry as CompactionEntry<CodexCheckpointDetails>, details }
124
- : undefined;
125
- }
126
- return undefined;
173
+ | {
174
+ entry: CompactionEntry<CodexCheckpointDetails>;
175
+ details: CodexCheckpointDetails;
176
+ }
177
+ | undefined {
178
+ for (let index = entries.length - 1; index >= 0; index--) {
179
+ const entry = entries[index];
180
+ if (entry.type !== "compaction") continue;
181
+ const details = parseCheckpointDetails(entry.details);
182
+ return details ? { entry: entry as CompactionEntry<CodexCheckpointDetails>, details } : undefined;
183
+ }
184
+ return undefined;
127
185
  }
128
186
 
129
187
  function isOlderCompactionSummary(message: AgentMessage, timestamp: number): boolean {
130
- return (
131
- message.role === "compactionSummary" &&
132
- Number.isFinite(message.timestamp) &&
133
- Number.isFinite(timestamp) &&
134
- message.timestamp < timestamp
135
- );
188
+ return (
189
+ message.role === "compactionSummary" &&
190
+ Number.isFinite(message.timestamp) &&
191
+ Number.isFinite(timestamp) &&
192
+ message.timestamp < timestamp
193
+ );
136
194
  }
137
195
 
138
196
  export function projectCheckpointContext(
139
- messages: readonly AgentMessage[],
140
- details: CodexCheckpointDetails,
141
- checkpointSummary: string,
197
+ messages: readonly AgentMessage[],
198
+ details: CodexCheckpointDetails,
199
+ checkpointSummary: string,
142
200
  ): AgentMessage[] | undefined {
143
- const summaryIndex = messages.findIndex(
144
- (message) => message.role === "compactionSummary" && message.summary === checkpointSummary,
145
- );
146
- if (summaryIndex < 0) return undefined;
147
- const timestamp = messages[summaryIndex].timestamp;
148
- let messageIndex = summaryIndex + 1;
149
- let fingerprintIndex = 0;
150
- while (fingerprintIndex < details.keptMessageFingerprints.length) {
151
- if (messageIndex >= messages.length) return undefined;
152
- const message = messages[messageIndex];
153
- if (fingerprintMessage(message) === details.keptMessageFingerprints[fingerprintIndex]) {
154
- messageIndex += 1;
155
- fingerprintIndex += 1;
156
- continue;
157
- }
158
- if (isOlderCompactionSummary(message, timestamp)) {
159
- messageIndex += 1;
160
- continue;
161
- }
162
- return undefined;
163
- }
164
- while (
165
- messageIndex < messages.length &&
166
- isOlderCompactionSummary(messages[messageIndex], timestamp)
167
- ) {
168
- messageIndex += 1;
169
- }
170
- return [
171
- ...messages.slice(0, summaryIndex),
172
- markerMessage(details.checkpointId, timestamp),
173
- ...messages.slice(messageIndex),
174
- ];
201
+ const summaryIndex = messages.findIndex(
202
+ (message) => message.role === "compactionSummary" && message.summary === checkpointSummary,
203
+ );
204
+ if (summaryIndex < 0) return undefined;
205
+ const timestamp = messages[summaryIndex].timestamp;
206
+ let messageIndex = summaryIndex + 1;
207
+ let fingerprintIndex = 0;
208
+ while (fingerprintIndex < details.keptMessageFingerprints.length) {
209
+ if (messageIndex >= messages.length) return undefined;
210
+ const message = messages[messageIndex];
211
+ if (fingerprintMessage(message) === details.keptMessageFingerprints[fingerprintIndex]) {
212
+ messageIndex += 1;
213
+ fingerprintIndex += 1;
214
+ continue;
215
+ }
216
+ if (isOlderCompactionSummary(message, timestamp)) {
217
+ messageIndex += 1;
218
+ continue;
219
+ }
220
+ return undefined;
221
+ }
222
+ while (messageIndex < messages.length && isOlderCompactionSummary(messages[messageIndex], timestamp)) {
223
+ messageIndex += 1;
224
+ }
225
+ return [
226
+ ...messages.slice(0, summaryIndex),
227
+ markerMessage(details.checkpointId, timestamp),
228
+ ...messages.slice(messageIndex),
229
+ ];
175
230
  }
176
231
 
177
232
  function rawText(item: JsonObject): string {
178
- if (!Array.isArray(item.content)) return "";
179
- return item.content
180
- .flatMap((part) =>
181
- isObject(part) && typeof part.text === "string" && part.type === "input_text"
182
- ? [part.text]
183
- : [],
184
- )
185
- .join("\n");
233
+ if (!Array.isArray(item.content)) return "";
234
+ return item.content
235
+ .flatMap((part) =>
236
+ isObject(part) && typeof part.text === "string" && part.type === "input_text" ? [part.text] : [],
237
+ )
238
+ .join("\n");
186
239
  }
187
240
 
188
241
  function hasMedia(item: JsonObject): boolean {
189
- return (
190
- Array.isArray(item.content) &&
191
- item.content.some((part) => isObject(part) && part.type === "input_image")
192
- );
242
+ return Array.isArray(item.content) && item.content.some((part) => isObject(part) && part.type === "input_image");
193
243
  }
194
244
 
195
245
  function truncateTextItem(item: JsonObject, maxChars: number): JsonObject | undefined {
196
- if (!Array.isArray(item.content) || maxChars <= 32) return undefined;
197
- let remaining = maxChars - 16;
198
- const content = [...item.content].reverse().flatMap((part) => {
199
- if (
200
- !isObject(part) ||
201
- part.type !== "input_text" ||
202
- typeof part.text !== "string" ||
203
- remaining <= 0
204
- ) {
205
- return [];
206
- }
207
- const text = part.text.slice(-remaining);
208
- remaining -= text.length;
209
- return [{ ...part, text: `[truncated]\n${text}` }];
210
- });
211
- if (content.length === 0) return undefined;
212
- return { ...item, content: content.reverse() };
246
+ if (!Array.isArray(item.content) || maxChars <= 32) return undefined;
247
+ let remaining = maxChars - 16;
248
+ const content = [...item.content].reverse().flatMap((part) => {
249
+ if (!isObject(part) || part.type !== "input_text" || typeof part.text !== "string" || remaining <= 0) {
250
+ return [];
251
+ }
252
+ const text = part.text.slice(-remaining);
253
+ remaining -= text.length;
254
+ return [{ ...part, text: `[truncated]\n${text}` }];
255
+ });
256
+ if (content.length === 0) return undefined;
257
+ return { ...item, content: content.reverse() };
213
258
  }
214
259
 
215
260
  export function buildReplacementHistory(
216
- input: readonly unknown[],
217
- compactionItem: JsonObject,
218
- options: { tokenBudget?: number; byteBudget?: number } = {},
261
+ input: readonly unknown[],
262
+ compactionItem: JsonObject,
263
+ options: { tokenBudget?: number; byteBudget?: number } = {},
219
264
  ): JsonObject[] {
220
- const tokenBudget = options.tokenBudget ?? REPLACEMENT_TOKEN_BUDGET;
221
- const byteBudget = options.byteBudget ?? REPLACEMENT_BYTE_BUDGET;
222
- const opaque = validateCompactionItem(compactionItem);
223
- let remainingBytes = byteBudget - serializedBytes(opaque);
224
- let remainingChars = tokenBudget * 4;
225
- if (remainingBytes <= 0)
226
- throw new Error("Opaque compaction item exceeds replacement history budget");
227
- const retainedNewestFirst: JsonObject[] = [];
228
- const candidates = input.filter(
229
- (item): item is JsonObject =>
230
- isObject(item) && item.role === "user" && item.type !== "compaction_trigger",
231
- );
232
- for (let index = candidates.length - 1; index >= 0; index--) {
233
- const candidate = candidates[index];
234
- const bytes = serializedBytes(candidate);
235
- if (hasMedia(candidate) && bytes > MAX_MEDIA_ITEM_BYTES) continue;
236
- const text = rawText(candidate);
237
- let retained = candidate;
238
- if (text.length > remainingChars) {
239
- if (hasMedia(candidate)) continue;
240
- const truncated = truncateTextItem(candidate, remainingChars);
241
- if (!truncated) continue;
242
- retained = truncated;
243
- }
244
- if (serializedBytes(retained) > remainingBytes) {
245
- if (hasMedia(retained)) continue;
246
- const maxCharsByBytes = Math.max(0, remainingBytes - 128);
247
- const truncated = truncateTextItem(retained, Math.min(remainingChars, maxCharsByBytes));
248
- if (!truncated || serializedBytes(truncated) > remainingBytes) continue;
249
- retained = truncated;
250
- }
251
- retainedNewestFirst.push(structuredClone(retained));
252
- remainingBytes -= serializedBytes(retained);
253
- remainingChars -= Math.min(remainingChars, rawText(retained).length);
254
- if (remainingBytes <= 128 || remainingChars <= 32) break;
255
- }
256
- return [...retainedNewestFirst.reverse(), opaque];
265
+ const tokenBudget = options.tokenBudget ?? REPLACEMENT_TOKEN_BUDGET;
266
+ const byteBudget = options.byteBudget ?? REPLACEMENT_BYTE_BUDGET;
267
+ const opaque = validateCompactionItem(compactionItem);
268
+ let remainingBytes = byteBudget - serializedBytes(opaque);
269
+ let remainingChars = tokenBudget * 4;
270
+ if (remainingBytes <= 0) throw new Error("Opaque compaction item exceeds replacement history budget");
271
+ const retainedNewestFirst: JsonObject[] = [];
272
+ const candidates = input.filter(
273
+ (item): item is JsonObject => isObject(item) && item.role === "user" && item.type !== "compaction_trigger",
274
+ );
275
+ for (let index = candidates.length - 1; index >= 0; index--) {
276
+ const candidate = candidates[index];
277
+ const bytes = serializedBytes(candidate);
278
+ if (hasMedia(candidate) && bytes > MAX_MEDIA_ITEM_BYTES) continue;
279
+ const text = rawText(candidate);
280
+ let retained = candidate;
281
+ if (text.length > remainingChars) {
282
+ if (hasMedia(candidate)) continue;
283
+ const truncated = truncateTextItem(candidate, remainingChars);
284
+ if (!truncated) continue;
285
+ retained = truncated;
286
+ }
287
+ if (serializedBytes(retained) > remainingBytes) {
288
+ if (hasMedia(retained)) continue;
289
+ const maxCharsByBytes = Math.max(0, remainingBytes - 128);
290
+ const truncated = truncateTextItem(retained, Math.min(remainingChars, maxCharsByBytes));
291
+ if (!truncated || serializedBytes(truncated) > remainingBytes) continue;
292
+ retained = truncated;
293
+ }
294
+ retainedNewestFirst.push(structuredClone(retained));
295
+ remainingBytes -= serializedBytes(retained);
296
+ remainingChars -= Math.min(remainingChars, rawText(retained).length);
297
+ if (remainingBytes <= 128 || remainingChars <= 32) break;
298
+ }
299
+ return [...retainedNewestFirst.reverse(), opaque];
257
300
  }
258
301
 
259
302
  export function createCheckpointDetails(input: {
260
- provider: string;
261
- modelId: string;
262
- replacementHistory: JsonObject[];
263
- keptMessages: readonly AgentMessage[];
264
- checkpointId?: string;
265
- createdAt?: string;
303
+ provider: string;
304
+ api: Api;
305
+ profile: ResponsesCompactionProfile;
306
+ modelId: string;
307
+ protocol: RemoteCompactionProtocol;
308
+ replacementHistory: JsonObject[];
309
+ keptMessages: readonly AgentMessage[];
310
+ checkpointId?: string;
311
+ createdAt?: string;
266
312
  }): CodexCheckpointDetails {
267
- const details: CodexCheckpointDetails = {
268
- kind: CHECKPOINT_KIND,
269
- version: CHECKPOINT_VERSION,
270
- checkpointId: input.checkpointId ?? randomUUID(),
271
- provider: input.provider,
272
- api: "openai-codex-responses",
273
- modelId: input.modelId,
274
- protocol: "remote-compaction-v2",
275
- replacementHistory: structuredClone(input.replacementHistory),
276
- keptMessageFingerprints: input.keptMessages.map(fingerprintMessage),
277
- createdAt: input.createdAt ?? new Date().toISOString(),
278
- };
279
- const parsed = parseCheckpointDetails(details);
280
- if (!parsed) throw new Error("Created an invalid Codex checkpoint");
281
- return parsed;
313
+ const details: CodexCheckpointDetails = {
314
+ kind: CHECKPOINT_KIND,
315
+ version: CHECKPOINT_VERSION,
316
+ checkpointId: input.checkpointId ?? randomUUID(),
317
+ provider: input.provider,
318
+ api: input.api,
319
+ profile: input.profile,
320
+ modelId: input.modelId,
321
+ protocol: input.protocol,
322
+ replacementHistory: structuredClone(input.replacementHistory),
323
+ keptMessageFingerprints: input.keptMessages.map(fingerprintMessage),
324
+ createdAt: input.createdAt ?? new Date().toISOString(),
325
+ };
326
+ const parsed = parseCheckpointDetails(details);
327
+ if (!parsed) throw new Error("Created an invalid Codex checkpoint");
328
+ return parsed;
282
329
  }