pi-openai-toolkit 0.20.7 → 0.21.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/LICENSE +21 -21
- package/NOTICE +83 -83
- package/README.md +8 -26
- package/README.zh.md +8 -26
- package/config.schema.json +278 -366
- package/docs/configuration.md +294 -310
- package/docs/internals.md +166 -175
- package/package.json +3 -13
- package/src/compact-client.ts +412 -389
- package/src/compaction-errors.ts +96 -0
- package/src/compaction-projection.ts +35 -35
- package/src/config/legacy.ts +519 -717
- package/src/config/migration.ts +103 -100
- package/src/config/notifications.ts +36 -28
- package/src/config/policy.ts +10 -18
- package/src/config/v2.ts +1167 -1380
- package/src/config.ts +310 -319
- package/src/context-management/codex-provider.ts +270 -270
- package/src/context-management/history-notes.ts +357 -357
- package/src/context-management/manual-compact.ts +0 -11
- package/src/context-management/tools.ts +371 -371
- package/src/context-management/types.ts +168 -168
- package/src/context-management/window-manager.ts +801 -801
- package/src/debug.ts +238 -237
- package/src/deferred-tool-carryover.ts +282 -282
- package/src/details-store.ts +151 -151
- package/src/extension-runtime.ts +1662 -1623
- package/src/image-generation/extension.ts +189 -189
- package/src/image-generation/protocol.ts +579 -579
- package/src/image-generation/types.ts +300 -300
- package/src/native-fallback.ts +4 -132
- package/src/payload-rewrite.ts +515 -518
- package/src/pi-context-hook.ts +117 -117
- package/src/remote-v2-client.ts +811 -814
- package/src/request-context-cache.ts +155 -155
- package/src/responses-headers.ts +180 -180
- package/src/runtime.ts +865 -832
- package/src/serializer.ts +882 -882
- package/src/supported-environment.ts +27 -27
- package/src/types.ts +565 -648
- package/src/web-search/alpha-client.ts +876 -876
- package/src/web-search/extension.ts +530 -530
- package/src/web-search/payload.ts +299 -299
- package/src/web-search/prompt.ts +60 -60
- package/src/web-search/types.ts +166 -166
- package/tsconfig.check.json +50 -54
- package/src/auto-mode/classifier.ts +0 -161
- package/src/auto-mode/extension.ts +0 -691
- package/src/auto-mode/model-selection-guard.ts +0 -52
- package/src/auto-mode/policy.ts +0 -146
- package/src/auto-mode/prompt.ts +0 -279
- package/src/auto-mode/reviewer.ts +0 -325
- package/src/auto-mode/tool-review-tui.ts +0 -211
- package/src/auto-mode/transcript.ts +0 -135
- package/src/auto-mode/types.ts +0 -137
package/src/remote-v2-client.ts
CHANGED
|
@@ -1,814 +1,811 @@
|
|
|
1
|
-
import { isDeepStrictEqual } from "node:util";
|
|
2
|
-
import { writeDebugArtifact } from "./debug";
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import type {
|
|
6
|
-
import type {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
output
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
| "
|
|
35
|
-
| "
|
|
36
|
-
| "
|
|
37
|
-
| "
|
|
38
|
-
| "
|
|
39
|
-
| "
|
|
40
|
-
| "
|
|
41
|
-
| "
|
|
42
|
-
| "
|
|
43
|
-
| "
|
|
44
|
-
| "
|
|
45
|
-
| "invalid-
|
|
46
|
-
| "
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
(error instanceof
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
private
|
|
163
|
-
private
|
|
164
|
-
private
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
this.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
type
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
| "
|
|
272
|
-
| "
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
return
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function
|
|
289
|
-
if (!hasOwn(value,
|
|
290
|
-
return { valid: true };
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (!
|
|
338
|
-
return { ok: false, reason: "
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
return { ok: false, reason: "invalid-compaction-metadata" };
|
|
352
|
-
}
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
) {
|
|
358
|
-
return { ok: false, reason: "invalid-compaction-metadata" };
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
if (
|
|
435
|
-
return { ok: false, reason: "
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
return { ok: false, reason: "invalid-compaction-metadata", responseJson: completedResponse };
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
if (
|
|
520
|
-
itemDoneCandidate?.
|
|
521
|
-
terminalCandidate?.
|
|
522
|
-
itemDoneCandidate.
|
|
523
|
-
) {
|
|
524
|
-
return { ok: false, reason: "invalid-compaction-metadata", responseJson: terminalCandidate.item };
|
|
525
|
-
}
|
|
526
|
-
if (
|
|
527
|
-
itemDoneCandidate?.outputIndex !== undefined &&
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
itemDoneCandidate.
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
responseEnvelope
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
);
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
)
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
return failure;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { writeDebugArtifact } from "./debug";
|
|
3
|
+
import { decodeCompactionError, readCompactionResponseText, type CompactionErrorInfo } from "./compaction-errors";
|
|
4
|
+
import { buildResponsesRequestHeaders } from "./responses-headers";
|
|
5
|
+
import type { NativeCompactionRuntime } from "./runtime";
|
|
6
|
+
import type { NativeCompactionRequestBody, ResponsesInputItem } from "./serializer";
|
|
7
|
+
import type { ArtifactContext, CompactionConfig } from "./types";
|
|
8
|
+
|
|
9
|
+
const JSON_CONTENT_TYPE = "application/json";
|
|
10
|
+
const SSE_CONTENT_TYPE = "text/event-stream";
|
|
11
|
+
|
|
12
|
+
export type RemoteV2CompactionItem = Record<string, unknown> & {
|
|
13
|
+
type: "compaction";
|
|
14
|
+
encrypted_content: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type RemoteV2ResponseUsage = {
|
|
18
|
+
input_tokens?: number;
|
|
19
|
+
output_tokens?: number;
|
|
20
|
+
total_tokens?: number;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type RemoteV2ResponseEnvelope = Record<string, unknown> & {
|
|
25
|
+
id?: string;
|
|
26
|
+
created_at?: number | string;
|
|
27
|
+
status: "completed";
|
|
28
|
+
/** Normalized output always includes the reconciled checkpoint. */
|
|
29
|
+
output: unknown[];
|
|
30
|
+
usage?: RemoteV2ResponseUsage;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type RemoteV2CompactionClientFailureReason =
|
|
34
|
+
| "aborted"
|
|
35
|
+
| "network-error"
|
|
36
|
+
| "non-2xx"
|
|
37
|
+
| "empty-body"
|
|
38
|
+
| "invalid-sse"
|
|
39
|
+
| "error-event"
|
|
40
|
+
| "missing-completed-event"
|
|
41
|
+
| "incomplete-response"
|
|
42
|
+
| "invalid-compaction-count"
|
|
43
|
+
| "malformed-compaction-item"
|
|
44
|
+
| "conflicting-compaction-item"
|
|
45
|
+
| "invalid-compaction-metadata"
|
|
46
|
+
| "invalid-event-order"
|
|
47
|
+
| "duplicate-completed-event";
|
|
48
|
+
|
|
49
|
+
export type RemoteV2CompactionClientSuccess = {
|
|
50
|
+
ok: true;
|
|
51
|
+
status: number;
|
|
52
|
+
compactedWindow: [RemoteV2CompactionItem];
|
|
53
|
+
compactResponseId?: string;
|
|
54
|
+
createdAt?: string;
|
|
55
|
+
usage?: RemoteV2ResponseUsage;
|
|
56
|
+
response: RemoteV2ResponseEnvelope;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type RemoteV2CompactionClientFailure = CompactionErrorInfo & {
|
|
60
|
+
ok: false;
|
|
61
|
+
reason: RemoteV2CompactionClientFailureReason;
|
|
62
|
+
status?: number;
|
|
63
|
+
errorMessage?: string;
|
|
64
|
+
responseText?: string;
|
|
65
|
+
responseJson?: unknown;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type RemoteV2CompactionClientResult = RemoteV2CompactionClientSuccess | RemoteV2CompactionClientFailure;
|
|
69
|
+
|
|
70
|
+
export type RemoteV2CompactionRequestBody = Omit<NativeCompactionRequestBody, "input"> & {
|
|
71
|
+
input: [...ResponsesInputItem[], { type: "compaction_trigger" }];
|
|
72
|
+
store: false;
|
|
73
|
+
stream: true;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type ExecuteRemoteV2CompactionOptions = {
|
|
77
|
+
runtime: NativeCompactionRuntime;
|
|
78
|
+
request: NativeCompactionRequestBody;
|
|
79
|
+
signal?: AbortSignal;
|
|
80
|
+
settings?: CompactionConfig;
|
|
81
|
+
context?: ArtifactContext;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
type ParsedSseEvent = {
|
|
85
|
+
event?: string;
|
|
86
|
+
dataText: string;
|
|
87
|
+
data?: unknown;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
91
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isAbortError(error: unknown): boolean {
|
|
95
|
+
return (
|
|
96
|
+
(error instanceof DOMException && error.name === "AbortError") ||
|
|
97
|
+
(error instanceof Error && (error.name === "AbortError" || error.name === "ABORT_ERR"))
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
102
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeResponseTimestamp(value: unknown): string | undefined {
|
|
106
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
107
|
+
const milliseconds = value > 1_000_000_000_000 ? value : value * 1000;
|
|
108
|
+
return new Date(milliseconds).toISOString();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (typeof value !== "string") {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const trimmed = value.trim();
|
|
116
|
+
if (!trimmed) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const parsed = Date.parse(trimmed);
|
|
121
|
+
return Number.isNaN(parsed) ? trimmed : new Date(parsed).toISOString();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function toHeaders(runtime: NativeCompactionRuntime): Record<string, string> {
|
|
125
|
+
return Object.fromEntries(
|
|
126
|
+
buildResponsesRequestHeaders(runtime, {
|
|
127
|
+
accept: SSE_CONTENT_TYPE,
|
|
128
|
+
contentType: JSON_CONTENT_TYPE,
|
|
129
|
+
sessionId: runtime.sessionId,
|
|
130
|
+
}).entries(),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Codex `/responses` rejects histories that mix this Astra-only item into a
|
|
136
|
+
* compaction request (verified upstream in Oh My Pi: the compaction path is
|
|
137
|
+
* deliberately built outside the effort planner). The host never emits the
|
|
138
|
+
* item into a payload we would replay today, but the item is transparently
|
|
139
|
+
* carried by our opaque-window replay, so strip it defensively on every
|
|
140
|
+
* remote v2 request regardless of API family.
|
|
141
|
+
*/
|
|
142
|
+
export function stripConfigurationUpdateItems(input: readonly unknown[]): unknown[] {
|
|
143
|
+
return input.filter((item) => !isRecord(item) || item["type"] !== "configuration_update");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function buildRemoteV2CompactionRequest(
|
|
147
|
+
request: NativeCompactionRequestBody,
|
|
148
|
+
): RemoteV2CompactionRequestBody {
|
|
149
|
+
return {
|
|
150
|
+
...structuredClone(request),
|
|
151
|
+
input: [
|
|
152
|
+
...(stripConfigurationUpdateItems(structuredClone(request.input)) as ResponsesInputItem[]),
|
|
153
|
+
{ type: "compaction_trigger" },
|
|
154
|
+
],
|
|
155
|
+
store: false,
|
|
156
|
+
stream: true,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Shared line framing; yielding lazily keeps bytes after a terminal out of parsing. */
|
|
161
|
+
class SseEventParser {
|
|
162
|
+
private line = "";
|
|
163
|
+
private skipLf = false;
|
|
164
|
+
private event: string | undefined;
|
|
165
|
+
private dataLines: string[] = [];
|
|
166
|
+
|
|
167
|
+
*push(text: string): Generator<ParsedSseEvent> {
|
|
168
|
+
for (const character of text) {
|
|
169
|
+
if (this.skipLf) {
|
|
170
|
+
this.skipLf = false;
|
|
171
|
+
if (character === "\n") continue;
|
|
172
|
+
}
|
|
173
|
+
if (character !== "\r" && character !== "\n") {
|
|
174
|
+
this.line += character;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
this.skipLf = character === "\r";
|
|
178
|
+
const line = this.line;
|
|
179
|
+
this.line = "";
|
|
180
|
+
if (line === "") {
|
|
181
|
+
const event = this.event;
|
|
182
|
+
const dataLines = this.dataLines;
|
|
183
|
+
this.event = undefined;
|
|
184
|
+
this.dataLines = [];
|
|
185
|
+
if (dataLines.length === 0) continue;
|
|
186
|
+
const dataText = dataLines.join("\n");
|
|
187
|
+
yield dataText === "[DONE]"
|
|
188
|
+
? { event, dataText }
|
|
189
|
+
: { event, dataText, data: JSON.parse(dataText) };
|
|
190
|
+
} else if (line.startsWith("event:")) {
|
|
191
|
+
this.event = line.slice("event:".length).trim();
|
|
192
|
+
} else if (line === "data" || line.startsWith("data:")) {
|
|
193
|
+
this.dataLines.push(line.slice("data:".length).replace(/^ /, ""));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function parseSseEvents(raw: string): ParsedSseEvent[] | undefined {
|
|
200
|
+
try {
|
|
201
|
+
// Retain the buffered helper's historical support for a final unterminated block.
|
|
202
|
+
const events = [...new SseEventParser().push(`${raw}\n\n`)];
|
|
203
|
+
return events.length > 0 ? events : undefined;
|
|
204
|
+
} catch {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isRemoteV2CompactionItem(value: unknown): value is RemoteV2CompactionItem {
|
|
210
|
+
return isRecord(value) && value.type === "compaction" && isNonEmptyString(value.encrypted_content);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function getEventType(event: ParsedSseEvent): string | undefined {
|
|
214
|
+
return isRecord(event.data) && isNonEmptyString(event.data.type) ? event.data.type : event.event;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function writeCompactArtifact(
|
|
218
|
+
data: unknown,
|
|
219
|
+
settings: CompactionConfig | undefined,
|
|
220
|
+
context: ArtifactContext | undefined,
|
|
221
|
+
): void {
|
|
222
|
+
if (!settings || !context) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
writeDebugArtifact("compact-response", data, settings, context);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function summarizeEvents(events: readonly ParsedSseEvent[]): unknown[] {
|
|
230
|
+
return events.map((event) => {
|
|
231
|
+
const data = isRecord(event.data) ? event.data : undefined;
|
|
232
|
+
const response = data && isRecord(data.response) ? data.response : undefined;
|
|
233
|
+
const item = data && isRecord(data.item) ? data.item : undefined;
|
|
234
|
+
return {
|
|
235
|
+
event: event.event,
|
|
236
|
+
type: data?.type,
|
|
237
|
+
responseStatus: response?.status,
|
|
238
|
+
responseId: response?.id,
|
|
239
|
+
itemType: item?.type,
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
type OptionalMetadata<T> =
|
|
245
|
+
| { valid: true; value?: T }
|
|
246
|
+
| { valid: false };
|
|
247
|
+
|
|
248
|
+
type CompactionCheckpointCandidate = {
|
|
249
|
+
item: RemoteV2CompactionItem;
|
|
250
|
+
responseId?: string;
|
|
251
|
+
outputIndex?: number;
|
|
252
|
+
outputPosition?: number;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
type CheckpointReconciliation =
|
|
256
|
+
| {
|
|
257
|
+
ok: true;
|
|
258
|
+
response: RemoteV2ResponseEnvelope;
|
|
259
|
+
compactedWindow: [RemoteV2CompactionItem];
|
|
260
|
+
}
|
|
261
|
+
| {
|
|
262
|
+
ok: false;
|
|
263
|
+
reason:
|
|
264
|
+
| "error-event"
|
|
265
|
+
| "missing-completed-event"
|
|
266
|
+
| "incomplete-response"
|
|
267
|
+
| "invalid-compaction-count"
|
|
268
|
+
| "malformed-compaction-item"
|
|
269
|
+
| "conflicting-compaction-item"
|
|
270
|
+
| "invalid-compaction-metadata"
|
|
271
|
+
| "invalid-event-order"
|
|
272
|
+
| "duplicate-completed-event";
|
|
273
|
+
responseJson?: unknown;
|
|
274
|
+
errorMessage?: string;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
function hasOwn(value: Record<string, unknown>, key: string): boolean {
|
|
278
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function readOptionalNonEmptyString(value: Record<string, unknown>, key: string): OptionalMetadata<string> {
|
|
282
|
+
if (!hasOwn(value, key)) {
|
|
283
|
+
return { valid: true };
|
|
284
|
+
}
|
|
285
|
+
return isNonEmptyString(value[key]) ? { valid: true, value: value[key] } : { valid: false };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function readOptionalOutputIndex(value: Record<string, unknown>): OptionalMetadata<number> {
|
|
289
|
+
if (!hasOwn(value, "output_index")) {
|
|
290
|
+
return { valid: true };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const outputIndex = value.output_index;
|
|
294
|
+
return typeof outputIndex === "number" && Number.isSafeInteger(outputIndex) && outputIndex >= 0
|
|
295
|
+
? { valid: true, value: outputIndex }
|
|
296
|
+
: { valid: false };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function mergeCompactionItems(
|
|
300
|
+
itemDone: RemoteV2CompactionItem,
|
|
301
|
+
terminalOutput: RemoteV2CompactionItem,
|
|
302
|
+
): RemoteV2CompactionItem | undefined {
|
|
303
|
+
for (const [key, value] of Object.entries(itemDone)) {
|
|
304
|
+
if (hasOwn(terminalOutput, key) && !isDeepStrictEqual(value, terminalOutput[key])) return undefined;
|
|
305
|
+
}
|
|
306
|
+
return structuredClone({ ...itemDone, ...terminalOutput });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function readResponseId(value: Record<string, unknown>): OptionalMetadata<string> {
|
|
310
|
+
const direct = readOptionalNonEmptyString(value, "response_id");
|
|
311
|
+
if (!direct.valid) {
|
|
312
|
+
return direct;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const nestedResponse: OptionalMetadata<string> = isRecord(value.response) ? readOptionalNonEmptyString(value.response, "id") : { valid: true };
|
|
316
|
+
if (!nestedResponse.valid) {
|
|
317
|
+
return nestedResponse;
|
|
318
|
+
}
|
|
319
|
+
if (direct.value !== undefined && nestedResponse.value !== undefined && direct.value !== nestedResponse.value) {
|
|
320
|
+
return { valid: false };
|
|
321
|
+
}
|
|
322
|
+
return { valid: true, value: direct.value ?? nestedResponse.value };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function readCheckpointCandidate(
|
|
326
|
+
item: Record<string, unknown>,
|
|
327
|
+
metadataSource: Record<string, unknown> | undefined,
|
|
328
|
+
outputPosition?: number,
|
|
329
|
+
): { ok: true; candidate: CompactionCheckpointCandidate } | { ok: false; reason: "malformed-compaction-item" | "invalid-compaction-metadata" } {
|
|
330
|
+
if (!isRemoteV2CompactionItem(item)) {
|
|
331
|
+
return { ok: false, reason: "malformed-compaction-item" };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const itemId = readOptionalNonEmptyString(item, "id");
|
|
335
|
+
const itemResponseId = readResponseId(item);
|
|
336
|
+
const itemOutputIndex = readOptionalOutputIndex(item);
|
|
337
|
+
if (!itemId.valid || !itemResponseId.valid || !itemOutputIndex.valid) {
|
|
338
|
+
return { ok: false, reason: "invalid-compaction-metadata" };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const sourceResponseId: OptionalMetadata<string> = metadataSource ? readResponseId(metadataSource) : { valid: true };
|
|
342
|
+
const sourceOutputIndex: OptionalMetadata<number> = metadataSource ? readOptionalOutputIndex(metadataSource) : { valid: true };
|
|
343
|
+
if (!sourceResponseId.valid || !sourceOutputIndex.valid) {
|
|
344
|
+
return { ok: false, reason: "invalid-compaction-metadata" };
|
|
345
|
+
}
|
|
346
|
+
if (
|
|
347
|
+
itemResponseId.value !== undefined &&
|
|
348
|
+
sourceResponseId.value !== undefined &&
|
|
349
|
+
itemResponseId.value !== sourceResponseId.value
|
|
350
|
+
) {
|
|
351
|
+
return { ok: false, reason: "invalid-compaction-metadata" };
|
|
352
|
+
}
|
|
353
|
+
if (
|
|
354
|
+
itemOutputIndex.value !== undefined &&
|
|
355
|
+
sourceOutputIndex.value !== undefined &&
|
|
356
|
+
itemOutputIndex.value !== sourceOutputIndex.value
|
|
357
|
+
) {
|
|
358
|
+
return { ok: false, reason: "invalid-compaction-metadata" };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const outputIndex = sourceOutputIndex.value ?? itemOutputIndex.value;
|
|
362
|
+
if (outputPosition !== undefined && outputIndex !== undefined && outputIndex !== outputPosition) {
|
|
363
|
+
return { ok: false, reason: "invalid-compaction-metadata" };
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
ok: true,
|
|
367
|
+
candidate: {
|
|
368
|
+
item: structuredClone(item),
|
|
369
|
+
responseId: sourceResponseId.value ?? itemResponseId.value,
|
|
370
|
+
outputIndex,
|
|
371
|
+
outputPosition,
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function isPostTerminalEvent(event: ParsedSseEvent): boolean {
|
|
377
|
+
if (event.dataText === "[DONE]") {
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
return getEventType(event) === "keepalive";
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function reconcileSseCheckpoint(events: readonly ParsedSseEvent[]): CheckpointReconciliation {
|
|
384
|
+
const errorEvent = events.find((event) => {
|
|
385
|
+
const type = getEventType(event);
|
|
386
|
+
return type === "error" || type === "response.failed" || type === "response.incomplete";
|
|
387
|
+
});
|
|
388
|
+
if (errorEvent) {
|
|
389
|
+
return {
|
|
390
|
+
ok: false,
|
|
391
|
+
reason: "error-event",
|
|
392
|
+
responseJson: errorEvent.data,
|
|
393
|
+
errorMessage: decodeCompactionError(errorEvent.data).errorMessage,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const completedIndexes = events.flatMap((event, index) => (getEventType(event) === "response.completed" ? [index] : []));
|
|
398
|
+
if (completedIndexes.length === 0) {
|
|
399
|
+
return { ok: false, reason: "missing-completed-event" };
|
|
400
|
+
}
|
|
401
|
+
if (completedIndexes.length > 1) {
|
|
402
|
+
return { ok: false, reason: "duplicate-completed-event" };
|
|
403
|
+
}
|
|
404
|
+
const terminalIndex = completedIndexes[0]!;
|
|
405
|
+
|
|
406
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
407
|
+
const event = events[index]!;
|
|
408
|
+
if (index < terminalIndex && event.dataText === "[DONE]") {
|
|
409
|
+
return { ok: false, reason: "invalid-event-order", responseJson: event.dataText };
|
|
410
|
+
}
|
|
411
|
+
if (index > terminalIndex && !isPostTerminalEvent(event)) {
|
|
412
|
+
return { ok: false, reason: "invalid-event-order", responseJson: event.data };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const completedEvent = events[terminalIndex]!;
|
|
417
|
+
const completedData = isRecord(completedEvent.data) ? completedEvent.data : undefined;
|
|
418
|
+
const completedResponse = completedData && isRecord(completedData.response) ? completedData.response : undefined;
|
|
419
|
+
if (!completedResponse) {
|
|
420
|
+
return { ok: false, reason: "incomplete-response" };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (completedResponse.status !== "completed") {
|
|
424
|
+
return { ok: false, reason: "incomplete-response", responseJson: completedResponse };
|
|
425
|
+
}
|
|
426
|
+
const hasOutput = hasOwn(completedResponse, "output");
|
|
427
|
+
if (hasOutput && !Array.isArray(completedResponse.output)) {
|
|
428
|
+
return { ok: false, reason: "incomplete-response", responseJson: completedResponse };
|
|
429
|
+
}
|
|
430
|
+
const terminalOutput: unknown[] | undefined = Array.isArray(completedResponse.output) ? completedResponse.output : undefined;
|
|
431
|
+
|
|
432
|
+
const terminalResponseId = readOptionalNonEmptyString(completedResponse, "id");
|
|
433
|
+
const completedEventResponseId: OptionalMetadata<string> = completedData ? readResponseId(completedData) : { valid: true };
|
|
434
|
+
if (!terminalResponseId.valid || !completedEventResponseId.valid) {
|
|
435
|
+
return { ok: false, reason: "invalid-compaction-metadata", responseJson: completedResponse };
|
|
436
|
+
}
|
|
437
|
+
if (
|
|
438
|
+
terminalResponseId.value !== undefined &&
|
|
439
|
+
completedEventResponseId.value !== undefined &&
|
|
440
|
+
terminalResponseId.value !== completedEventResponseId.value
|
|
441
|
+
) {
|
|
442
|
+
return { ok: false, reason: "invalid-compaction-metadata", responseJson: completedResponse };
|
|
443
|
+
}
|
|
444
|
+
let responseId = terminalResponseId.value ?? completedEventResponseId.value;
|
|
445
|
+
for (const event of events.slice(0, terminalIndex)) {
|
|
446
|
+
if (getEventType(event) !== "response.created" || !isRecord(event.data)) continue;
|
|
447
|
+
const createdId = readResponseId(event.data);
|
|
448
|
+
if (!createdId.valid || (createdId.value !== undefined && responseId !== undefined && createdId.value !== responseId)) {
|
|
449
|
+
return { ok: false, reason: "invalid-compaction-metadata", responseJson: event.data };
|
|
450
|
+
}
|
|
451
|
+
responseId ??= createdId.value;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
let itemDoneCandidate: CompactionCheckpointCandidate | undefined;
|
|
455
|
+
for (let index = 0; index < terminalIndex; index += 1) {
|
|
456
|
+
const event = events[index]!;
|
|
457
|
+
if (getEventType(event) !== "response.output_item.done") {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (!isRecord(event.data) || !isRecord(event.data.item)) {
|
|
461
|
+
return { ok: false, reason: "malformed-compaction-item", responseJson: event.data };
|
|
462
|
+
}
|
|
463
|
+
if (event.data.item.type !== "compaction") {
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (itemDoneCandidate) {
|
|
467
|
+
return { ok: false, reason: "invalid-compaction-count", responseJson: event.data.item };
|
|
468
|
+
}
|
|
469
|
+
const candidate = readCheckpointCandidate(event.data.item, event.data);
|
|
470
|
+
if (!candidate.ok) {
|
|
471
|
+
return { ok: false, reason: candidate.reason, responseJson: event.data.item };
|
|
472
|
+
}
|
|
473
|
+
itemDoneCandidate = candidate.candidate;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
let terminalCandidate: CompactionCheckpointCandidate | undefined;
|
|
477
|
+
let terminalPosition: number | undefined;
|
|
478
|
+
if (terminalOutput) {
|
|
479
|
+
const compactionPositions = terminalOutput.flatMap((item, index) =>
|
|
480
|
+
isRecord(item) && item.type === "compaction" ? [index] : [],
|
|
481
|
+
);
|
|
482
|
+
if (compactionPositions.length > 1) {
|
|
483
|
+
return { ok: false, reason: "invalid-compaction-count", responseJson: completedResponse };
|
|
484
|
+
}
|
|
485
|
+
if (compactionPositions.length === 1) {
|
|
486
|
+
terminalPosition = compactionPositions[0]!;
|
|
487
|
+
const outputItem = terminalOutput[terminalPosition];
|
|
488
|
+
if (!isRecord(outputItem)) {
|
|
489
|
+
return { ok: false, reason: "malformed-compaction-item", responseJson: outputItem };
|
|
490
|
+
}
|
|
491
|
+
const candidate = readCheckpointCandidate(outputItem, undefined, terminalPosition);
|
|
492
|
+
if (!candidate.ok) {
|
|
493
|
+
return { ok: false, reason: candidate.reason, responseJson: outputItem };
|
|
494
|
+
}
|
|
495
|
+
terminalCandidate = candidate.candidate;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (!itemDoneCandidate && !terminalCandidate) {
|
|
500
|
+
return { ok: false, reason: "invalid-compaction-count", responseJson: completedResponse };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
for (const candidate of [itemDoneCandidate, terminalCandidate]) {
|
|
504
|
+
if (
|
|
505
|
+
candidate?.responseId !== undefined &&
|
|
506
|
+
responseId !== undefined &&
|
|
507
|
+
candidate.responseId !== responseId
|
|
508
|
+
) {
|
|
509
|
+
return { ok: false, reason: "invalid-compaction-metadata", responseJson: candidate.item };
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (
|
|
513
|
+
itemDoneCandidate?.responseId !== undefined &&
|
|
514
|
+
terminalCandidate?.responseId !== undefined &&
|
|
515
|
+
itemDoneCandidate.responseId !== terminalCandidate.responseId
|
|
516
|
+
) {
|
|
517
|
+
return { ok: false, reason: "invalid-compaction-metadata", responseJson: terminalCandidate.item };
|
|
518
|
+
}
|
|
519
|
+
if (
|
|
520
|
+
itemDoneCandidate?.outputIndex !== undefined &&
|
|
521
|
+
terminalCandidate?.outputPosition !== undefined &&
|
|
522
|
+
itemDoneCandidate.outputIndex !== terminalCandidate.outputPosition
|
|
523
|
+
) {
|
|
524
|
+
return { ok: false, reason: "invalid-compaction-metadata", responseJson: terminalCandidate.item };
|
|
525
|
+
}
|
|
526
|
+
if (
|
|
527
|
+
itemDoneCandidate?.outputIndex !== undefined &&
|
|
528
|
+
terminalOutput !== undefined &&
|
|
529
|
+
terminalOutput.length > 0 &&
|
|
530
|
+
terminalCandidate === undefined &&
|
|
531
|
+
itemDoneCandidate.outputIndex > terminalOutput.length
|
|
532
|
+
) {
|
|
533
|
+
return { ok: false, reason: "invalid-compaction-metadata", responseJson: completedResponse };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
let canonicalItem: RemoteV2CompactionItem;
|
|
537
|
+
if (itemDoneCandidate && terminalCandidate) {
|
|
538
|
+
const merged = mergeCompactionItems(itemDoneCandidate.item, terminalCandidate.item);
|
|
539
|
+
if (!merged) {
|
|
540
|
+
return { ok: false, reason: "conflicting-compaction-item", responseJson: terminalCandidate.item };
|
|
541
|
+
}
|
|
542
|
+
canonicalItem = merged;
|
|
543
|
+
} else {
|
|
544
|
+
canonicalItem = structuredClone((itemDoneCandidate ?? terminalCandidate)!.item);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const responseEnvelope = structuredClone(completedResponse) as RemoteV2ResponseEnvelope;
|
|
548
|
+
if (terminalOutput) {
|
|
549
|
+
const output = structuredClone(terminalOutput);
|
|
550
|
+
if (terminalPosition !== undefined) {
|
|
551
|
+
output[terminalPosition] = structuredClone(canonicalItem);
|
|
552
|
+
} else {
|
|
553
|
+
const insertionIndex = itemDoneCandidate?.outputIndex ?? output.length;
|
|
554
|
+
output.splice(insertionIndex, 0, structuredClone(canonicalItem));
|
|
555
|
+
}
|
|
556
|
+
responseEnvelope.output = output;
|
|
557
|
+
} else {
|
|
558
|
+
responseEnvelope.output = [structuredClone(canonicalItem)];
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
ok: true,
|
|
563
|
+
response: responseEnvelope,
|
|
564
|
+
compactedWindow: [structuredClone(canonicalItem)],
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
type CollectedSse = {
|
|
569
|
+
responseText: string;
|
|
570
|
+
events?: ParsedSseEvent[];
|
|
571
|
+
reconciliation?: CheckpointReconciliation;
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
async function collectSseCheckpoint(response: Response, signal?: AbortSignal): Promise<CollectedSse> {
|
|
575
|
+
const reader = response.body?.getReader();
|
|
576
|
+
if (!reader) {
|
|
577
|
+
if (signal?.aborted) throw new DOMException("Compaction aborted", "AbortError");
|
|
578
|
+
return { responseText: "" };
|
|
579
|
+
}
|
|
580
|
+
const decoder = new TextDecoder();
|
|
581
|
+
const parser = new SseEventParser();
|
|
582
|
+
const events: ParsedSseEvent[] = [];
|
|
583
|
+
const text: string[] = [];
|
|
584
|
+
let onAbort: (() => void) | undefined;
|
|
585
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
586
|
+
onAbort = () => reject(new DOMException("Compaction aborted", "AbortError"));
|
|
587
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
588
|
+
});
|
|
589
|
+
try {
|
|
590
|
+
while (true) {
|
|
591
|
+
if (signal?.aborted) throw new DOMException("Compaction aborted", "AbortError");
|
|
592
|
+
const chunk = await Promise.race([reader.read(), aborted]);
|
|
593
|
+
if (signal?.aborted) throw new DOMException("Compaction aborted", "AbortError");
|
|
594
|
+
const decoded = chunk.done ? decoder.decode() : decoder.decode(chunk.value, { stream: true });
|
|
595
|
+
text.push(decoded);
|
|
596
|
+
try {
|
|
597
|
+
for (const event of parser.push(decoded)) {
|
|
598
|
+
events.push(event);
|
|
599
|
+
const type = getEventType(event);
|
|
600
|
+
if (type === "response.completed" || type === "error" || type === "response.failed" || type === "response.incomplete") {
|
|
601
|
+
// Validate synchronously before cleanup; later cancellation cannot undo this boundary.
|
|
602
|
+
return { responseText: "", events, reconciliation: reconcileSseCheckpoint(events) };
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
} catch (error) {
|
|
606
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
607
|
+
return { responseText: text.join("") };
|
|
608
|
+
}
|
|
609
|
+
// EOF never dispatches an unfinished frame, even if its JSON happens to be complete.
|
|
610
|
+
if (chunk.done) return { responseText: text.join(""), events: events.length ? events : undefined };
|
|
611
|
+
}
|
|
612
|
+
} finally {
|
|
613
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
614
|
+
// Cancellation is best-effort cleanup, not part of the completion boundary.
|
|
615
|
+
// Observe rejection immediately and release even if the transport never settles cancel().
|
|
616
|
+
try {
|
|
617
|
+
void reader.cancel().catch(() => undefined);
|
|
618
|
+
} catch {
|
|
619
|
+
// A custom reader may throw synchronously during cleanup.
|
|
620
|
+
} finally {
|
|
621
|
+
try {
|
|
622
|
+
reader.releaseLock();
|
|
623
|
+
} catch {
|
|
624
|
+
// Cleanup failures must not replace a validated checkpoint or cancellation.
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export async function executeRemoteV2Compaction(
|
|
631
|
+
options: ExecuteRemoteV2CompactionOptions,
|
|
632
|
+
): Promise<RemoteV2CompactionClientResult> {
|
|
633
|
+
const { runtime, signal, settings, context } = options;
|
|
634
|
+
const request = buildRemoteV2CompactionRequest(options.request);
|
|
635
|
+
const headers = toHeaders(runtime);
|
|
636
|
+
|
|
637
|
+
if (signal?.aborted) {
|
|
638
|
+
const aborted: RemoteV2CompactionClientFailure = { ok: false, reason: "aborted" };
|
|
639
|
+
writeCompactArtifact(
|
|
640
|
+
{
|
|
641
|
+
protocol: "remote_compaction_v2",
|
|
642
|
+
request: { url: runtime.responsesUrl, headers, body: request },
|
|
643
|
+
outcome: aborted,
|
|
644
|
+
},
|
|
645
|
+
settings,
|
|
646
|
+
context,
|
|
647
|
+
);
|
|
648
|
+
return aborted;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
try {
|
|
652
|
+
const response = await fetch(runtime.responsesUrl, {
|
|
653
|
+
method: "POST",
|
|
654
|
+
headers,
|
|
655
|
+
body: JSON.stringify(request),
|
|
656
|
+
signal,
|
|
657
|
+
});
|
|
658
|
+
const responseHeaders: Record<string, string> = {};
|
|
659
|
+
response.headers.forEach((value, key) => {
|
|
660
|
+
responseHeaders[key] = value;
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
if (!response.ok) {
|
|
664
|
+
const responseText = await readCompactionResponseText(response, signal);
|
|
665
|
+
let responseJson: unknown;
|
|
666
|
+
if (responseText.trim()) {
|
|
667
|
+
try {
|
|
668
|
+
responseJson = JSON.parse(responseText);
|
|
669
|
+
} catch {
|
|
670
|
+
responseJson = undefined;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const failure: RemoteV2CompactionClientFailure = {
|
|
674
|
+
ok: false,
|
|
675
|
+
reason: "non-2xx",
|
|
676
|
+
status: response.status,
|
|
677
|
+
...decodeCompactionError(responseJson),
|
|
678
|
+
responseText: responseText || undefined,
|
|
679
|
+
responseJson,
|
|
680
|
+
};
|
|
681
|
+
writeCompactArtifact(
|
|
682
|
+
{
|
|
683
|
+
protocol: "remote_compaction_v2",
|
|
684
|
+
request: { url: runtime.responsesUrl, headers, body: request },
|
|
685
|
+
response: { status: response.status, headers: responseHeaders, body: responseJson ?? responseText },
|
|
686
|
+
outcome: failure,
|
|
687
|
+
},
|
|
688
|
+
settings,
|
|
689
|
+
context,
|
|
690
|
+
);
|
|
691
|
+
return failure;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const collected = await collectSseCheckpoint(response, signal);
|
|
695
|
+
const { responseText, events } = collected;
|
|
696
|
+
if (!events && !responseText.trim()) {
|
|
697
|
+
const failure: RemoteV2CompactionClientFailure = {
|
|
698
|
+
ok: false,
|
|
699
|
+
reason: "empty-body",
|
|
700
|
+
status: response.status,
|
|
701
|
+
};
|
|
702
|
+
writeCompactArtifact(
|
|
703
|
+
{
|
|
704
|
+
protocol: "remote_compaction_v2",
|
|
705
|
+
request: { url: runtime.responsesUrl, headers, body: request },
|
|
706
|
+
response: { status: response.status, headers: responseHeaders },
|
|
707
|
+
outcome: failure,
|
|
708
|
+
},
|
|
709
|
+
settings,
|
|
710
|
+
context,
|
|
711
|
+
);
|
|
712
|
+
return failure;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (!events) {
|
|
716
|
+
let bodyError: CompactionErrorInfo = {};
|
|
717
|
+
try { bodyError = decodeCompactionError(JSON.parse(responseText)); } catch { /* Not a JSON error. */ }
|
|
718
|
+
const failure: RemoteV2CompactionClientFailure = {
|
|
719
|
+
ok: false,
|
|
720
|
+
reason: "invalid-sse",
|
|
721
|
+
status: response.status,
|
|
722
|
+
...bodyError,
|
|
723
|
+
responseText,
|
|
724
|
+
};
|
|
725
|
+
writeCompactArtifact(
|
|
726
|
+
{
|
|
727
|
+
protocol: "remote_compaction_v2",
|
|
728
|
+
request: { url: runtime.responsesUrl, headers, body: request },
|
|
729
|
+
response: { status: response.status, headers: responseHeaders, body: responseText },
|
|
730
|
+
outcome: failure,
|
|
731
|
+
},
|
|
732
|
+
settings,
|
|
733
|
+
context,
|
|
734
|
+
);
|
|
735
|
+
return failure;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const reconciliation = collected.reconciliation ?? reconcileSseCheckpoint(events);
|
|
739
|
+
if (!reconciliation.ok) {
|
|
740
|
+
const failure: RemoteV2CompactionClientFailure = {
|
|
741
|
+
ok: false,
|
|
742
|
+
reason: reconciliation.reason,
|
|
743
|
+
status: response.status,
|
|
744
|
+
...decodeCompactionError(reconciliation.responseJson),
|
|
745
|
+
responseJson: reconciliation.responseJson,
|
|
746
|
+
};
|
|
747
|
+
writeCompactArtifact(
|
|
748
|
+
{
|
|
749
|
+
protocol: "remote_compaction_v2",
|
|
750
|
+
request: { url: runtime.responsesUrl, headers, body: request },
|
|
751
|
+
response: { status: response.status, headers: responseHeaders, events: summarizeEvents(events) },
|
|
752
|
+
outcome: failure,
|
|
753
|
+
},
|
|
754
|
+
settings,
|
|
755
|
+
context,
|
|
756
|
+
);
|
|
757
|
+
return failure;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const responseEnvelope = reconciliation.response;
|
|
761
|
+
const success: RemoteV2CompactionClientSuccess = {
|
|
762
|
+
ok: true,
|
|
763
|
+
status: response.status,
|
|
764
|
+
compactedWindow: reconciliation.compactedWindow,
|
|
765
|
+
compactResponseId: isNonEmptyString(responseEnvelope.id) ? responseEnvelope.id.trim() : undefined,
|
|
766
|
+
createdAt: normalizeResponseTimestamp(responseEnvelope.created_at),
|
|
767
|
+
usage: isRecord(responseEnvelope.usage) ? (structuredClone(responseEnvelope.usage) as RemoteV2ResponseUsage) : undefined,
|
|
768
|
+
response: responseEnvelope,
|
|
769
|
+
};
|
|
770
|
+
writeCompactArtifact(
|
|
771
|
+
{
|
|
772
|
+
protocol: "remote_compaction_v2",
|
|
773
|
+
request: { url: runtime.responsesUrl, headers, body: request },
|
|
774
|
+
response: {
|
|
775
|
+
status: response.status,
|
|
776
|
+
headers: responseHeaders,
|
|
777
|
+
events: summarizeEvents(events),
|
|
778
|
+
completed: responseEnvelope,
|
|
779
|
+
},
|
|
780
|
+
outcome: {
|
|
781
|
+
ok: true,
|
|
782
|
+
status: success.status,
|
|
783
|
+
compactResponseId: success.compactResponseId,
|
|
784
|
+
createdAt: success.createdAt,
|
|
785
|
+
compactedItems: 1,
|
|
786
|
+
},
|
|
787
|
+
},
|
|
788
|
+
settings,
|
|
789
|
+
context,
|
|
790
|
+
);
|
|
791
|
+
return success;
|
|
792
|
+
} catch (error) {
|
|
793
|
+
const failure: RemoteV2CompactionClientFailure = signal?.aborted || isAbortError(error)
|
|
794
|
+
? { ok: false, reason: "aborted" }
|
|
795
|
+
: {
|
|
796
|
+
ok: false,
|
|
797
|
+
reason: "network-error",
|
|
798
|
+
errorMessage: "Compaction transport failed; no remote checkpoint was produced.",
|
|
799
|
+
};
|
|
800
|
+
writeCompactArtifact(
|
|
801
|
+
{
|
|
802
|
+
protocol: "remote_compaction_v2",
|
|
803
|
+
request: { url: runtime.responsesUrl, headers, body: request },
|
|
804
|
+
outcome: failure,
|
|
805
|
+
},
|
|
806
|
+
settings,
|
|
807
|
+
context,
|
|
808
|
+
);
|
|
809
|
+
return failure;
|
|
810
|
+
}
|
|
811
|
+
}
|