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.
@@ -1,1329 +1,1375 @@
1
- import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
- import type {
3
- BeforeProviderRequestEvent,
4
- ContextEvent,
5
- CompactionResult,
6
- ExtensionAPI,
7
- ExtensionContext,
8
- SessionBeforeCompactEvent,
9
- } from "@earendil-works/pi-coding-agent";
10
- import { loadToolkitConfig } from "./config";
11
- import {
12
- codexContextProviderHeaders,
13
- resolveCodexContextProvider,
14
- isCodexGatewayModel,
15
- isNativeCodexModel,
16
- } from "./context-management/codex-provider";
17
- import { routeContextNamespaceToolMessage } from "./context-management/namespace-tools";
18
- import { loadHistoryNotesThreadHint } from "./context-management/history-notes";
19
- import { CodexContextWindowManager } from "./context-management/window-manager";
20
- import {
21
- BULK_CLIFF_RATIO,
22
- decideBulkCliffAction,
23
- decideBulkCloseOut,
24
- evaluateWindowBulk,
25
- } from "./context-management/window-bulk";
26
- import {
27
- registerContextManagementTools,
28
- type ContextToolRegistrationState,
29
- } from "./context-management/tools";
30
- import { writeDebugArtifact, writeReplayFailureArtifact } from "./debug";
31
- import {
32
- COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
33
- COMPACTION_PROJECTION_UNAVAILABLE,
34
- COMPACTION_SESSION_CONTEXT_UNAVAILABLE,
35
- COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
36
- hasVerifiedCompactionInputProvenance,
37
- type UnprojectedCompactionReason,
38
- } from "./compaction-projection";
39
- import { CODEX_GATEWAY_FORWARD_HEADERS } from "./responses-headers";
40
- import { resolveLatestNativeCompactionEntry } from "./details-store";
41
- import {
42
- getPiContextHookProjector,
43
- installPiContextHookPatch,
44
- reportPiContextHookFailure,
45
- type PiContextHookPatchResult,
46
- } from "./pi-context-hook";
47
- import { runNativeFallbackCompaction } from "./native-fallback";
48
- import {
49
- rewriteResponsesPayloadWithNativeReplay,
50
- removeNativeCompactionRetainedMessages,
51
- serializeLiveTailToResponsesInput,
52
- } from "./payload-rewrite";
53
- import { getCompactionRequestExtras, rememberRequestContext } from "./request-context-cache";
54
- import { resolveWebSearchRoute } from "./web-search/types";
55
- import { executeRemoteV2Compaction } from "./remote-v2-client";
56
- import {
57
- resolveNativeCompactionEnvironment,
58
- resolveRemoteCompactionExecution,
59
- type RemoteCompactionExecution,
60
- } from "./runtime";
61
- import {
62
- serializeMessagesToCompactRequest,
63
- serializeMessagesToResponsesInput,
64
- type NativeCompactionRequestBody,
65
- type ResponsesInputItem,
66
- } from "./serializer";
67
- import {
68
- createNativeCompactionDetails,
69
- createNativeCompactionResult,
70
- COMPACTION_EXTENSION_ID,
71
- getLatestDeferredToolCarryover,
72
- getRemoteV2InputProvenance,
73
- isNativeCompactionDetails,
74
- type CompactionConfig,
75
- type NativeCompactionDetails,
76
- type NativeCompactionRequestMeta,
77
- } from "./types";
78
-
79
- type CompactionContextProjection = (
80
- messages: readonly AgentMessage[],
81
- ctx: ExtensionContext,
82
- ) => readonly AgentMessage[] | undefined | Promise<readonly AgentMessage[] | undefined>;
83
-
84
- type CompactionDependencies = {
85
- loadConfig: typeof loadToolkitConfig;
86
- remoteCompact: typeof executeRemoteV2Compaction;
87
- nativeFallback: typeof runNativeFallbackCompaction;
88
- contextWindows: CodexContextWindowManager;
89
- /** Test seam for the same ordered projection exposed by the Pi host patch. */
90
- projectCompactionContext?: CompactionContextProjection;
91
- piContextHookPatch: PiContextHookPatchResult;
92
- };
93
-
94
- type RemoteContextActive = (
95
- ctx: ExtensionContext,
96
- config: CompactionConfig,
97
- model?: ExtensionContext["model"],
98
- ) => Promise<boolean>;
99
-
100
- type ContextToolSyncOutcome = {
101
- eligible: boolean;
102
- toolsSynced: boolean;
103
- registrationState: ContextToolRegistrationState;
104
- windowInitialized: boolean;
105
- };
106
-
107
- type ResponsesCompactOutcome =
108
- | { outcome: "success"; compaction: CompactionResult<NativeCompactionDetails> }
109
- | { outcome: "aborted" }
110
- | { outcome: "failed" }
111
- | {
112
- outcome: "unprojected-input";
113
- reason: UnprojectedCompactionReason;
114
- };
115
-
116
- function buildCompactionRequestMeta(event: SessionBeforeCompactEvent): NativeCompactionRequestMeta {
117
- return {
118
- tokensBefore: event.preparation.tokensBefore,
119
- previousSummaryPresent: Boolean(event.preparation.previousSummary),
120
- };
121
- }
122
-
123
- function getCurrentModelDebugInfo(ctx: ExtensionContext) {
124
- return ctx.model
125
- ? {
126
- provider: ctx.model.provider,
127
- id: ctx.model.id,
128
- }
129
- : undefined;
130
- }
131
-
132
- function getCompactionIdentityDebugInfo(entry: { details?: unknown } | undefined) {
133
- return isNativeCompactionDetails(entry?.details)
134
- ? {
135
- provider: entry.details.provider,
136
- api: entry.details.api,
137
- model: entry.details.model,
138
- baseUrl: entry.details.baseUrl,
139
- compactionModel: entry.details.compactionModel,
140
- }
141
- : undefined;
142
- }
143
-
144
- function getSessionId(ctx: ExtensionContext): string | undefined {
145
- try {
146
- return ctx.sessionManager.getSessionId();
147
- } catch {
148
- return undefined;
149
- }
150
- }
151
-
152
- function notifyWarning(ctx: ExtensionContext, message: string): void {
153
- if (ctx.hasUI) {
154
- ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: ${message}`, "warning");
155
- }
156
- }
157
-
158
- async function isRemoteContextActive(
159
- ctx: ExtensionContext,
160
- config: CompactionConfig,
161
- model: ExtensionContext["model"] = ctx.model,
162
- ): Promise<boolean> {
163
- if (!config.enabled || config.contextManagement !== "remote") return false;
164
- const resolution = await resolveCodexContextProvider(ctx, model, config.gatewayContextModels);
165
- return resolution.ok;
166
- }
167
-
168
- function isCodexContextModel(
169
- model: ExtensionContext["model"] | undefined,
170
- config: CompactionConfig,
171
- ): boolean {
172
- return config.contextManagement === "remote"
173
- && (isNativeCodexModel(model) || isCodexGatewayModel(model, config.gatewayContextModels));
174
- }
175
-
176
- function notifyRemoteContextFailure(ctx: ExtensionContext, reason: string): void {
177
- if (ctx.hasUI) {
178
- ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: Remote Context management inactive (${reason})`, "warning");
179
- }
180
- }
181
-
182
- /**
183
- * Surface every window trim that had to be refused to keep the tool loadout intact.
184
- *
185
- * The refusal itself is safe: the model keeps the previous turns instead of losing its
186
- * tools. It is still a continuity incident, so it is recorded once per window and reason
187
- * instead of being allowed to pass silently.
188
- */
189
- function reportProjectionDiagnostics(
190
- contextWindows: CodexContextWindowManager,
191
- config: CompactionConfig,
192
- ctx: ExtensionContext,
193
- ): void {
194
- const diagnostics = contextWindows.takeProjectionDiagnostics();
195
- if (diagnostics.length === 0) return;
196
- writeDebugArtifact(
197
- "compaction-event",
198
- { event: "context.projection.tool_loadout_shrink", diagnostics },
199
- config,
200
- ctx,
201
- );
202
- if (ctx.hasUI) {
203
- ctx.ui.notify(
204
- `${COMPACTION_EXTENSION_ID}: kept previous context to preserve tools (${diagnostics.join("; ")})`,
205
- "warning",
206
- );
207
- }
208
- }
209
-
210
- function cloneOpaqueWindow(window: readonly unknown[]): unknown[] {
211
- return window.map((item) => structuredClone(item));
212
- }
213
-
214
- function buildCompactionInstructions(systemPrompt: string, customInstructions?: string): string {
215
- const guidance = customInstructions?.trim();
216
- if (!guidance) {
217
- return systemPrompt;
218
- }
219
-
220
- return `${systemPrompt}\n\nAdditional user guidance for this manual /compact request:\n${guidance}`;
221
- }
222
-
223
- function findLatestProjectedCompactionSummaryIndex(
224
- messages: readonly AgentMessage[],
225
- summary: string,
226
- ): number {
227
- for (let index = messages.length - 1; index >= 0; index -= 1) {
228
- const message = messages[index];
229
- if (message?.role === "compactionSummary" && message.summary === summary) {
230
- return index;
231
- }
232
- }
233
-
234
- return -1;
235
- }
236
-
237
- type SessionManagerWithOptionalContext = ExtensionContext["sessionManager"] & {
238
- buildSessionContext?: () => { messages?: readonly AgentMessage[] };
239
- };
240
-
241
- function readSessionContextMessages(ctx: ExtensionContext): AgentMessage[] | undefined {
242
- const sessionManager = ctx.sessionManager as SessionManagerWithOptionalContext;
243
- if (typeof sessionManager.buildSessionContext !== "function") {
244
- return undefined;
245
- }
246
-
247
- try {
248
- const sessionContext = sessionManager.buildSessionContext();
249
- if (!sessionContext || !Array.isArray(sessionContext.messages)) {
250
- return undefined;
251
- }
252
- return structuredClone([...sessionContext.messages]);
253
- } catch {
254
- return undefined;
255
- }
256
- }
257
-
258
- type CompactionProjectionResult =
259
- | { ok: true; messages: readonly AgentMessage[] }
260
- | { ok: false; reason: typeof COMPACTION_PROJECTION_UNAVAILABLE | typeof COMPACTION_SESSION_CONTEXT_UNAVAILABLE };
261
-
262
- async function projectSessionContextForCompaction(
263
- ctx: ExtensionContext,
264
- projectCompactionContext: CompactionContextProjection | undefined,
265
- ): Promise<CompactionProjectionResult> {
266
- const messages = readSessionContextMessages(ctx);
267
- if (!messages) {
268
- return { ok: false, reason: COMPACTION_SESSION_CONTEXT_UNAVAILABLE };
269
- }
270
-
271
- const projector = projectCompactionContext ?? getPiContextHookProjector(ctx);
272
- if (!projector) {
273
- return { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
274
- }
275
-
276
- try {
277
- const projected = await projector(messages, ctx);
278
- return Array.isArray(projected)
279
- ? { ok: true, messages: projected }
280
- : { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
281
- } catch {
282
- return { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
283
- }
284
- }
285
-
286
- function getLegacySessionContextMessages(
287
- event: SessionBeforeCompactEvent,
288
- ctx: ExtensionContext,
289
- ): AgentMessage[] {
290
- return readSessionContextMessages(ctx) ?? [
291
- ...event.preparation.messagesToSummarize,
292
- ...event.preparation.turnPrefixMessages,
293
- ];
294
- }
295
-
296
- async function runResponsesNativeCompact(
297
- event: SessionBeforeCompactEvent,
298
- ctx: ExtensionContext,
299
- config: CompactionConfig,
300
- execution: RemoteCompactionExecution,
301
- remoteCompact: typeof executeRemoteV2Compaction,
302
- projectCompactionContext: CompactionContextProjection | undefined,
303
- ): Promise<ResponsesCompactOutcome> {
304
- const { consumer, compactor } = execution;
305
- const instructions = buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions);
306
- const branchEntries = event.branchEntries ?? ctx.sessionManager.getBranch();
307
- const deferredToolCarryover = getLatestDeferredToolCarryover(branchEntries);
308
- const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
309
- baseUrl: consumer.baseUrl,
310
- });
311
-
312
- const inputProvenance = getRemoteV2InputProvenance(config.remoteV2ContextSource);
313
- let requestSource: "session-context" | "non-native-session-context" | "latest-native-replay";
314
- let request: NativeCompactionRequestBody;
315
- if (latestNativeCompaction.ok) {
316
- const details = latestNativeCompaction.entry.details;
317
- if (!details) {
318
- return { outcome: "failed" };
319
- }
320
- if (!hasVerifiedCompactionInputProvenance(details, inputProvenance)) {
321
- return {
322
- outcome: "unprojected-input",
323
- reason: COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
324
- };
325
- }
326
- requestSource = "latest-native-replay";
327
-
328
- if (config.remoteV2ContextSource === "pi-context-hook") {
329
- const projection = await projectSessionContextForCompaction(ctx, projectCompactionContext);
330
- if (!projection.ok) {
331
- return { outcome: "unprojected-input", reason: projection.reason };
332
- }
333
- const summaryIndex = findLatestProjectedCompactionSummaryIndex(
334
- projection.messages,
335
- latestNativeCompaction.entry.summary,
336
- );
337
- if (summaryIndex < 0) {
338
- return {
339
- outcome: "unprojected-input",
340
- reason: COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
341
- };
342
- }
343
- const input: ResponsesInputItem[] = [
344
- ...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
345
- ...serializeMessagesToResponsesInput(
346
- compactor.currentModel,
347
- [...projection.messages.slice(summaryIndex + 1)],
348
- { firstSystemMessageIsUpdate: true },
349
- ),
350
- ];
351
- request = {
352
- model: compactor.model,
353
- input,
354
- instructions,
355
- };
356
- } else {
357
- const liveTailEntries = branchEntries.slice(latestNativeCompaction.index + 1);
358
- const input: ResponsesInputItem[] = [
359
- ...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
360
- ...serializeLiveTailToResponsesInput({ model: compactor.currentModel, entries: liveTailEntries }),
361
- ];
362
- request = {
363
- model: compactor.model,
364
- input,
365
- instructions,
366
- };
367
- }
368
- } else if (
369
- latestNativeCompaction.reason === "no-compaction" ||
370
- (latestNativeCompaction.reason === "latest-compaction-not-native" &&
371
- config.allowCompactionContinuityBreak)
372
- ) {
373
- requestSource =
374
- latestNativeCompaction.reason === "no-compaction" ? "session-context" : "non-native-session-context";
375
- if (config.remoteV2ContextSource === "pi-context-hook") {
376
- const projection = await projectSessionContextForCompaction(ctx, projectCompactionContext);
377
- if (!projection.ok) {
378
- return { outcome: "unprojected-input", reason: projection.reason };
379
- }
380
- request = serializeMessagesToCompactRequest({
381
- model: compactor.currentModel,
382
- messages: [...projection.messages],
383
- instructions,
384
- });
385
- } else {
386
- request = serializeMessagesToCompactRequest({
387
- model: compactor.currentModel,
388
- messages: getLegacySessionContextMessages(event, ctx),
389
- instructions,
390
- });
391
- }
392
- } else {
393
- writeDebugArtifact(
394
- "compaction-event",
395
- {
396
- event: "session_before_compact.remote-v2-skip",
397
- reason: latestNativeCompaction.reason,
398
- consumer: {
399
- provider: consumer.provider,
400
- api: consumer.api,
401
- model: consumer.model,
402
- baseUrl: consumer.baseUrl,
403
- },
404
- compactor: {
405
- provider: compactor.provider,
406
- api: compactor.api,
407
- model: compactor.model,
408
- baseUrl: compactor.baseUrl,
409
- },
410
- latestCompactionIndex: latestNativeCompaction.latestCompactionIndex,
411
- latestCompactionIdentity: getCompactionIdentityDebugInfo(latestNativeCompaction.latestCompaction),
412
- },
413
- config,
414
- ctx,
415
- );
416
- return { outcome: "failed" };
417
- }
418
-
419
- // Mirror the latest codex_rs CompactionInput fields captured from the most
420
- // recent live provider request for this model (tools, reasoning, etc.).
421
- const extras = getCompactionRequestExtras({
422
- provider: consumer.provider,
423
- api: consumer.api,
424
- model: consumer.model,
425
- baseUrl: consumer.baseUrl,
426
- sessionId: getSessionId(ctx),
427
- }, compactor.currentModel);
428
- if (extras) {
429
- request = { ...request, ...extras };
430
- }
431
-
432
- const compactResult = await remoteCompact({
433
- runtime: compactor,
434
- request,
435
- signal: event.signal,
436
- settings: config,
437
- context: ctx,
438
- });
439
-
440
- if (compactResult.ok === false) {
441
- writeDebugArtifact(
442
- "compaction-event",
443
- {
444
- event: "session_before_compact.remote-v2-failure",
445
- reason: compactResult.reason,
446
- status: compactResult.status,
447
- errorMessage: compactResult.errorMessage,
448
- },
449
- config,
450
- ctx,
451
- );
452
- return compactResult.reason === "aborted" ? { outcome: "aborted" } : { outcome: "failed" };
453
- }
454
-
455
- let details: NativeCompactionDetails;
456
- try {
457
- details = createNativeCompactionDetails({
458
- provider: consumer.provider,
459
- api: consumer.api,
460
- model: consumer.model,
461
- baseUrl: consumer.baseUrl,
462
- compactionModel: {
463
- provider: compactor.provider,
464
- api: compactor.api,
465
- model: compactor.model,
466
- baseUrl: compactor.baseUrl,
467
- },
468
- deferredToolCarryover,
469
- compactedWindow: compactResult.compactedWindow,
470
- compactResponseId: compactResult.compactResponseId,
471
- createdAt: compactResult.createdAt,
472
- requestMeta: buildCompactionRequestMeta(event),
473
- inputProvenance,
474
- });
475
- } catch (error) {
476
- writeDebugArtifact(
477
- "compaction-event",
478
- {
479
- event: "session_before_compact.invalid-native-details",
480
- reason: error instanceof Error ? error.message : String(error),
481
- consumer: {
482
- provider: consumer.provider,
483
- api: consumer.api,
484
- model: consumer.model,
485
- baseUrl: consumer.baseUrl,
486
- },
487
- compactor: {
488
- provider: compactor.provider,
489
- api: compactor.api,
490
- model: compactor.model,
491
- baseUrl: compactor.baseUrl,
492
- },
493
- },
494
- config,
495
- ctx,
496
- );
497
- return { outcome: "failed" };
498
- }
499
-
500
- const compaction = createNativeCompactionResult({
501
- firstKeptEntryId: event.preparation.firstKeptEntryId,
502
- tokensBefore: event.preparation.tokensBefore,
503
- details,
504
- });
505
-
506
- writeDebugArtifact(
507
- "compaction-event",
508
- {
509
- event: "session_before_compact.remote-v2-success",
510
- consumer: {
511
- provider: consumer.provider,
512
- api: consumer.api,
513
- model: consumer.model,
514
- baseUrl: consumer.baseUrl,
515
- },
516
- compactor: {
517
- provider: compactor.provider,
518
- api: compactor.api,
519
- model: compactor.model,
520
- baseUrl: compactor.baseUrl,
521
- },
522
- requestSource,
523
- remoteV2ContextSource: config.remoteV2ContextSource,
524
- inputProvenance,
525
- requestInputItems: request.input.length,
526
- requestExtras: extras ? Object.keys(extras) : [],
527
- compactResponseId: compactResult.compactResponseId,
528
- compactedItems: compactResult.compactedWindow.length,
529
- firstKeptEntryId: event.preparation.firstKeptEntryId,
530
- },
531
- config,
532
- ctx,
533
- );
534
-
535
- return { outcome: "success", compaction };
536
- }
537
-
538
- async function handleSessionBeforeCompact(
539
- event: SessionBeforeCompactEvent,
540
- ctx: ExtensionContext,
541
- dependencies: CompactionDependencies,
542
- remoteContextActive: RemoteContextActive,
543
- ) {
544
- const { config: toolkitConfig } = dependencies.loadConfig();
545
- const config = toolkitConfig.compaction;
546
- if (!config.enabled) {
547
- return undefined;
548
- }
549
-
550
- writeDebugArtifact(
551
- "compaction-event",
552
- {
553
- event: "session_before_compact",
554
- customInstructions: event.customInstructions,
555
- preparation: {
556
- tokensBefore: event.preparation.tokensBefore,
557
- firstKeptEntryId: event.preparation.firstKeptEntryId,
558
- previousSummaryPresent: Boolean(event.preparation.previousSummary),
559
- messagesToSummarizeCount: event.preparation.messagesToSummarize.length,
560
- turnPrefixMessagesCount: event.preparation.turnPrefixMessages.length,
561
- },
562
- },
563
- config,
564
- ctx,
565
- );
566
-
567
- if (event.signal.aborted) {
568
- return { cancel: true };
569
- }
570
-
571
- // Remote Context management owns this eligible Codex session. It persists a
572
- // no-summary boundary and deliberately never calls remote_compaction_v2.
573
- if (isCodexContextModel(ctx.model, config)) {
574
- if (await remoteContextActive(ctx, config)) {
575
- try {
576
- dependencies.contextWindows.synchronize(ctx);
577
- return dependencies.contextWindows.prepareCompaction(event);
578
- } catch {
579
- notifyRemoteContextFailure(ctx, "malformed-window-state");
580
- return { cancel: true };
581
- }
582
- }
583
- // A configured native Codex Remote session must never re-enter this
584
- // extension's Remote V2 compaction chain, and Pi's native compaction is
585
- // deliberately disabled for Remote-managed models: cancelling here keeps
586
- // the boundary the only rollover mechanism. The inactive reason is
587
- // surfaced so the user can fix activation instead of losing context to a
588
- // silent native summary.
589
- notifyRemoteContextFailure(ctx, "native-codex-context-unavailable");
590
- return { cancel: true };
591
- }
592
-
593
- // Branch 1: Responses-family APIs use remote_compaction_v2 on the normal Responses stream.
594
- let remoteAttempted = false;
595
- const resolution = await resolveRemoteCompactionExecution(
596
- ctx,
597
- {
598
- enabled: config.enabled,
599
- responsesApis: config.responsesApis,
600
- codexGatewayModels: config.contextManagement === "remote" ? config.gatewayContextModels : [],
601
- },
602
- config.remoteCompactModel,
603
- );
604
- if (resolution.ok) {
605
- remoteAttempted = true;
606
- const responsesOutcome = await runResponsesNativeCompact(
607
- event,
608
- ctx,
609
- config,
610
- resolution.execution,
611
- dependencies.remoteCompact,
612
- dependencies.projectCompactionContext,
613
- );
614
- if (responsesOutcome.outcome === "success") {
615
- return { compaction: responsesOutcome.compaction };
616
- }
617
- if (responsesOutcome.outcome === "aborted") {
618
- return { cancel: true };
619
- }
620
- if (responsesOutcome.outcome === "unprojected-input") {
621
- const message = responsesOutcome.reason === COMPACTION_PROJECTION_UNAVAILABLE
622
- ? "Pi's ordered context-hook projection is unavailable; Remote V2 was not sent raw session history."
623
- : responsesOutcome.reason === COMPACTION_SESSION_CONTEXT_UNAVAILABLE
624
- ? "Pi did not provide the current session context required by its context-hook projection."
625
- : responsesOutcome.reason === COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE
626
- ? "Pi's projected context did not include the current compaction summary anchor."
627
- : "The latest opaque checkpoint has no marker for the configured Remote V2 context source, or was created in the other mode.";
628
- writeDebugArtifact(
629
- "compaction-event",
630
- {
631
- event: "session_before_compact.remote-v2-unprojected-input",
632
- reason: responsesOutcome.reason,
633
- contextSource: config.remoteV2ContextSource,
634
- inputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
635
- piContextHookPatch: dependencies.piContextHookPatch,
636
- message,
637
- },
638
- config,
639
- ctx,
640
- );
641
- notifyWarning(
642
- ctx,
643
- `Remote V2 compaction cancelled (${responsesOutcome.reason}); ${message} Keep the context-source setting aligned with the checkpoint or choose legacy mode explicitly.`,
644
- );
645
- return { cancel: true };
646
- }
647
- // failed: fall through to the configured-model fallback below.
648
- } else {
649
- writeDebugArtifact(
650
- "compaction-event",
651
- {
652
- event: "session_before_compact.remote-v2-unavailable",
653
- reason: resolution.reason,
654
- provider: resolution.provider,
655
- api: resolution.api,
656
- model: resolution.model,
657
- baseUrl: resolution.baseUrl,
658
- modelSpec: resolution.modelSpec,
659
- errorMessage: resolution.errorMessage,
660
- },
661
- config,
662
- ctx,
663
- );
664
- if (config.remoteCompactModel) {
665
- notifyWarning(
666
- ctx,
667
- `remote compaction model "${config.remoteCompactModel}" unusable (${resolution.reason}); using the native fallback chain`,
668
- );
669
- }
670
- }
671
-
672
- // Branch 2: run pi's native compaction method. A failed remote request is compacted by the
673
- // remote producer itself; a model that cannot use remote v2 at all uses nativeFallback.model.
674
- const fallbackModelSpec = remoteAttempted ? config.remoteCompactModel : config.nativeFallback.model;
675
- const fallback = await dependencies.nativeFallback({ ctx, event, config, modelSpec: fallbackModelSpec });
676
- if (fallback.ok) {
677
- if (ctx.hasUI) {
678
- ctx.ui.notify(
679
- `${COMPACTION_EXTENSION_ID}: compacted with ${fallback.model.provider}/${fallback.model.id} (native method)`,
680
- "info",
681
- );
682
- }
683
- writeDebugArtifact(
684
- "compaction-event",
685
- {
686
- event: "session_before_compact.fallback-success",
687
- model: fallback.model,
688
- },
689
- config,
690
- ctx,
691
- );
692
- return { compaction: fallback.result };
693
- }
694
-
695
- if (fallback.reason === "aborted") {
696
- return { cancel: true };
697
- }
698
-
699
- writeDebugArtifact(
700
- "compaction-event",
701
- {
702
- event: "session_before_compact.fallback-skip",
703
- reason: fallback.reason,
704
- modelSpec: fallback.modelSpec,
705
- errorMessage: fallback.errorMessage,
706
- ...(fallback.estimatedTokens === undefined ? {} : { estimatedTokens: fallback.estimatedTokens }),
707
- ...(fallback.contextWindow === undefined ? {} : { contextWindow: fallback.contextWindow }),
708
- },
709
- config,
710
- ctx,
711
- );
712
-
713
- // Intentional pi-default paths: feature disabled, nothing configured, or it matches the current one.
714
- const intentionalSkip =
715
- fallback.reason === "disabled" ||
716
- fallback.reason === "no-model-configured" ||
717
- fallback.reason === "same-as-current-model";
718
- if (fallback.reason === "model-window-too-small") {
719
- // The configured summary model is narrower than the request Pi would send for it.
720
- // Skipping is the success path here: pi's current model gets one chance instead of
721
- // the provider terminating the oversized summary.
722
- notifyWarning(
723
- ctx,
724
- `compaction summary model "${fallback.modelSpec}" cannot fit ~${fallback.estimatedTokens} tokens in its ${fallback.contextWindow} window; compacting with the current model instead`,
725
- );
726
- } else if (!intentionalSkip) {
727
- notifyWarning(
728
- ctx,
729
- `compaction model "${fallback.modelSpec}" unusable (${fallback.reason}${fallback.errorMessage ? `: ${fallback.errorMessage}` : ""}); using pi's default compaction`,
730
- );
731
- }
732
-
733
- // Branch 3: pi's default native compaction with the current model.
734
- return undefined;
735
- }
736
-
737
- /**
738
- * Surface (and optionally close) the gap between the durable transcript and the remote window.
739
- *
740
- * Remote context management retires previous windows in the request projection, so a covered
741
- * model never notices that the branch still carries every one of them. Switching to a model
742
- * without remote context - or resuming such a session - hands that whole pile to the provider
743
- * at once, and the first manual `/compact` then has to summarize it. Runs once per agent turn,
744
- * never per request, and only once per window and model so the notice cannot become a nag.
745
- */
746
- async function handleWindowBulk(
747
- ctx: ExtensionContext,
748
- contextWindows: CodexContextWindowManager,
749
- loadConfig: typeof loadToolkitConfig,
750
- trigger: "model-switch" | "turn-end",
751
- ): Promise<void> {
752
- const { config: config } = loadConfig();
753
- const compaction = config.compaction;
754
- if (!compaction.enabled || compaction.contextManagement !== "remote") return;
755
- if (!ctx.model) return;
756
-
757
- const modelKey = `${ctx.model.provider}/${ctx.model.id}`;
758
- const report = evaluateWindowBulk(ctx);
759
- if (!report) return;
760
- contextWindows.noteWindowBulk(report, modelKey);
761
- const cliff = contextWindows.takeBulkCliff(modelKey);
762
- if (!cliff) return;
763
- const action = decideBulkCloseOut({
764
- action: decideBulkCliffAction(cliff, compaction.leaveManagedMode),
765
- mode: ctx.mode,
766
- trigger,
767
- hasPendingTrim: contextWindows.hasPendingTrim(),
768
- });
769
- if (action === "ignore") return;
770
-
771
- writeDebugArtifact(
772
- "compaction-event",
773
- {
774
- event: `window-bulk.${action}`,
775
- trigger,
776
- policy: compaction.leaveManagedMode,
777
- model: modelKey,
778
- unmanagedTokens: cliff.unmanagedTokens,
779
- managedTokens: cliff.managedTokens,
780
- targetContextWindow: cliff.targetContextWindow,
781
- overBudget: cliff.overBudget,
782
- },
783
- compaction,
784
- ctx,
785
- );
786
-
787
- const gap = `${cliff.unmanagedTokens.toLocaleString()} tokens of retired windows are still in this session's transcript, while remote context management has been sending ${cliff.managedTokens.toLocaleString()}`;
788
- if (action === "warn") {
789
- notifyWarning(
790
- ctx,
791
- `${gap} to ${modelKey}${cliff.overBudget ? `, past ${Math.round(BULK_CLIFF_RATIO * 100)}% of its window` : ""}. Checkpoint with notes and run /compact (or new_context) before continuing on a model without remote context, or set compaction.leaveManagedMode="compact" to do it automatically.`,
792
- );
793
- return;
794
- }
795
-
796
- notifyWarning(ctx, `${gap} to ${modelKey}, past ${Math.round(BULK_CLIFF_RATIO * 100)}% of its window; compacting the retired windows first.`);
797
- if (typeof ctx.compact !== "function") return;
798
- ctx.compact({
799
- onError: (error: Error) => {
800
- notifyWarning(ctx, `automatic boundary compaction failed: ${error.message}`);
801
- },
802
- });
803
- }
804
-
805
- async function handleContextInternal(
806
- event: ContextEvent,
807
- ctx: ExtensionContext,
808
- pi: ExtensionAPI,
809
- loadConfig: typeof loadToolkitConfig,
810
- contextWindows: CodexContextWindowManager,
811
- remoteContextActive: RemoteContextActive,
812
- ) {
813
- const { config: { compaction: config } } = loadConfig();
814
- if (!config.enabled) {
815
- const visibleMessages = contextWindows.project(event.messages, "off");
816
- return visibleMessages.length === event.messages.length && visibleMessages.every((message, index) => message === event.messages[index])
817
- ? undefined
818
- : { messages: visibleMessages };
819
- }
820
-
821
- if (config.contextManagement === "remote") {
822
- try {
823
- contextWindows.synchronize(ctx);
824
- } catch {
825
- notifyRemoteContextFailure(ctx, "malformed-window-state");
826
- reportPiContextHookFailure(ctx, "malformed-window-state");
827
- ctx.abort();
828
- return undefined;
829
- }
830
-
831
- const remoteActive = await remoteContextActive(ctx, config);
832
- if (remoteActive) {
833
- try {
834
- contextWindows.recordBudget(
835
- pi,
836
- ctx,
837
- true,
838
- config.contextReminderThresholdPercent,
839
- );
840
- const projected = contextWindows.project(event.messages, "remote");
841
- reportProjectionDiagnostics(contextWindows, config, ctx);
842
- return projected.length === event.messages.length && projected.every((message, index) => message === event.messages[index])
843
- ? undefined
844
- : { messages: projected };
845
- } catch (error) {
846
- notifyRemoteContextFailure(ctx, "malformed-window-state");
847
- reportPiContextHookFailure(ctx, "malformed-window-state");
848
- ctx.abort();
849
- return undefined;
850
- }
851
- }
852
-
853
- // An eligible Codex Remote session is never allowed to fall through to the
854
- // older compaction-replay path. Gateway traffic must also fail closed when
855
- // its transport capability is unavailable; otherwise Pi could send an
856
- // ordinary unscoped request and lose CPA OAuth/session affinity.
857
- if (isCodexContextModel(ctx.model, config)) {
858
- if (isCodexGatewayModel(ctx.model, config.gatewayContextModels)) {
859
- notifyRemoteContextFailure(ctx, "codex-context-unavailable");
860
- reportPiContextHookFailure(ctx, "codex-context-unavailable");
861
- ctx.abort();
862
- return undefined;
863
- }
864
- const visibleMessages = contextWindows.project(event.messages, "off");
865
- return visibleMessages.length === event.messages.length && visibleMessages.every((message, index) => message === event.messages[index])
866
- ? undefined
867
- : { messages: visibleMessages };
868
- }
869
- }
870
-
871
- // Inactive/ineligible Remote mode must not expose internal window markers to a
872
- // gateway or another provider. The existing compaction replay path remains the
873
- // owner for this safe fallback.
874
- const visibleMessages = contextWindows.project(event.messages, "off");
875
- const replayEvent = visibleMessages === event.messages ? event : { ...event, messages: visibleMessages };
876
- const resolution = await resolveNativeCompactionEnvironment(ctx, {
877
- enabled: config.enabled,
878
- responsesApis: config.responsesApis,
879
- codexGatewayModels: config.contextManagement === "remote" ? config.gatewayContextModels : [],
880
- });
881
- if (!resolution.ok) return undefined;
882
- const branchEntries = ctx.sessionManager.getBranch();
883
- const latest = resolveLatestNativeCompactionEntry(branchEntries, { baseUrl: resolution.runtime.baseUrl });
884
- if (!latest.ok) return undefined;
885
- const result = removeNativeCompactionRetainedMessages({
886
- messages: replayEvent.messages,
887
- branchEntries,
888
- compactionEntry: latest.entry,
889
- expectedInputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
890
- });
891
- if (!result.ok) {
892
- writeReplayFailureArtifact({ reason: result.reason, compactionEntryId: latest.entry.id }, config, ctx);
893
- if (ctx.hasUI) ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: replay failed (${result.reason}); request aborted`, "error");
894
- reportPiContextHookFailure(ctx, `replay-failed:${result.reason}`);
895
- ctx.abort();
896
- return undefined;
897
- }
898
- return result.messages === replayEvent.messages ? undefined : { messages: result.messages };
899
- }
900
-
901
- /**
902
- * Pi's emitContext() catches handler exceptions. Re-throw as usual for Pi, but
903
- * also report the failure so the compaction projector cannot mistake the
904
- * caught exception for a successful projection.
905
- */
906
- async function handleContext(
907
- event: ContextEvent,
908
- ctx: ExtensionContext,
909
- pi: ExtensionAPI,
910
- loadConfig: typeof loadToolkitConfig,
911
- contextWindows: CodexContextWindowManager,
912
- remoteContextActive: RemoteContextActive,
913
- ) {
914
- try {
915
- return await handleContextInternal(event, ctx, pi, loadConfig, contextWindows, remoteContextActive);
916
- } catch (error) {
917
- reportPiContextHookFailure(ctx, error instanceof Error ? error.message : String(error));
918
- throw error;
919
- }
920
- }
921
-
922
- async function handleBeforeProviderRequest(
923
- event: BeforeProviderRequestEvent,
924
- ctx: ExtensionContext,
925
- loadConfig: typeof loadToolkitConfig,
926
- contextWindows: CodexContextWindowManager,
927
- remoteContextActive: RemoteContextActive,
928
- ) {
929
- const { config: toolkitConfig } = loadConfig();
930
- const config = toolkitConfig.compaction;
931
- if (!config.enabled) {
932
- return undefined;
933
- }
934
-
935
- if (config.contextManagement === "remote") {
936
- try {
937
- contextWindows.synchronize(ctx);
938
- } catch {
939
- notifyRemoteContextFailure(ctx, "malformed-window-state");
940
- ctx.abort();
941
- return undefined;
942
- }
943
- }
944
- if (config.contextManagement === "remote" && await remoteContextActive(ctx, config)) {
945
- try {
946
- return contextWindows.rewritePayload(event.payload, ctx);
947
- } catch {
948
- notifyRemoteContextFailure(ctx, "malformed-request-state");
949
- ctx.abort();
950
- return undefined;
951
- }
952
- }
953
- if (isCodexContextModel(ctx.model, config)) {
954
- // Keep Codex Remote mutually exclusive with the legacy replay pipeline
955
- // when authentication or tool ownership is unavailable. Gateway traffic
956
- // cannot continue as an ordinary Responses request because that would
957
- // silently lose the required CPA session/account selection.
958
- if (isCodexGatewayModel(ctx.model, config.gatewayContextModels)) {
959
- notifyRemoteContextFailure(ctx, "codex-context-unavailable");
960
- ctx.abort();
961
- }
962
- return undefined;
963
- }
964
-
965
- const resolution = await resolveNativeCompactionEnvironment(
966
- ctx,
967
- {
968
- enabled: config.enabled,
969
- responsesApis: config.responsesApis,
970
- codexGatewayModels: config.contextManagement === "remote" ? config.gatewayContextModels : [],
971
- },
972
- event.payload,
973
- );
974
- if (resolution.ok === false) {
975
- writeDebugArtifact(
976
- "provider-request",
977
- {
978
- event: "before_provider_request.skip",
979
- reason: resolution.reason,
980
- provider: resolution.provider,
981
- api: resolution.api,
982
- model: resolution.model,
983
- baseUrl: resolution.baseUrl,
984
- errorMessage: resolution.errorMessage,
985
- currentModel: getCurrentModelDebugInfo(ctx),
986
- payload: event.payload,
987
- },
988
- config,
989
- ctx,
990
- );
991
- return undefined;
992
- }
993
-
994
- const runtime = resolution.runtime;
995
- const payload = runtime.payload;
996
- if (!payload) {
997
- return undefined;
998
- }
999
-
1000
- // Capture compact-relevant request fields (tools, reasoning, ...) for the next
1001
- // synthetic compact request using the active consumer's effective runtime identity.
1002
- // This hook runs before the separate Web Search transform, so injected native search
1003
- // tools are not copied into remote_compaction_v2.
1004
- const webSearchRoute = resolveWebSearchRoute({ model: ctx.model, config: toolkitConfig.webSearch });
1005
- rememberRequestContext(
1006
- payload,
1007
- {
1008
- provider: runtime.provider,
1009
- api: runtime.api,
1010
- model: runtime.model,
1011
- baseUrl: runtime.baseUrl,
1012
- sessionId: getSessionId(ctx),
1013
- },
1014
- {
1015
- excludeWebSearchTools: webSearchRoute.route === "standalone-alpha",
1016
- },
1017
- );
1018
-
1019
- const branchEntries = ctx.sessionManager.getBranch();
1020
- const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
1021
- baseUrl: runtime.baseUrl,
1022
- });
1023
- if (!latestNativeCompaction.ok) {
1024
- writeDebugArtifact(
1025
- "provider-request",
1026
- {
1027
- event: "before_provider_request.no-native-compaction",
1028
- reason: latestNativeCompaction.reason,
1029
- provider: runtime.provider,
1030
- api: runtime.api,
1031
- model: runtime.model,
1032
- baseUrl: runtime.baseUrl,
1033
- branchEntries: branchEntries.length,
1034
- latestCompactionIndex: latestNativeCompaction.latestCompactionIndex,
1035
- latestCompactionIdentity: getCompactionIdentityDebugInfo(latestNativeCompaction.latestCompaction),
1036
- payload,
1037
- },
1038
- config,
1039
- ctx,
1040
- );
1041
- return undefined;
1042
- }
1043
-
1044
- const latestNativeCompactionEntry = latestNativeCompaction.entry;
1045
- const rewrite = rewriteResponsesPayloadWithNativeReplay({
1046
- model: runtime.currentModel,
1047
- payload,
1048
- branchEntries,
1049
- compactionEntry: latestNativeCompactionEntry,
1050
- expectedInputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
1051
- });
1052
- if (!rewrite.ok) {
1053
- writeDebugArtifact(
1054
- "provider-request",
1055
- {
1056
- event: "before_provider_request.rewrite-failed",
1057
- reason: rewrite.reason,
1058
- provider: runtime.provider,
1059
- api: runtime.api,
1060
- model: runtime.model,
1061
- baseUrl: runtime.baseUrl,
1062
- compactionEntryId: latestNativeCompactionEntry.id,
1063
- parity: rewrite.parity,
1064
- payload,
1065
- },
1066
- config,
1067
- ctx,
1068
- );
1069
-
1070
- // Fail loud instead of letting Pi send the sentinel-only payload: the
1071
- // compacted history would be silently lost while the request still succeeds.
1072
- // A forced redacted failure record is written even when logProviderPayloads
1073
- // is disabled so the incident is diagnosable without leaking content.
1074
- writeReplayFailureArtifact(
1075
- {
1076
- reason: rewrite.reason,
1077
- parity: rewrite.parity,
1078
- compactionEntryId: latestNativeCompactionEntry.id,
1079
- provider: runtime.provider,
1080
- api: runtime.api,
1081
- model: runtime.model,
1082
- },
1083
- config,
1084
- ctx,
1085
- );
1086
- if (ctx.hasUI) {
1087
- ctx.ui.notify(
1088
- `${COMPACTION_EXTENSION_ID}: native compaction replay failed (${rewrite.reason}); provider request aborted`,
1089
- "error",
1090
- );
1091
- }
1092
- ctx.abort();
1093
- return undefined;
1094
- }
1095
-
1096
- writeDebugArtifact(
1097
- "provider-request",
1098
- {
1099
- event: "before_provider_request.native-rewrite",
1100
- provider: runtime.provider,
1101
- api: runtime.api,
1102
- model: runtime.model,
1103
- baseUrl: runtime.baseUrl,
1104
- compactionEntryId: latestNativeCompactionEntry.id,
1105
- boundaryIndex: rewrite.segments.boundaryIndex,
1106
- firstKeptEntryIndex: rewrite.segments.firstKeptEntryIndex,
1107
- originalInputItems: payload.input.length,
1108
- rewrittenInputItems: rewrite.rewrittenPayload.input.length,
1109
- leadingItems: rewrite.segments.leading.length,
1110
- compactionSummaryItems: rewrite.segments.compactionSummary.length,
1111
- compactedItems: rewrite.segments.compactedWindow.length,
1112
- postItems: rewrite.segments.post.length,
1113
- payload: rewrite.rewrittenPayload,
1114
- originalPayload: payload,
1115
- },
1116
- config,
1117
- ctx,
1118
- );
1119
-
1120
- return rewrite.rewrittenPayload;
1121
- }
1122
-
1123
- export default function registerCompactionExtension(
1124
- pi: ExtensionAPI,
1125
- overrides: Partial<CompactionDependencies> = {},
1126
- ) {
1127
- const loadConfig = overrides.loadConfig ?? loadToolkitConfig;
1128
- const piContextHookPatch = installPiContextHookPatch();
1129
- const contextWindows = overrides.contextWindows ?? new CodexContextWindowManager((ctx, signal) =>
1130
- loadHistoryNotesThreadHint(ctx, signal, loadConfig().config.compaction.gatewayContextModels)
1131
- );
1132
- const dependencies: CompactionDependencies = {
1133
- loadConfig,
1134
- remoteCompact: executeRemoteV2Compaction,
1135
- nativeFallback: runNativeFallbackCompaction,
1136
- contextWindows,
1137
- ...overrides,
1138
- piContextHookPatch: overrides.piContextHookPatch ?? piContextHookPatch,
1139
- };
1140
- let tools!: ReturnType<typeof registerContextManagementTools>;
1141
- let contextWindowReady = false;
1142
- const isContextRuntimeActive = async (
1143
- ctx: ExtensionContext,
1144
- config: CompactionConfig,
1145
- model = ctx.model,
1146
- ): Promise<boolean> =>
1147
- contextWindowReady && tools.isRegistered && await isRemoteContextActive(ctx, config, model);
1148
- tools = registerContextManagementTools(
1149
- pi,
1150
- contextWindows,
1151
- async (ctx) => {
1152
- const config = dependencies.loadConfig().config.compaction;
1153
- return isContextRuntimeActive(ctx, config);
1154
- },
1155
- () => dependencies.loadConfig().config.compaction.gatewayContextModels,
1156
- );
1157
- const remoteContextActive: RemoteContextActive = isContextRuntimeActive;
1158
- const syncTools = async (
1159
- ctx: ExtensionContext,
1160
- model = ctx.model,
1161
- options: { notifyWindowFailure?: boolean } = {},
1162
- ): Promise<ContextToolSyncOutcome> => {
1163
- // Until this call proves otherwise, do not let a previous session/window
1164
- // identity make a failed activation look usable.
1165
- contextWindowReady = false;
1166
- const config = dependencies.loadConfig().config.compaction;
1167
- const eligible = await isRemoteContextActive(ctx, config, model);
1168
- const toolSync = tools.sync(eligible);
1169
- const outcome: ContextToolSyncOutcome = {
1170
- eligible,
1171
- toolsSynced: toolSync.synced,
1172
- registrationState: toolSync.registrationState,
1173
- windowInitialized: false,
1174
- };
1175
- if (!eligible || !toolSync.synced) return outcome;
1176
- // Activation can succeed after `session_start` missed it, because Pi 0.86 may
1177
- // reject the registration read while a session replacement is still binding.
1178
- // The window lifecycle has to open on that later activation too, or requests
1179
- // carry no window metadata and the backend never ingests the turns.
1180
- try {
1181
- contextWindows.ensureInitialized(pi, ctx, true);
1182
- contextWindowReady = true;
1183
- return { ...outcome, windowInitialized: true };
1184
- } catch {
1185
- // Do not leave tools exposed while the request path has no valid window
1186
- // identity. The next lifecycle hook may retry once Pi is usable again.
1187
- tools.sync(false);
1188
- if (options.notifyWindowFailure) notifyRemoteContextFailure(ctx, "malformed-window-state");
1189
- return outcome;
1190
- }
1191
- };
1192
- pi.on("session_start", async (_event, ctx) => {
1193
- const syncOutcome = await syncTools(ctx, ctx.model, { notifyWindowFailure: true });
1194
- const active = syncOutcome.eligible && syncOutcome.toolsSynced && syncOutcome.windowInitialized;
1195
- const { config: toolkitConfig, source, warnings } = dependencies.loadConfig();
1196
- const config = toolkitConfig.compaction;
1197
- if (!config.enabled) return;
1198
-
1199
- let activationReason: string | undefined;
1200
- // Only models the built-in Remote Context coverage targets may activate or
1201
- // warn; everything else silently runs Pi's normal compaction path.
1202
- if (config.contextManagement === "remote" && isCodexContextModel(ctx.model, config) && !active) {
1203
- if (syncOutcome.registrationState === "conflict") {
1204
- activationReason = "tool-name-conflict";
1205
- notifyRemoteContextFailure(ctx, activationReason);
1206
- } else if (syncOutcome.registrationState === "unverified" || !syncOutcome.toolsSynced) {
1207
- // Transient: the runtime has not published or accepted our tools yet. A
1208
- // later before_agent_start re-verifies, so do not report provider failure.
1209
- activationReason = "tool-registration-pending";
1210
- } else if (syncOutcome.eligible && !syncOutcome.windowInitialized) {
1211
- activationReason = "malformed-window-state";
1212
- } else {
1213
- const remoteResolution = await resolveCodexContextProvider(ctx, ctx.model, config.gatewayContextModels);
1214
- activationReason = remoteResolution.ok ? "codex-context-unavailable" : remoteResolution.reason;
1215
- notifyRemoteContextFailure(ctx, activationReason);
1216
- }
1217
- }
1218
-
1219
- if (warnings.length > 0 && ctx.hasUI && config.debug) {
1220
- ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: ${warnings[0]}`, "warning");
1221
- }
1222
-
1223
- const artifactPath = writeDebugArtifact(
1224
- "lifecycle",
1225
- {
1226
- event: "session_start",
1227
- config,
1228
- configSource: source,
1229
- warnings,
1230
- activation: {
1231
- active,
1232
- contextManagement: config.contextManagement,
1233
- remoteV2ContextSource: config.remoteV2ContextSource,
1234
- model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined,
1235
- ...(activationReason ? { reason: activationReason } : {}),
1236
- },
1237
- piContextHookPatch: dependencies.piContextHookPatch,
1238
- },
1239
- config,
1240
- ctx,
1241
- );
1242
-
1243
- if (ctx.hasUI && (config.notifyOnLoad || config.debug)) {
1244
- ctx.ui.notify(
1245
- artifactPath
1246
- ? `${COMPACTION_EXTENSION_ID} loaded • debug artifacts → ${artifactPath}`
1247
- : `${COMPACTION_EXTENSION_ID} loaded`,
1248
- "info",
1249
- );
1250
- }
1251
- });
1252
-
1253
- pi.on("context", (event, ctx) => handleContext(event, ctx, pi, dependencies.loadConfig, contextWindows, remoteContextActive));
1254
- pi.on("session_before_compact", (event, ctx) => handleSessionBeforeCompact(event, ctx, dependencies, remoteContextActive));
1255
- pi.on("session_compact", (event, _ctx) => contextWindows.recordCompaction(event.compactionEntry.details));
1256
- pi.on("session_shutdown", () => {
1257
- contextWindowReady = false;
1258
- contextWindows.reset();
1259
- tools.reset();
1260
- });
1261
- pi.on("agent_settled", async (_event, ctx) => {
1262
- // Idle: no retry, compaction, or queued continuation is left running, so this is the
1263
- // only safe place to spend a model call on the user's behalf.
1264
- await handleWindowBulk(ctx, contextWindows, dependencies.loadConfig, "turn-end");
1265
- });
1266
- pi.on("model_select", async (event, ctx) => {
1267
- // Switching into a covered model mid-session must open the window
1268
- // lifecycle immediately: without an identity the request rewrite skips
1269
- // window metadata, the backend never ingests those turns, and the first
1270
- // new_context would trim pre-switch history that no history can recover.
1271
- // syncTools() activates and initializes the window when the model is covered.
1272
- await syncTools(ctx, event.model, { notifyWindowFailure: true });
1273
- // The switch itself is the moment the durable transcript changes consumer, and the
1274
- // session is idle here, so the close-out must happen now rather than mid-turn.
1275
- await handleWindowBulk(ctx, contextWindows, dependencies.loadConfig, "model-switch");
1276
- });
1277
- pi.on("before_agent_start", async (_event, ctx) => {
1278
- await syncTools(ctx, ctx.model, { notifyWindowFailure: true });
1279
- });
1280
- pi.on("before_provider_request", (event, ctx) => handleBeforeProviderRequest(event, ctx, dependencies.loadConfig, contextWindows, remoteContextActive));
1281
- pi.on("before_provider_headers", async (event, ctx) => {
1282
- const config = dependencies.loadConfig().config.compaction;
1283
- if (!isCodexContextModel(ctx.model, config)) return;
1284
- const active = await remoteContextActive(ctx, config);
1285
- if (!active) {
1286
- if (isCodexGatewayModel(ctx.model, config.gatewayContextModels)) {
1287
- notifyRemoteContextFailure(ctx, "codex-context-unavailable");
1288
- ctx.abort();
1289
- }
1290
- return;
1291
- }
1292
- const provider = await resolveCodexContextProvider(ctx, ctx.model, config.gatewayContextModels);
1293
- if (provider.ok && provider.provider.kind === "codex-gateway") {
1294
- const sessionId = getSessionId(ctx);
1295
- const gatewayHeaders = codexContextProviderHeaders(provider.provider, {
1296
- sessionId,
1297
- clientRequestId: sessionId,
1298
- });
1299
- const allowedGatewayHeaders = new Set<string>(CODEX_GATEWAY_FORWARD_HEADERS);
1300
- for (const existing of Object.keys(event.headers)) {
1301
- if (!allowedGatewayHeaders.has(existing.toLowerCase())) delete event.headers[existing];
1302
- }
1303
- for (const name of [
1304
- "authorization",
1305
- "originator",
1306
- "user-agent",
1307
- "version",
1308
- "session-id",
1309
- "x-client-request-id",
1310
- "x-codex-affinity-scope",
1311
- "x-codex-model",
1312
- ]) {
1313
- for (const existing of Object.keys(event.headers)) {
1314
- if (existing.toLowerCase() === name) delete event.headers[existing];
1315
- }
1316
- const value = gatewayHeaders.get(name);
1317
- if (value) event.headers[name] = value;
1318
- }
1319
- }
1320
- contextWindows.rewriteHeaders(event.headers, ctx);
1321
- });
1322
- pi.on("message_end", async (event, ctx) => {
1323
- const config = dependencies.loadConfig().config.compaction;
1324
- if (!isCodexContextModel(ctx.model, config) || !tools.isRegistered) return undefined;
1325
- if (!(await remoteContextActive(ctx, config))) return undefined;
1326
- const message = routeContextNamespaceToolMessage(event.message);
1327
- return message === event.message ? undefined : { message };
1328
- });
1329
- }
1
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
+ import type {
3
+ BeforeProviderRequestEvent,
4
+ ContextEvent,
5
+ CompactionResult,
6
+ ExtensionAPI,
7
+ ExtensionContext,
8
+ SessionBeforeCompactEvent,
9
+ } from "@earendil-works/pi-coding-agent";
10
+ import { assertConfigValid, loadToolkitConfig, resolveToolkitConfig, type ResolvedToolkitConfig } from "./config";
11
+ import { notifyConfigIssues } from "./config/notifications";
12
+ import { registerToolkitConfigCommand } from "./config-command";
13
+ import {
14
+ codexContextProviderHeaders,
15
+ resolveCodexContextProvider,
16
+ isCodexGatewayModel,
17
+ isNativeCodexModel,
18
+ } from "./context-management/codex-provider";
19
+ import { routeContextNamespaceToolMessage } from "./context-management/namespace-tools";
20
+ import { loadHistoryNotesThreadHint } from "./context-management/history-notes";
21
+ import { CodexContextWindowManager } from "./context-management/window-manager";
22
+ import {
23
+ BULK_CLIFF_RATIO,
24
+ decideBulkCliffAction,
25
+ decideBulkCloseOut,
26
+ evaluateWindowBulk,
27
+ } from "./context-management/window-bulk";
28
+ import {
29
+ registerContextManagementTools,
30
+ type ContextToolRegistrationState,
31
+ } from "./context-management/tools";
32
+ import { writeDebugArtifact, writeReplayFailureArtifact } from "./debug";
33
+ import {
34
+ COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
35
+ COMPACTION_PROJECTION_UNAVAILABLE,
36
+ COMPACTION_SESSION_CONTEXT_UNAVAILABLE,
37
+ COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
38
+ hasVerifiedCompactionInputProvenance,
39
+ type UnprojectedCompactionReason,
40
+ } from "./compaction-projection";
41
+ import { CODEX_GATEWAY_FORWARD_HEADERS } from "./responses-headers";
42
+ import { resolveLatestNativeCompactionEntry } from "./details-store";
43
+ import {
44
+ getPiContextHookProjector,
45
+ installPiContextHookPatch,
46
+ reportPiContextHookFailure,
47
+ type PiContextHookPatchResult,
48
+ } from "./pi-context-hook";
49
+ import { runNativeFallbackCompaction } from "./native-fallback";
50
+ import {
51
+ rewriteResponsesPayloadWithNativeReplay,
52
+ removeNativeCompactionRetainedMessages,
53
+ serializeLiveTailToResponsesInput,
54
+ } from "./payload-rewrite";
55
+ import { getCompactionRequestExtras, rememberRequestContext } from "./request-context-cache";
56
+ import { resolveWebSearchRoute } from "./web-search/types";
57
+ import { executeRemoteV2Compaction } from "./remote-v2-client";
58
+ import {
59
+ resolveNativeCompactionEnvironment,
60
+ resolveRemoteCompactionExecution,
61
+ parseModelSpec,
62
+ type RemoteCompactionExecution,
63
+ } from "./runtime";
64
+ import {
65
+ serializeMessagesToCompactRequest,
66
+ serializeMessagesToResponsesInput,
67
+ type NativeCompactionRequestBody,
68
+ type ResponsesInputItem,
69
+ } from "./serializer";
70
+ import {
71
+ createNativeCompactionDetails,
72
+ createNativeCompactionResult,
73
+ COMPACTION_EXTENSION_ID,
74
+ getLatestDeferredToolCarryover,
75
+ getRemoteV2InputProvenance,
76
+ isNativeCompactionDetails,
77
+ type CompactionConfig,
78
+ type NativeCompactionDetails,
79
+ type NativeCompactionRequestMeta,
80
+ } from "./types";
81
+
82
+ type CompactionContextProjection = (
83
+ messages: readonly AgentMessage[],
84
+ ctx: ExtensionContext,
85
+ ) => readonly AgentMessage[] | undefined | Promise<readonly AgentMessage[] | undefined>;
86
+
87
+ function contextOperation(loadConfig: typeof loadToolkitConfig, ctx: ExtensionContext, model = ctx.model): ResolvedToolkitConfig {
88
+ const loaded = loadConfig();
89
+ const resolved = resolveToolkitConfig(loaded, model);
90
+ notifyConfigIssues(ctx, resolved);
91
+ return resolved;
92
+ }
93
+
94
+ function requireContextPolicy(resolved: ResolvedToolkitConfig, ctx: ExtensionContext): void {
95
+ try {
96
+ assertConfigValid(resolved, "context", "compatibility", "diagnostics");
97
+ } catch (error) {
98
+ if (typeof ctx.abort === "function") ctx.abort();
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ function compactGatewayModels(resolved: ResolvedToolkitConfig): readonly string[] {
104
+ return resolved.format === "v2" || resolved.config.compaction.contextManagement === "remote"
105
+ ? resolved.gatewayModelKeys : [];
106
+ }
107
+
108
+ type CompactionDependencies = {
109
+ loadConfig: typeof loadToolkitConfig;
110
+ remoteCompact: typeof executeRemoteV2Compaction;
111
+ nativeFallback: typeof runNativeFallbackCompaction;
112
+ contextWindows: CodexContextWindowManager;
113
+ /** Test seam for the same ordered projection exposed by the Pi host patch. */
114
+ projectCompactionContext?: CompactionContextProjection;
115
+ piContextHookPatch: PiContextHookPatchResult;
116
+ };
117
+
118
+ type RemoteContextActive = (
119
+ ctx: ExtensionContext,
120
+ config: CompactionConfig,
121
+ model?: ExtensionContext["model"],
122
+ ) => Promise<boolean>;
123
+
124
+ type ContextToolSyncOutcome = {
125
+ eligible: boolean;
126
+ toolsSynced: boolean;
127
+ registrationState: ContextToolRegistrationState;
128
+ windowInitialized: boolean;
129
+ };
130
+
131
+ type ResponsesCompactOutcome =
132
+ | { outcome: "success"; compaction: CompactionResult<NativeCompactionDetails> }
133
+ | { outcome: "aborted" }
134
+ | { outcome: "failed" }
135
+ | {
136
+ outcome: "unprojected-input";
137
+ reason: UnprojectedCompactionReason;
138
+ };
139
+
140
+ function buildCompactionRequestMeta(event: SessionBeforeCompactEvent): NativeCompactionRequestMeta {
141
+ return {
142
+ tokensBefore: event.preparation.tokensBefore,
143
+ previousSummaryPresent: Boolean(event.preparation.previousSummary),
144
+ };
145
+ }
146
+
147
+ function getCurrentModelDebugInfo(ctx: ExtensionContext) {
148
+ return ctx.model
149
+ ? {
150
+ provider: ctx.model.provider,
151
+ id: ctx.model.id,
152
+ }
153
+ : undefined;
154
+ }
155
+
156
+ function getCompactionIdentityDebugInfo(entry: { details?: unknown } | undefined) {
157
+ return isNativeCompactionDetails(entry?.details)
158
+ ? {
159
+ provider: entry.details.provider,
160
+ api: entry.details.api,
161
+ model: entry.details.model,
162
+ baseUrl: entry.details.baseUrl,
163
+ compactionModel: entry.details.compactionModel,
164
+ }
165
+ : undefined;
166
+ }
167
+
168
+ function getSessionId(ctx: ExtensionContext): string | undefined {
169
+ try {
170
+ return ctx.sessionManager.getSessionId();
171
+ } catch {
172
+ return undefined;
173
+ }
174
+ }
175
+
176
+ function notifyWarning(ctx: ExtensionContext, message: string): void {
177
+ if (ctx.hasUI) {
178
+ ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: ${message}`, "warning");
179
+ }
180
+ }
181
+
182
+ async function isRemoteContextActive(
183
+ ctx: ExtensionContext,
184
+ config: CompactionConfig,
185
+ model: ExtensionContext["model"] = ctx.model,
186
+ ): Promise<boolean> {
187
+ if (!config.enabled || config.contextManagement !== "remote") return false;
188
+ const resolution = await resolveCodexContextProvider(ctx, model, config.gatewayContextModels);
189
+ return resolution.ok;
190
+ }
191
+
192
+ function isCodexContextModel(
193
+ model: ExtensionContext["model"] | undefined,
194
+ config: CompactionConfig,
195
+ ): boolean {
196
+ return config.contextManagement === "remote"
197
+ && (isNativeCodexModel(model) || isCodexGatewayModel(model, config.gatewayContextModels));
198
+ }
199
+
200
+ function notifyRemoteContextFailure(ctx: ExtensionContext, reason: string): void {
201
+ if (ctx.hasUI) {
202
+ ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: Remote Context management inactive (${reason})`, "warning");
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Surface every window trim that had to be refused to keep the tool loadout intact.
208
+ *
209
+ * The refusal itself is safe: the model keeps the previous turns instead of losing its
210
+ * tools. It is still a continuity incident, so it is recorded once per window and reason
211
+ * instead of being allowed to pass silently.
212
+ */
213
+ function reportProjectionDiagnostics(
214
+ contextWindows: CodexContextWindowManager,
215
+ config: CompactionConfig,
216
+ ctx: ExtensionContext,
217
+ ): void {
218
+ const diagnostics = contextWindows.takeProjectionDiagnostics();
219
+ if (diagnostics.length === 0) return;
220
+ writeDebugArtifact(
221
+ "compaction-event",
222
+ { event: "context.projection.tool_loadout_shrink", diagnostics },
223
+ config,
224
+ ctx,
225
+ );
226
+ if (ctx.hasUI) {
227
+ ctx.ui.notify(
228
+ `${COMPACTION_EXTENSION_ID}: kept previous context to preserve tools (${diagnostics.join("; ")})`,
229
+ "warning",
230
+ );
231
+ }
232
+ }
233
+
234
+ function cloneOpaqueWindow(window: readonly unknown[]): unknown[] {
235
+ return window.map((item) => structuredClone(item));
236
+ }
237
+
238
+ function buildCompactionInstructions(systemPrompt: string, customInstructions?: string): string {
239
+ const guidance = customInstructions?.trim();
240
+ if (!guidance) {
241
+ return systemPrompt;
242
+ }
243
+
244
+ return `${systemPrompt}\n\nAdditional user guidance for this manual /compact request:\n${guidance}`;
245
+ }
246
+
247
+ function findLatestProjectedCompactionSummaryIndex(
248
+ messages: readonly AgentMessage[],
249
+ summary: string,
250
+ ): number {
251
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
252
+ const message = messages[index];
253
+ if (message?.role === "compactionSummary" && message.summary === summary) {
254
+ return index;
255
+ }
256
+ }
257
+
258
+ return -1;
259
+ }
260
+
261
+ type SessionManagerWithOptionalContext = ExtensionContext["sessionManager"] & {
262
+ buildSessionContext?: () => { messages?: readonly AgentMessage[] };
263
+ };
264
+
265
+ function readSessionContextMessages(ctx: ExtensionContext): AgentMessage[] | undefined {
266
+ const sessionManager = ctx.sessionManager as SessionManagerWithOptionalContext;
267
+ if (typeof sessionManager.buildSessionContext !== "function") {
268
+ return undefined;
269
+ }
270
+
271
+ try {
272
+ const sessionContext = sessionManager.buildSessionContext();
273
+ if (!sessionContext || !Array.isArray(sessionContext.messages)) {
274
+ return undefined;
275
+ }
276
+ return structuredClone([...sessionContext.messages]);
277
+ } catch {
278
+ return undefined;
279
+ }
280
+ }
281
+
282
+ type CompactionProjectionResult =
283
+ | { ok: true; messages: readonly AgentMessage[] }
284
+ | { ok: false; reason: typeof COMPACTION_PROJECTION_UNAVAILABLE | typeof COMPACTION_SESSION_CONTEXT_UNAVAILABLE };
285
+
286
+ async function projectSessionContextForCompaction(
287
+ ctx: ExtensionContext,
288
+ projectCompactionContext: CompactionContextProjection | undefined,
289
+ ): Promise<CompactionProjectionResult> {
290
+ const messages = readSessionContextMessages(ctx);
291
+ if (!messages) {
292
+ return { ok: false, reason: COMPACTION_SESSION_CONTEXT_UNAVAILABLE };
293
+ }
294
+
295
+ const projector = projectCompactionContext ?? getPiContextHookProjector(ctx);
296
+ if (!projector) {
297
+ return { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
298
+ }
299
+
300
+ try {
301
+ const projected = await projector(messages, ctx);
302
+ return Array.isArray(projected)
303
+ ? { ok: true, messages: projected }
304
+ : { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
305
+ } catch {
306
+ return { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
307
+ }
308
+ }
309
+
310
+ function getLegacySessionContextMessages(
311
+ event: SessionBeforeCompactEvent,
312
+ ctx: ExtensionContext,
313
+ ): AgentMessage[] {
314
+ return readSessionContextMessages(ctx) ?? [
315
+ ...event.preparation.messagesToSummarize,
316
+ ...event.preparation.turnPrefixMessages,
317
+ ];
318
+ }
319
+
320
+ async function runResponsesNativeCompact(
321
+ event: SessionBeforeCompactEvent,
322
+ ctx: ExtensionContext,
323
+ config: CompactionConfig,
324
+ execution: RemoteCompactionExecution,
325
+ remoteCompact: typeof executeRemoteV2Compaction,
326
+ projectCompactionContext: CompactionContextProjection | undefined,
327
+ ): Promise<ResponsesCompactOutcome> {
328
+ const { consumer, compactor } = execution;
329
+ const instructions = buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions);
330
+ const branchEntries = event.branchEntries ?? ctx.sessionManager.getBranch();
331
+ const deferredToolCarryover = getLatestDeferredToolCarryover(branchEntries);
332
+ const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
333
+ baseUrl: consumer.baseUrl,
334
+ });
335
+
336
+ const inputProvenance = getRemoteV2InputProvenance(config.remoteV2ContextSource);
337
+ let requestSource: "session-context" | "non-native-session-context" | "latest-native-replay";
338
+ let request: NativeCompactionRequestBody;
339
+ if (latestNativeCompaction.ok) {
340
+ const details = latestNativeCompaction.entry.details;
341
+ if (!details) {
342
+ return { outcome: "failed" };
343
+ }
344
+ if (!hasVerifiedCompactionInputProvenance(details, inputProvenance)) {
345
+ return {
346
+ outcome: "unprojected-input",
347
+ reason: COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
348
+ };
349
+ }
350
+ requestSource = "latest-native-replay";
351
+
352
+ if (config.remoteV2ContextSource === "pi-context-hook") {
353
+ const projection = await projectSessionContextForCompaction(ctx, projectCompactionContext);
354
+ if (!projection.ok) {
355
+ return { outcome: "unprojected-input", reason: projection.reason };
356
+ }
357
+ const summaryIndex = findLatestProjectedCompactionSummaryIndex(
358
+ projection.messages,
359
+ latestNativeCompaction.entry.summary,
360
+ );
361
+ if (summaryIndex < 0) {
362
+ return {
363
+ outcome: "unprojected-input",
364
+ reason: COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
365
+ };
366
+ }
367
+ const input: ResponsesInputItem[] = [
368
+ ...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
369
+ ...serializeMessagesToResponsesInput(
370
+ compactor.currentModel,
371
+ [...projection.messages.slice(summaryIndex + 1)],
372
+ { firstSystemMessageIsUpdate: true },
373
+ ),
374
+ ];
375
+ request = {
376
+ model: compactor.model,
377
+ input,
378
+ instructions,
379
+ };
380
+ } else {
381
+ const liveTailEntries = branchEntries.slice(latestNativeCompaction.index + 1);
382
+ const input: ResponsesInputItem[] = [
383
+ ...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
384
+ ...serializeLiveTailToResponsesInput({ model: compactor.currentModel, entries: liveTailEntries }),
385
+ ];
386
+ request = {
387
+ model: compactor.model,
388
+ input,
389
+ instructions,
390
+ };
391
+ }
392
+ } else if (
393
+ latestNativeCompaction.reason === "no-compaction" ||
394
+ (latestNativeCompaction.reason === "latest-compaction-not-native" &&
395
+ config.allowCompactionContinuityBreak)
396
+ ) {
397
+ requestSource =
398
+ latestNativeCompaction.reason === "no-compaction" ? "session-context" : "non-native-session-context";
399
+ if (config.remoteV2ContextSource === "pi-context-hook") {
400
+ const projection = await projectSessionContextForCompaction(ctx, projectCompactionContext);
401
+ if (!projection.ok) {
402
+ return { outcome: "unprojected-input", reason: projection.reason };
403
+ }
404
+ request = serializeMessagesToCompactRequest({
405
+ model: compactor.currentModel,
406
+ messages: [...projection.messages],
407
+ instructions,
408
+ });
409
+ } else {
410
+ request = serializeMessagesToCompactRequest({
411
+ model: compactor.currentModel,
412
+ messages: getLegacySessionContextMessages(event, ctx),
413
+ instructions,
414
+ });
415
+ }
416
+ } else {
417
+ writeDebugArtifact(
418
+ "compaction-event",
419
+ {
420
+ event: "session_before_compact.remote-v2-skip",
421
+ reason: latestNativeCompaction.reason,
422
+ consumer: {
423
+ provider: consumer.provider,
424
+ api: consumer.api,
425
+ model: consumer.model,
426
+ baseUrl: consumer.baseUrl,
427
+ },
428
+ compactor: {
429
+ provider: compactor.provider,
430
+ api: compactor.api,
431
+ model: compactor.model,
432
+ baseUrl: compactor.baseUrl,
433
+ },
434
+ latestCompactionIndex: latestNativeCompaction.latestCompactionIndex,
435
+ latestCompactionIdentity: getCompactionIdentityDebugInfo(latestNativeCompaction.latestCompaction),
436
+ },
437
+ config,
438
+ ctx,
439
+ );
440
+ return { outcome: "failed" };
441
+ }
442
+
443
+ // Mirror the latest codex_rs CompactionInput fields captured from the most
444
+ // recent live provider request for this model (tools, reasoning, etc.).
445
+ const extras = getCompactionRequestExtras({
446
+ provider: consumer.provider,
447
+ api: consumer.api,
448
+ model: consumer.model,
449
+ baseUrl: consumer.baseUrl,
450
+ sessionId: getSessionId(ctx),
451
+ }, compactor.currentModel);
452
+ if (extras) {
453
+ request = { ...request, ...extras };
454
+ }
455
+
456
+ const compactResult = await remoteCompact({
457
+ runtime: compactor,
458
+ request,
459
+ signal: event.signal,
460
+ settings: config,
461
+ context: ctx,
462
+ });
463
+
464
+ if (compactResult.ok === false) {
465
+ writeDebugArtifact(
466
+ "compaction-event",
467
+ {
468
+ event: "session_before_compact.remote-v2-failure",
469
+ reason: compactResult.reason,
470
+ status: compactResult.status,
471
+ errorMessage: compactResult.errorMessage,
472
+ },
473
+ config,
474
+ ctx,
475
+ );
476
+ return compactResult.reason === "aborted" ? { outcome: "aborted" } : { outcome: "failed" };
477
+ }
478
+
479
+ let details: NativeCompactionDetails;
480
+ try {
481
+ details = createNativeCompactionDetails({
482
+ provider: consumer.provider,
483
+ api: consumer.api,
484
+ model: consumer.model,
485
+ baseUrl: consumer.baseUrl,
486
+ compactionModel: {
487
+ provider: compactor.provider,
488
+ api: compactor.api,
489
+ model: compactor.model,
490
+ baseUrl: compactor.baseUrl,
491
+ },
492
+ deferredToolCarryover,
493
+ compactedWindow: compactResult.compactedWindow,
494
+ compactResponseId: compactResult.compactResponseId,
495
+ createdAt: compactResult.createdAt,
496
+ requestMeta: buildCompactionRequestMeta(event),
497
+ inputProvenance,
498
+ });
499
+ } catch (error) {
500
+ writeDebugArtifact(
501
+ "compaction-event",
502
+ {
503
+ event: "session_before_compact.invalid-native-details",
504
+ reason: error instanceof Error ? error.message : String(error),
505
+ consumer: {
506
+ provider: consumer.provider,
507
+ api: consumer.api,
508
+ model: consumer.model,
509
+ baseUrl: consumer.baseUrl,
510
+ },
511
+ compactor: {
512
+ provider: compactor.provider,
513
+ api: compactor.api,
514
+ model: compactor.model,
515
+ baseUrl: compactor.baseUrl,
516
+ },
517
+ },
518
+ config,
519
+ ctx,
520
+ );
521
+ return { outcome: "failed" };
522
+ }
523
+
524
+ const compaction = createNativeCompactionResult({
525
+ firstKeptEntryId: event.preparation.firstKeptEntryId,
526
+ tokensBefore: event.preparation.tokensBefore,
527
+ details,
528
+ });
529
+
530
+ writeDebugArtifact(
531
+ "compaction-event",
532
+ {
533
+ event: "session_before_compact.remote-v2-success",
534
+ consumer: {
535
+ provider: consumer.provider,
536
+ api: consumer.api,
537
+ model: consumer.model,
538
+ baseUrl: consumer.baseUrl,
539
+ },
540
+ compactor: {
541
+ provider: compactor.provider,
542
+ api: compactor.api,
543
+ model: compactor.model,
544
+ baseUrl: compactor.baseUrl,
545
+ },
546
+ requestSource,
547
+ remoteV2ContextSource: config.remoteV2ContextSource,
548
+ inputProvenance,
549
+ requestInputItems: request.input.length,
550
+ requestExtras: extras ? Object.keys(extras) : [],
551
+ compactResponseId: compactResult.compactResponseId,
552
+ compactedItems: compactResult.compactedWindow.length,
553
+ firstKeptEntryId: event.preparation.firstKeptEntryId,
554
+ },
555
+ config,
556
+ ctx,
557
+ );
558
+
559
+ return { outcome: "success", compaction };
560
+ }
561
+
562
+ async function handleSessionBeforeCompact(
563
+ event: SessionBeforeCompactEvent,
564
+ ctx: ExtensionContext,
565
+ dependencies: CompactionDependencies,
566
+ remoteContextActive: RemoteContextActive,
567
+ ) {
568
+ const resolved = contextOperation(dependencies.loadConfig, ctx);
569
+ if (resolved.invalidFeatures.some((feature) => ["context", "compatibility", "diagnostics"].includes(feature))) return { cancel: true };
570
+ const config = resolved.config.compaction;
571
+ if (!config.enabled) {
572
+ return undefined;
573
+ }
574
+
575
+ writeDebugArtifact(
576
+ "compaction-event",
577
+ {
578
+ event: "session_before_compact",
579
+ customInstructions: event.customInstructions,
580
+ preparation: {
581
+ tokensBefore: event.preparation.tokensBefore,
582
+ firstKeptEntryId: event.preparation.firstKeptEntryId,
583
+ previousSummaryPresent: Boolean(event.preparation.previousSummary),
584
+ messagesToSummarizeCount: event.preparation.messagesToSummarize.length,
585
+ turnPrefixMessagesCount: event.preparation.turnPrefixMessages.length,
586
+ },
587
+ },
588
+ config,
589
+ ctx,
590
+ );
591
+
592
+ if (event.signal.aborted) {
593
+ return { cancel: true };
594
+ }
595
+
596
+ // Remote Context management owns this eligible Codex session. It persists a
597
+ // no-summary boundary and deliberately never calls remote_compaction_v2.
598
+ if (isCodexContextModel(ctx.model, config)) {
599
+ if (await remoteContextActive(ctx, config)) {
600
+ try {
601
+ dependencies.contextWindows.synchronize(ctx);
602
+ return dependencies.contextWindows.prepareCompaction(event);
603
+ } catch {
604
+ notifyRemoteContextFailure(ctx, "malformed-window-state");
605
+ return { cancel: true };
606
+ }
607
+ }
608
+ // A configured native Codex Remote session must never re-enter this
609
+ // extension's Remote V2 compaction chain, and Pi's native compaction is
610
+ // deliberately disabled for Remote-managed models: cancelling here keeps
611
+ // the boundary the only rollover mechanism. The inactive reason is
612
+ // surfaced so the user can fix activation instead of losing context to a
613
+ // silent native summary.
614
+ notifyRemoteContextFailure(ctx, "native-codex-context-unavailable");
615
+ return { cancel: true };
616
+ }
617
+
618
+ // Resolve producer protocol policy from this operation's document, not a later disk read.
619
+ if (resolved.format === "v2" && config.remoteCompactModel) {
620
+ const producer = parseModelSpec(config.remoteCompactModel);
621
+ if (producer) {
622
+ const producerPolicy = resolveToolkitConfig(resolved.snapshot, { provider: producer.provider, id: producer.modelId });
623
+ notifyConfigIssues(ctx, producerPolicy);
624
+ if (producerPolicy.invalidFeatures.includes("compatibility")) return { cancel: true };
625
+ }
626
+ }
627
+ // Branch 1: Responses-family APIs use remote_compaction_v2 on the normal Responses stream.
628
+ let remoteAttempted = false;
629
+ const resolution = await resolveRemoteCompactionExecution(
630
+ ctx,
631
+ {
632
+ enabled: config.enabled,
633
+ responsesApis: config.responsesApis,
634
+ codexGatewayModels: compactGatewayModels(resolved),
635
+ },
636
+ config.remoteCompactModel,
637
+ );
638
+ if (resolution.ok) {
639
+ remoteAttempted = true;
640
+ const responsesOutcome = await runResponsesNativeCompact(
641
+ event,
642
+ ctx,
643
+ config,
644
+ resolution.execution,
645
+ dependencies.remoteCompact,
646
+ dependencies.projectCompactionContext,
647
+ );
648
+ if (responsesOutcome.outcome === "success") {
649
+ return { compaction: responsesOutcome.compaction };
650
+ }
651
+ if (responsesOutcome.outcome === "aborted") {
652
+ return { cancel: true };
653
+ }
654
+ if (responsesOutcome.outcome === "unprojected-input") {
655
+ const message = responsesOutcome.reason === COMPACTION_PROJECTION_UNAVAILABLE
656
+ ? "Pi's ordered context-hook projection is unavailable; Remote V2 was not sent raw session history."
657
+ : responsesOutcome.reason === COMPACTION_SESSION_CONTEXT_UNAVAILABLE
658
+ ? "Pi did not provide the current session context required by its context-hook projection."
659
+ : responsesOutcome.reason === COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE
660
+ ? "Pi's projected context did not include the current compaction summary anchor."
661
+ : "The latest opaque checkpoint has no marker for the configured Remote V2 context source, or was created in the other mode.";
662
+ writeDebugArtifact(
663
+ "compaction-event",
664
+ {
665
+ event: "session_before_compact.remote-v2-unprojected-input",
666
+ reason: responsesOutcome.reason,
667
+ contextSource: config.remoteV2ContextSource,
668
+ inputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
669
+ piContextHookPatch: dependencies.piContextHookPatch,
670
+ message,
671
+ },
672
+ config,
673
+ ctx,
674
+ );
675
+ notifyWarning(
676
+ ctx,
677
+ `Remote V2 compaction cancelled (${responsesOutcome.reason}); ${message} Keep the context-source setting aligned with the checkpoint or choose legacy mode explicitly.`,
678
+ );
679
+ return { cancel: true };
680
+ }
681
+ // failed: fall through to the configured-model fallback below.
682
+ } else {
683
+ writeDebugArtifact(
684
+ "compaction-event",
685
+ {
686
+ event: "session_before_compact.remote-v2-unavailable",
687
+ reason: resolution.reason,
688
+ provider: resolution.provider,
689
+ api: resolution.api,
690
+ model: resolution.model,
691
+ baseUrl: resolution.baseUrl,
692
+ modelSpec: resolution.modelSpec,
693
+ errorMessage: resolution.errorMessage,
694
+ },
695
+ config,
696
+ ctx,
697
+ );
698
+ if (config.remoteCompactModel) {
699
+ notifyWarning(
700
+ ctx,
701
+ `remote compaction model "${config.remoteCompactModel}" unusable (${resolution.reason}); using the native fallback chain`,
702
+ );
703
+ }
704
+ }
705
+
706
+ // Branch 2: run pi's native compaction method. A failed remote request is compacted by the
707
+ // remote producer itself; a model that cannot use remote v2 at all uses nativeFallback.model.
708
+ const fallbackModelSpec = remoteAttempted ? config.remoteCompactModel : config.nativeFallback.model;
709
+ const fallback = await dependencies.nativeFallback({ ctx, event, config, modelSpec: fallbackModelSpec });
710
+ if (fallback.ok) {
711
+ if (ctx.hasUI) {
712
+ ctx.ui.notify(
713
+ `${COMPACTION_EXTENSION_ID}: compacted with ${fallback.model.provider}/${fallback.model.id} (native method)`,
714
+ "info",
715
+ );
716
+ }
717
+ writeDebugArtifact(
718
+ "compaction-event",
719
+ {
720
+ event: "session_before_compact.fallback-success",
721
+ model: fallback.model,
722
+ },
723
+ config,
724
+ ctx,
725
+ );
726
+ return { compaction: fallback.result };
727
+ }
728
+
729
+ if (fallback.reason === "aborted") {
730
+ return { cancel: true };
731
+ }
732
+
733
+ writeDebugArtifact(
734
+ "compaction-event",
735
+ {
736
+ event: "session_before_compact.fallback-skip",
737
+ reason: fallback.reason,
738
+ modelSpec: fallback.modelSpec,
739
+ errorMessage: fallback.errorMessage,
740
+ ...(fallback.estimatedTokens === undefined ? {} : { estimatedTokens: fallback.estimatedTokens }),
741
+ ...(fallback.contextWindow === undefined ? {} : { contextWindow: fallback.contextWindow }),
742
+ },
743
+ config,
744
+ ctx,
745
+ );
746
+
747
+ // Intentional pi-default paths: feature disabled, nothing configured, or it matches the current one.
748
+ const intentionalSkip =
749
+ fallback.reason === "disabled" ||
750
+ fallback.reason === "no-model-configured" ||
751
+ fallback.reason === "same-as-current-model";
752
+ if (fallback.reason === "model-window-too-small") {
753
+ // The configured summary model is narrower than the request Pi would send for it.
754
+ // Skipping is the success path here: pi's current model gets one chance instead of
755
+ // the provider terminating the oversized summary.
756
+ notifyWarning(
757
+ ctx,
758
+ `compaction summary model "${fallback.modelSpec}" cannot fit ~${fallback.estimatedTokens} tokens in its ${fallback.contextWindow} window; compacting with the current model instead`,
759
+ );
760
+ } else if (!intentionalSkip) {
761
+ notifyWarning(
762
+ ctx,
763
+ `compaction model "${fallback.modelSpec}" unusable (${fallback.reason}${fallback.errorMessage ? `: ${fallback.errorMessage}` : ""}); using pi's default compaction`,
764
+ );
765
+ }
766
+
767
+ // Branch 3: pi's default native compaction with the current model.
768
+ return undefined;
769
+ }
770
+
771
+ /**
772
+ * Surface (and optionally close) the gap between the durable transcript and the remote window.
773
+ *
774
+ * Remote context management retires previous windows in the request projection, so a covered
775
+ * model never notices that the branch still carries every one of them. Switching to a model
776
+ * without remote context - or resuming such a session - hands that whole pile to the provider
777
+ * at once, and the first manual `/compact` then has to summarize it. Runs once per agent turn,
778
+ * never per request, and only once per window and model so the notice cannot become a nag.
779
+ */
780
+ async function handleWindowBulk(
781
+ ctx: ExtensionContext,
782
+ contextWindows: CodexContextWindowManager,
783
+ loadConfig: typeof loadToolkitConfig,
784
+ trigger: "model-switch" | "turn-end",
785
+ resolved = contextOperation(loadConfig, ctx),
786
+ ): Promise<void> {
787
+ requireContextPolicy(resolved, ctx);
788
+ const compaction = resolved.config.compaction;
789
+ if (!compaction.enabled || compaction.contextManagement !== "remote") return;
790
+ if (!ctx.model) return;
791
+
792
+ const modelKey = `${ctx.model.provider}/${ctx.model.id}`;
793
+ const report = evaluateWindowBulk(ctx);
794
+ if (!report) return;
795
+ contextWindows.noteWindowBulk(report, modelKey);
796
+ const cliff = contextWindows.takeBulkCliff(modelKey);
797
+ if (!cliff) return;
798
+ const action = decideBulkCloseOut({
799
+ action: decideBulkCliffAction(cliff, compaction.leaveManagedMode),
800
+ mode: ctx.mode,
801
+ trigger,
802
+ hasPendingTrim: contextWindows.hasPendingTrim(),
803
+ });
804
+ if (action === "ignore") return;
805
+
806
+ writeDebugArtifact(
807
+ "compaction-event",
808
+ {
809
+ event: `window-bulk.${action}`,
810
+ trigger,
811
+ policy: compaction.leaveManagedMode,
812
+ model: modelKey,
813
+ unmanagedTokens: cliff.unmanagedTokens,
814
+ managedTokens: cliff.managedTokens,
815
+ targetContextWindow: cliff.targetContextWindow,
816
+ overBudget: cliff.overBudget,
817
+ },
818
+ compaction,
819
+ ctx,
820
+ );
821
+
822
+ const gap = `${cliff.unmanagedTokens.toLocaleString()} tokens of retired windows are still in this session's transcript, while remote context management has been sending ${cliff.managedTokens.toLocaleString()}`;
823
+ if (action === "warn") {
824
+ notifyWarning(
825
+ ctx,
826
+ `${gap} to ${modelKey}${cliff.overBudget ? `, past ${Math.round(BULK_CLIFF_RATIO * 100)}% of its window` : ""}. Checkpoint with notes and run /compact (or new_context) before continuing on a model without remote context, or set compaction.leaveManagedMode="compact" to do it automatically.`,
827
+ );
828
+ return;
829
+ }
830
+
831
+ notifyWarning(ctx, `${gap} to ${modelKey}, past ${Math.round(BULK_CLIFF_RATIO * 100)}% of its window; compacting the retired windows first.`);
832
+ if (typeof ctx.compact !== "function") return;
833
+ ctx.compact({
834
+ onError: (error: Error) => {
835
+ notifyWarning(ctx, `automatic boundary compaction failed: ${error.message}`);
836
+ },
837
+ });
838
+ }
839
+
840
+ async function handleContextInternal(
841
+ event: ContextEvent,
842
+ ctx: ExtensionContext,
843
+ pi: ExtensionAPI,
844
+ loadConfig: typeof loadToolkitConfig,
845
+ contextWindows: CodexContextWindowManager,
846
+ remoteContextActive: RemoteContextActive,
847
+ ) {
848
+ const resolved = contextOperation(loadConfig, ctx);
849
+ requireContextPolicy(resolved, ctx);
850
+ const config = resolved.config.compaction;
851
+ if (!config.enabled) {
852
+ const visibleMessages = contextWindows.project(event.messages, "off");
853
+ return visibleMessages.length === event.messages.length && visibleMessages.every((message, index) => message === event.messages[index])
854
+ ? undefined
855
+ : { messages: visibleMessages };
856
+ }
857
+
858
+ if (config.contextManagement === "remote") {
859
+ try {
860
+ contextWindows.synchronize(ctx);
861
+ } catch {
862
+ notifyRemoteContextFailure(ctx, "malformed-window-state");
863
+ reportPiContextHookFailure(ctx, "malformed-window-state");
864
+ ctx.abort();
865
+ return undefined;
866
+ }
867
+
868
+ const remoteActive = await remoteContextActive(ctx, config);
869
+ if (remoteActive) {
870
+ try {
871
+ contextWindows.recordBudget(
872
+ pi,
873
+ ctx,
874
+ true,
875
+ config.contextReminderThresholdPercent,
876
+ );
877
+ const projected = contextWindows.project(event.messages, "remote");
878
+ reportProjectionDiagnostics(contextWindows, config, ctx);
879
+ return projected.length === event.messages.length && projected.every((message, index) => message === event.messages[index])
880
+ ? undefined
881
+ : { messages: projected };
882
+ } catch (error) {
883
+ notifyRemoteContextFailure(ctx, "malformed-window-state");
884
+ reportPiContextHookFailure(ctx, "malformed-window-state");
885
+ ctx.abort();
886
+ return undefined;
887
+ }
888
+ }
889
+
890
+ // An eligible Codex Remote session is never allowed to fall through to the
891
+ // older compaction-replay path. Gateway traffic must also fail closed when
892
+ // its transport capability is unavailable; otherwise Pi could send an
893
+ // ordinary unscoped request and lose CPA OAuth/session affinity.
894
+ if (isCodexContextModel(ctx.model, config)) {
895
+ if (isCodexGatewayModel(ctx.model, config.gatewayContextModels)) {
896
+ notifyRemoteContextFailure(ctx, "codex-context-unavailable");
897
+ reportPiContextHookFailure(ctx, "codex-context-unavailable");
898
+ ctx.abort();
899
+ return undefined;
900
+ }
901
+ const visibleMessages = contextWindows.project(event.messages, "off");
902
+ return visibleMessages.length === event.messages.length && visibleMessages.every((message, index) => message === event.messages[index])
903
+ ? undefined
904
+ : { messages: visibleMessages };
905
+ }
906
+ }
907
+
908
+ // Inactive/ineligible Remote mode must not expose internal window markers to a
909
+ // gateway or another provider. The existing compaction replay path remains the
910
+ // owner for this safe fallback.
911
+ const visibleMessages = contextWindows.project(event.messages, "off");
912
+ const replayEvent = visibleMessages === event.messages ? event : { ...event, messages: visibleMessages };
913
+ const resolution = await resolveNativeCompactionEnvironment(ctx, {
914
+ enabled: config.enabled,
915
+ responsesApis: config.responsesApis,
916
+ codexGatewayModels: compactGatewayModels(resolved),
917
+ });
918
+ if (!resolution.ok) return undefined;
919
+ const branchEntries = ctx.sessionManager.getBranch();
920
+ const latest = resolveLatestNativeCompactionEntry(branchEntries, { baseUrl: resolution.runtime.baseUrl });
921
+ if (!latest.ok) return undefined;
922
+ const result = removeNativeCompactionRetainedMessages({
923
+ messages: replayEvent.messages,
924
+ branchEntries,
925
+ compactionEntry: latest.entry,
926
+ expectedInputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
927
+ });
928
+ if (!result.ok) {
929
+ writeReplayFailureArtifact({ reason: result.reason, compactionEntryId: latest.entry.id }, config, ctx);
930
+ if (ctx.hasUI) ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: replay failed (${result.reason}); request aborted`, "error");
931
+ reportPiContextHookFailure(ctx, `replay-failed:${result.reason}`);
932
+ ctx.abort();
933
+ return undefined;
934
+ }
935
+ return result.messages === replayEvent.messages ? undefined : { messages: result.messages };
936
+ }
937
+
938
+ /**
939
+ * Pi's emitContext() catches handler exceptions. Re-throw as usual for Pi, but
940
+ * also report the failure so the compaction projector cannot mistake the
941
+ * caught exception for a successful projection.
942
+ */
943
+ async function handleContext(
944
+ event: ContextEvent,
945
+ ctx: ExtensionContext,
946
+ pi: ExtensionAPI,
947
+ loadConfig: typeof loadToolkitConfig,
948
+ contextWindows: CodexContextWindowManager,
949
+ remoteContextActive: RemoteContextActive,
950
+ ) {
951
+ try {
952
+ return await handleContextInternal(event, ctx, pi, loadConfig, contextWindows, remoteContextActive);
953
+ } catch (error) {
954
+ reportPiContextHookFailure(ctx, error instanceof Error ? error.message : String(error));
955
+ throw error;
956
+ }
957
+ }
958
+
959
+ async function handleBeforeProviderRequest(
960
+ event: BeforeProviderRequestEvent,
961
+ ctx: ExtensionContext,
962
+ loadConfig: typeof loadToolkitConfig,
963
+ contextWindows: CodexContextWindowManager,
964
+ remoteContextActive: RemoteContextActive,
965
+ ) {
966
+ const resolved = contextOperation(loadConfig, ctx);
967
+ requireContextPolicy(resolved, ctx);
968
+ const toolkitConfig = resolved.config;
969
+ const config = toolkitConfig.compaction;
970
+ if (!config.enabled) {
971
+ return undefined;
972
+ }
973
+
974
+ if (config.contextManagement === "remote") {
975
+ try {
976
+ contextWindows.synchronize(ctx);
977
+ } catch {
978
+ notifyRemoteContextFailure(ctx, "malformed-window-state");
979
+ ctx.abort();
980
+ return undefined;
981
+ }
982
+ }
983
+ if (config.contextManagement === "remote" && await remoteContextActive(ctx, config)) {
984
+ try {
985
+ return contextWindows.rewritePayload(event.payload, ctx);
986
+ } catch {
987
+ notifyRemoteContextFailure(ctx, "malformed-request-state");
988
+ ctx.abort();
989
+ return undefined;
990
+ }
991
+ }
992
+ if (isCodexContextModel(ctx.model, config)) {
993
+ // Keep Codex Remote mutually exclusive with the legacy replay pipeline
994
+ // when authentication or tool ownership is unavailable. Gateway traffic
995
+ // cannot continue as an ordinary Responses request because that would
996
+ // silently lose the required CPA session/account selection.
997
+ if (isCodexGatewayModel(ctx.model, config.gatewayContextModels)) {
998
+ notifyRemoteContextFailure(ctx, "codex-context-unavailable");
999
+ ctx.abort();
1000
+ }
1001
+ return undefined;
1002
+ }
1003
+
1004
+ const resolution = await resolveNativeCompactionEnvironment(
1005
+ ctx,
1006
+ {
1007
+ enabled: config.enabled,
1008
+ responsesApis: config.responsesApis,
1009
+ codexGatewayModels: compactGatewayModels(resolved),
1010
+ },
1011
+ event.payload,
1012
+ );
1013
+ if (resolution.ok === false) {
1014
+ writeDebugArtifact(
1015
+ "provider-request",
1016
+ {
1017
+ event: "before_provider_request.skip",
1018
+ reason: resolution.reason,
1019
+ provider: resolution.provider,
1020
+ api: resolution.api,
1021
+ model: resolution.model,
1022
+ baseUrl: resolution.baseUrl,
1023
+ errorMessage: resolution.errorMessage,
1024
+ currentModel: getCurrentModelDebugInfo(ctx),
1025
+ payload: event.payload,
1026
+ },
1027
+ config,
1028
+ ctx,
1029
+ );
1030
+ return undefined;
1031
+ }
1032
+
1033
+ const runtime = resolution.runtime;
1034
+ const payload = runtime.payload;
1035
+ if (!payload) {
1036
+ return undefined;
1037
+ }
1038
+
1039
+ // Capture compact-relevant request fields (tools, reasoning, ...) for the next
1040
+ // synthetic compact request using the active consumer's effective runtime identity.
1041
+ // This hook runs before the separate Web Search transform, so injected native search
1042
+ // tools are not copied into remote_compaction_v2.
1043
+ const webSearchRoute = resolveWebSearchRoute({ model: ctx.model, config: toolkitConfig.webSearch });
1044
+ rememberRequestContext(
1045
+ payload,
1046
+ {
1047
+ provider: runtime.provider,
1048
+ api: runtime.api,
1049
+ model: runtime.model,
1050
+ baseUrl: runtime.baseUrl,
1051
+ sessionId: getSessionId(ctx),
1052
+ },
1053
+ {
1054
+ excludeWebSearchTools: webSearchRoute.route === "standalone-alpha",
1055
+ },
1056
+ );
1057
+
1058
+ const branchEntries = ctx.sessionManager.getBranch();
1059
+ const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
1060
+ baseUrl: runtime.baseUrl,
1061
+ });
1062
+ if (!latestNativeCompaction.ok) {
1063
+ writeDebugArtifact(
1064
+ "provider-request",
1065
+ {
1066
+ event: "before_provider_request.no-native-compaction",
1067
+ reason: latestNativeCompaction.reason,
1068
+ provider: runtime.provider,
1069
+ api: runtime.api,
1070
+ model: runtime.model,
1071
+ baseUrl: runtime.baseUrl,
1072
+ branchEntries: branchEntries.length,
1073
+ latestCompactionIndex: latestNativeCompaction.latestCompactionIndex,
1074
+ latestCompactionIdentity: getCompactionIdentityDebugInfo(latestNativeCompaction.latestCompaction),
1075
+ payload,
1076
+ },
1077
+ config,
1078
+ ctx,
1079
+ );
1080
+ return undefined;
1081
+ }
1082
+
1083
+ const latestNativeCompactionEntry = latestNativeCompaction.entry;
1084
+ const rewrite = rewriteResponsesPayloadWithNativeReplay({
1085
+ model: runtime.currentModel,
1086
+ payload,
1087
+ branchEntries,
1088
+ compactionEntry: latestNativeCompactionEntry,
1089
+ expectedInputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
1090
+ });
1091
+ if (!rewrite.ok) {
1092
+ writeDebugArtifact(
1093
+ "provider-request",
1094
+ {
1095
+ event: "before_provider_request.rewrite-failed",
1096
+ reason: rewrite.reason,
1097
+ provider: runtime.provider,
1098
+ api: runtime.api,
1099
+ model: runtime.model,
1100
+ baseUrl: runtime.baseUrl,
1101
+ compactionEntryId: latestNativeCompactionEntry.id,
1102
+ parity: rewrite.parity,
1103
+ payload,
1104
+ },
1105
+ config,
1106
+ ctx,
1107
+ );
1108
+
1109
+ // Fail loud instead of letting Pi send the sentinel-only payload: the
1110
+ // compacted history would be silently lost while the request still succeeds.
1111
+ // A forced redacted failure record is written even when logProviderPayloads
1112
+ // is disabled so the incident is diagnosable without leaking content.
1113
+ writeReplayFailureArtifact(
1114
+ {
1115
+ reason: rewrite.reason,
1116
+ parity: rewrite.parity,
1117
+ compactionEntryId: latestNativeCompactionEntry.id,
1118
+ provider: runtime.provider,
1119
+ api: runtime.api,
1120
+ model: runtime.model,
1121
+ },
1122
+ config,
1123
+ ctx,
1124
+ );
1125
+ if (ctx.hasUI) {
1126
+ ctx.ui.notify(
1127
+ `${COMPACTION_EXTENSION_ID}: native compaction replay failed (${rewrite.reason}); provider request aborted`,
1128
+ "error",
1129
+ );
1130
+ }
1131
+ ctx.abort();
1132
+ return undefined;
1133
+ }
1134
+
1135
+ writeDebugArtifact(
1136
+ "provider-request",
1137
+ {
1138
+ event: "before_provider_request.native-rewrite",
1139
+ provider: runtime.provider,
1140
+ api: runtime.api,
1141
+ model: runtime.model,
1142
+ baseUrl: runtime.baseUrl,
1143
+ compactionEntryId: latestNativeCompactionEntry.id,
1144
+ boundaryIndex: rewrite.segments.boundaryIndex,
1145
+ firstKeptEntryIndex: rewrite.segments.firstKeptEntryIndex,
1146
+ originalInputItems: payload.input.length,
1147
+ rewrittenInputItems: rewrite.rewrittenPayload.input.length,
1148
+ leadingItems: rewrite.segments.leading.length,
1149
+ compactionSummaryItems: rewrite.segments.compactionSummary.length,
1150
+ compactedItems: rewrite.segments.compactedWindow.length,
1151
+ postItems: rewrite.segments.post.length,
1152
+ payload: rewrite.rewrittenPayload,
1153
+ originalPayload: payload,
1154
+ },
1155
+ config,
1156
+ ctx,
1157
+ );
1158
+
1159
+ return rewrite.rewrittenPayload;
1160
+ }
1161
+
1162
+ export default function registerCompactionExtension(
1163
+ pi: ExtensionAPI,
1164
+ overrides: Partial<CompactionDependencies> = {},
1165
+ ) {
1166
+ const loadConfig = overrides.loadConfig ?? loadToolkitConfig;
1167
+ registerToolkitConfigCommand(pi, loadConfig);
1168
+ const piContextHookPatch = installPiContextHookPatch();
1169
+ const contextWindows = overrides.contextWindows ?? new CodexContextWindowManager((ctx, signal, gatewayModels) =>
1170
+ loadHistoryNotesThreadHint(ctx, signal, gatewayModels)
1171
+ );
1172
+ const dependencies: CompactionDependencies = {
1173
+ loadConfig,
1174
+ remoteCompact: executeRemoteV2Compaction,
1175
+ nativeFallback: runNativeFallbackCompaction,
1176
+ contextWindows,
1177
+ ...overrides,
1178
+ piContextHookPatch: overrides.piContextHookPatch ?? piContextHookPatch,
1179
+ };
1180
+ let tools!: ReturnType<typeof registerContextManagementTools>;
1181
+ let contextWindowReady = false;
1182
+ const isContextRuntimeActive = async (
1183
+ ctx: ExtensionContext,
1184
+ config: CompactionConfig,
1185
+ model = ctx.model,
1186
+ ): Promise<boolean> =>
1187
+ contextWindowReady && tools.isRegistered && await isRemoteContextActive(ctx, config, model);
1188
+ tools = registerContextManagementTools(
1189
+ pi,
1190
+ contextWindows,
1191
+ async (ctx) => {
1192
+ const resolved = contextOperation(dependencies.loadConfig, ctx);
1193
+ assertConfigValid(resolved, "context", "compatibility");
1194
+ return { active: await isContextRuntimeActive(ctx, resolved.config.compaction), gatewayModels: resolved.gatewayModelKeys };
1195
+ },
1196
+ );
1197
+ const remoteContextActive: RemoteContextActive = isContextRuntimeActive;
1198
+ const syncTools = async (
1199
+ ctx: ExtensionContext,
1200
+ model = ctx.model,
1201
+ options: { notifyWindowFailure?: boolean } = {},
1202
+ resolved = contextOperation(dependencies.loadConfig, ctx, model),
1203
+ ): Promise<ContextToolSyncOutcome> => {
1204
+ // Until this call proves otherwise, do not let a previous session/window
1205
+ // identity make a failed activation look usable.
1206
+ contextWindowReady = false;
1207
+ const config = resolved.config.compaction;
1208
+ const eligible = !resolved.invalidFeatures.some((feature) => ["context", "compatibility"].includes(feature)) && await isRemoteContextActive(ctx, config, model);
1209
+ const toolSync = tools.sync(eligible);
1210
+ const outcome: ContextToolSyncOutcome = {
1211
+ eligible,
1212
+ toolsSynced: toolSync.synced,
1213
+ registrationState: toolSync.registrationState,
1214
+ windowInitialized: false,
1215
+ };
1216
+ if (!eligible || !toolSync.synced) return outcome;
1217
+ // Activation can succeed after `session_start` missed it, because Pi 0.86 may
1218
+ // reject the registration read while a session replacement is still binding.
1219
+ // The window lifecycle has to open on that later activation too, or requests
1220
+ // carry no window metadata and the backend never ingests the turns.
1221
+ try {
1222
+ contextWindows.ensureInitialized(pi, ctx, true);
1223
+ contextWindowReady = true;
1224
+ return { ...outcome, windowInitialized: true };
1225
+ } catch {
1226
+ // Do not leave tools exposed while the request path has no valid window
1227
+ // identity. The next lifecycle hook may retry once Pi is usable again.
1228
+ tools.sync(false);
1229
+ if (options.notifyWindowFailure) notifyRemoteContextFailure(ctx, "malformed-window-state");
1230
+ return outcome;
1231
+ }
1232
+ };
1233
+ pi.on("session_start", async (_event, ctx) => {
1234
+ const resolved = contextOperation(dependencies.loadConfig, ctx);
1235
+ const syncOutcome = await syncTools(ctx, ctx.model, { notifyWindowFailure: true }, resolved);
1236
+ const active = syncOutcome.eligible && syncOutcome.toolsSynced && syncOutcome.windowInitialized;
1237
+ const { source } = resolved;
1238
+ const warnings = resolved.issues.map((issue) => `${issue.path}: ${issue.code}`);
1239
+ const config = resolved.config.compaction;
1240
+ if (resolved.invalidFeatures.length > 0) return;
1241
+ if (!config.enabled) return;
1242
+
1243
+ let activationReason: string | undefined;
1244
+ // Only models the built-in Remote Context coverage targets may activate or
1245
+ // warn; everything else silently runs Pi's normal compaction path.
1246
+ if (config.contextManagement === "remote" && isCodexContextModel(ctx.model, config) && !active) {
1247
+ if (syncOutcome.registrationState === "conflict") {
1248
+ activationReason = "tool-name-conflict";
1249
+ notifyRemoteContextFailure(ctx, activationReason);
1250
+ } else if (syncOutcome.registrationState === "unverified" || !syncOutcome.toolsSynced) {
1251
+ // Transient: the runtime has not published or accepted our tools yet. A
1252
+ // later before_agent_start re-verifies, so do not report provider failure.
1253
+ activationReason = "tool-registration-pending";
1254
+ } else if (syncOutcome.eligible && !syncOutcome.windowInitialized) {
1255
+ activationReason = "malformed-window-state";
1256
+ } else {
1257
+ const remoteResolution = await resolveCodexContextProvider(ctx, ctx.model, config.gatewayContextModels);
1258
+ activationReason = remoteResolution.ok ? "codex-context-unavailable" : remoteResolution.reason;
1259
+ notifyRemoteContextFailure(ctx, activationReason);
1260
+ }
1261
+ }
1262
+
1263
+
1264
+ const artifactPath = writeDebugArtifact(
1265
+ "lifecycle",
1266
+ {
1267
+ event: "session_start",
1268
+ config,
1269
+ configSource: source,
1270
+ warnings,
1271
+ activation: {
1272
+ active,
1273
+ contextManagement: config.contextManagement,
1274
+ remoteV2ContextSource: config.remoteV2ContextSource,
1275
+ model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined,
1276
+ ...(activationReason ? { reason: activationReason } : {}),
1277
+ },
1278
+ piContextHookPatch: dependencies.piContextHookPatch,
1279
+ },
1280
+ config,
1281
+ ctx,
1282
+ );
1283
+
1284
+ if (ctx.hasUI && ["info", "debug"].includes(resolved.policy.diagnostics.level) && (config.notifyOnLoad || config.debug)) {
1285
+ ctx.ui.notify(
1286
+ artifactPath
1287
+ ? `${COMPACTION_EXTENSION_ID} loaded • debug artifacts → ${artifactPath}`
1288
+ : `${COMPACTION_EXTENSION_ID} loaded`,
1289
+ "info",
1290
+ );
1291
+ }
1292
+ });
1293
+
1294
+ pi.on("context", (event, ctx) => handleContext(event, ctx, pi, dependencies.loadConfig, contextWindows, remoteContextActive));
1295
+ pi.on("session_before_compact", (event, ctx) => handleSessionBeforeCompact(event, ctx, dependencies, remoteContextActive));
1296
+ pi.on("session_compact", (event, _ctx) => contextWindows.recordCompaction(event.compactionEntry.details));
1297
+ pi.on("session_shutdown", () => {
1298
+ contextWindowReady = false;
1299
+ contextWindows.reset();
1300
+ tools.reset();
1301
+ });
1302
+ pi.on("agent_settled", async (_event, ctx) => {
1303
+ // Idle: no retry, compaction, or queued continuation is left running, so this is the
1304
+ // only safe place to spend a model call on the user's behalf.
1305
+ await handleWindowBulk(ctx, contextWindows, dependencies.loadConfig, "turn-end");
1306
+ });
1307
+ pi.on("model_select", async (event, ctx) => {
1308
+ // Switching into a covered model mid-session must open the window
1309
+ // lifecycle immediately: without an identity the request rewrite skips
1310
+ // window metadata, the backend never ingests those turns, and the first
1311
+ // new_context would trim pre-switch history that no history can recover.
1312
+ // syncTools() activates and initializes the window when the model is covered.
1313
+ const resolved = contextOperation(dependencies.loadConfig, ctx, event.model);
1314
+ await syncTools(ctx, event.model, { notifyWindowFailure: true }, resolved);
1315
+ // The switch itself is the moment the durable transcript changes consumer, and the
1316
+ // session is idle here, so the close-out must happen now rather than mid-turn.
1317
+ await handleWindowBulk(ctx, contextWindows, dependencies.loadConfig, "model-switch", resolved);
1318
+ });
1319
+ pi.on("before_agent_start", async (_event, ctx) => {
1320
+ await syncTools(ctx, ctx.model, { notifyWindowFailure: true });
1321
+ });
1322
+ pi.on("before_provider_request", (event, ctx) => handleBeforeProviderRequest(event, ctx, dependencies.loadConfig, contextWindows, remoteContextActive));
1323
+ pi.on("before_provider_headers", async (event, ctx) => {
1324
+ const resolved = contextOperation(dependencies.loadConfig, ctx);
1325
+ requireContextPolicy(resolved, ctx);
1326
+ const config = resolved.config.compaction;
1327
+ if (!isCodexContextModel(ctx.model, config)) return;
1328
+ const active = await remoteContextActive(ctx, config);
1329
+ if (!active) {
1330
+ if (isCodexGatewayModel(ctx.model, config.gatewayContextModels)) {
1331
+ notifyRemoteContextFailure(ctx, "codex-context-unavailable");
1332
+ ctx.abort();
1333
+ }
1334
+ return;
1335
+ }
1336
+ const provider = await resolveCodexContextProvider(ctx, ctx.model, config.gatewayContextModels);
1337
+ if (provider.ok && provider.provider.kind === "codex-gateway") {
1338
+ const sessionId = getSessionId(ctx);
1339
+ const gatewayHeaders = codexContextProviderHeaders(provider.provider, {
1340
+ sessionId,
1341
+ clientRequestId: sessionId,
1342
+ });
1343
+ const allowedGatewayHeaders = new Set<string>(CODEX_GATEWAY_FORWARD_HEADERS);
1344
+ for (const existing of Object.keys(event.headers)) {
1345
+ if (!allowedGatewayHeaders.has(existing.toLowerCase())) delete event.headers[existing];
1346
+ }
1347
+ for (const name of [
1348
+ "authorization",
1349
+ "originator",
1350
+ "user-agent",
1351
+ "version",
1352
+ "session-id",
1353
+ "x-client-request-id",
1354
+ "x-codex-affinity-scope",
1355
+ "x-codex-model",
1356
+ ]) {
1357
+ for (const existing of Object.keys(event.headers)) {
1358
+ if (existing.toLowerCase() === name) delete event.headers[existing];
1359
+ }
1360
+ const value = gatewayHeaders.get(name);
1361
+ if (value) event.headers[name] = value;
1362
+ }
1363
+ }
1364
+ contextWindows.rewriteHeaders(event.headers, ctx);
1365
+ });
1366
+ pi.on("message_end", async (event, ctx) => {
1367
+ const resolved = contextOperation(dependencies.loadConfig, ctx);
1368
+ requireContextPolicy(resolved, ctx);
1369
+ const config = resolved.config.compaction;
1370
+ if (!isCodexContextModel(ctx.model, config) || !tools.isRegistered) return undefined;
1371
+ if (!(await remoteContextActive(ctx, config))) return undefined;
1372
+ const message = routeContextNamespaceToolMessage(event.message);
1373
+ return message === event.message ? undefined : { message };
1374
+ });
1375
+ }