pi-openai-toolkit 0.14.13 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +250 -241
- package/README.zh.md +250 -241
- package/config.schema.json +352 -0
- package/docs/configuration.md +176 -0
- package/docs/internals.md +139 -128
- package/package.json +135 -127
- package/src/auto-mode/extension.ts +35 -13
- package/src/config/legacy.ts +717 -0
- package/src/config/migration.ts +100 -0
- package/src/config/notifications.ts +28 -0
- package/src/config/policy.ts +122 -0
- package/src/config/v2.ts +1347 -0
- package/src/config-command.ts +73 -0
- package/src/config.ts +318 -739
- package/src/context-management/tools.ts +353 -347
- package/src/context-management/window-manager.ts +696 -693
- package/src/debug.ts +5 -5
- package/src/deferred-tool-carryover.ts +3 -1
- package/src/extension-runtime.ts +1375 -1329
- package/src/image-generation/extension.ts +14 -18
- package/src/image-generation/protocol.ts +10 -4
- package/src/image-generation/service.ts +9 -3
- package/src/request-context-cache.ts +2 -1
- package/src/types.ts +641 -628
- package/src/web-search/alpha-client.ts +876 -875
- package/src/web-search/extension.ts +529 -523
- package/src/web-search/payload.ts +299 -295
- package/src/web-search/prompt.ts +60 -59
- package/src/web-search/types.ts +144 -144
|
@@ -1,693 +1,696 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
|
-
import type { ProviderHeaders } from "@earendil-works/pi-ai";
|
|
4
|
-
import type {
|
|
5
|
-
CompactionResult,
|
|
6
|
-
ExtensionAPI,
|
|
7
|
-
ExtensionContext,
|
|
8
|
-
SessionBeforeCompactEvent,
|
|
9
|
-
SessionEntry,
|
|
10
|
-
} from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { ContextWindowBudget, type ContextRemaining } from "./window-budget";
|
|
12
|
-
import type { WindowBulkReport } from "./window-bulk";
|
|
13
|
-
import {
|
|
14
|
-
rewriteEncryptedToolOutputs,
|
|
15
|
-
rewriteWindowHeaders,
|
|
16
|
-
rewriteWindowPayload,
|
|
17
|
-
} from "./window-request";
|
|
18
|
-
import {
|
|
19
|
-
CONTEXT_WINDOW_COMPACTION_STRATEGY,
|
|
20
|
-
CONTEXT_WINDOW_COMPACTION_SUMMARY,
|
|
21
|
-
isContextWindowBoundary,
|
|
22
|
-
preserveSystemHead,
|
|
23
|
-
renderContextWindowMessage,
|
|
24
|
-
sendContextWindowMessage,
|
|
25
|
-
} from "./messages";
|
|
26
|
-
import {
|
|
27
|
-
CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
|
|
28
|
-
CONTEXT_MANAGEMENT_PROTOCOL,
|
|
29
|
-
type CodexContextManagementMessageDetails,
|
|
30
|
-
type ContextWindowCompactionDetails,
|
|
31
|
-
type ContextWindowIdentity,
|
|
32
|
-
isCodexContextManagementMessageDetails,
|
|
33
|
-
isNonEmptyString,
|
|
34
|
-
isRecord,
|
|
35
|
-
isContextWindowCompactionDetails,
|
|
36
|
-
type NotesCheckpointReceipt,
|
|
37
|
-
} from "./types";
|
|
38
|
-
import {
|
|
39
|
-
encodeEncryptedOutputForContext,
|
|
40
|
-
isSuccessfulHistoryNotesToolResult,
|
|
41
|
-
loadHistoryNotesThreadHint,
|
|
42
|
-
} from "./history-notes";
|
|
43
|
-
import { rewriteContextNamespaceTools } from "./namespace-tools";
|
|
44
|
-
|
|
45
|
-
interface StartContextWindowOptions {
|
|
46
|
-
triggerTurn: boolean;
|
|
47
|
-
signal?: AbortSignal;
|
|
48
|
-
trimPreviousWindow: boolean;
|
|
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
|
-
private
|
|
76
|
-
private
|
|
77
|
-
private
|
|
78
|
-
private readonly
|
|
79
|
-
private
|
|
80
|
-
private
|
|
81
|
-
private
|
|
82
|
-
private
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
this.
|
|
93
|
-
this.
|
|
94
|
-
this.
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
this.
|
|
142
|
-
this.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (!
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (details.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
this.
|
|
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
|
-
if (!
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
this.identity =
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
this.
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
return
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
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
|
-
this.
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
this.
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
"
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
if (
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
if (!
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
if (!
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
if (
|
|
645
|
-
|
|
646
|
-
if (
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
if (
|
|
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
|
-
}
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import type { ProviderHeaders } from "@earendil-works/pi-ai";
|
|
4
|
+
import type {
|
|
5
|
+
CompactionResult,
|
|
6
|
+
ExtensionAPI,
|
|
7
|
+
ExtensionContext,
|
|
8
|
+
SessionBeforeCompactEvent,
|
|
9
|
+
SessionEntry,
|
|
10
|
+
} from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { ContextWindowBudget, type ContextRemaining } from "./window-budget";
|
|
12
|
+
import type { WindowBulkReport } from "./window-bulk";
|
|
13
|
+
import {
|
|
14
|
+
rewriteEncryptedToolOutputs,
|
|
15
|
+
rewriteWindowHeaders,
|
|
16
|
+
rewriteWindowPayload,
|
|
17
|
+
} from "./window-request";
|
|
18
|
+
import {
|
|
19
|
+
CONTEXT_WINDOW_COMPACTION_STRATEGY,
|
|
20
|
+
CONTEXT_WINDOW_COMPACTION_SUMMARY,
|
|
21
|
+
isContextWindowBoundary,
|
|
22
|
+
preserveSystemHead,
|
|
23
|
+
renderContextWindowMessage,
|
|
24
|
+
sendContextWindowMessage,
|
|
25
|
+
} from "./messages";
|
|
26
|
+
import {
|
|
27
|
+
CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
|
|
28
|
+
CONTEXT_MANAGEMENT_PROTOCOL,
|
|
29
|
+
type CodexContextManagementMessageDetails,
|
|
30
|
+
type ContextWindowCompactionDetails,
|
|
31
|
+
type ContextWindowIdentity,
|
|
32
|
+
isCodexContextManagementMessageDetails,
|
|
33
|
+
isNonEmptyString,
|
|
34
|
+
isRecord,
|
|
35
|
+
isContextWindowCompactionDetails,
|
|
36
|
+
type NotesCheckpointReceipt,
|
|
37
|
+
} from "./types";
|
|
38
|
+
import {
|
|
39
|
+
encodeEncryptedOutputForContext,
|
|
40
|
+
isSuccessfulHistoryNotesToolResult,
|
|
41
|
+
loadHistoryNotesThreadHint,
|
|
42
|
+
} from "./history-notes";
|
|
43
|
+
import { rewriteContextNamespaceTools } from "./namespace-tools";
|
|
44
|
+
|
|
45
|
+
interface StartContextWindowOptions {
|
|
46
|
+
triggerTurn: boolean;
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
trimPreviousWindow: boolean;
|
|
49
|
+
/** Captured with the tool activation policy, before any awaited backend work. */
|
|
50
|
+
gatewayModels?: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type ThreadHintLoader = (
|
|
54
|
+
ctx: ExtensionContext,
|
|
55
|
+
signal?: AbortSignal,
|
|
56
|
+
gatewayModels?: readonly string[],
|
|
57
|
+
) => Promise<string | undefined>;
|
|
58
|
+
|
|
59
|
+
type WindowBoundaryEntry = Extract<SessionEntry, { type: "custom_message" }> & {
|
|
60
|
+
details: CodexContextManagementMessageDetails;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* In-process duplicate guard for one scheduled rollover. It is anchored to the
|
|
65
|
+
* session that scheduled it and to the exact target window id, so it can never
|
|
66
|
+
* be satisfied by an older or foreign marker. It is not checkpoint evidence:
|
|
67
|
+
* only a persisted notes result unlocks the gate.
|
|
68
|
+
*/
|
|
69
|
+
type PendingRollover = {
|
|
70
|
+
sessionId?: string;
|
|
71
|
+
targetWindowId: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export class CodexContextWindowManager {
|
|
75
|
+
private identity: ContextWindowIdentity | undefined;
|
|
76
|
+
private sessionId: string | undefined;
|
|
77
|
+
private restoredMarkerId: string | undefined;
|
|
78
|
+
private readonly budget = new ContextWindowBudget();
|
|
79
|
+
private pendingRollover: PendingRollover | undefined;
|
|
80
|
+
private trimPendingWindowId: string | undefined;
|
|
81
|
+
private readonly projectionDiagnostics = new Set<string>();
|
|
82
|
+
private lastKnownSystemHead: AgentMessage | undefined;
|
|
83
|
+
private bulkReport: WindowBulkReport | undefined;
|
|
84
|
+
private bulkSurfacedKey: string | undefined;
|
|
85
|
+
private readonly loadThreadHint: ThreadHintLoader;
|
|
86
|
+
|
|
87
|
+
constructor(loadThreadHint?: ThreadHintLoader) {
|
|
88
|
+
this.loadThreadHint = loadThreadHint ?? ((ctx, signal, gatewayModels) => loadHistoryNotesThreadHint(ctx, signal, gatewayModels));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
reset(): void {
|
|
92
|
+
this.resetWindowState();
|
|
93
|
+
this.pendingRollover = undefined;
|
|
94
|
+
this.projectionDiagnostics.clear();
|
|
95
|
+
this.lastKnownSystemHead = undefined;
|
|
96
|
+
this.bulkReport = undefined;
|
|
97
|
+
this.bulkSurfacedKey = undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
currentIdentity(): ContextWindowIdentity | undefined {
|
|
101
|
+
return this.identity ? { ...this.identity } : undefined;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Whether a rollover trim is queued for the next eligible compaction. A turn-end bulk
|
|
105
|
+
* close-out is only worth a model call while that trim is still outstanding.
|
|
106
|
+
*/
|
|
107
|
+
hasPendingTrim(): boolean {
|
|
108
|
+
return this.trimPendingWindowId !== undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Remember the latest measurement for this window; see `takeBulkCliff`. */
|
|
112
|
+
noteWindowBulk(report: WindowBulkReport | undefined, modelKey: string): void {
|
|
113
|
+
if (report) this.bulkReport = report;
|
|
114
|
+
else if (this.bulkKey(modelKey) === undefined) this.bulkReport = undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Drain the pending bulk-cliff notice for this window and model. The decision what to
|
|
119
|
+
* do about it (warn, or compact before the next request) belongs to the runtime; this
|
|
120
|
+
* only guarantees the same window and model is never surfaced twice.
|
|
121
|
+
*/
|
|
122
|
+
takeBulkCliff(modelKey: string): WindowBulkReport | undefined {
|
|
123
|
+
const report = this.bulkReport;
|
|
124
|
+
if (!report || !report.expanded) return undefined;
|
|
125
|
+
const key = this.bulkKey(modelKey);
|
|
126
|
+
if (key === undefined || this.bulkSurfacedKey === key) return undefined;
|
|
127
|
+
this.bulkSurfacedKey = key;
|
|
128
|
+
return report;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private bulkKey(modelKey: string): string | undefined {
|
|
132
|
+
// The report's own window id is authoritative: this notice exists precisely for the
|
|
133
|
+
// models that never open the window lifecycle, where `identity` stays undefined.
|
|
134
|
+
const windowId = this.bulkReport?.windowId ?? this.identity?.currentWindowId;
|
|
135
|
+
return windowId ? `${windowId}|${modelKey}` : undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
private resetWindowState(): void {
|
|
141
|
+
this.identity = undefined;
|
|
142
|
+
this.sessionId = undefined;
|
|
143
|
+
this.restoredMarkerId = undefined;
|
|
144
|
+
this.budget.reset();
|
|
145
|
+
this.trimPendingWindowId = undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
restore(entries: readonly SessionEntry[], sessionId?: string): void {
|
|
149
|
+
// A rebuild of derived state must not drop an outstanding rollover guard;
|
|
150
|
+
// only a changed session or an observed target marker retires it below.
|
|
151
|
+
this.resetWindowState();
|
|
152
|
+
this.sessionId = sessionId;
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
if (entry.type === "compaction") {
|
|
155
|
+
this.recordCompaction(entry.details);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
|
|
159
|
+
if (!couldBelongToSession(entry.details, sessionId)) continue;
|
|
160
|
+
if (!isCodexContextManagementMessageDetails(entry.details)) {
|
|
161
|
+
throw new Error("Malformed persisted Codex context-window message");
|
|
162
|
+
}
|
|
163
|
+
if (!matchesSession(entry.details.sessionId, sessionId)) continue;
|
|
164
|
+
const details = entry.details.contextManagement;
|
|
165
|
+
this.restoredMarkerId = entry.id;
|
|
166
|
+
if (details.kind === "window") {
|
|
167
|
+
this.identity = identityFromDetails(entry.details);
|
|
168
|
+
this.trimPendingWindowId = details.trimPreviousWindow ? details.currentWindowId : undefined;
|
|
169
|
+
}
|
|
170
|
+
this.budget.restore(details.kind, details.currentWindowId);
|
|
171
|
+
}
|
|
172
|
+
this.retireSatisfiedOrStalePending(entries, sessionId);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Rebuild state when Pi navigates to a different session/branch. */
|
|
176
|
+
synchronize(ctx: Pick<ExtensionContext, "sessionManager">): void {
|
|
177
|
+
const entries = ctx.sessionManager.getBranch();
|
|
178
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
179
|
+
this.retireSatisfiedOrStalePending(entries, sessionId);
|
|
180
|
+
const latestMarkerId = findLatestContextMarkerId(entries, sessionId);
|
|
181
|
+
if (this.sessionId !== sessionId || this.restoredMarkerId !== latestMarkerId) {
|
|
182
|
+
this.restore(entries, sessionId);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Drop a pending rollover only for durable reasons: the target window marker
|
|
188
|
+
* is now persisted in this session, or the session changed. A queued marker
|
|
189
|
+
* visible in an in-memory message list is not durable evidence.
|
|
190
|
+
*/
|
|
191
|
+
private retireSatisfiedOrStalePending(
|
|
192
|
+
entries: readonly SessionEntry[],
|
|
193
|
+
sessionId: string | undefined,
|
|
194
|
+
): void {
|
|
195
|
+
const pending = this.pendingRollover;
|
|
196
|
+
if (!pending) return;
|
|
197
|
+
if (pending.sessionId !== undefined && pending.sessionId !== sessionId) {
|
|
198
|
+
this.pendingRollover = undefined;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (hasPersistedWindowBoundary(entries, sessionId, pending.targetWindowId)) {
|
|
202
|
+
this.pendingRollover = undefined;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
ensureInitialized(pi: ExtensionAPI, ctx: ExtensionContext, active: boolean): void {
|
|
207
|
+
if (!active) return;
|
|
208
|
+
this.synchronize(ctx);
|
|
209
|
+
if (this.identity) return;
|
|
210
|
+
const windowId = randomUUID();
|
|
211
|
+
this.sendWindowMessage(
|
|
212
|
+
pi,
|
|
213
|
+
ctx,
|
|
214
|
+
{ firstWindowId: windowId, currentWindowId: windowId, windowNumber: 0 },
|
|
215
|
+
{ triggerTurn: false, trimPreviousWindow: false },
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
project(
|
|
220
|
+
messages: readonly AgentMessage[],
|
|
221
|
+
mode: "off" | "remote",
|
|
222
|
+
): AgentMessage[] {
|
|
223
|
+
if (mode === "off") {
|
|
224
|
+
return messages.filter(
|
|
225
|
+
(message) => message.role !== "custom" || message.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
let boundaryIndex = -1;
|
|
229
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
230
|
+
const message = messages[index]!;
|
|
231
|
+
if (
|
|
232
|
+
message.role === "custom" &&
|
|
233
|
+
message.customType === CODEX_CONTEXT_WINDOW_MESSAGE_TYPE
|
|
234
|
+
) {
|
|
235
|
+
if (!couldBelongToSession(message.details, this.sessionId)) continue;
|
|
236
|
+
if (!isCodexContextManagementMessageDetails(message.details)) {
|
|
237
|
+
throw new Error("Malformed persisted Codex context-window message");
|
|
238
|
+
}
|
|
239
|
+
if (!matchesSession(message.details.sessionId, this.sessionId)) continue;
|
|
240
|
+
}
|
|
241
|
+
if (!isContextWindowBoundary(message)) continue;
|
|
242
|
+
boundaryIndex = index;
|
|
243
|
+
this.identity = identityFromDetails(message.details);
|
|
244
|
+
}
|
|
245
|
+
if (boundaryIndex < 0) {
|
|
246
|
+
this.identity = undefined;
|
|
247
|
+
this.restoredMarkerId = undefined;
|
|
248
|
+
this.budget.reset();
|
|
249
|
+
this.trimPendingWindowId = undefined;
|
|
250
|
+
}
|
|
251
|
+
// Projection observes the request view, never durable session state. A
|
|
252
|
+
// queued rollover marker must not clear the duplicate guard here.
|
|
253
|
+
const trimmed = boundaryIndex < 0 ? [...messages] : this.trimToWindow(messages, boundaryIndex);
|
|
254
|
+
return mode === "remote" ? projectEncryptedToolResults(trimmed) : trimmed;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Trim everything before the window boundary while keeping Pi's prompt/tool head.
|
|
259
|
+
*
|
|
260
|
+
* When the head cannot be preserved the trim is refused and the full message list is
|
|
261
|
+
* sent, because a window without tools silently degrades into a chat the model can
|
|
262
|
+
* only narrate. Each refusal is reported once per window and reason.
|
|
263
|
+
*/
|
|
264
|
+
private trimToWindow(messages: readonly AgentMessage[], boundaryIndex: number): AgentMessage[] {
|
|
265
|
+
const repair = preserveSystemHead({ messages, boundaryIndex, fallbackHead: this.lastKnownSystemHead });
|
|
266
|
+
if (repair.head) this.lastKnownSystemHead = repair.head;
|
|
267
|
+
if (repair.shrinkRejected) {
|
|
268
|
+
this.recordProjectionDiagnostic(repair.lostToolNames);
|
|
269
|
+
}
|
|
270
|
+
return [...repair.messages];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private recordProjectionDiagnostic(lostToolNames: readonly string[]): void {
|
|
274
|
+
const key = `${this.identity?.currentWindowId ?? "unknown"}|${[...lostToolNames].sort().join(",")}`;
|
|
275
|
+
this.projectionDiagnostics.add(key);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Drain the pending `tool-loadout-shrink` reports. Returns one entry per window and
|
|
280
|
+
* lost-tool set, so a caller can surface the incident without flooding the UI.
|
|
281
|
+
*/
|
|
282
|
+
takeProjectionDiagnostics(): string[] {
|
|
283
|
+
if (this.projectionDiagnostics.size === 0) return [];
|
|
284
|
+
const pending = [...this.projectionDiagnostics];
|
|
285
|
+
this.projectionDiagnostics.clear();
|
|
286
|
+
return pending;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async startNewWindow(
|
|
290
|
+
pi: ExtensionAPI,
|
|
291
|
+
ctx: ExtensionContext,
|
|
292
|
+
options: StartContextWindowOptions,
|
|
293
|
+
): Promise<boolean> {
|
|
294
|
+
this.synchronize(ctx);
|
|
295
|
+
// A same-session rollover whose target marker is not persisted yet is
|
|
296
|
+
// still in flight: report it instead of scheduling a second window.
|
|
297
|
+
if (this.hasPendingRollover(ctx)) return false;
|
|
298
|
+
if (options.signal?.aborted) throw new Error("Remote context rollover was aborted");
|
|
299
|
+
const current = this.identity;
|
|
300
|
+
const liveSessionId = ctx.sessionManager.getSessionId();
|
|
301
|
+
const currentWindowId = randomUUID();
|
|
302
|
+
const next: ContextWindowIdentity = current
|
|
303
|
+
? {
|
|
304
|
+
firstWindowId: current.firstWindowId,
|
|
305
|
+
currentWindowId,
|
|
306
|
+
previousWindowId: current.currentWindowId,
|
|
307
|
+
windowNumber: current.windowNumber + 1,
|
|
308
|
+
}
|
|
309
|
+
: { firstWindowId: currentWindowId, currentWindowId, windowNumber: 0 };
|
|
310
|
+
this.pendingRollover = { sessionId: liveSessionId, targetWindowId: currentWindowId };
|
|
311
|
+
try {
|
|
312
|
+
const checkpoint = current
|
|
313
|
+
? findLatestNotesCheckpointSinceBoundary(ctx.sessionManager.getBranch(), liveSessionId)
|
|
314
|
+
: undefined;
|
|
315
|
+
let threadHint: string | undefined;
|
|
316
|
+
if (current) {
|
|
317
|
+
try {
|
|
318
|
+
threadHint = await this.loadThreadHint(ctx, options.signal, options.gatewayModels);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
if (options.signal?.aborted) throw error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (options.signal?.aborted) throw new Error("Remote context rollover was aborted");
|
|
324
|
+
this.sendWindowMessage(pi, ctx, next, options, threadHint, checkpoint);
|
|
325
|
+
return true;
|
|
326
|
+
} catch (error) {
|
|
327
|
+
this.pendingRollover = undefined;
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
recordBudget(
|
|
333
|
+
pi: ExtensionAPI,
|
|
334
|
+
ctx: ExtensionContext,
|
|
335
|
+
active: boolean,
|
|
336
|
+
contextReminderThresholdPercent: number,
|
|
337
|
+
contextTokens?: number,
|
|
338
|
+
): void {
|
|
339
|
+
if (!active || !this.identity || contextReminderThresholdPercent <= 0) return;
|
|
340
|
+
// Right after a rollover, Pi's usage anchor still reports the previous
|
|
341
|
+
// window's last request until the new window's first request completes.
|
|
342
|
+
// Budget decisions taken in that gap act on stale numbers: they burn the
|
|
343
|
+
// once-per-window reminder on a false alarm seconds after a successful
|
|
344
|
+
// rollover, leaving the window silent for the rest of its life.
|
|
345
|
+
if (!hasAssistantUsageSinceWindowBoundary(ctx.sessionManager.getBranch(), this.sessionId)) return;
|
|
346
|
+
const reminder = this.budget.record(ctx, this.identity, contextTokens, contextReminderThresholdPercent);
|
|
347
|
+
if (!reminder) return;
|
|
348
|
+
sendContextWindowMessage(
|
|
349
|
+
pi,
|
|
350
|
+
reminder.content,
|
|
351
|
+
reminder.kind,
|
|
352
|
+
this.identity,
|
|
353
|
+
{ triggerTurn: reminder.kind === "fallback", sessionId: ctx.sessionManager.getSessionId() },
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
remaining(ctx: ExtensionContext, contextTokens?: number): ContextRemaining {
|
|
358
|
+
return this.budget.remaining(ctx, this.identity, contextTokens);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* True when a notes checkpoint succeeded in the current window (after the
|
|
363
|
+
* latest boundary). The scan always uses the live session id: a cached id can
|
|
364
|
+
* belong to a session Pi already navigated away from, which would filter the
|
|
365
|
+
* current window's marker and result out as foreign.
|
|
366
|
+
*/
|
|
367
|
+
hasNotesCheckpointSinceBoundary(
|
|
368
|
+
ctx: Pick<ExtensionContext, "sessionManager">,
|
|
369
|
+
): boolean {
|
|
370
|
+
return findNotesCheckpointSinceBoundary(
|
|
371
|
+
ctx.sessionManager.getBranch(),
|
|
372
|
+
ctx.sessionManager.getSessionId(),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* True while a rollover scheduled for the current session is awaiting its
|
|
378
|
+
* persisted target marker. Call `synchronize()` first so a marker that has
|
|
379
|
+
* already been persisted or a session switch retires the guard.
|
|
380
|
+
*/
|
|
381
|
+
hasPendingRollover(ctx: Pick<ExtensionContext, "sessionManager">): boolean {
|
|
382
|
+
const pending = this.pendingRollover;
|
|
383
|
+
if (!pending) return false;
|
|
384
|
+
return pending.sessionId === undefined || pending.sessionId === ctx.sessionManager.getSessionId();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Whether a rollover may be scheduled from the current window.
|
|
389
|
+
*
|
|
390
|
+
* A window that was entered by a context switch has to do real work before it switches
|
|
391
|
+
* again: without this guard a model can checkpoint and roll over on its very first turn,
|
|
392
|
+
* which is how long sessions start looping between notes and new_context.
|
|
393
|
+
*/
|
|
394
|
+
canRolloverFromCurrentWindow(ctx: ExtensionContext): boolean {
|
|
395
|
+
if (!this.identity || this.identity.windowNumber === 0) return true;
|
|
396
|
+
// An exhausted window must always be able to escape: the budget fallback tells the
|
|
397
|
+
// model to checkpoint and switch, and refusing that would strand the session. An
|
|
398
|
+
// unmeasurable window keeps the guard, since nothing there is pushing for a switch.
|
|
399
|
+
const { remainingTokens } = this.remaining(ctx);
|
|
400
|
+
if (remainingTokens !== undefined && remainingTokens <= 0) return true;
|
|
401
|
+
return hasSubstantiveToolResultSinceBoundary(ctx.sessionManager.getBranch(), this.sessionId);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
prepareCompaction(
|
|
405
|
+
event: SessionBeforeCompactEvent,
|
|
406
|
+
): { cancel: true } | { compaction: CompactionResult<ContextWindowCompactionDetails> } {
|
|
407
|
+
// Only the compaction that consumes a scheduled rollover may write a
|
|
408
|
+
// boundary. Every other path — threshold, manual /compact, overflow —
|
|
409
|
+
// stays cancelled: a boundary written without a pending trim does not
|
|
410
|
+
// shrink anything, becomes Pi's latest-compaction anchor, and blinds
|
|
411
|
+
// getContextUsage (null tokens) until the next assistant usage lands —
|
|
412
|
+
// which is exactly how the exhausted-window fallback can go silent.
|
|
413
|
+
const boundary = findLatestWindowBoundaryEntry(event.branchEntries, this.sessionId);
|
|
414
|
+
if (!boundary || boundary.details.contextManagement.currentWindowId !== this.trimPendingWindowId) {
|
|
415
|
+
return { cancel: true };
|
|
416
|
+
}
|
|
417
|
+
const compaction = this.createCompaction(event);
|
|
418
|
+
// Consume the trim synchronously. Pi does not re-fire session_compact
|
|
419
|
+
// for hook-written entries, so relying on that event leaks the pending
|
|
420
|
+
// id and lets every later compaction pass this gate again, writing an
|
|
421
|
+
// endless run of no-op boundaries for the same window.
|
|
422
|
+
this.trimPendingWindowId = undefined;
|
|
423
|
+
return { compaction };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
recordCompaction(details: unknown): void {
|
|
427
|
+
if (!isContextWindowCompactionDetails(details)) return;
|
|
428
|
+
if (details.windowId === this.trimPendingWindowId) this.trimPendingWindowId = undefined;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
createCompaction(event: SessionBeforeCompactEvent): CompactionResult<ContextWindowCompactionDetails> {
|
|
432
|
+
const boundary = findLatestWindowBoundaryEntry(event.branchEntries, this.sessionId);
|
|
433
|
+
return {
|
|
434
|
+
summary: CONTEXT_WINDOW_COMPACTION_SUMMARY,
|
|
435
|
+
firstKeptEntryId: boundary?.id ?? event.preparation.firstKeptEntryId,
|
|
436
|
+
tokensBefore: event.preparation.tokensBefore,
|
|
437
|
+
details: {
|
|
438
|
+
protocol: CONTEXT_MANAGEMENT_PROTOCOL,
|
|
439
|
+
strategy: CONTEXT_WINDOW_COMPACTION_STRATEGY,
|
|
440
|
+
...(this.identity ? { windowId: this.identity.currentWindowId } : {}),
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
rewritePayload(payload: unknown, ctx: ExtensionContext): unknown {
|
|
446
|
+
const withMetadata = rewriteWindowPayload(payload, ctx, this.identity);
|
|
447
|
+
return rewriteContextNamespaceTools(withMetadata, { encrypted: true });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
rewriteHeaders(headers: ProviderHeaders, ctx: ExtensionContext): void {
|
|
451
|
+
rewriteWindowHeaders(headers, ctx, this.identity);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private sendWindowMessage(
|
|
455
|
+
pi: ExtensionAPI,
|
|
456
|
+
ctx: ExtensionContext,
|
|
457
|
+
identity: ContextWindowIdentity,
|
|
458
|
+
options: StartContextWindowOptions,
|
|
459
|
+
threadHint?: string,
|
|
460
|
+
checkpoint?: NotesCheckpointReceipt,
|
|
461
|
+
): void {
|
|
462
|
+
sendContextWindowMessage(
|
|
463
|
+
pi,
|
|
464
|
+
renderContextWindowMessage(identity, threadHint, checkpoint),
|
|
465
|
+
"window",
|
|
466
|
+
identity,
|
|
467
|
+
{ triggerTurn: options.triggerTurn, sessionId: ctx.sessionManager.getSessionId() },
|
|
468
|
+
options.trimPreviousWindow,
|
|
469
|
+
);
|
|
470
|
+
this.identity = identity;
|
|
471
|
+
this.sessionId = ctx.sessionManager.getSessionId();
|
|
472
|
+
this.restoredMarkerId = undefined;
|
|
473
|
+
this.trimPendingWindowId = options.trimPreviousWindow ? identity.currentWindowId : undefined;
|
|
474
|
+
// sendMessage() acceptance is not persistence. The pending rollover stays
|
|
475
|
+
// armed until synchronize() observes this target window in the persisted
|
|
476
|
+
// branch (or the session changes).
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function identityFromDetails(details: CodexContextManagementMessageDetails): ContextWindowIdentity {
|
|
481
|
+
const context = details.contextManagement;
|
|
482
|
+
return {
|
|
483
|
+
firstWindowId: context.firstWindowId,
|
|
484
|
+
currentWindowId: context.currentWindowId,
|
|
485
|
+
...(context.previousWindowId ? { previousWindowId: context.previousWindowId } : {}),
|
|
486
|
+
windowNumber: context.windowNumber,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const NOTES_CHECKPOINT_ACTIONS: ReadonlySet<string> = new Set(["append_to_file", "write_file"]);
|
|
491
|
+
|
|
492
|
+
/** Tools that only move context around; they never count as work done in a window. */
|
|
493
|
+
const CONTEXT_MANAGEMENT_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
494
|
+
"new_context",
|
|
495
|
+
"get_context_remaining",
|
|
496
|
+
"history",
|
|
497
|
+
"notes",
|
|
498
|
+
]);
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Whether the current window produced a tool result from anything other than the
|
|
502
|
+
* context-handoff tools. Derived from persisted entries so it survives restarts.
|
|
503
|
+
*/
|
|
504
|
+
export function hasSubstantiveToolResultSinceBoundary(
|
|
505
|
+
entries: readonly SessionEntry[],
|
|
506
|
+
sessionId?: string,
|
|
507
|
+
): boolean {
|
|
508
|
+
const boundaryIndex = findLatestWindowBoundaryIndex(entries, sessionId);
|
|
509
|
+
if (boundaryIndex < 0) return true;
|
|
510
|
+
for (let index = boundaryIndex + 1; index < entries.length; index += 1) {
|
|
511
|
+
const entry = entries[index]!;
|
|
512
|
+
if (entry.type !== "message") continue;
|
|
513
|
+
const message = entry.message as unknown as { role?: string; toolName?: string };
|
|
514
|
+
if (message.role !== "toolResult" || typeof message.toolName !== "string") continue;
|
|
515
|
+
if (!CONTEXT_MANAGEMENT_TOOL_NAMES.has(message.toolName)) return true;
|
|
516
|
+
}
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Whether the branch contains a successful assistant usage after the latest
|
|
522
|
+
* window boundary. Before that first usage lands, any context measurement is
|
|
523
|
+
* still anchored to the previous window's last request.
|
|
524
|
+
*/
|
|
525
|
+
export function hasAssistantUsageSinceWindowBoundary(
|
|
526
|
+
entries: readonly SessionEntry[],
|
|
527
|
+
sessionId?: string,
|
|
528
|
+
): boolean {
|
|
529
|
+
const boundaryIndex = findLatestWindowBoundaryIndex(entries, sessionId);
|
|
530
|
+
if (boundaryIndex < 0) return false;
|
|
531
|
+
for (let index = boundaryIndex + 1; index < entries.length; index += 1) {
|
|
532
|
+
const entry = entries[index]!;
|
|
533
|
+
if (entry.type !== "message") continue;
|
|
534
|
+
const message = entry.message;
|
|
535
|
+
if (message.role !== "assistant") continue;
|
|
536
|
+
const candidate = message as unknown as {
|
|
537
|
+
stopReason?: string;
|
|
538
|
+
usage?: { totalTokens?: number; input?: number; output?: number; cacheRead?: number; cacheWrite?: number };
|
|
539
|
+
};
|
|
540
|
+
if (candidate.stopReason === "aborted" || candidate.stopReason === "error") continue;
|
|
541
|
+
const usage = candidate.usage;
|
|
542
|
+
if (!usage) continue;
|
|
543
|
+
const tokens = usage.totalTokens ?? (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
|
544
|
+
if (tokens > 0) return true;
|
|
545
|
+
}
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Return the most recent successful notes write after the latest window
|
|
551
|
+
* boundary. The receipt is derived from persisted session entries so it
|
|
552
|
+
* survives restarts and can be used for an explicit rollover handoff.
|
|
553
|
+
*/
|
|
554
|
+
export function findLatestNotesCheckpointSinceBoundary(
|
|
555
|
+
entries: readonly SessionEntry[],
|
|
556
|
+
sessionId?: string,
|
|
557
|
+
): NotesCheckpointReceipt | undefined {
|
|
558
|
+
const boundaryIndex = findLatestWindowBoundaryIndex(entries, sessionId);
|
|
559
|
+
if (boundaryIndex < 0) return undefined;
|
|
560
|
+
const checkpointCalls = new Map<string, NotesCheckpointReceipt>();
|
|
561
|
+
let latest: NotesCheckpointReceipt | undefined;
|
|
562
|
+
for (let index = boundaryIndex + 1; index < entries.length; index += 1) {
|
|
563
|
+
const entry = entries[index]!;
|
|
564
|
+
if (entry.type !== "message") continue;
|
|
565
|
+
const message = entry.message;
|
|
566
|
+
if (message.role === "assistant") {
|
|
567
|
+
const parts = Array.isArray(message.content) ? message.content : [];
|
|
568
|
+
for (const part of parts) {
|
|
569
|
+
if (!isRecord(part) || part.type !== "toolCall" || part.name !== "notes") continue;
|
|
570
|
+
const args = isRecord(part.arguments) ? part.arguments : undefined;
|
|
571
|
+
const action = typeof args?.action === "string" ? args.action : "";
|
|
572
|
+
const path = isNonEmptyString(args?.path) ? args.path : undefined;
|
|
573
|
+
if (isNonEmptyString(part.id) && path !== undefined && NOTES_CHECKPOINT_ACTIONS.has(action)) {
|
|
574
|
+
checkpointCalls.set(part.id, { path, toolCallId: part.id });
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
if (message.role !== "toolResult" || message.toolName !== "notes") continue;
|
|
580
|
+
const receipt = checkpointCalls.get(message.toolCallId);
|
|
581
|
+
if (!receipt) continue;
|
|
582
|
+
checkpointCalls.delete(message.toolCallId);
|
|
583
|
+
if (message.isError !== false || !isSuccessfulHistoryNotesToolResult(message.details)) continue;
|
|
584
|
+
latest = receipt;
|
|
585
|
+
}
|
|
586
|
+
return latest;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Whether the branch contains a successful notes append/write result after the
|
|
591
|
+
* latest window boundary. Reads and failed writes never count as checkpoints.
|
|
592
|
+
*/
|
|
593
|
+
export function findNotesCheckpointSinceBoundary(
|
|
594
|
+
entries: readonly SessionEntry[],
|
|
595
|
+
sessionId?: string,
|
|
596
|
+
): boolean {
|
|
597
|
+
return findLatestNotesCheckpointSinceBoundary(entries, sessionId) !== undefined;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function findLatestWindowBoundaryIndex(
|
|
601
|
+
entries: readonly SessionEntry[],
|
|
602
|
+
sessionId?: string,
|
|
603
|
+
): number {
|
|
604
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
605
|
+
const entry = entries[index]!;
|
|
606
|
+
if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
|
|
607
|
+
if (!couldBelongToSession(entry.details, sessionId)) continue;
|
|
608
|
+
if (!isCodexContextManagementMessageDetails(entry.details)) {
|
|
609
|
+
throw new Error("Malformed persisted Codex context-window message");
|
|
610
|
+
}
|
|
611
|
+
if (!matchesSession(entry.details.sessionId, sessionId)) continue;
|
|
612
|
+
if (entry.details.contextManagement.kind === "window") return index;
|
|
613
|
+
}
|
|
614
|
+
return -1;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export function findLatestWindowBoundaryEntry(
|
|
618
|
+
entries: readonly SessionEntry[],
|
|
619
|
+
sessionId?: string,
|
|
620
|
+
): WindowBoundaryEntry | undefined {
|
|
621
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
622
|
+
const entry = entries[index]!;
|
|
623
|
+
if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
|
|
624
|
+
if (!couldBelongToSession(entry.details, sessionId)) continue;
|
|
625
|
+
if (!isCodexContextManagementMessageDetails(entry.details)) {
|
|
626
|
+
throw new Error("Malformed persisted Codex context-window message");
|
|
627
|
+
}
|
|
628
|
+
if (!matchesSession(entry.details.sessionId, sessionId)) continue;
|
|
629
|
+
if (entry.details.contextManagement.kind === "window") return entry as WindowBoundaryEntry;
|
|
630
|
+
}
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Whether this exact window boundary is already persisted for the session.
|
|
636
|
+
* Only durable entries count; a queued/visible marker does not.
|
|
637
|
+
*/
|
|
638
|
+
function hasPersistedWindowBoundary(
|
|
639
|
+
entries: readonly SessionEntry[],
|
|
640
|
+
sessionId: string | undefined,
|
|
641
|
+
windowId: string,
|
|
642
|
+
): boolean {
|
|
643
|
+
for (const entry of entries) {
|
|
644
|
+
if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
|
|
645
|
+
if (!couldBelongToSession(entry.details, sessionId)) continue;
|
|
646
|
+
if (!isCodexContextManagementMessageDetails(entry.details)) continue;
|
|
647
|
+
if (!matchesSession(entry.details.sessionId, sessionId)) continue;
|
|
648
|
+
const details = entry.details.contextManagement;
|
|
649
|
+
if (details.kind === "window" && details.currentWindowId === windowId) return true;
|
|
650
|
+
}
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function findLatestContextMarkerId(
|
|
655
|
+
entries: readonly SessionEntry[],
|
|
656
|
+
sessionId?: string,
|
|
657
|
+
): string | undefined {
|
|
658
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
659
|
+
const entry = entries[index]!;
|
|
660
|
+
if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
|
|
661
|
+
if (!couldBelongToSession(entry.details, sessionId)) continue;
|
|
662
|
+
if (!isCodexContextManagementMessageDetails(entry.details)) {
|
|
663
|
+
throw new Error("Malformed persisted Codex context-window message");
|
|
664
|
+
}
|
|
665
|
+
if (matchesSession(entry.details.sessionId, sessionId)) return entry.id;
|
|
666
|
+
}
|
|
667
|
+
return undefined;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function couldBelongToSession(details: unknown, sessionId: string | undefined): boolean {
|
|
671
|
+
if (sessionId === undefined || !isRecord(details)) return true;
|
|
672
|
+
const markerSessionId = details.sessionId;
|
|
673
|
+
return !isNonEmptyString(markerSessionId) || markerSessionId === sessionId;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function matchesSession(markerSessionId: string | undefined, sessionId: string | undefined): boolean {
|
|
677
|
+
return sessionId === undefined || markerSessionId === sessionId;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function projectEncryptedToolResults(messages: readonly AgentMessage[]): AgentMessage[] {
|
|
681
|
+
let changed = false;
|
|
682
|
+
const projected = messages.map((message) => {
|
|
683
|
+
if (message.role !== "toolResult" || !isRecord(message.details)) return message;
|
|
684
|
+
const historyNotes = message.details.codexHistoryNotes;
|
|
685
|
+
if (!isRecord(historyNotes) || typeof historyNotes.encrypted_output !== "string") return message;
|
|
686
|
+
changed = true;
|
|
687
|
+
return {
|
|
688
|
+
...message,
|
|
689
|
+
content: [
|
|
690
|
+
{ type: "text" as const, text: encodeEncryptedOutputForContext(historyNotes.encrypted_output) },
|
|
691
|
+
...message.content.filter((item) => item.type === "image"),
|
|
692
|
+
],
|
|
693
|
+
};
|
|
694
|
+
});
|
|
695
|
+
return changed ? projected : [...messages];
|
|
696
|
+
}
|