pi-openai-toolkit 0.20.7 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +21 -21
  2. package/NOTICE +83 -83
  3. package/README.md +8 -26
  4. package/README.zh.md +8 -26
  5. package/config.schema.json +278 -366
  6. package/docs/configuration.md +294 -310
  7. package/docs/internals.md +166 -175
  8. package/package.json +3 -13
  9. package/src/compact-client.ts +412 -389
  10. package/src/compaction-errors.ts +96 -0
  11. package/src/compaction-projection.ts +35 -35
  12. package/src/config/legacy.ts +519 -717
  13. package/src/config/migration.ts +103 -100
  14. package/src/config/notifications.ts +36 -28
  15. package/src/config/policy.ts +10 -18
  16. package/src/config/v2.ts +1167 -1380
  17. package/src/config.ts +310 -319
  18. package/src/context-management/codex-provider.ts +270 -270
  19. package/src/context-management/history-notes.ts +357 -357
  20. package/src/context-management/manual-compact.ts +0 -11
  21. package/src/context-management/tools.ts +371 -371
  22. package/src/context-management/types.ts +168 -168
  23. package/src/context-management/window-manager.ts +801 -801
  24. package/src/debug.ts +238 -237
  25. package/src/deferred-tool-carryover.ts +282 -282
  26. package/src/details-store.ts +151 -151
  27. package/src/extension-runtime.ts +1662 -1623
  28. package/src/image-generation/extension.ts +189 -189
  29. package/src/image-generation/protocol.ts +579 -579
  30. package/src/image-generation/types.ts +300 -300
  31. package/src/native-fallback.ts +4 -132
  32. package/src/payload-rewrite.ts +515 -518
  33. package/src/pi-context-hook.ts +117 -117
  34. package/src/remote-v2-client.ts +811 -814
  35. package/src/request-context-cache.ts +155 -155
  36. package/src/responses-headers.ts +180 -180
  37. package/src/runtime.ts +865 -832
  38. package/src/serializer.ts +882 -882
  39. package/src/supported-environment.ts +27 -27
  40. package/src/types.ts +565 -648
  41. package/src/web-search/alpha-client.ts +876 -876
  42. package/src/web-search/extension.ts +530 -530
  43. package/src/web-search/payload.ts +299 -299
  44. package/src/web-search/prompt.ts +60 -60
  45. package/src/web-search/types.ts +166 -166
  46. package/tsconfig.check.json +50 -54
  47. package/src/auto-mode/classifier.ts +0 -161
  48. package/src/auto-mode/extension.ts +0 -691
  49. package/src/auto-mode/model-selection-guard.ts +0 -52
  50. package/src/auto-mode/policy.ts +0 -146
  51. package/src/auto-mode/prompt.ts +0 -279
  52. package/src/auto-mode/reviewer.ts +0 -325
  53. package/src/auto-mode/tool-review-tui.ts +0 -211
  54. package/src/auto-mode/transcript.ts +0 -135
  55. package/src/auto-mode/types.ts +0 -137
@@ -1,1623 +1,1662 @@
1
- import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
- import type {
3
- BeforeProviderRequestEvent,
4
- ContextWithSystemEvent,
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 { ManagedManualCompact } from "./context-management/manual-compact";
23
- import {
24
- registerContextManagementTools,
25
- type ContextToolRegistrationState,
26
- } from "./context-management/tools";
27
- import { writeDebugArtifact, writeReplayFailureArtifact } from "./debug";
28
- import {
29
- COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
30
- COMPACTION_CHECKPOINT_CONTEXT_EDITED,
31
- COMPACTION_PROJECTION_UNAVAILABLE,
32
- COMPACTION_SESSION_CONTEXT_UNAVAILABLE,
33
- COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
34
- hasVerifiedCompactionInputProvenance,
35
- type UnprojectedCompactionReason,
36
- } from "./compaction-projection";
37
- import { CODEX_GATEWAY_FORWARD_HEADERS } from "./responses-headers";
38
- import { resolveLatestNativeCompactionEntry } from "./details-store";
39
- import {
40
- getPiContextHookProjector,
41
- installPiContextHookPatch,
42
- reportPiContextHookFailure,
43
- type PiContextHookPatchResult,
44
- } from "./pi-context-hook";
45
- import { runNativeFallbackCompaction } from "./native-fallback";
46
- import {
47
- rewriteResponsesPayloadWithNativeReplay,
48
- hasInvalidatedCompactionContext,
49
- removeNativeCompactionRetainedMessages,
50
- serializeLiveTailToResponsesInputWithLimits,
51
- } from "./payload-rewrite";
52
- import { getCompactionRequestExtras, rememberRequestContext } from "./request-context-cache";
53
- import { resolveWebSearchRoute } from "./web-search/types";
54
- import { executeRemoteV2Compaction } from "./remote-v2-client";
55
- import {
56
- rememberPhysicalModel,
57
- resolveEffectiveModel,
58
- resolveNativeCompactionEnvironment,
59
- resolveRemoteCompactionExecution,
60
- parseModelSpec,
61
- type RemoteCompactionExecution,
62
- } from "./runtime";
63
- import {
64
- serializeMessagesToCompactRequestWithLimits,
65
- serializeMessagesToResponsesInputWithLimits,
66
- type NativeCompactionRequestBody,
67
- type ResponsesInputItem,
68
- } from "./serializer";
69
- import {
70
- createNativeCompactionDetails,
71
- createNativeCompactionResult,
72
- COMPACTION_EXTENSION_ID,
73
- getLatestDeferredToolCarryover,
74
- getRemoteV2InputProvenance,
75
- isNativeCompactionDetails,
76
- type CompactionConfig,
77
- type NativeCompactionDetails,
78
- type NativeCompactionRequestMeta,
79
- } from "./types";
80
-
81
- type CompactionContextProjection = (
82
- messages: readonly AgentMessage[],
83
- ctx: ExtensionContext,
84
- ) => readonly AgentMessage[] | undefined | Promise<readonly AgentMessage[] | undefined>;
85
-
86
- function contextOperation(loadConfig: typeof loadToolkitConfig, ctx: ExtensionContext, model = ctx.model): ResolvedToolkitConfig {
87
- const loaded = loadConfig();
88
- const resolved = resolveToolkitConfig(loaded, model);
89
- notifyConfigIssues(ctx, resolved);
90
- return resolved;
91
- }
92
-
93
- function requireContextPolicy(resolved: ResolvedToolkitConfig, ctx: ExtensionContext): void {
94
- try {
95
- assertConfigValid(resolved, "context", "compatibility", "diagnostics");
96
- } catch (error) {
97
- if (typeof ctx.abort === "function") ctx.abort();
98
- throw error;
99
- }
100
- }
101
-
102
- function compactGatewayModels(resolved: ResolvedToolkitConfig): readonly string[] {
103
- return resolved.format === "v2" || resolved.config.compaction.contextManagement === "remote"
104
- ? resolved.gatewayModelKeys : [];
105
- }
106
-
107
- type CompactionDependencies = {
108
- loadConfig: typeof loadToolkitConfig;
109
- remoteCompact: typeof executeRemoteV2Compaction;
110
- nativeFallback: typeof runNativeFallbackCompaction;
111
- contextWindows: CodexContextWindowManager;
112
- manualCompact?: ManagedManualCompact;
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
- type ContextToolRegistrationStateReader = () => ContextToolRegistrationState;
124
-
125
- type ContextToolSyncOutcome = {
126
- eligible: boolean;
127
- toolsSynced: boolean;
128
- registrationState: ContextToolRegistrationState;
129
- windowInitialized: boolean;
130
- };
131
-
132
- type ResponsesCompactOutcome =
133
- | { outcome: "success"; compaction: CompactionResult<NativeCompactionDetails> }
134
- | { outcome: "aborted" }
135
- | { outcome: "failed" }
136
- | {
137
- outcome: "unprojected-input";
138
- reason: UnprojectedCompactionReason;
139
- };
140
-
141
- function buildCompactionRequestMeta(event: SessionBeforeCompactEvent): NativeCompactionRequestMeta {
142
- return {
143
- tokensBefore: event.preparation.tokensBefore,
144
- previousSummaryPresent: Boolean(event.preparation.previousSummary),
145
- };
146
- }
147
-
148
- function getCurrentModelDebugInfo(ctx: ExtensionContext) {
149
- return ctx.model
150
- ? {
151
- provider: ctx.model.provider,
152
- id: ctx.model.id,
153
- }
154
- : undefined;
155
- }
156
-
157
- function getCompactionIdentityDebugInfo(entry: { details?: unknown } | undefined) {
158
- return isNativeCompactionDetails(entry?.details)
159
- ? {
160
- provider: entry.details.provider,
161
- api: entry.details.api,
162
- model: entry.details.model,
163
- baseUrl: entry.details.baseUrl,
164
- compactionModel: entry.details.compactionModel,
165
- }
166
- : undefined;
167
- }
168
-
169
- function getSessionId(ctx: ExtensionContext): string | undefined {
170
- try {
171
- return ctx.sessionManager.getSessionId();
172
- } catch {
173
- return undefined;
174
- }
175
- }
176
-
177
- function notifyWarning(ctx: ExtensionContext, message: string): void {
178
- if (ctx.hasUI) {
179
- ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: ${message}`, "warning");
180
- }
181
- }
182
-
183
- async function resolveContextCapabilityModel(
184
- ctx: ExtensionContext,
185
- model: ExtensionContext["model"] = ctx.model,
186
- ): Promise<ExtensionContext["model"]> {
187
- const effective = await resolveEffectiveModel(ctx, model);
188
- return effective.ok ? effective.model : undefined;
189
- }
190
-
191
- async function isRemoteContextActive(
192
- ctx: ExtensionContext,
193
- config: CompactionConfig,
194
- model: ExtensionContext["model"] = ctx.model,
195
- ): Promise<boolean> {
196
- if (!config.enabled || config.contextManagement !== "remote") return false;
197
- const resolution = await resolveCodexContextProvider(ctx, model, config.gatewayContextModels);
198
- return resolution.ok;
199
- }
200
-
201
- function isCodexContextModel(
202
- model: ExtensionContext["model"] | undefined,
203
- config: CompactionConfig,
204
- ): boolean {
205
- return config.contextManagement === "remote"
206
- && (isNativeCodexModel(model) || isCodexGatewayModel(model, config.gatewayContextModels));
207
- }
208
-
209
- function isExcludedGatewayContext(
210
- model: ExtensionContext["model"] | undefined,
211
- config: CompactionConfig,
212
- registrationState: ContextToolRegistrationState,
213
- ): boolean {
214
- return registrationState === "excluded" && isCodexGatewayModel(model, config.gatewayContextModels);
215
- }
216
-
217
- function contextManagementFailureReason(registrationState: ContextToolRegistrationState): string {
218
- switch (registrationState) {
219
- case "excluded":
220
- return "context-tools-excluded";
221
- case "conflict":
222
- return "tool-name-conflict";
223
- case "unverified":
224
- return "tool-registration-pending";
225
- case "verified":
226
- return "context-management-unavailable";
227
- }
228
- }
229
-
230
- function notifyRemoteContextFailure(ctx: ExtensionContext, reason: string): void {
231
- if (ctx.hasUI) {
232
- ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: Remote Context management inactive (${reason})`, "warning");
233
- }
234
- }
235
-
236
- /**
237
- * Surface every window trim that had to be refused to keep the tool loadout intact.
238
- *
239
- * The refusal itself is safe: the model keeps the previous turns instead of losing its
240
- * tools. It is still a continuity incident, so it is recorded once per window and reason
241
- * instead of being allowed to pass silently.
242
- */
243
- function reportProjectionDiagnostics(
244
- contextWindows: CodexContextWindowManager,
245
- config: CompactionConfig,
246
- ctx: ExtensionContext,
247
- ): void {
248
- const diagnostics = contextWindows.takeProjectionDiagnostics();
249
- if (diagnostics.length === 0) return;
250
- writeDebugArtifact(
251
- "compaction-event",
252
- { event: "context.projection.tool_loadout_shrink", diagnostics },
253
- config,
254
- ctx,
255
- );
256
- if (ctx.hasUI) {
257
- ctx.ui.notify(
258
- `${COMPACTION_EXTENSION_ID}: kept previous context to preserve tools (${diagnostics.join("; ")})`,
259
- "warning",
260
- );
261
- }
262
- }
263
-
264
- function cloneOpaqueWindow(window: readonly unknown[]): unknown[] {
265
- return window.map((item) => structuredClone(item));
266
- }
267
-
268
- function buildCompactionInstructions(systemPrompt: string, customInstructions?: string): string {
269
- const guidance = customInstructions?.trim();
270
- if (!guidance) {
271
- return systemPrompt;
272
- }
273
-
274
- return `${systemPrompt}\n\nAdditional user guidance for this manual /compact request:\n${guidance}`;
275
- }
276
-
277
- function findLatestProjectedCompactionSummaryIndex(
278
- messages: readonly AgentMessage[],
279
- summary: string,
280
- ): number {
281
- for (let index = messages.length - 1; index >= 0; index -= 1) {
282
- const message = messages[index];
283
- if (message?.role === "compactionSummary" && message.summary === summary) {
284
- return index;
285
- }
286
- }
287
-
288
- return -1;
289
- }
290
-
291
- function readSessionContextMessages(ctx: ExtensionContext): AgentMessage[] | undefined {
292
- try {
293
- const projection = ctx.sessionManager.buildSessionProjection();
294
- if (!projection || !Array.isArray(projection.messages)) return undefined;
295
- return structuredClone(projection.messages);
296
- } catch {
297
- return undefined;
298
- }
299
- }
300
-
301
- type CompactionProjectionResult =
302
- | { ok: true; messages: readonly AgentMessage[] }
303
- | { ok: false; reason: typeof COMPACTION_PROJECTION_UNAVAILABLE | typeof COMPACTION_SESSION_CONTEXT_UNAVAILABLE };
304
-
305
- async function projectSessionContextForCompaction(
306
- ctx: ExtensionContext,
307
- projectCompactionContext: CompactionContextProjection | undefined,
308
- ): Promise<CompactionProjectionResult> {
309
- const messages = readSessionContextMessages(ctx);
310
- if (!messages) {
311
- return { ok: false, reason: COMPACTION_SESSION_CONTEXT_UNAVAILABLE };
312
- }
313
-
314
- const projector = projectCompactionContext ?? getPiContextHookProjector(ctx);
315
- if (!projector) {
316
- return { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
317
- }
318
-
319
- try {
320
- const projected = await projector(messages, ctx);
321
- return Array.isArray(projected)
322
- ? { ok: true, messages: projected }
323
- : { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
324
- } catch {
325
- return { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
326
- }
327
- }
328
-
329
- function getLegacySessionContextMessages(
330
- event: SessionBeforeCompactEvent,
331
- ctx: ExtensionContext,
332
- ): AgentMessage[] {
333
- return readSessionContextMessages(ctx) ?? [
334
- ...event.preparation.messagesToSummarize,
335
- ...event.preparation.turnPrefixMessages,
336
- ];
337
- }
338
-
339
- async function runResponsesNativeCompact(
340
- event: SessionBeforeCompactEvent,
341
- ctx: ExtensionContext,
342
- config: CompactionConfig,
343
- execution: RemoteCompactionExecution,
344
- remoteCompact: typeof executeRemoteV2Compaction,
345
- projectCompactionContext: CompactionContextProjection | undefined,
346
- ): Promise<ResponsesCompactOutcome> {
347
- const { consumer, compactor } = execution;
348
- const instructions = buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions);
349
- const branchEntries = event.branchEntries ?? ctx.sessionManager.getBranch();
350
- const deferredToolCarryover = getLatestDeferredToolCarryover(branchEntries);
351
- const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
352
- baseUrl: consumer.baseUrl,
353
- });
354
-
355
- const inputProvenance = getRemoteV2InputProvenance(config.remoteV2ContextSource);
356
- let requestSource: "session-context" | "non-native-session-context" | "latest-native-replay";
357
- let request: NativeCompactionRequestBody;
358
- if (latestNativeCompaction.ok) {
359
- if (hasInvalidatedCompactionContext(branchEntries, latestNativeCompaction.entry.id)) {
360
- return { outcome: "unprojected-input", reason: COMPACTION_CHECKPOINT_CONTEXT_EDITED };
361
- }
362
- const details = latestNativeCompaction.entry.details;
363
- if (!details) {
364
- return { outcome: "failed" };
365
- }
366
- if (!hasVerifiedCompactionInputProvenance(details, inputProvenance)) {
367
- return {
368
- outcome: "unprojected-input",
369
- reason: COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
370
- };
371
- }
372
- requestSource = "latest-native-replay";
373
-
374
- if (config.remoteV2ContextSource === "pi-context-hook") {
375
- const projection = await projectSessionContextForCompaction(ctx, projectCompactionContext);
376
- if (!projection.ok) {
377
- return { outcome: "unprojected-input", reason: projection.reason };
378
- }
379
- const summaryIndex = findLatestProjectedCompactionSummaryIndex(
380
- projection.messages,
381
- latestNativeCompaction.entry.summary,
382
- );
383
- if (summaryIndex < 0) {
384
- return {
385
- outcome: "unprojected-input",
386
- reason: COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
387
- };
388
- }
389
- const input: ResponsesInputItem[] = [
390
- ...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
391
- ...(await serializeMessagesToResponsesInputWithLimits(
392
- compactor.currentModel,
393
- [...projection.messages.slice(summaryIndex + 1)],
394
- { firstSystemMessageIsUpdate: true },
395
- )),
396
- ];
397
- request = {
398
- model: compactor.requestModel ?? compactor.model,
399
- input,
400
- instructions,
401
- };
402
- } else {
403
- const liveTailEntries = branchEntries.slice(latestNativeCompaction.index + 1);
404
- const input: ResponsesInputItem[] = [
405
- ...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
406
- ...(await serializeLiveTailToResponsesInputWithLimits({ model: compactor.currentModel, entries: liveTailEntries })),
407
- ];
408
- request = {
409
- model: compactor.requestModel ?? compactor.model,
410
- input,
411
- instructions,
412
- };
413
- }
414
- } else if (
415
- latestNativeCompaction.reason === "no-compaction" ||
416
- (latestNativeCompaction.reason === "latest-compaction-not-native" &&
417
- config.allowCompactionContinuityBreak)
418
- ) {
419
- requestSource =
420
- latestNativeCompaction.reason === "no-compaction" ? "session-context" : "non-native-session-context";
421
- if (config.remoteV2ContextSource === "pi-context-hook") {
422
- const projection = await projectSessionContextForCompaction(ctx, projectCompactionContext);
423
- if (!projection.ok) {
424
- return { outcome: "unprojected-input", reason: projection.reason };
425
- }
426
- const serialized = await serializeMessagesToCompactRequestWithLimits({
427
- model: compactor.currentModel,
428
- messages: [...projection.messages],
429
- instructions,
430
- });
431
- request = { ...serialized, model: compactor.requestModel ?? serialized.model };
432
- } else {
433
- const serialized = await serializeMessagesToCompactRequestWithLimits({
434
- model: compactor.currentModel,
435
- messages: getLegacySessionContextMessages(event, ctx),
436
- instructions,
437
- });
438
- request = { ...serialized, model: compactor.requestModel ?? serialized.model };
439
- }
440
- } else {
441
- writeDebugArtifact(
442
- "compaction-event",
443
- {
444
- event: "session_before_compact.remote-v2-skip",
445
- reason: latestNativeCompaction.reason,
446
- consumer: {
447
- provider: consumer.provider,
448
- api: consumer.api,
449
- model: consumer.model,
450
- baseUrl: consumer.baseUrl,
451
- },
452
- compactor: {
453
- provider: compactor.provider,
454
- api: compactor.api,
455
- model: compactor.model,
456
- baseUrl: compactor.baseUrl,
457
- },
458
- latestCompactionIndex: latestNativeCompaction.latestCompactionIndex,
459
- latestCompactionIdentity: getCompactionIdentityDebugInfo(latestNativeCompaction.latestCompaction),
460
- },
461
- config,
462
- ctx,
463
- );
464
- return { outcome: "failed" };
465
- }
466
-
467
- // Mirror the latest codex_rs CompactionInput fields captured from the most
468
- // recent live provider request for this model (tools, reasoning, etc.).
469
- const extras = getCompactionRequestExtras({
470
- provider: consumer.provider,
471
- api: consumer.api,
472
- model: consumer.requestModel ?? consumer.model,
473
- baseUrl: consumer.baseUrl,
474
- sessionId: getSessionId(ctx),
475
- }, compactor.currentModel);
476
- if (extras) {
477
- request = { ...request, ...extras };
478
- }
479
-
480
- const compactResult = await remoteCompact({
481
- runtime: compactor,
482
- request,
483
- signal: event.signal,
484
- settings: config,
485
- context: ctx,
486
- });
487
-
488
- if (compactResult.ok === false) {
489
- writeDebugArtifact(
490
- "compaction-event",
491
- {
492
- event: "session_before_compact.remote-v2-failure",
493
- reason: compactResult.reason,
494
- status: compactResult.status,
495
- errorMessage: compactResult.errorMessage,
496
- },
497
- config,
498
- ctx,
499
- );
500
- return compactResult.reason === "aborted" ? { outcome: "aborted" } : { outcome: "failed" };
501
- }
502
-
503
- let details: NativeCompactionDetails;
504
- try {
505
- details = createNativeCompactionDetails({
506
- provider: consumer.provider,
507
- api: consumer.api,
508
- model: consumer.model,
509
- baseUrl: consumer.baseUrl,
510
- compactionModel: {
511
- provider: compactor.provider,
512
- api: compactor.api,
513
- model: compactor.model,
514
- baseUrl: compactor.baseUrl,
515
- },
516
- deferredToolCarryover,
517
- compactedWindow: compactResult.compactedWindow,
518
- compactResponseId: compactResult.compactResponseId,
519
- createdAt: compactResult.createdAt,
520
- requestMeta: buildCompactionRequestMeta(event),
521
- inputProvenance,
522
- });
523
- } catch (error) {
524
- writeDebugArtifact(
525
- "compaction-event",
526
- {
527
- event: "session_before_compact.invalid-native-details",
528
- reason: error instanceof Error ? error.message : String(error),
529
- consumer: {
530
- provider: consumer.provider,
531
- api: consumer.api,
532
- model: consumer.model,
533
- baseUrl: consumer.baseUrl,
534
- },
535
- compactor: {
536
- provider: compactor.provider,
537
- api: compactor.api,
538
- model: compactor.model,
539
- baseUrl: compactor.baseUrl,
540
- },
541
- },
542
- config,
543
- ctx,
544
- );
545
- return { outcome: "failed" };
546
- }
547
-
548
- const compaction = createNativeCompactionResult({
549
- firstKeptEntryId: event.preparation.firstKeptEntryId,
550
- tokensBefore: event.preparation.tokensBefore,
551
- details,
552
- });
553
-
554
- writeDebugArtifact(
555
- "compaction-event",
556
- {
557
- event: "session_before_compact.remote-v2-success",
558
- consumer: {
559
- provider: consumer.provider,
560
- api: consumer.api,
561
- model: consumer.model,
562
- baseUrl: consumer.baseUrl,
563
- },
564
- compactor: {
565
- provider: compactor.provider,
566
- api: compactor.api,
567
- model: compactor.model,
568
- baseUrl: compactor.baseUrl,
569
- },
570
- requestSource,
571
- remoteV2ContextSource: config.remoteV2ContextSource,
572
- inputProvenance,
573
- requestInputItems: request.input.length,
574
- requestExtras: extras ? Object.keys(extras) : [],
575
- compactResponseId: compactResult.compactResponseId,
576
- compactedItems: compactResult.compactedWindow.length,
577
- firstKeptEntryId: event.preparation.firstKeptEntryId,
578
- },
579
- config,
580
- ctx,
581
- );
582
-
583
- return { outcome: "success", compaction };
584
- }
585
-
586
- type ContextUnavailableCompactionResult =
587
- | { compaction: CompactionResult<unknown> }
588
- | { cancel: true };
589
-
590
- async function runUnavailableContextCompaction(
591
- event: SessionBeforeCompactEvent,
592
- ctx: ExtensionContext,
593
- dependencies: CompactionDependencies,
594
- resolved: ResolvedToolkitConfig,
595
- config: CompactionConfig,
596
- registrationState: ContextToolRegistrationState,
597
- ): Promise<ContextUnavailableCompactionResult> {
598
- const contextReason = contextManagementFailureReason(registrationState);
599
- const remoteModelSpec = config.remoteCompactModel?.trim();
600
- let remoteFailure: string | undefined;
601
-
602
- if (remoteModelSpec) {
603
- const resolution = await resolveRemoteCompactionExecution(
604
- ctx,
605
- {
606
- enabled: config.enabled,
607
- responsesApis: config.responsesApis,
608
- codexGatewayModels: compactGatewayModels(resolved),
609
- },
610
- remoteModelSpec,
611
- );
612
- if (resolution.ok) {
613
- const outcome = await runResponsesNativeCompact(
614
- event,
615
- ctx,
616
- config,
617
- resolution.execution,
618
- dependencies.remoteCompact,
619
- dependencies.projectCompactionContext,
620
- );
621
- if (outcome.outcome === "success") {
622
- writeDebugArtifact(
623
- "compaction-event",
624
- {
625
- event: "session_before_compact.context-fallback-success",
626
- contextReason,
627
- registrationState,
628
- compactor: remoteModelSpec,
629
- },
630
- config,
631
- ctx,
632
- );
633
- return { compaction: outcome.compaction };
634
- }
635
- if (outcome.outcome === "aborted" || outcome.outcome === "unprojected-input") {
636
- return { cancel: true };
637
- }
638
- remoteFailure = "remote-v2-failed";
639
- } else {
640
- remoteFailure = resolution.reason;
641
- writeDebugArtifact(
642
- "compaction-event",
643
- {
644
- event: "session_before_compact.context-fallback-unavailable",
645
- contextReason,
646
- registrationState,
647
- reason: resolution.reason,
648
- modelSpec: remoteModelSpec,
649
- errorMessage: resolution.errorMessage,
650
- },
651
- config,
652
- ctx,
653
- );
654
- }
655
- } else {
656
- remoteFailure = "missing-remote-compaction-model";
657
- }
658
-
659
- const nativeModelSpec = config.nativeFallback.model?.trim();
660
- if (config.nativeFallback.enabled && nativeModelSpec) {
661
- const fallback = await dependencies.nativeFallback({
662
- ctx,
663
- event,
664
- config,
665
- modelSpec: nativeModelSpec,
666
- });
667
- if (fallback.ok) {
668
- writeDebugArtifact(
669
- "compaction-event",
670
- {
671
- event: "session_before_compact.context-fallback-native-success",
672
- contextReason,
673
- registrationState,
674
- remoteFailure,
675
- fallbackModel: nativeModelSpec,
676
- },
677
- config,
678
- ctx,
679
- );
680
- return { compaction: fallback.result };
681
- }
682
- writeDebugArtifact(
683
- "compaction-event",
684
- {
685
- event: "session_before_compact.context-fallback-native-failure",
686
- contextReason,
687
- registrationState,
688
- remoteFailure,
689
- fallbackReason: fallback.reason,
690
- fallbackModel: nativeModelSpec,
691
- errorMessage: fallback.errorMessage,
692
- },
693
- config,
694
- ctx,
695
- );
696
- } else {
697
- writeDebugArtifact(
698
- "compaction-event",
699
- {
700
- event: "session_before_compact.context-fallback-cancelled",
701
- contextReason,
702
- registrationState,
703
- remoteFailure,
704
- reason: config.nativeFallback.enabled ? "missing-native-fallback-model" : "native-fallback-disabled",
705
- },
706
- config,
707
- ctx,
708
- );
709
- }
710
-
711
- return { cancel: true };
712
- }
713
-
714
- async function handleSessionBeforeCompact(
715
- event: SessionBeforeCompactEvent,
716
- ctx: ExtensionContext,
717
- dependencies: CompactionDependencies,
718
- remoteContextActive: RemoteContextActive,
719
- getContextToolRegistrationState: ContextToolRegistrationStateReader,
720
- ) {
721
- const resolved = contextOperation(dependencies.loadConfig, ctx);
722
- if (resolved.invalidFeatures.some((feature) => ["context", "compatibility", "diagnostics"].includes(feature))) return { cancel: true };
723
- const config = resolved.config.compaction;
724
- if (!config.enabled) {
725
- return undefined;
726
- }
727
-
728
- writeDebugArtifact(
729
- "compaction-event",
730
- {
731
- event: "session_before_compact",
732
- customInstructions: event.customInstructions,
733
- preparation: {
734
- tokensBefore: event.preparation.tokensBefore,
735
- firstKeptEntryId: event.preparation.firstKeptEntryId,
736
- previousSummaryPresent: Boolean(event.preparation.previousSummary),
737
- messagesToSummarizeCount: event.preparation.messagesToSummarize.length,
738
- turnPrefixMessagesCount: event.preparation.turnPrefixMessages.length,
739
- },
740
- },
741
- config,
742
- ctx,
743
- );
744
-
745
- if (event.signal.aborted) {
746
- return { cancel: true };
747
- }
748
-
749
- // Capability changes do not retire the persisted Remote session. Resolve its
750
- // branch boundary before deciding whether any compactor may see this input.
751
- if (config.contextManagement === "remote") {
752
- try {
753
- dependencies.contextWindows.synchronize(ctx);
754
- } catch {
755
- notifyRemoteContextFailure(ctx, "malformed-window-state");
756
- return { cancel: true };
757
- }
758
- }
759
-
760
- // Active and inactive covered Codex consumers retain their existing routing.
761
- const contextModel = await resolveContextCapabilityModel(ctx);
762
- if (isCodexContextModel(contextModel, config)) {
763
- if (await remoteContextActive(ctx, config, contextModel)) {
764
- try {
765
- if (event.reason === "manual" && dependencies.manualCompact && !dependencies.manualCompact.isMaintenance) {
766
- await dependencies.manualCompact.request(event, ctx, resolved);
767
- return { cancel: true };
768
- }
769
- return dependencies.contextWindows.prepareCompaction(event);
770
- } catch {
771
- notifyRemoteContextFailure(ctx, "malformed-window-state");
772
- return { cancel: true };
773
- }
774
- }
775
-
776
- const registrationState = getContextToolRegistrationState();
777
- const failureReason = contextManagementFailureReason(registrationState);
778
- if (failureReason !== "context-tools-excluded") {
779
- notifyRemoteContextFailure(ctx, failureReason);
780
- }
781
- return runUnavailableContextCompaction(
782
- event,
783
- ctx,
784
- dependencies,
785
- resolved,
786
- config,
787
- registrationState,
788
- );
789
- }
790
-
791
- if (config.contextManagement === "remote") {
792
- try {
793
- const detached = dependencies.contextWindows.prepareDetachedCompaction(event);
794
- // A local consumer cannot summarize history owned by a Remote window,
795
- // even through a configured Luna producer or Pi's default compactor.
796
- if (detached) return detached;
797
- } catch {
798
- notifyRemoteContextFailure(ctx, "malformed-window-state");
799
- return { cancel: true };
800
- }
801
- }
802
-
803
- // Resolve producer protocol policy from this operation's document, not a later disk read.
804
- if (resolved.format === "v2" && config.remoteCompactModel) {
805
- const producer = parseModelSpec(config.remoteCompactModel);
806
- if (producer) {
807
- const producerPolicy = resolveToolkitConfig(resolved.snapshot, { provider: producer.provider, id: producer.modelId });
808
- notifyConfigIssues(ctx, producerPolicy);
809
- if (producerPolicy.invalidFeatures.includes("compatibility")) return { cancel: true };
810
- }
811
- }
812
- // Branch 1: Responses-family APIs use remote_compaction_v2 on the normal Responses stream.
813
- let remoteAttempted = false;
814
- const resolution = await resolveRemoteCompactionExecution(
815
- ctx,
816
- {
817
- enabled: config.enabled,
818
- responsesApis: config.responsesApis,
819
- codexGatewayModels: compactGatewayModels(resolved),
820
- },
821
- config.remoteCompactModel,
822
- );
823
- if (resolution.ok) {
824
- remoteAttempted = true;
825
- const responsesOutcome = await runResponsesNativeCompact(
826
- event,
827
- ctx,
828
- config,
829
- resolution.execution,
830
- dependencies.remoteCompact,
831
- dependencies.projectCompactionContext,
832
- );
833
- if (responsesOutcome.outcome === "success") {
834
- return { compaction: responsesOutcome.compaction };
835
- }
836
- if (responsesOutcome.outcome === "aborted") {
837
- return { cancel: true };
838
- }
839
- if (responsesOutcome.outcome === "unprojected-input") {
840
- const message = responsesOutcome.reason === COMPACTION_CHECKPOINT_CONTEXT_EDITED
841
- ? "A context edit changed history sealed in the opaque checkpoint. Start a new session or navigate before that checkpoint; its encrypted content cannot be selectively rewritten."
842
- : responsesOutcome.reason === COMPACTION_PROJECTION_UNAVAILABLE
843
- ? "Pi's ordered context-hook projection is unavailable; Remote V2 was not sent raw session history."
844
- : responsesOutcome.reason === COMPACTION_SESSION_CONTEXT_UNAVAILABLE
845
- ? "Pi did not provide the current session context required by its context-hook projection."
846
- : responsesOutcome.reason === COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE
847
- ? "Pi's projected context did not include the current compaction summary anchor."
848
- : "The latest opaque checkpoint has no marker for the configured Remote V2 context source, or was created in the other mode.";
849
- writeDebugArtifact(
850
- "compaction-event",
851
- {
852
- event: "session_before_compact.remote-v2-unprojected-input",
853
- reason: responsesOutcome.reason,
854
- contextSource: config.remoteV2ContextSource,
855
- inputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
856
- piContextHookPatch: dependencies.piContextHookPatch,
857
- message,
858
- },
859
- config,
860
- ctx,
861
- );
862
- notifyWarning(
863
- ctx,
864
- `Remote V2 compaction cancelled (${responsesOutcome.reason}); ${message} The existing checkpoint and session history were left unchanged.`,
865
- );
866
- return { cancel: true };
867
- }
868
- // failed: fall through to the configured-model fallback below.
869
- } else {
870
- writeDebugArtifact(
871
- "compaction-event",
872
- {
873
- event: "session_before_compact.remote-v2-unavailable",
874
- reason: resolution.reason,
875
- provider: resolution.provider,
876
- api: resolution.api,
877
- model: resolution.model,
878
- baseUrl: resolution.baseUrl,
879
- modelSpec: resolution.modelSpec,
880
- errorMessage: resolution.errorMessage,
881
- },
882
- config,
883
- ctx,
884
- );
885
- const expectedConsumerCapabilityMiss =
886
- resolution.reason === "unsupported-api" && resolution.modelSpec === undefined;
887
- if (config.remoteCompactModel && !expectedConsumerCapabilityMiss) {
888
- notifyWarning(
889
- ctx,
890
- `remote compaction model "${config.remoteCompactModel}" unusable (${resolution.reason}); using the native fallback chain`,
891
- );
892
- }
893
- }
894
-
895
- // Branch 2: run pi's native compaction method. A failed remote request is compacted by the
896
- // remote producer itself; a model that cannot use remote v2 at all uses nativeFallback.model.
897
- const fallbackModelSpec = remoteAttempted ? config.remoteCompactModel : config.nativeFallback.model;
898
- const fallback = await dependencies.nativeFallback({ ctx, event, config, modelSpec: fallbackModelSpec });
899
- if (fallback.ok) {
900
- if (ctx.hasUI) {
901
- ctx.ui.notify(
902
- `${COMPACTION_EXTENSION_ID}: compacted with ${fallback.model.provider}/${fallback.model.id} (native method)`,
903
- "info",
904
- );
905
- }
906
- writeDebugArtifact(
907
- "compaction-event",
908
- {
909
- event: "session_before_compact.fallback-success",
910
- model: fallback.model,
911
- },
912
- config,
913
- ctx,
914
- );
915
- return { compaction: fallback.result };
916
- }
917
-
918
- if (fallback.reason === "aborted") {
919
- return { cancel: true };
920
- }
921
-
922
- writeDebugArtifact(
923
- "compaction-event",
924
- {
925
- event: "session_before_compact.fallback-skip",
926
- reason: fallback.reason,
927
- modelSpec: fallback.modelSpec,
928
- errorMessage: fallback.errorMessage,
929
- ...(fallback.estimatedTokens === undefined ? {} : { estimatedTokens: fallback.estimatedTokens }),
930
- ...(fallback.contextWindow === undefined ? {} : { contextWindow: fallback.contextWindow }),
931
- },
932
- config,
933
- ctx,
934
- );
935
-
936
- // Intentional pi-default paths: feature disabled, nothing configured, or it matches the current one.
937
- const intentionalSkip =
938
- fallback.reason === "disabled" ||
939
- fallback.reason === "no-model-configured" ||
940
- fallback.reason === "same-as-current-model";
941
- if (fallback.reason === "model-window-too-small") {
942
- // The configured summary model is narrower than the request Pi would send for it.
943
- // Skipping is the success path here: pi's current model gets one chance instead of
944
- // the provider terminating the oversized summary.
945
- notifyWarning(
946
- ctx,
947
- `compaction summary model "${fallback.modelSpec}" cannot fit ~${fallback.estimatedTokens} tokens in its ${fallback.contextWindow} window; compacting with the current model instead`,
948
- );
949
- } else if (!intentionalSkip) {
950
- notifyWarning(
951
- ctx,
952
- `compaction model "${fallback.modelSpec}" unusable (${fallback.reason}${fallback.errorMessage ? `: ${fallback.errorMessage}` : ""}); using pi's default compaction`,
953
- );
954
- }
955
-
956
- // Branch 3: pi's default native compaction with the current model.
957
- return undefined;
958
- }
959
-
960
- function rejectInheritedCheckpointReplay(
961
- ctx: ExtensionContext,
962
- config: CompactionConfig,
963
- compactionEntryId: string,
964
- reason: string,
965
- ): undefined {
966
- writeReplayFailureArtifact({ reason, compactionEntryId }, config, ctx);
967
- reportPiContextHookFailure(ctx, `replay-failed:${reason}`);
968
- if (ctx.hasUI) ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: inherited checkpoint replay failed (${reason}); request aborted`, "error");
969
- ctx.abort();
970
- return undefined;
971
- }
972
-
973
- async function handleContextInternal(
974
- event: ContextWithSystemEvent,
975
- ctx: ExtensionContext,
976
- pi: ExtensionAPI,
977
- loadConfig: typeof loadToolkitConfig,
978
- contextWindows: CodexContextWindowManager,
979
- remoteContextActive: RemoteContextActive,
980
- getContextToolRegistrationState: ContextToolRegistrationStateReader,
981
- ) {
982
- const resolved = contextOperation(loadConfig, ctx);
983
- requireContextPolicy(resolved, ctx);
984
- const config = resolved.config.compaction;
985
- if (!config.enabled) {
986
- const visibleMessages = contextWindows.project(event.messages, "off");
987
- return visibleMessages.length === event.messages.length && visibleMessages.every((message, index) => message === event.messages[index])
988
- ? undefined
989
- : { messages: visibleMessages };
990
- }
991
-
992
- let localMessages: AgentMessage[] | undefined;
993
- let remoteReplayProjection = false;
994
- if (config.contextManagement === "remote") {
995
- const contextModel = await resolveContextCapabilityModel(ctx);
996
- try {
997
- contextWindows.synchronize(ctx);
998
- } catch {
999
- notifyRemoteContextFailure(ctx, "malformed-window-state");
1000
- reportPiContextHookFailure(ctx, "malformed-window-state");
1001
- ctx.abort();
1002
- return undefined;
1003
- }
1004
-
1005
- const remoteActive = await remoteContextActive(ctx, config, contextModel);
1006
- if (remoteActive) {
1007
- try {
1008
- // Measure the provider-visible current window, not Pi's durable transcript.
1009
- // The latter still contains retired remote windows and can be inflated by
1010
- // an upstream error even though this request only carries the live window.
1011
- let projected = contextWindows.project(event.messages, "remote", false);
1012
- // Projection may observe the exact queued rollover before Pi persists
1013
- // it. Decide replay ownership from this request, not the old branch.
1014
- const ownsWindow = contextWindows.hasEffectiveWindow() || !contextWindows.currentIdentity();
1015
- if (ownsWindow) projected = contextWindows.project(projected, "remote");
1016
- contextWindows.recordBudget(
1017
- pi,
1018
- ctx,
1019
- true,
1020
- config.contextReminderThresholdPercent,
1021
- );
1022
- reportProjectionDiagnostics(contextWindows, config, ctx);
1023
- if (ownsWindow) {
1024
- return projected.length === event.messages.length && projected.every((message, index) => message === event.messages[index])
1025
- ? undefined
1026
- : { messages: projected };
1027
- }
1028
- localMessages = projected;
1029
- remoteReplayProjection = true;
1030
- } catch (error) {
1031
- notifyRemoteContextFailure(ctx, "malformed-window-state");
1032
- reportPiContextHookFailure(ctx, "malformed-window-state");
1033
- ctx.abort();
1034
- return undefined;
1035
- }
1036
- }
1037
-
1038
- // An unavailable covered runtime must not silently become an ordinary
1039
- // replay consumer. Explicit host exclusion remains the one intentional
1040
- // downgrade; active Remote can replay a newer inherited checkpoint.
1041
- if (!remoteActive && isCodexContextModel(contextModel, config)) {
1042
- const registrationState = getContextToolRegistrationState();
1043
- if (isExcludedGatewayContext(contextModel, config, registrationState)) {
1044
- const visibleMessages = contextWindows.project(event.messages, "off");
1045
- return visibleMessages.length === event.messages.length && visibleMessages.every((message, index) => message === event.messages[index])
1046
- ? undefined
1047
- : { messages: visibleMessages };
1048
- }
1049
- if (isCodexGatewayModel(contextModel, config.gatewayContextModels)) {
1050
- const reason = contextManagementFailureReason(registrationState);
1051
- notifyRemoteContextFailure(ctx, reason);
1052
- reportPiContextHookFailure(ctx, reason);
1053
- ctx.abort();
1054
- return undefined;
1055
- }
1056
- const visibleMessages = contextWindows.project(event.messages, "off");
1057
- return visibleMessages.length === event.messages.length && visibleMessages.every((message, index) => message === event.messages[index])
1058
- ? undefined
1059
- : { messages: visibleMessages };
1060
- }
1061
-
1062
- if (!remoteActive) {
1063
- try {
1064
- const projected = contextWindows.project(event.messages, "local");
1065
- localMessages = projected;
1066
- if (contextWindows.hasEffectiveWindow()) {
1067
- // This boundary owns earlier history, including any older Remote V2
1068
- // checkpoint. A local request must not replay that history back in.
1069
- return projected.length === event.messages.length && projected.every((message, index) => message === event.messages[index])
1070
- ? undefined
1071
- : { messages: projected };
1072
- }
1073
- } catch (error) {
1074
- notifyRemoteContextFailure(ctx, "malformed-window-state");
1075
- reportPiContextHookFailure(ctx, error instanceof Error ? error.message : String(error));
1076
- ctx.abort();
1077
- return undefined;
1078
- }
1079
- }
1080
- }
1081
-
1082
- // Without an effective Remote boundary, the latest checkpoint owns replay.
1083
- // This also covers a newer checkpoint superseding a historical window.
1084
- const visibleMessages = localMessages ?? contextWindows.project(event.messages, "off");
1085
- const finishProjection = (messages: AgentMessage[]) => {
1086
- // Compare canonical retained copies before Remote encrypted-output encoding.
1087
- const projected = remoteReplayProjection ? contextWindows.project(messages, "remote") : messages;
1088
- return projected.length === event.messages.length && projected.every((message, index) => message === event.messages[index])
1089
- ? undefined : { messages: projected };
1090
- };
1091
- const replayEvent = visibleMessages === event.messages ? event : { ...event, messages: visibleMessages };
1092
- const branchEntries = ctx.sessionManager.getBranch();
1093
- const inherited = config.contextManagement === "remote" && contextWindows.hasSupersededWindow()
1094
- ? resolveLatestNativeCompactionEntry(branchEntries) : undefined;
1095
- const inheritedCheckpoint = inherited?.ok ? inherited.entry : undefined;
1096
- const resolution = await resolveNativeCompactionEnvironment(ctx, {
1097
- enabled: config.enabled,
1098
- // Disabling checkpoint generation cannot turn an inherited opaque
1099
- // checkpoint into a usable text summary. Replay checks transport support.
1100
- responsesApis: inheritedCheckpoint ? undefined : config.responsesApis,
1101
- codexGatewayModels: compactGatewayModels(resolved),
1102
- });
1103
- if (!resolution.ok) {
1104
- return inheritedCheckpoint
1105
- ? rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, resolution.reason)
1106
- : finishProjection(visibleMessages);
1107
- }
1108
- const latest = resolveLatestNativeCompactionEntry(branchEntries, { baseUrl: resolution.runtime.baseUrl });
1109
- if (!latest.ok) {
1110
- return inheritedCheckpoint
1111
- ? rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, latest.reason)
1112
- : finishProjection(visibleMessages);
1113
- }
1114
- const result = removeNativeCompactionRetainedMessages({
1115
- messages: replayEvent.messages,
1116
- branchEntries,
1117
- compactionEntry: latest.entry,
1118
- expectedInputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
1119
- });
1120
- if (!result.ok) {
1121
- writeReplayFailureArtifact({ reason: result.reason, compactionEntryId: latest.entry.id }, config, ctx);
1122
- if (ctx.hasUI) ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: replay failed (${result.reason}); request aborted`, "error");
1123
- reportPiContextHookFailure(ctx, `replay-failed:${result.reason}`);
1124
- ctx.abort();
1125
- return undefined;
1126
- }
1127
- return finishProjection(result.messages);
1128
- }
1129
-
1130
- /**
1131
- * Pi's emitContext() catches handler exceptions. Re-throw as usual for Pi, but
1132
- * also report the failure so the compaction projector cannot mistake the
1133
- * caught exception for a successful projection.
1134
- */
1135
- async function handleContext(
1136
- event: ContextWithSystemEvent,
1137
- ctx: ExtensionContext,
1138
- pi: ExtensionAPI,
1139
- loadConfig: typeof loadToolkitConfig,
1140
- contextWindows: CodexContextWindowManager,
1141
- remoteContextActive: RemoteContextActive,
1142
- getContextToolRegistrationState: ContextToolRegistrationStateReader,
1143
- ) {
1144
- try {
1145
- return await handleContextInternal(event, ctx, pi, loadConfig, contextWindows, remoteContextActive, getContextToolRegistrationState);
1146
- } catch (error) {
1147
- reportPiContextHookFailure(ctx, error instanceof Error ? error.message : String(error));
1148
- throw error;
1149
- }
1150
- }
1151
-
1152
- async function handleBeforeProviderRequest(
1153
- event: BeforeProviderRequestEvent,
1154
- ctx: ExtensionContext,
1155
- loadConfig: typeof loadToolkitConfig,
1156
- contextWindows: CodexContextWindowManager,
1157
- remoteContextActive: RemoteContextActive,
1158
- getContextToolRegistrationState: ContextToolRegistrationStateReader,
1159
- ) {
1160
- const resolved = contextOperation(loadConfig, ctx);
1161
- requireContextPolicy(resolved, ctx);
1162
- const toolkitConfig = resolved.config;
1163
- const config = toolkitConfig.compaction;
1164
- if (!config.enabled) {
1165
- return undefined;
1166
- }
1167
-
1168
- const contextModel = config.contextManagement === "remote"
1169
- ? await resolveContextCapabilityModel(ctx)
1170
- : undefined;
1171
- if (config.contextManagement === "remote") {
1172
- try {
1173
- contextWindows.synchronize(ctx);
1174
- } catch {
1175
- notifyRemoteContextFailure(ctx, "malformed-window-state");
1176
- ctx.abort();
1177
- return undefined;
1178
- }
1179
- }
1180
- const remoteActive = config.contextManagement === "remote" && await remoteContextActive(ctx, config, contextModel);
1181
- let remotePayload: unknown;
1182
- if (remoteActive) {
1183
- try {
1184
- remotePayload = contextWindows.rewritePayload(event.payload, ctx);
1185
- if (contextWindows.hasEffectiveWindow() || !contextWindows.currentIdentity()) return remotePayload;
1186
- event = { ...event, payload: remotePayload };
1187
- } catch {
1188
- notifyRemoteContextFailure(ctx, "malformed-request-state");
1189
- ctx.abort();
1190
- return undefined;
1191
- }
1192
- }
1193
- if (!remoteActive && isCodexContextModel(contextModel, config)) {
1194
- const registrationState = getContextToolRegistrationState();
1195
- // A readable empty registry is an intentional host exclusion. Do not
1196
- // rewrite gateway identity or abort an ordinary child-session request.
1197
- if (isExcludedGatewayContext(contextModel, config, registrationState)) return undefined;
1198
- // Keep Codex Remote mutually exclusive with the legacy replay pipeline
1199
- // when authentication or tool ownership is unavailable. Gateway traffic
1200
- // cannot continue as an ordinary Responses request because that would
1201
- // silently lose the required CPA session/account selection.
1202
- if (isCodexGatewayModel(contextModel, config.gatewayContextModels)) {
1203
- const reason = contextManagementFailureReason(registrationState);
1204
- notifyRemoteContextFailure(ctx, reason);
1205
- ctx.abort();
1206
- }
1207
- return undefined;
1208
- }
1209
-
1210
- // Do not reintroduce a pre-window opaque checkpoint after local projection.
1211
- // Request affinity/encrypted-output rewriting is reserved for active Remote.
1212
- if (config.contextManagement === "remote" && contextWindows.hasEffectiveWindow()) return undefined;
1213
-
1214
- const branchEntries = ctx.sessionManager.getBranch();
1215
- const inherited = config.contextManagement === "remote" && contextWindows.hasSupersededWindow()
1216
- ? resolveLatestNativeCompactionEntry(branchEntries) : undefined;
1217
- const inheritedCheckpoint = inherited?.ok ? inherited.entry : undefined;
1218
- const resolution = await resolveNativeCompactionEnvironment(
1219
- ctx,
1220
- {
1221
- enabled: config.enabled,
1222
- responsesApis: inheritedCheckpoint ? undefined : config.responsesApis,
1223
- codexGatewayModels: compactGatewayModels(resolved),
1224
- },
1225
- event.payload,
1226
- );
1227
- if (resolution.ok === false) {
1228
- if (inheritedCheckpoint) return rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, resolution.reason);
1229
- writeDebugArtifact(
1230
- "provider-request",
1231
- {
1232
- event: "before_provider_request.skip",
1233
- reason: resolution.reason,
1234
- provider: resolution.provider,
1235
- api: resolution.api,
1236
- model: resolution.model,
1237
- baseUrl: resolution.baseUrl,
1238
- errorMessage: resolution.errorMessage,
1239
- currentModel: getCurrentModelDebugInfo(ctx),
1240
- payload: event.payload,
1241
- },
1242
- config,
1243
- ctx,
1244
- );
1245
- return remotePayload;
1246
- }
1247
-
1248
- const runtime = resolution.runtime;
1249
- const payload = runtime.payload;
1250
- if (!payload) {
1251
- return undefined;
1252
- }
1253
-
1254
- // Capture compact-relevant request fields (tools, reasoning, ...) for the next
1255
- // synthetic compact request using the active consumer's effective runtime identity.
1256
- // This hook runs before the separate Web Search transform, so injected native search
1257
- // tools are not copied into remote_compaction_v2.
1258
- const webSearchRoute = resolveWebSearchRoute({ model: ctx.model, config: toolkitConfig.webSearch });
1259
- rememberRequestContext(
1260
- payload,
1261
- {
1262
- provider: runtime.provider,
1263
- api: runtime.api,
1264
- model: runtime.requestModel ?? runtime.model,
1265
- baseUrl: runtime.baseUrl,
1266
- sessionId: getSessionId(ctx),
1267
- },
1268
- {
1269
- excludeWebSearchTools: webSearchRoute.route === "standalone-alpha",
1270
- },
1271
- );
1272
-
1273
- const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
1274
- baseUrl: runtime.baseUrl,
1275
- });
1276
- if (!latestNativeCompaction.ok) {
1277
- if (inheritedCheckpoint) return rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, latestNativeCompaction.reason);
1278
- writeDebugArtifact(
1279
- "provider-request",
1280
- {
1281
- event: "before_provider_request.no-native-compaction",
1282
- reason: latestNativeCompaction.reason,
1283
- provider: runtime.provider,
1284
- api: runtime.api,
1285
- model: runtime.model,
1286
- baseUrl: runtime.baseUrl,
1287
- branchEntries: branchEntries.length,
1288
- latestCompactionIndex: latestNativeCompaction.latestCompactionIndex,
1289
- latestCompactionIdentity: getCompactionIdentityDebugInfo(latestNativeCompaction.latestCompaction),
1290
- payload,
1291
- },
1292
- config,
1293
- ctx,
1294
- );
1295
- return remotePayload;
1296
- }
1297
-
1298
- const latestNativeCompactionEntry = latestNativeCompaction.entry;
1299
- const rewrite = rewriteResponsesPayloadWithNativeReplay({
1300
- model: runtime.currentModel,
1301
- payload,
1302
- branchEntries,
1303
- compactionEntry: latestNativeCompactionEntry,
1304
- expectedInputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
1305
- });
1306
- if (!rewrite.ok) {
1307
- writeDebugArtifact(
1308
- "provider-request",
1309
- {
1310
- event: "before_provider_request.rewrite-failed",
1311
- reason: rewrite.reason,
1312
- provider: runtime.provider,
1313
- api: runtime.api,
1314
- model: runtime.model,
1315
- baseUrl: runtime.baseUrl,
1316
- compactionEntryId: latestNativeCompactionEntry.id,
1317
- parity: rewrite.parity,
1318
- payload,
1319
- },
1320
- config,
1321
- ctx,
1322
- );
1323
-
1324
- // Fail loud instead of letting Pi send the sentinel-only payload: the
1325
- // compacted history would be silently lost while the request still succeeds.
1326
- // A forced redacted failure record is written even when logProviderPayloads
1327
- // is disabled so the incident is diagnosable without leaking content.
1328
- writeReplayFailureArtifact(
1329
- {
1330
- reason: rewrite.reason,
1331
- parity: rewrite.parity,
1332
- compactionEntryId: latestNativeCompactionEntry.id,
1333
- provider: runtime.provider,
1334
- api: runtime.api,
1335
- model: runtime.model,
1336
- },
1337
- config,
1338
- ctx,
1339
- );
1340
- if (ctx.hasUI) {
1341
- ctx.ui.notify(
1342
- `${COMPACTION_EXTENSION_ID}: native compaction replay failed (${rewrite.reason}); provider request aborted`,
1343
- "error",
1344
- );
1345
- }
1346
- ctx.abort();
1347
- return undefined;
1348
- }
1349
-
1350
- writeDebugArtifact(
1351
- "provider-request",
1352
- {
1353
- event: "before_provider_request.native-rewrite",
1354
- provider: runtime.provider,
1355
- api: runtime.api,
1356
- model: runtime.model,
1357
- baseUrl: runtime.baseUrl,
1358
- compactionEntryId: latestNativeCompactionEntry.id,
1359
- boundaryIndex: rewrite.segments.boundaryIndex,
1360
- firstKeptEntryIndex: rewrite.segments.firstKeptEntryIndex,
1361
- originalInputItems: payload.input.length,
1362
- rewrittenInputItems: rewrite.rewrittenPayload.input.length,
1363
- leadingItems: rewrite.segments.leading.length,
1364
- compactionSummaryItems: rewrite.segments.compactionSummary.length,
1365
- compactedItems: rewrite.segments.compactedWindow.length,
1366
- postItems: rewrite.segments.post.length,
1367
- payload: rewrite.rewrittenPayload,
1368
- originalPayload: payload,
1369
- },
1370
- config,
1371
- ctx,
1372
- );
1373
-
1374
- return rewrite.rewrittenPayload;
1375
- }
1376
-
1377
- export default function registerCompactionExtension(
1378
- pi: ExtensionAPI,
1379
- overrides: Partial<CompactionDependencies> = {},
1380
- ) {
1381
- const loadConfig = overrides.loadConfig ?? loadToolkitConfig;
1382
- registerToolkitConfigCommand(pi, loadConfig);
1383
- const piContextHookPatch = installPiContextHookPatch();
1384
- const contextWindows = overrides.contextWindows ?? new CodexContextWindowManager((ctx, signal, gatewayModels) =>
1385
- loadHistoryNotesThreadHint(ctx, signal, gatewayModels)
1386
- );
1387
- const manualCompact = new ManagedManualCompact(pi, contextWindows);
1388
- const dependencies: CompactionDependencies = {
1389
- remoteCompact: executeRemoteV2Compaction,
1390
- nativeFallback: runNativeFallbackCompaction,
1391
- contextWindows,
1392
- ...overrides,
1393
- manualCompact,
1394
- loadConfig: () => manualCompact.snapshot ?? loadConfig(),
1395
- piContextHookPatch: overrides.piContextHookPatch ?? piContextHookPatch,
1396
- };
1397
- let tools!: ReturnType<typeof registerContextManagementTools>;
1398
- let contextWindowReady = false;
1399
- const isContextRuntimeActive = async (
1400
- ctx: ExtensionContext,
1401
- config: CompactionConfig,
1402
- model = ctx.model,
1403
- ): Promise<boolean> =>
1404
- contextWindowReady && tools.isRegistered && await isRemoteContextActive(ctx, config, model);
1405
- tools = registerContextManagementTools(
1406
- pi,
1407
- contextWindows,
1408
- async (ctx) => {
1409
- const resolved = contextOperation(dependencies.loadConfig, ctx);
1410
- assertConfigValid(resolved, "context", "compatibility");
1411
- return { active: await isContextRuntimeActive(ctx, resolved.config.compaction), gatewayModels: resolved.gatewayModelKeys };
1412
- },
1413
- undefined,
1414
- manualCompact,
1415
- );
1416
- const remoteContextActive: RemoteContextActive = isContextRuntimeActive;
1417
- const syncTools = async (
1418
- ctx: ExtensionContext,
1419
- model = ctx.model,
1420
- options: { notifyWindowFailure?: boolean } = {},
1421
- resolved = contextOperation(dependencies.loadConfig, ctx, model),
1422
- ): Promise<ContextToolSyncOutcome> => {
1423
- // Until this call proves otherwise, do not let a previous session/window
1424
- // identity make a failed activation look usable.
1425
- contextWindowReady = false;
1426
- const config = resolved.config.compaction;
1427
- const eligible = !resolved.invalidFeatures.some((feature) => ["context", "compatibility"].includes(feature)) && await isRemoteContextActive(ctx, config, model);
1428
- const toolSync = tools.sync(eligible);
1429
- const outcome: ContextToolSyncOutcome = {
1430
- eligible,
1431
- toolsSynced: toolSync.synced,
1432
- registrationState: toolSync.registrationState,
1433
- windowInitialized: false,
1434
- };
1435
- if (!eligible || !toolSync.synced) return outcome;
1436
- // Activation can succeed after `session_start` missed it, because Pi 0.86 may
1437
- // reject the registration read while a session replacement is still binding.
1438
- // The window lifecycle has to open on that later activation too, or requests
1439
- // carry no window metadata and the backend never ingests the turns.
1440
- try {
1441
- contextWindows.ensureInitialized(pi, ctx, true);
1442
- contextWindowReady = true;
1443
- return { ...outcome, windowInitialized: true };
1444
- } catch {
1445
- // Do not leave tools exposed while the request path has no valid window
1446
- // identity. The next lifecycle hook may retry once Pi is usable again.
1447
- tools.sync(false);
1448
- if (options.notifyWindowFailure) notifyRemoteContextFailure(ctx, "malformed-window-state");
1449
- return outcome;
1450
- }
1451
- };
1452
- pi.on("session_start", async (_event, ctx) => {
1453
- const resolved = contextOperation(dependencies.loadConfig, ctx);
1454
- const syncOutcome = await syncTools(ctx, ctx.model, { notifyWindowFailure: true }, resolved);
1455
- await manualCompact.recover(ctx);
1456
- const active = syncOutcome.eligible && syncOutcome.toolsSynced && syncOutcome.windowInitialized;
1457
- const { source } = resolved;
1458
- const warnings = resolved.issues.map((issue) => `${issue.path}: ${issue.code}`);
1459
- const config = resolved.config.compaction;
1460
- if (resolved.invalidFeatures.length > 0) return;
1461
- if (!config.enabled) return;
1462
- const contextModel = await resolveContextCapabilityModel(ctx);
1463
-
1464
- let activationReason: string | undefined;
1465
- // Only models the built-in Remote Context coverage targets may activate or
1466
- // warn; everything else silently runs Pi's normal compaction path.
1467
- if (config.contextManagement === "remote" && isCodexContextModel(contextModel, config) && !active) {
1468
- if (syncOutcome.registrationState === "excluded") {
1469
- activationReason = "context-tools-excluded";
1470
- } else if (syncOutcome.registrationState === "conflict") {
1471
- activationReason = "tool-name-conflict";
1472
- notifyRemoteContextFailure(ctx, activationReason);
1473
- } else if (syncOutcome.registrationState === "unverified" || !syncOutcome.toolsSynced) {
1474
- // Transient: the runtime has not published or accepted our tools yet. A
1475
- // later before_agent_start re-verifies, so do not report provider failure.
1476
- activationReason = "tool-registration-pending";
1477
- } else if (syncOutcome.eligible && !syncOutcome.windowInitialized) {
1478
- activationReason = "malformed-window-state";
1479
- } else {
1480
- const remoteResolution = await resolveCodexContextProvider(ctx, ctx.model, config.gatewayContextModels);
1481
- activationReason = remoteResolution.ok ? "codex-context-unavailable" : remoteResolution.reason;
1482
- notifyRemoteContextFailure(ctx, activationReason);
1483
- }
1484
- }
1485
-
1486
-
1487
- const artifactPath = writeDebugArtifact(
1488
- "lifecycle",
1489
- {
1490
- event: "session_start",
1491
- config,
1492
- configSource: source,
1493
- warnings,
1494
- activation: {
1495
- active,
1496
- contextManagement: config.contextManagement,
1497
- remoteV2ContextSource: config.remoteV2ContextSource,
1498
- model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined,
1499
- ...(activationReason ? { reason: activationReason } : {}),
1500
- },
1501
- piContextHookPatch: dependencies.piContextHookPatch,
1502
- },
1503
- config,
1504
- ctx,
1505
- );
1506
-
1507
- if (ctx.hasUI && ["info", "debug"].includes(resolved.policy.diagnostics.level) && (config.notifyOnLoad || config.debug)) {
1508
- ctx.ui.notify(
1509
- artifactPath
1510
- ? `${COMPACTION_EXTENSION_ID} loaded • debug artifacts → ${artifactPath}`
1511
- : `${COMPACTION_EXTENSION_ID} loaded`,
1512
- "info",
1513
- );
1514
- }
1515
- });
1516
-
1517
- const getContextToolRegistrationState: ContextToolRegistrationStateReader = () => tools.registrationState;
1518
- pi.on("context_with_system", (event, ctx) => {
1519
- manualCompact.guardRequest(ctx);
1520
- return handleContext(event, ctx, pi, dependencies.loadConfig, contextWindows, remoteContextActive, getContextToolRegistrationState);
1521
- });
1522
- pi.on("session_before_compact", (event, ctx) => handleSessionBeforeCompact(event, ctx, dependencies, remoteContextActive, getContextToolRegistrationState));
1523
- pi.on("session_compact", (_event, ctx) => contextWindows.synchronize(ctx));
1524
- pi.on("session_compact_failed", async (event, ctx) => {
1525
- if (event.reason === "manual" && !manualCompact.isMaintenance) await manualCompact.compactFailed(ctx);
1526
- });
1527
- pi.on("session_shutdown", async (_event, ctx) => {
1528
- await manualCompact.shutdown(ctx);
1529
- contextWindowReady = false;
1530
- contextWindows.reset();
1531
- tools.reset();
1532
- });
1533
- pi.on("agent_settled", async (_event, ctx) => {
1534
- // Only the owned manual handoff restores/continues here; Pi owns normal
1535
- // compaction scheduling. Never compact retired windows on model changes.
1536
- await manualCompact.settled(ctx);
1537
- });
1538
- pi.on("model_select", async (event, ctx) => {
1539
- manualCompact.modelSelected(event.model, ctx);
1540
- // Switching into a covered model mid-session must open the window
1541
- // lifecycle immediately: without an identity the request rewrite skips
1542
- // window metadata, the backend never ingests those turns, and the first
1543
- // new_context would trim pre-switch history that no history can recover.
1544
- // syncTools() activates and initializes the window when the model is covered.
1545
- const resolved = contextOperation(dependencies.loadConfig, ctx, event.model);
1546
- await syncTools(ctx, event.model, { notifyWindowFailure: true }, resolved);
1547
- });
1548
- pi.on("before_agent_start", async (_event, ctx) => {
1549
- await syncTools(ctx, ctx.model, { notifyWindowFailure: true });
1550
- });
1551
- pi.on("thinking_level_select", (event, ctx) => manualCompact.thinkingSelected(event.level, ctx));
1552
- pi.on("message_start", (event) => manualCompact.messageStarted(event.message));
1553
- pi.on("turn_end", (_event, ctx) => manualCompact.turnEnded(ctx));
1554
- pi.on("tool_call", (event) => manualCompact.guardTool(event.toolName));
1555
- pi.on("session_tree", async (_event, ctx) => {
1556
- contextWindows.synchronize(ctx);
1557
- await manualCompact.recover(ctx);
1558
- await syncTools(ctx);
1559
- });
1560
- pi.on("before_provider_request", (event, ctx) => {
1561
- manualCompact.guardRequest(ctx);
1562
- return handleBeforeProviderRequest(event, ctx, dependencies.loadConfig, contextWindows, remoteContextActive, getContextToolRegistrationState);
1563
- });
1564
- pi.on("before_provider_headers", async (event, ctx) => {
1565
- const resolved = contextOperation(dependencies.loadConfig, ctx);
1566
- requireContextPolicy(resolved, ctx);
1567
- const config = resolved.config.compaction;
1568
- const contextModel = await resolveContextCapabilityModel(ctx);
1569
- if (!isCodexContextModel(contextModel, config)) return;
1570
- const active = await remoteContextActive(ctx, config, contextModel);
1571
- if (!active) {
1572
- const registrationState = getContextToolRegistrationState();
1573
- if (isExcludedGatewayContext(contextModel, config, registrationState)) return;
1574
- if (isCodexGatewayModel(contextModel, config.gatewayContextModels)) {
1575
- const reason = contextManagementFailureReason(registrationState);
1576
- notifyRemoteContextFailure(ctx, reason);
1577
- ctx.abort();
1578
- }
1579
- return;
1580
- }
1581
- const provider = await resolveCodexContextProvider(ctx, ctx.model, config.gatewayContextModels);
1582
- if (provider.ok && provider.provider.kind === "codex-gateway") {
1583
- const sessionId = getSessionId(ctx);
1584
- const gatewayHeaders = codexContextProviderHeaders(provider.provider, {
1585
- sessionId,
1586
- clientRequestId: sessionId,
1587
- });
1588
- const allowedGatewayHeaders = new Set<string>(CODEX_GATEWAY_FORWARD_HEADERS);
1589
- for (const existing of Object.keys(event.headers)) {
1590
- if (!allowedGatewayHeaders.has(existing.toLowerCase())) delete event.headers[existing];
1591
- }
1592
- // Keep any caller-supplied version without forcing a Toolkit pin.
1593
- // The gateway allowlist above already permits it.
1594
- for (const name of [
1595
- "authorization",
1596
- "originator",
1597
- "user-agent",
1598
- "session-id",
1599
- "x-client-request-id",
1600
- "x-codex-affinity-scope",
1601
- "x-codex-model",
1602
- ]) {
1603
- for (const existing of Object.keys(event.headers)) {
1604
- if (existing.toLowerCase() === name) delete event.headers[existing];
1605
- }
1606
- const value = gatewayHeaders.get(name);
1607
- if (value) event.headers[name] = value;
1608
- }
1609
- }
1610
- contextWindows.rewriteHeaders(event.headers, ctx);
1611
- });
1612
- pi.on("message_end", async (event, ctx) => {
1613
- rememberPhysicalModel(ctx, event.message);
1614
- const resolved = contextOperation(dependencies.loadConfig, ctx);
1615
- requireContextPolicy(resolved, ctx);
1616
- const config = resolved.config.compaction;
1617
- const contextModel = await resolveContextCapabilityModel(ctx);
1618
- if (!isCodexContextModel(contextModel, config) || !tools.isRegistered) return undefined;
1619
- if (!(await remoteContextActive(ctx, config, contextModel))) return undefined;
1620
- const message = routeContextNamespaceToolMessage(event.message);
1621
- return message === event.message ? undefined : { message };
1622
- });
1623
- }
1
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
+ import type {
3
+ BeforeProviderRequestEvent,
4
+ ContextWithSystemEvent,
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 { ManagedManualCompact } from "./context-management/manual-compact";
23
+ import {
24
+ registerContextManagementTools,
25
+ type ContextToolRegistrationState,
26
+ } from "./context-management/tools";
27
+ import { writeDebugArtifact, writeReplayFailureArtifact } from "./debug";
28
+ import {
29
+ COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
30
+ COMPACTION_CHECKPOINT_CONTEXT_EDITED,
31
+ COMPACTION_PROJECTION_UNAVAILABLE,
32
+ COMPACTION_SESSION_CONTEXT_UNAVAILABLE,
33
+ COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
34
+ hasVerifiedCompactionInputProvenance,
35
+ type UnprojectedCompactionReason,
36
+ } from "./compaction-projection";
37
+ import { CODEX_GATEWAY_FORWARD_HEADERS } from "./responses-headers";
38
+ import { executeNativeCompaction, type NativeCompactionClientResult } from "./compact-client";
39
+ import { qualifiesForStandaloneCompaction, type CompactionErrorInfo } from "./compaction-errors";
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
+ hasInvalidatedCompactionContext,
51
+ removeNativeCompactionRetainedMessages,
52
+ serializeLiveTailToResponsesInputWithLimits,
53
+ } from "./payload-rewrite";
54
+ import { getCompactionRequestExtras, rememberRequestContext } from "./request-context-cache";
55
+ import { resolveWebSearchRoute } from "./web-search/types";
56
+ import { executeRemoteV2Compaction } from "./remote-v2-client";
57
+ import {
58
+ rememberPhysicalModel,
59
+ resolveEffectiveModel,
60
+ resolveNativeCompactionEnvironment,
61
+ resolveRemoteCompactionExecution,
62
+ isStandaloneCompactionEligible,
63
+ parseModelSpec,
64
+ type RemoteCompactionExecution,
65
+ } from "./runtime";
66
+ import {
67
+ serializeMessagesToCompactRequestWithLimits,
68
+ serializeMessagesToResponsesInputWithLimits,
69
+ type NativeCompactionRequestBody,
70
+ type ResponsesInputItem,
71
+ } from "./serializer";
72
+ import {
73
+ createNativeCompactionDetails,
74
+ createNativeCompactionResult,
75
+ COMPACTION_EXTENSION_ID,
76
+ LEGACY_NATIVE_COMPACTION_STRATEGY,
77
+ REMOTE_V2_COMPACTION_STRATEGY,
78
+ getLatestDeferredToolCarryover,
79
+ getRemoteV2InputProvenance,
80
+ isNativeCompactionDetails,
81
+ type CompactionConfig,
82
+ type NativeCompactionDetails,
83
+ type NativeCompactionRequestMeta,
84
+ } from "./types";
85
+
86
+ type CompactionContextProjection = (
87
+ messages: readonly AgentMessage[],
88
+ ctx: ExtensionContext,
89
+ ) => readonly AgentMessage[] | undefined | Promise<readonly AgentMessage[] | undefined>;
90
+
91
+ function contextOperation(loadConfig: typeof loadToolkitConfig, ctx: ExtensionContext, model = ctx.model): ResolvedToolkitConfig {
92
+ const loaded = loadConfig();
93
+ const resolved = resolveToolkitConfig(loaded, model);
94
+ notifyConfigIssues(ctx, resolved);
95
+ return resolved;
96
+ }
97
+
98
+ function requireContextPolicy(resolved: ResolvedToolkitConfig, ctx: ExtensionContext): void {
99
+ try {
100
+ assertConfigValid(resolved, "context", "compatibility", "diagnostics");
101
+ } catch (error) {
102
+ if (typeof ctx.abort === "function") ctx.abort();
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ function compactGatewayModels(resolved: ResolvedToolkitConfig): readonly string[] {
108
+ return resolved.format === "v2" || resolved.config.compaction.contextManagement === "remote"
109
+ ? resolved.gatewayModelKeys : [];
110
+ }
111
+
112
+ type CompactionDependencies = {
113
+ loadConfig: typeof loadToolkitConfig;
114
+ remoteCompact: typeof executeRemoteV2Compaction;
115
+ standaloneCompact: typeof executeNativeCompaction;
116
+ nativeFallback: typeof runNativeFallbackCompaction;
117
+ contextWindows: CodexContextWindowManager;
118
+ manualCompact?: ManagedManualCompact;
119
+ /** Test seam for the same ordered projection exposed by the Pi host patch. */
120
+ projectCompactionContext?: CompactionContextProjection;
121
+ piContextHookPatch: PiContextHookPatchResult;
122
+ };
123
+
124
+ type RemoteContextActive = (
125
+ ctx: ExtensionContext,
126
+ config: CompactionConfig,
127
+ model?: ExtensionContext["model"],
128
+ ) => Promise<boolean>;
129
+ type ContextToolRegistrationStateReader = () => ContextToolRegistrationState;
130
+
131
+ type ContextToolSyncOutcome = {
132
+ eligible: boolean;
133
+ toolsSynced: boolean;
134
+ registrationState: ContextToolRegistrationState;
135
+ windowInitialized: boolean;
136
+ };
137
+
138
+ type ResponsesCompactOutcome =
139
+ | { outcome: "success"; compaction: CompactionResult<NativeCompactionDetails> }
140
+ | { outcome: "aborted" }
141
+ | { outcome: "failed"; remoteFailure?: CompactionErrorInfo & { reason: string; status?: number }; protocol?: string }
142
+ | {
143
+ outcome: "unprojected-input";
144
+ reason: UnprojectedCompactionReason;
145
+ };
146
+
147
+ function buildCompactionRequestMeta(event: SessionBeforeCompactEvent): NativeCompactionRequestMeta {
148
+ return {
149
+ tokensBefore: event.preparation.tokensBefore,
150
+ previousSummaryPresent: Boolean(event.preparation.previousSummary),
151
+ };
152
+ }
153
+
154
+ function getCurrentModelDebugInfo(ctx: ExtensionContext) {
155
+ return ctx.model
156
+ ? {
157
+ provider: ctx.model.provider,
158
+ id: ctx.model.id,
159
+ }
160
+ : undefined;
161
+ }
162
+
163
+ function getCompactionIdentityDebugInfo(entry: { details?: unknown } | undefined) {
164
+ return isNativeCompactionDetails(entry?.details)
165
+ ? {
166
+ provider: entry.details.provider,
167
+ api: entry.details.api,
168
+ model: entry.details.model,
169
+ baseUrl: entry.details.baseUrl,
170
+ compactionModel: entry.details.compactionModel,
171
+ }
172
+ : undefined;
173
+ }
174
+
175
+ function getSessionId(ctx: ExtensionContext): string | undefined {
176
+ try {
177
+ return ctx.sessionManager.getSessionId();
178
+ } catch {
179
+ return undefined;
180
+ }
181
+ }
182
+
183
+ function notifyWarning(ctx: ExtensionContext, message: string): void {
184
+ if (ctx.hasUI) {
185
+ ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: ${message}`, "warning");
186
+ }
187
+ }
188
+
189
+ async function resolveContextCapabilityModel(
190
+ ctx: ExtensionContext,
191
+ model: ExtensionContext["model"] = ctx.model,
192
+ ): Promise<ExtensionContext["model"]> {
193
+ const effective = await resolveEffectiveModel(ctx, model);
194
+ return effective.ok ? effective.model : undefined;
195
+ }
196
+
197
+ async function isRemoteContextActive(
198
+ ctx: ExtensionContext,
199
+ config: CompactionConfig,
200
+ model: ExtensionContext["model"] = ctx.model,
201
+ ): Promise<boolean> {
202
+ if (!config.enabled || config.contextManagement !== "remote") return false;
203
+ const resolution = await resolveCodexContextProvider(ctx, model, config.gatewayContextModels);
204
+ return resolution.ok;
205
+ }
206
+
207
+ function isCodexContextModel(
208
+ model: ExtensionContext["model"] | undefined,
209
+ config: CompactionConfig,
210
+ ): boolean {
211
+ return config.contextManagement === "remote"
212
+ && (isNativeCodexModel(model) || isCodexGatewayModel(model, config.gatewayContextModels));
213
+ }
214
+
215
+ function isExcludedGatewayContext(
216
+ model: ExtensionContext["model"] | undefined,
217
+ config: CompactionConfig,
218
+ registrationState: ContextToolRegistrationState,
219
+ ): boolean {
220
+ return registrationState === "excluded" && isCodexGatewayModel(model, config.gatewayContextModels);
221
+ }
222
+
223
+ function contextManagementFailureReason(registrationState: ContextToolRegistrationState): string {
224
+ switch (registrationState) {
225
+ case "excluded":
226
+ return "context-tools-excluded";
227
+ case "conflict":
228
+ return "tool-name-conflict";
229
+ case "unverified":
230
+ return "tool-registration-pending";
231
+ case "verified":
232
+ return "context-management-unavailable";
233
+ }
234
+ }
235
+
236
+ function notifyRemoteContextFailure(ctx: ExtensionContext, reason: string): void {
237
+ if (ctx.hasUI) {
238
+ ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: Remote Context management inactive (${reason})`, "warning");
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Surface every window trim that had to be refused to keep the tool loadout intact.
244
+ *
245
+ * The refusal itself is safe: the model keeps the previous turns instead of losing its
246
+ * tools. It is still a continuity incident, so it is recorded once per window and reason
247
+ * instead of being allowed to pass silently.
248
+ */
249
+ function reportProjectionDiagnostics(
250
+ contextWindows: CodexContextWindowManager,
251
+ config: CompactionConfig,
252
+ ctx: ExtensionContext,
253
+ ): void {
254
+ const diagnostics = contextWindows.takeProjectionDiagnostics();
255
+ if (diagnostics.length === 0) return;
256
+ writeDebugArtifact(
257
+ "compaction-event",
258
+ { event: "context.projection.tool_loadout_shrink", diagnostics },
259
+ config,
260
+ ctx,
261
+ );
262
+ if (ctx.hasUI) {
263
+ ctx.ui.notify(
264
+ `${COMPACTION_EXTENSION_ID}: kept previous context to preserve tools (${diagnostics.join("; ")})`,
265
+ "warning",
266
+ );
267
+ }
268
+ }
269
+
270
+ function cloneOpaqueWindow(window: readonly unknown[]): unknown[] {
271
+ return window.map((item) => structuredClone(item));
272
+ }
273
+
274
+ function buildCompactionInstructions(systemPrompt: string, customInstructions?: string): string {
275
+ const guidance = customInstructions?.trim();
276
+ if (!guidance) {
277
+ return systemPrompt;
278
+ }
279
+
280
+ return `${systemPrompt}\n\nAdditional user guidance for this manual /compact request:\n${guidance}`;
281
+ }
282
+
283
+ function findLatestProjectedCompactionSummaryIndex(
284
+ messages: readonly AgentMessage[],
285
+ summary: string,
286
+ ): number {
287
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
288
+ const message = messages[index];
289
+ if (message?.role === "compactionSummary" && message.summary === summary) {
290
+ return index;
291
+ }
292
+ }
293
+
294
+ return -1;
295
+ }
296
+
297
+ function readSessionContextMessages(ctx: ExtensionContext): AgentMessage[] | undefined {
298
+ try {
299
+ const projection = ctx.sessionManager.buildSessionProjection();
300
+ if (!projection || !Array.isArray(projection.messages)) return undefined;
301
+ return structuredClone(projection.messages);
302
+ } catch {
303
+ return undefined;
304
+ }
305
+ }
306
+
307
+ type CompactionProjectionResult =
308
+ | { ok: true; messages: readonly AgentMessage[] }
309
+ | { ok: false; reason: typeof COMPACTION_PROJECTION_UNAVAILABLE | typeof COMPACTION_SESSION_CONTEXT_UNAVAILABLE };
310
+
311
+ async function projectSessionContextForCompaction(
312
+ ctx: ExtensionContext,
313
+ projectCompactionContext: CompactionContextProjection | undefined,
314
+ ): Promise<CompactionProjectionResult> {
315
+ const messages = readSessionContextMessages(ctx);
316
+ if (!messages) {
317
+ return { ok: false, reason: COMPACTION_SESSION_CONTEXT_UNAVAILABLE };
318
+ }
319
+
320
+ const projector = projectCompactionContext ?? getPiContextHookProjector(ctx);
321
+ if (!projector) {
322
+ return { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
323
+ }
324
+
325
+ try {
326
+ const projected = await projector(messages, ctx);
327
+ return Array.isArray(projected)
328
+ ? { ok: true, messages: projected }
329
+ : { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
330
+ } catch {
331
+ return { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
332
+ }
333
+ }
334
+
335
+ function getLegacySessionContextMessages(
336
+ event: SessionBeforeCompactEvent,
337
+ ctx: ExtensionContext,
338
+ ): AgentMessage[] {
339
+ return readSessionContextMessages(ctx) ?? [
340
+ ...event.preparation.messagesToSummarize,
341
+ ...event.preparation.turnPrefixMessages,
342
+ ];
343
+ }
344
+
345
+ async function runResponsesNativeCompact(
346
+ event: SessionBeforeCompactEvent,
347
+ ctx: ExtensionContext,
348
+ config: CompactionConfig,
349
+ execution: RemoteCompactionExecution,
350
+ remoteCompact: typeof executeRemoteV2Compaction,
351
+ projectCompactionContext: CompactionContextProjection | undefined,
352
+ /** Only ordinary compaction supplies this; managed recovery remains V2-only. */
353
+ standaloneCompact?: typeof executeNativeCompaction,
354
+ ): Promise<ResponsesCompactOutcome> {
355
+ const { consumer, compactor } = execution;
356
+ const instructions = buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions);
357
+ const branchEntries = event.branchEntries ?? ctx.sessionManager.getBranch();
358
+ const deferredToolCarryover = getLatestDeferredToolCarryover(branchEntries);
359
+ const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
360
+ baseUrl: consumer.baseUrl,
361
+ });
362
+
363
+ const inputProvenance = getRemoteV2InputProvenance(config.remoteV2ContextSource);
364
+ let requestSource: "session-context" | "non-native-session-context" | "latest-native-replay";
365
+ let request: NativeCompactionRequestBody;
366
+ if (latestNativeCompaction.ok) {
367
+ if (hasInvalidatedCompactionContext(branchEntries, latestNativeCompaction.entry.id)) {
368
+ return { outcome: "unprojected-input", reason: COMPACTION_CHECKPOINT_CONTEXT_EDITED };
369
+ }
370
+ const details = latestNativeCompaction.entry.details;
371
+ if (!details) {
372
+ return { outcome: "failed" };
373
+ }
374
+ if (!hasVerifiedCompactionInputProvenance(details, inputProvenance)) {
375
+ return {
376
+ outcome: "unprojected-input",
377
+ reason: COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
378
+ };
379
+ }
380
+ requestSource = "latest-native-replay";
381
+
382
+ if (config.remoteV2ContextSource === "pi-context-hook") {
383
+ const projection = await projectSessionContextForCompaction(ctx, projectCompactionContext);
384
+ if (!projection.ok) {
385
+ return { outcome: "unprojected-input", reason: projection.reason };
386
+ }
387
+ const summaryIndex = findLatestProjectedCompactionSummaryIndex(
388
+ projection.messages,
389
+ latestNativeCompaction.entry.summary,
390
+ );
391
+ if (summaryIndex < 0) {
392
+ return {
393
+ outcome: "unprojected-input",
394
+ reason: COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
395
+ };
396
+ }
397
+ const input: ResponsesInputItem[] = [
398
+ ...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
399
+ ...(await serializeMessagesToResponsesInputWithLimits(
400
+ compactor.currentModel,
401
+ [...projection.messages.slice(summaryIndex + 1)],
402
+ { firstSystemMessageIsUpdate: true },
403
+ )),
404
+ ];
405
+ request = {
406
+ model: compactor.requestModel ?? compactor.model,
407
+ input,
408
+ instructions,
409
+ };
410
+ } else {
411
+ const liveTailEntries = branchEntries.slice(latestNativeCompaction.index + 1);
412
+ const input: ResponsesInputItem[] = [
413
+ ...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
414
+ ...(await serializeLiveTailToResponsesInputWithLimits({ model: compactor.currentModel, entries: liveTailEntries })),
415
+ ];
416
+ request = {
417
+ model: compactor.requestModel ?? compactor.model,
418
+ input,
419
+ instructions,
420
+ };
421
+ }
422
+ } else if (
423
+ latestNativeCompaction.reason === "no-compaction" ||
424
+ (latestNativeCompaction.reason === "latest-compaction-not-native" &&
425
+ config.allowCompactionContinuityBreak)
426
+ ) {
427
+ requestSource =
428
+ latestNativeCompaction.reason === "no-compaction" ? "session-context" : "non-native-session-context";
429
+ if (config.remoteV2ContextSource === "pi-context-hook") {
430
+ const projection = await projectSessionContextForCompaction(ctx, projectCompactionContext);
431
+ if (!projection.ok) {
432
+ return { outcome: "unprojected-input", reason: projection.reason };
433
+ }
434
+ const serialized = await serializeMessagesToCompactRequestWithLimits({
435
+ model: compactor.currentModel,
436
+ messages: [...projection.messages],
437
+ instructions,
438
+ });
439
+ request = { ...serialized, model: compactor.requestModel ?? serialized.model };
440
+ } else {
441
+ const serialized = await serializeMessagesToCompactRequestWithLimits({
442
+ model: compactor.currentModel,
443
+ messages: getLegacySessionContextMessages(event, ctx),
444
+ instructions,
445
+ });
446
+ request = { ...serialized, model: compactor.requestModel ?? serialized.model };
447
+ }
448
+ } else {
449
+ writeDebugArtifact(
450
+ "compaction-event",
451
+ {
452
+ event: "session_before_compact.remote-v2-skip",
453
+ reason: latestNativeCompaction.reason,
454
+ consumer: {
455
+ provider: consumer.provider,
456
+ api: consumer.api,
457
+ model: consumer.model,
458
+ baseUrl: consumer.baseUrl,
459
+ },
460
+ compactor: {
461
+ provider: compactor.provider,
462
+ api: compactor.api,
463
+ model: compactor.model,
464
+ baseUrl: compactor.baseUrl,
465
+ },
466
+ latestCompactionIndex: latestNativeCompaction.latestCompactionIndex,
467
+ latestCompactionIdentity: getCompactionIdentityDebugInfo(latestNativeCompaction.latestCompaction),
468
+ },
469
+ config,
470
+ ctx,
471
+ );
472
+ return { outcome: "failed" };
473
+ }
474
+
475
+ // Mirror the latest codex_rs CompactionInput fields captured from the most
476
+ // recent live provider request for this model (tools, reasoning, etc.).
477
+ const extras = getCompactionRequestExtras({
478
+ provider: consumer.provider,
479
+ api: consumer.api,
480
+ model: consumer.requestModel ?? consumer.model,
481
+ baseUrl: consumer.baseUrl,
482
+ sessionId: getSessionId(ctx),
483
+ }, compactor.currentModel);
484
+ if (extras) {
485
+ request = { ...request, ...extras };
486
+ }
487
+
488
+ if (event.signal?.aborted) return { outcome: "aborted" };
489
+ let protocol: "remote-v2" | "standalone-compact" = "remote-v2";
490
+ let compactResult: Awaited<ReturnType<typeof executeRemoteV2Compaction>> | NativeCompactionClientResult = await remoteCompact({
491
+ runtime: compactor,
492
+ request,
493
+ signal: event.signal,
494
+ settings: config,
495
+ context: ctx,
496
+ });
497
+
498
+ if (!compactResult.ok) {
499
+ if (compactResult.reason === "aborted" || event.signal?.aborted) return { outcome: "aborted" };
500
+ const tryStandalone = !!standaloneCompact && isStandaloneCompactionEligible(compactor) && qualifiesForStandaloneCompaction(compactResult);
501
+ writeDebugArtifact("compaction-event", {
502
+ event: "session_before_compact.remote-v2-failure",
503
+ protocol,
504
+ reason: compactResult.reason,
505
+ status: compactResult.status,
506
+ errorCode: compactResult.errorCode,
507
+ errorType: compactResult.errorType,
508
+ errorParam: compactResult.errorParam,
509
+ errorMessage: compactResult.errorMessage,
510
+ nextStep: tryStandalone ? "standalone-compact" : "native-summary",
511
+ }, config, ctx);
512
+ if (tryStandalone && standaloneCompact) {
513
+ if (event.signal?.aborted) return { outcome: "aborted" };
514
+ protocol = "standalone-compact";
515
+ compactResult = await standaloneCompact({ runtime: compactor, request, signal: event.signal, settings: config, context: ctx });
516
+ }
517
+ if (!compactResult.ok) {
518
+ if (compactResult.reason === "aborted" || event.signal?.aborted) return { outcome: "aborted" };
519
+ const remoteFailure = {
520
+ reason: compactResult.reason,
521
+ status: compactResult.status,
522
+ errorCode: compactResult.errorCode,
523
+ errorType: compactResult.errorType,
524
+ errorParam: compactResult.errorParam,
525
+ errorMessage: compactResult.errorMessage,
526
+ };
527
+ if (protocol === "standalone-compact") writeDebugArtifact("compaction-event", {
528
+ event: "session_before_compact.standalone-compact-failure", protocol, ...remoteFailure, nextStep: "native-summary",
529
+ }, config, ctx);
530
+ return { outcome: "failed", remoteFailure, protocol };
531
+ }
532
+ }
533
+
534
+ let details: NativeCompactionDetails;
535
+ try {
536
+ details = createNativeCompactionDetails({
537
+ strategy: protocol === "standalone-compact" ? LEGACY_NATIVE_COMPACTION_STRATEGY : REMOTE_V2_COMPACTION_STRATEGY,
538
+ provider: consumer.provider,
539
+ api: consumer.api,
540
+ model: consumer.model,
541
+ baseUrl: consumer.baseUrl,
542
+ compactionModel: {
543
+ provider: compactor.provider,
544
+ api: compactor.api,
545
+ model: compactor.model,
546
+ baseUrl: compactor.baseUrl,
547
+ },
548
+ deferredToolCarryover,
549
+ compactedWindow: compactResult.compactedWindow,
550
+ compactResponseId: compactResult.compactResponseId,
551
+ createdAt: compactResult.createdAt,
552
+ requestMeta: buildCompactionRequestMeta(event),
553
+ inputProvenance,
554
+ });
555
+ } catch (error) {
556
+ writeDebugArtifact(
557
+ "compaction-event",
558
+ {
559
+ event: "session_before_compact.invalid-native-details",
560
+ reason: error instanceof Error ? error.message : String(error),
561
+ consumer: {
562
+ provider: consumer.provider,
563
+ api: consumer.api,
564
+ model: consumer.model,
565
+ baseUrl: consumer.baseUrl,
566
+ },
567
+ compactor: {
568
+ provider: compactor.provider,
569
+ api: compactor.api,
570
+ model: compactor.model,
571
+ baseUrl: compactor.baseUrl,
572
+ },
573
+ },
574
+ config,
575
+ ctx,
576
+ );
577
+ return { outcome: "failed", remoteFailure: { reason: "invalid-native-details" }, protocol };
578
+ }
579
+
580
+ const compaction = createNativeCompactionResult({
581
+ firstKeptEntryId: event.preparation.firstKeptEntryId,
582
+ tokensBefore: event.preparation.tokensBefore,
583
+ details,
584
+ });
585
+
586
+ writeDebugArtifact(
587
+ "compaction-event",
588
+ {
589
+ event: `session_before_compact.${protocol}-success`,
590
+ protocol,
591
+ consumer: {
592
+ provider: consumer.provider,
593
+ api: consumer.api,
594
+ model: consumer.model,
595
+ baseUrl: consumer.baseUrl,
596
+ },
597
+ compactor: {
598
+ provider: compactor.provider,
599
+ api: compactor.api,
600
+ model: compactor.model,
601
+ baseUrl: compactor.baseUrl,
602
+ },
603
+ requestSource,
604
+ remoteV2ContextSource: config.remoteV2ContextSource,
605
+ inputProvenance,
606
+ requestInputItems: request.input.length,
607
+ requestExtras: extras ? Object.keys(extras) : [],
608
+ compactResponseId: compactResult.compactResponseId,
609
+ compactedItems: compactResult.compactedWindow.length,
610
+ firstKeptEntryId: event.preparation.firstKeptEntryId,
611
+ },
612
+ config,
613
+ ctx,
614
+ );
615
+
616
+ return { outcome: "success", compaction };
617
+ }
618
+
619
+ type ContextUnavailableCompactionResult =
620
+ | { compaction: CompactionResult<unknown> }
621
+ | { cancel: true };
622
+
623
+ async function runUnavailableContextCompaction(
624
+ event: SessionBeforeCompactEvent,
625
+ ctx: ExtensionContext,
626
+ dependencies: CompactionDependencies,
627
+ resolved: ResolvedToolkitConfig,
628
+ config: CompactionConfig,
629
+ registrationState: ContextToolRegistrationState,
630
+ ): Promise<ContextUnavailableCompactionResult> {
631
+ const contextReason = contextManagementFailureReason(registrationState);
632
+ const remoteModelSpec = config.remoteCompactModel?.trim();
633
+ let remoteFailure: string | undefined;
634
+
635
+ if (remoteModelSpec) {
636
+ const resolution = await resolveRemoteCompactionExecution(
637
+ ctx,
638
+ {
639
+ enabled: config.enabled,
640
+ responsesApis: config.responsesApis,
641
+ codexGatewayModels: compactGatewayModels(resolved),
642
+ },
643
+ remoteModelSpec,
644
+ );
645
+ if (resolution.ok) {
646
+ const outcome = await runResponsesNativeCompact(
647
+ event,
648
+ ctx,
649
+ config,
650
+ resolution.execution,
651
+ dependencies.remoteCompact,
652
+ dependencies.projectCompactionContext,
653
+ );
654
+ if (outcome.outcome === "success") {
655
+ writeDebugArtifact(
656
+ "compaction-event",
657
+ {
658
+ event: "session_before_compact.context-fallback-success",
659
+ contextReason,
660
+ registrationState,
661
+ compactor: remoteModelSpec,
662
+ },
663
+ config,
664
+ ctx,
665
+ );
666
+ return { compaction: outcome.compaction };
667
+ }
668
+ if (outcome.outcome === "aborted" || outcome.outcome === "unprojected-input") {
669
+ return { cancel: true };
670
+ }
671
+ remoteFailure = "remote-v2-failed";
672
+ } else {
673
+ remoteFailure = resolution.reason;
674
+ writeDebugArtifact(
675
+ "compaction-event",
676
+ {
677
+ event: "session_before_compact.context-fallback-unavailable",
678
+ contextReason,
679
+ registrationState,
680
+ reason: resolution.reason,
681
+ modelSpec: remoteModelSpec,
682
+ errorMessage: resolution.errorMessage,
683
+ },
684
+ config,
685
+ ctx,
686
+ );
687
+ }
688
+ } else {
689
+ remoteFailure = "missing-remote-compaction-model";
690
+ }
691
+
692
+ const nativeModelSpec = config.nativeFallback.model?.trim();
693
+ if (config.nativeFallback.enabled && nativeModelSpec) {
694
+ const fallback = await dependencies.nativeFallback({
695
+ ctx,
696
+ event,
697
+ config,
698
+ modelSpec: nativeModelSpec,
699
+ });
700
+ if (fallback.ok) {
701
+ writeDebugArtifact(
702
+ "compaction-event",
703
+ {
704
+ event: "session_before_compact.context-fallback-native-success",
705
+ contextReason,
706
+ registrationState,
707
+ remoteFailure,
708
+ fallbackModel: nativeModelSpec,
709
+ },
710
+ config,
711
+ ctx,
712
+ );
713
+ return { compaction: fallback.result };
714
+ }
715
+ writeDebugArtifact(
716
+ "compaction-event",
717
+ {
718
+ event: "session_before_compact.context-fallback-native-failure",
719
+ contextReason,
720
+ registrationState,
721
+ remoteFailure,
722
+ fallbackReason: fallback.reason,
723
+ fallbackModel: nativeModelSpec,
724
+ errorMessage: fallback.errorMessage,
725
+ },
726
+ config,
727
+ ctx,
728
+ );
729
+ } else {
730
+ writeDebugArtifact(
731
+ "compaction-event",
732
+ {
733
+ event: "session_before_compact.context-fallback-cancelled",
734
+ contextReason,
735
+ registrationState,
736
+ remoteFailure,
737
+ reason: config.nativeFallback.enabled ? "missing-native-fallback-model" : "native-fallback-disabled",
738
+ },
739
+ config,
740
+ ctx,
741
+ );
742
+ }
743
+
744
+ return { cancel: true };
745
+ }
746
+
747
+ async function handleSessionBeforeCompact(
748
+ event: SessionBeforeCompactEvent,
749
+ ctx: ExtensionContext,
750
+ dependencies: CompactionDependencies,
751
+ remoteContextActive: RemoteContextActive,
752
+ getContextToolRegistrationState: ContextToolRegistrationStateReader,
753
+ ) {
754
+ const resolved = contextOperation(dependencies.loadConfig, ctx);
755
+ if (resolved.invalidFeatures.some((feature) => ["context", "compatibility", "diagnostics"].includes(feature))) return { cancel: true };
756
+ const config = resolved.config.compaction;
757
+ if (!config.enabled) {
758
+ return undefined;
759
+ }
760
+
761
+ writeDebugArtifact(
762
+ "compaction-event",
763
+ {
764
+ event: "session_before_compact",
765
+ customInstructions: event.customInstructions,
766
+ preparation: {
767
+ tokensBefore: event.preparation.tokensBefore,
768
+ firstKeptEntryId: event.preparation.firstKeptEntryId,
769
+ previousSummaryPresent: Boolean(event.preparation.previousSummary),
770
+ messagesToSummarizeCount: event.preparation.messagesToSummarize.length,
771
+ turnPrefixMessagesCount: event.preparation.turnPrefixMessages.length,
772
+ },
773
+ },
774
+ config,
775
+ ctx,
776
+ );
777
+
778
+ if (event.signal.aborted) {
779
+ return { cancel: true };
780
+ }
781
+
782
+ // Capability changes do not retire the persisted Remote session. Resolve its
783
+ // branch boundary before deciding whether any compactor may see this input.
784
+ if (config.contextManagement === "remote") {
785
+ try {
786
+ dependencies.contextWindows.synchronize(ctx);
787
+ } catch {
788
+ notifyRemoteContextFailure(ctx, "malformed-window-state");
789
+ return { cancel: true };
790
+ }
791
+ }
792
+
793
+ // Active and inactive covered Codex consumers retain their existing routing.
794
+ const contextModel = await resolveContextCapabilityModel(ctx);
795
+ if (isCodexContextModel(contextModel, config)) {
796
+ if (await remoteContextActive(ctx, config, contextModel)) {
797
+ try {
798
+ if (event.reason === "manual" && dependencies.manualCompact && !dependencies.manualCompact.isMaintenance) {
799
+ await dependencies.manualCompact.request(event, ctx, resolved);
800
+ return { cancel: true };
801
+ }
802
+ return dependencies.contextWindows.prepareCompaction(event);
803
+ } catch {
804
+ notifyRemoteContextFailure(ctx, "malformed-window-state");
805
+ return { cancel: true };
806
+ }
807
+ }
808
+
809
+ const registrationState = getContextToolRegistrationState();
810
+ const failureReason = contextManagementFailureReason(registrationState);
811
+ if (failureReason !== "context-tools-excluded") {
812
+ notifyRemoteContextFailure(ctx, failureReason);
813
+ }
814
+ return runUnavailableContextCompaction(
815
+ event,
816
+ ctx,
817
+ dependencies,
818
+ resolved,
819
+ config,
820
+ registrationState,
821
+ );
822
+ }
823
+
824
+ if (config.contextManagement === "remote") {
825
+ try {
826
+ const detached = dependencies.contextWindows.prepareDetachedCompaction(event);
827
+ // A local consumer cannot summarize history owned by a Remote window,
828
+ // even through a configured Luna producer or Pi's default compactor.
829
+ if (detached) return detached;
830
+ } catch {
831
+ notifyRemoteContextFailure(ctx, "malformed-window-state");
832
+ return { cancel: true };
833
+ }
834
+ }
835
+
836
+ // Resolve producer protocol policy from this operation's document, not a later disk read.
837
+ if (resolved.format === "v2" && config.remoteCompactModel) {
838
+ const producer = parseModelSpec(config.remoteCompactModel);
839
+ if (producer) {
840
+ const producerPolicy = resolveToolkitConfig(resolved.snapshot, { provider: producer.provider, id: producer.modelId });
841
+ notifyConfigIssues(ctx, producerPolicy);
842
+ if (producerPolicy.invalidFeatures.includes("compatibility")) return { cancel: true };
843
+ }
844
+ }
845
+ // Branch 1: Responses-family APIs use remote_compaction_v2 on the normal Responses stream.
846
+ let remoteAttempted = false;
847
+ const resolution = await resolveRemoteCompactionExecution(
848
+ ctx,
849
+ {
850
+ enabled: config.enabled,
851
+ responsesApis: config.responsesApis,
852
+ codexGatewayModels: compactGatewayModels(resolved),
853
+ },
854
+ config.remoteCompactModel,
855
+ );
856
+ if (resolution.ok) {
857
+ remoteAttempted = true;
858
+ const responsesOutcome = await runResponsesNativeCompact(
859
+ event,
860
+ ctx,
861
+ config,
862
+ resolution.execution,
863
+ dependencies.remoteCompact,
864
+ dependencies.projectCompactionContext,
865
+ dependencies.standaloneCompact,
866
+ );
867
+ if (responsesOutcome.outcome === "success") {
868
+ return { compaction: responsesOutcome.compaction };
869
+ }
870
+ if (responsesOutcome.outcome === "aborted") {
871
+ return { cancel: true };
872
+ }
873
+ if (responsesOutcome.outcome === "unprojected-input") {
874
+ const message = responsesOutcome.reason === COMPACTION_CHECKPOINT_CONTEXT_EDITED
875
+ ? "A context edit changed history sealed in the opaque checkpoint. Start a new session or navigate before that checkpoint; its encrypted content cannot be selectively rewritten."
876
+ : responsesOutcome.reason === COMPACTION_PROJECTION_UNAVAILABLE
877
+ ? "Pi's ordered context-hook projection is unavailable; Remote V2 was not sent raw session history."
878
+ : responsesOutcome.reason === COMPACTION_SESSION_CONTEXT_UNAVAILABLE
879
+ ? "Pi did not provide the current session context required by its context-hook projection."
880
+ : responsesOutcome.reason === COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE
881
+ ? "Pi's projected context did not include the current compaction summary anchor."
882
+ : "The latest opaque checkpoint has no marker for the configured Remote V2 context source, or was created in the other mode.";
883
+ writeDebugArtifact(
884
+ "compaction-event",
885
+ {
886
+ event: "session_before_compact.remote-v2-unprojected-input",
887
+ reason: responsesOutcome.reason,
888
+ contextSource: config.remoteV2ContextSource,
889
+ inputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
890
+ piContextHookPatch: dependencies.piContextHookPatch,
891
+ message,
892
+ },
893
+ config,
894
+ ctx,
895
+ );
896
+ notifyWarning(
897
+ ctx,
898
+ `Remote V2 compaction cancelled (${responsesOutcome.reason}); ${message} The existing checkpoint and session history were left unchanged.`,
899
+ );
900
+ return { cancel: true };
901
+ }
902
+ // Only an actual exhausted remote attempt warns, never an input/routing skip.
903
+ if (event.signal?.aborted) return { cancel: true };
904
+ if (responsesOutcome.remoteFailure) {
905
+ const failure = responsesOutcome.remoteFailure;
906
+ writeDebugArtifact("compaction-event", {
907
+ event: "session_before_compact.remote-to-native-summary",
908
+ protocol: responsesOutcome.protocol,
909
+ ...failure,
910
+ nextStep: "native-summary",
911
+ }, config, ctx);
912
+ notifyWarning(ctx, `Remote compaction failed (${failure.reason}${failure.status ? `, HTTP ${failure.status}` : ""}${failure.errorCode ? `, ${failure.errorCode}` : ""}); continuing with a native text summary. No encrypted remote checkpoint was produced.${failure.errorMessage ? ` ${failure.errorMessage}` : ""}`);
913
+ }
914
+ } else {
915
+ writeDebugArtifact(
916
+ "compaction-event",
917
+ {
918
+ event: "session_before_compact.remote-v2-unavailable",
919
+ reason: resolution.reason,
920
+ provider: resolution.provider,
921
+ api: resolution.api,
922
+ model: resolution.model,
923
+ baseUrl: resolution.baseUrl,
924
+ modelSpec: resolution.modelSpec,
925
+ errorMessage: resolution.errorMessage,
926
+ },
927
+ config,
928
+ ctx,
929
+ );
930
+ const expectedConsumerCapabilityMiss =
931
+ resolution.reason === "unsupported-api" && resolution.modelSpec === undefined;
932
+ if (config.remoteCompactModel && !expectedConsumerCapabilityMiss) {
933
+ notifyWarning(
934
+ ctx,
935
+ `remote compaction model "${config.remoteCompactModel}" unusable (${resolution.reason}); using the native fallback chain`,
936
+ );
937
+ }
938
+ }
939
+
940
+ // Branch 2: run pi's native compaction method. A failed remote request is compacted by the
941
+ // remote producer itself; a model that cannot use remote v2 at all uses nativeFallback.model.
942
+ const fallbackModelSpec = remoteAttempted ? config.remoteCompactModel : config.nativeFallback.model;
943
+ const fallback = await dependencies.nativeFallback({ ctx, event, config, modelSpec: fallbackModelSpec });
944
+ if (fallback.ok) {
945
+ if (ctx.hasUI) {
946
+ ctx.ui.notify(
947
+ `${COMPACTION_EXTENSION_ID}: compacted with ${fallback.model.provider}/${fallback.model.id} (native method)`,
948
+ "info",
949
+ );
950
+ }
951
+ writeDebugArtifact(
952
+ "compaction-event",
953
+ {
954
+ event: "session_before_compact.fallback-success",
955
+ model: fallback.model,
956
+ },
957
+ config,
958
+ ctx,
959
+ );
960
+ return { compaction: fallback.result };
961
+ }
962
+
963
+ if (fallback.reason === "aborted") {
964
+ return { cancel: true };
965
+ }
966
+
967
+ writeDebugArtifact(
968
+ "compaction-event",
969
+ {
970
+ event: "session_before_compact.fallback-skip",
971
+ reason: fallback.reason,
972
+ modelSpec: fallback.modelSpec,
973
+ errorMessage: fallback.errorMessage,
974
+ },
975
+ config,
976
+ ctx,
977
+ );
978
+
979
+ // Intentional pi-default paths: feature disabled, nothing configured, or it matches the current one.
980
+ const intentionalSkip =
981
+ fallback.reason === "disabled" ||
982
+ fallback.reason === "no-model-configured" ||
983
+ fallback.reason === "same-as-current-model";
984
+ if (!intentionalSkip) {
985
+ // An explicit summary model is authoritative: the active model may be
986
+ // unable to summarize at all. Preserve context on failure, never reroute.
987
+ notifyWarning(
988
+ ctx,
989
+ `compaction model "${fallback.modelSpec ?? fallbackModelSpec}" failed (${fallback.reason}${fallback.errorMessage ? `: ${fallback.errorMessage}` : ""}); compaction cancelled, context preserved; not switching to the current model`,
990
+ );
991
+ return { cancel: true };
992
+ }
993
+
994
+ // Branch 3: pi's default native compaction with the current model.
995
+ return undefined;
996
+ }
997
+
998
+ function rejectInheritedCheckpointReplay(
999
+ ctx: ExtensionContext,
1000
+ config: CompactionConfig,
1001
+ compactionEntryId: string,
1002
+ reason: string,
1003
+ ): undefined {
1004
+ writeReplayFailureArtifact({ reason, compactionEntryId }, config, ctx);
1005
+ reportPiContextHookFailure(ctx, `replay-failed:${reason}`);
1006
+ if (ctx.hasUI) ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: inherited checkpoint replay failed (${reason}); request aborted`, "error");
1007
+ ctx.abort();
1008
+ return undefined;
1009
+ }
1010
+
1011
+ async function handleContextInternal(
1012
+ event: ContextWithSystemEvent,
1013
+ ctx: ExtensionContext,
1014
+ pi: ExtensionAPI,
1015
+ loadConfig: typeof loadToolkitConfig,
1016
+ contextWindows: CodexContextWindowManager,
1017
+ remoteContextActive: RemoteContextActive,
1018
+ getContextToolRegistrationState: ContextToolRegistrationStateReader,
1019
+ ) {
1020
+ const resolved = contextOperation(loadConfig, ctx);
1021
+ requireContextPolicy(resolved, ctx);
1022
+ const config = resolved.config.compaction;
1023
+ if (!config.enabled) {
1024
+ const visibleMessages = contextWindows.project(event.messages, "off");
1025
+ return visibleMessages.length === event.messages.length && visibleMessages.every((message, index) => message === event.messages[index])
1026
+ ? undefined
1027
+ : { messages: visibleMessages };
1028
+ }
1029
+
1030
+ let localMessages: AgentMessage[] | undefined;
1031
+ let remoteReplayProjection = false;
1032
+ if (config.contextManagement === "remote") {
1033
+ const contextModel = await resolveContextCapabilityModel(ctx);
1034
+ try {
1035
+ contextWindows.synchronize(ctx);
1036
+ } catch {
1037
+ notifyRemoteContextFailure(ctx, "malformed-window-state");
1038
+ reportPiContextHookFailure(ctx, "malformed-window-state");
1039
+ ctx.abort();
1040
+ return undefined;
1041
+ }
1042
+
1043
+ const remoteActive = await remoteContextActive(ctx, config, contextModel);
1044
+ if (remoteActive) {
1045
+ try {
1046
+ // Measure the provider-visible current window, not Pi's durable transcript.
1047
+ // The latter still contains retired remote windows and can be inflated by
1048
+ // an upstream error even though this request only carries the live window.
1049
+ let projected = contextWindows.project(event.messages, "remote", false);
1050
+ // Projection may observe the exact queued rollover before Pi persists
1051
+ // it. Decide replay ownership from this request, not the old branch.
1052
+ const ownsWindow = contextWindows.hasEffectiveWindow() || !contextWindows.currentIdentity();
1053
+ if (ownsWindow) projected = contextWindows.project(projected, "remote");
1054
+ contextWindows.recordBudget(
1055
+ pi,
1056
+ ctx,
1057
+ true,
1058
+ config.contextReminderThresholdPercent,
1059
+ );
1060
+ reportProjectionDiagnostics(contextWindows, config, ctx);
1061
+ if (ownsWindow) {
1062
+ return projected.length === event.messages.length && projected.every((message, index) => message === event.messages[index])
1063
+ ? undefined
1064
+ : { messages: projected };
1065
+ }
1066
+ localMessages = projected;
1067
+ remoteReplayProjection = true;
1068
+ } catch (error) {
1069
+ notifyRemoteContextFailure(ctx, "malformed-window-state");
1070
+ reportPiContextHookFailure(ctx, "malformed-window-state");
1071
+ ctx.abort();
1072
+ return undefined;
1073
+ }
1074
+ }
1075
+
1076
+ // An unavailable covered runtime must not silently become an ordinary
1077
+ // replay consumer. Explicit host exclusion remains the one intentional
1078
+ // downgrade; active Remote can replay a newer inherited checkpoint.
1079
+ if (!remoteActive && isCodexContextModel(contextModel, config)) {
1080
+ const registrationState = getContextToolRegistrationState();
1081
+ if (isExcludedGatewayContext(contextModel, config, registrationState)) {
1082
+ const visibleMessages = contextWindows.project(event.messages, "off");
1083
+ return visibleMessages.length === event.messages.length && visibleMessages.every((message, index) => message === event.messages[index])
1084
+ ? undefined
1085
+ : { messages: visibleMessages };
1086
+ }
1087
+ if (isCodexGatewayModel(contextModel, config.gatewayContextModels)) {
1088
+ const reason = contextManagementFailureReason(registrationState);
1089
+ notifyRemoteContextFailure(ctx, reason);
1090
+ reportPiContextHookFailure(ctx, reason);
1091
+ ctx.abort();
1092
+ return undefined;
1093
+ }
1094
+ const visibleMessages = contextWindows.project(event.messages, "off");
1095
+ return visibleMessages.length === event.messages.length && visibleMessages.every((message, index) => message === event.messages[index])
1096
+ ? undefined
1097
+ : { messages: visibleMessages };
1098
+ }
1099
+
1100
+ if (!remoteActive) {
1101
+ try {
1102
+ const projected = contextWindows.project(event.messages, "local");
1103
+ localMessages = projected;
1104
+ if (contextWindows.hasEffectiveWindow()) {
1105
+ // This boundary owns earlier history, including any older Remote V2
1106
+ // checkpoint. A local request must not replay that history back in.
1107
+ return projected.length === event.messages.length && projected.every((message, index) => message === event.messages[index])
1108
+ ? undefined
1109
+ : { messages: projected };
1110
+ }
1111
+ } catch (error) {
1112
+ notifyRemoteContextFailure(ctx, "malformed-window-state");
1113
+ reportPiContextHookFailure(ctx, error instanceof Error ? error.message : String(error));
1114
+ ctx.abort();
1115
+ return undefined;
1116
+ }
1117
+ }
1118
+ }
1119
+
1120
+ // Without an effective Remote boundary, the latest checkpoint owns replay.
1121
+ // This also covers a newer checkpoint superseding a historical window.
1122
+ const visibleMessages = localMessages ?? contextWindows.project(event.messages, "off");
1123
+ const finishProjection = (messages: AgentMessage[]) => {
1124
+ // Compare canonical retained copies before Remote encrypted-output encoding.
1125
+ const projected = remoteReplayProjection ? contextWindows.project(messages, "remote") : messages;
1126
+ return projected.length === event.messages.length && projected.every((message, index) => message === event.messages[index])
1127
+ ? undefined : { messages: projected };
1128
+ };
1129
+ const replayEvent = visibleMessages === event.messages ? event : { ...event, messages: visibleMessages };
1130
+ const branchEntries = ctx.sessionManager.getBranch();
1131
+ const inherited = config.contextManagement === "remote" && contextWindows.hasSupersededWindow()
1132
+ ? resolveLatestNativeCompactionEntry(branchEntries) : undefined;
1133
+ const inheritedCheckpoint = inherited?.ok ? inherited.entry : undefined;
1134
+ const resolution = await resolveNativeCompactionEnvironment(ctx, {
1135
+ enabled: config.enabled,
1136
+ // Disabling checkpoint generation cannot turn an inherited opaque
1137
+ // checkpoint into a usable text summary. Replay checks transport support.
1138
+ responsesApis: inheritedCheckpoint ? undefined : config.responsesApis,
1139
+ codexGatewayModels: compactGatewayModels(resolved),
1140
+ });
1141
+ if (!resolution.ok) {
1142
+ return inheritedCheckpoint
1143
+ ? rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, resolution.reason)
1144
+ : finishProjection(visibleMessages);
1145
+ }
1146
+ const latest = resolveLatestNativeCompactionEntry(branchEntries, { baseUrl: resolution.runtime.baseUrl });
1147
+ if (!latest.ok) {
1148
+ return inheritedCheckpoint
1149
+ ? rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, latest.reason)
1150
+ : finishProjection(visibleMessages);
1151
+ }
1152
+ const result = removeNativeCompactionRetainedMessages({
1153
+ messages: replayEvent.messages,
1154
+ branchEntries,
1155
+ compactionEntry: latest.entry,
1156
+ expectedInputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
1157
+ });
1158
+ if (!result.ok) {
1159
+ writeReplayFailureArtifact({ reason: result.reason, compactionEntryId: latest.entry.id }, config, ctx);
1160
+ if (ctx.hasUI) ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: replay failed (${result.reason}); request aborted`, "error");
1161
+ reportPiContextHookFailure(ctx, `replay-failed:${result.reason}`);
1162
+ ctx.abort();
1163
+ return undefined;
1164
+ }
1165
+ return finishProjection(result.messages);
1166
+ }
1167
+
1168
+ /**
1169
+ * Pi's emitContext() catches handler exceptions. Re-throw as usual for Pi, but
1170
+ * also report the failure so the compaction projector cannot mistake the
1171
+ * caught exception for a successful projection.
1172
+ */
1173
+ async function handleContext(
1174
+ event: ContextWithSystemEvent,
1175
+ ctx: ExtensionContext,
1176
+ pi: ExtensionAPI,
1177
+ loadConfig: typeof loadToolkitConfig,
1178
+ contextWindows: CodexContextWindowManager,
1179
+ remoteContextActive: RemoteContextActive,
1180
+ getContextToolRegistrationState: ContextToolRegistrationStateReader,
1181
+ ) {
1182
+ try {
1183
+ return await handleContextInternal(event, ctx, pi, loadConfig, contextWindows, remoteContextActive, getContextToolRegistrationState);
1184
+ } catch (error) {
1185
+ reportPiContextHookFailure(ctx, error instanceof Error ? error.message : String(error));
1186
+ throw error;
1187
+ }
1188
+ }
1189
+
1190
+ async function handleBeforeProviderRequest(
1191
+ event: BeforeProviderRequestEvent,
1192
+ ctx: ExtensionContext,
1193
+ loadConfig: typeof loadToolkitConfig,
1194
+ contextWindows: CodexContextWindowManager,
1195
+ remoteContextActive: RemoteContextActive,
1196
+ getContextToolRegistrationState: ContextToolRegistrationStateReader,
1197
+ ) {
1198
+ const resolved = contextOperation(loadConfig, ctx);
1199
+ requireContextPolicy(resolved, ctx);
1200
+ const toolkitConfig = resolved.config;
1201
+ const config = toolkitConfig.compaction;
1202
+ if (!config.enabled) {
1203
+ return undefined;
1204
+ }
1205
+
1206
+ const contextModel = config.contextManagement === "remote"
1207
+ ? await resolveContextCapabilityModel(ctx)
1208
+ : undefined;
1209
+ if (config.contextManagement === "remote") {
1210
+ try {
1211
+ contextWindows.synchronize(ctx);
1212
+ } catch {
1213
+ notifyRemoteContextFailure(ctx, "malformed-window-state");
1214
+ ctx.abort();
1215
+ return undefined;
1216
+ }
1217
+ }
1218
+ const remoteActive = config.contextManagement === "remote" && await remoteContextActive(ctx, config, contextModel);
1219
+ let remotePayload: unknown;
1220
+ if (remoteActive) {
1221
+ try {
1222
+ remotePayload = contextWindows.rewritePayload(event.payload, ctx);
1223
+ if (contextWindows.hasEffectiveWindow() || !contextWindows.currentIdentity()) return remotePayload;
1224
+ event = { ...event, payload: remotePayload };
1225
+ } catch {
1226
+ notifyRemoteContextFailure(ctx, "malformed-request-state");
1227
+ ctx.abort();
1228
+ return undefined;
1229
+ }
1230
+ }
1231
+ if (!remoteActive && isCodexContextModel(contextModel, config)) {
1232
+ const registrationState = getContextToolRegistrationState();
1233
+ // A readable empty registry is an intentional host exclusion. Do not
1234
+ // rewrite gateway identity or abort an ordinary child-session request.
1235
+ if (isExcludedGatewayContext(contextModel, config, registrationState)) return undefined;
1236
+ // Keep Codex Remote mutually exclusive with the legacy replay pipeline
1237
+ // when authentication or tool ownership is unavailable. Gateway traffic
1238
+ // cannot continue as an ordinary Responses request because that would
1239
+ // silently lose the required CPA session/account selection.
1240
+ if (isCodexGatewayModel(contextModel, config.gatewayContextModels)) {
1241
+ const reason = contextManagementFailureReason(registrationState);
1242
+ notifyRemoteContextFailure(ctx, reason);
1243
+ ctx.abort();
1244
+ }
1245
+ return undefined;
1246
+ }
1247
+
1248
+ // Do not reintroduce a pre-window opaque checkpoint after local projection.
1249
+ // Request affinity/encrypted-output rewriting is reserved for active Remote.
1250
+ if (config.contextManagement === "remote" && contextWindows.hasEffectiveWindow()) return undefined;
1251
+
1252
+ const branchEntries = ctx.sessionManager.getBranch();
1253
+ const inherited = config.contextManagement === "remote" && contextWindows.hasSupersededWindow()
1254
+ ? resolveLatestNativeCompactionEntry(branchEntries) : undefined;
1255
+ const inheritedCheckpoint = inherited?.ok ? inherited.entry : undefined;
1256
+ const resolution = await resolveNativeCompactionEnvironment(
1257
+ ctx,
1258
+ {
1259
+ enabled: config.enabled,
1260
+ responsesApis: inheritedCheckpoint ? undefined : config.responsesApis,
1261
+ codexGatewayModels: compactGatewayModels(resolved),
1262
+ },
1263
+ event.payload,
1264
+ );
1265
+ if (resolution.ok === false) {
1266
+ if (inheritedCheckpoint) return rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, resolution.reason);
1267
+ writeDebugArtifact(
1268
+ "provider-request",
1269
+ {
1270
+ event: "before_provider_request.skip",
1271
+ reason: resolution.reason,
1272
+ provider: resolution.provider,
1273
+ api: resolution.api,
1274
+ model: resolution.model,
1275
+ baseUrl: resolution.baseUrl,
1276
+ errorMessage: resolution.errorMessage,
1277
+ currentModel: getCurrentModelDebugInfo(ctx),
1278
+ payload: event.payload,
1279
+ },
1280
+ config,
1281
+ ctx,
1282
+ );
1283
+ return remotePayload;
1284
+ }
1285
+
1286
+ const runtime = resolution.runtime;
1287
+ const payload = runtime.payload;
1288
+ if (!payload) {
1289
+ return undefined;
1290
+ }
1291
+
1292
+ // Capture compact-relevant request fields (tools, reasoning, ...) for the next
1293
+ // synthetic compact request using the active consumer's effective runtime identity.
1294
+ // This hook runs before the separate Web Search transform, so injected native search
1295
+ // tools are not copied into remote_compaction_v2.
1296
+ const webSearchRoute = resolveWebSearchRoute({ model: ctx.model, config: toolkitConfig.webSearch });
1297
+ rememberRequestContext(
1298
+ payload,
1299
+ {
1300
+ provider: runtime.provider,
1301
+ api: runtime.api,
1302
+ model: runtime.requestModel ?? runtime.model,
1303
+ baseUrl: runtime.baseUrl,
1304
+ sessionId: getSessionId(ctx),
1305
+ },
1306
+ {
1307
+ excludeWebSearchTools: webSearchRoute.route === "standalone-alpha",
1308
+ },
1309
+ );
1310
+
1311
+ const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
1312
+ baseUrl: runtime.baseUrl,
1313
+ });
1314
+ if (!latestNativeCompaction.ok) {
1315
+ if (inheritedCheckpoint) return rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, latestNativeCompaction.reason);
1316
+ writeDebugArtifact(
1317
+ "provider-request",
1318
+ {
1319
+ event: "before_provider_request.no-native-compaction",
1320
+ reason: latestNativeCompaction.reason,
1321
+ provider: runtime.provider,
1322
+ api: runtime.api,
1323
+ model: runtime.model,
1324
+ baseUrl: runtime.baseUrl,
1325
+ branchEntries: branchEntries.length,
1326
+ latestCompactionIndex: latestNativeCompaction.latestCompactionIndex,
1327
+ latestCompactionIdentity: getCompactionIdentityDebugInfo(latestNativeCompaction.latestCompaction),
1328
+ payload,
1329
+ },
1330
+ config,
1331
+ ctx,
1332
+ );
1333
+ return remotePayload;
1334
+ }
1335
+
1336
+ const latestNativeCompactionEntry = latestNativeCompaction.entry;
1337
+ const rewrite = rewriteResponsesPayloadWithNativeReplay({
1338
+ model: runtime.currentModel,
1339
+ payload,
1340
+ branchEntries,
1341
+ compactionEntry: latestNativeCompactionEntry,
1342
+ expectedInputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
1343
+ });
1344
+ if (!rewrite.ok) {
1345
+ writeDebugArtifact(
1346
+ "provider-request",
1347
+ {
1348
+ event: "before_provider_request.rewrite-failed",
1349
+ reason: rewrite.reason,
1350
+ provider: runtime.provider,
1351
+ api: runtime.api,
1352
+ model: runtime.model,
1353
+ baseUrl: runtime.baseUrl,
1354
+ compactionEntryId: latestNativeCompactionEntry.id,
1355
+ parity: rewrite.parity,
1356
+ payload,
1357
+ },
1358
+ config,
1359
+ ctx,
1360
+ );
1361
+
1362
+ // Fail loud instead of letting Pi send the sentinel-only payload: the
1363
+ // compacted history would be silently lost while the request still succeeds.
1364
+ // A forced redacted failure record is written even when logProviderPayloads
1365
+ // is disabled so the incident is diagnosable without leaking content.
1366
+ writeReplayFailureArtifact(
1367
+ {
1368
+ reason: rewrite.reason,
1369
+ parity: rewrite.parity,
1370
+ compactionEntryId: latestNativeCompactionEntry.id,
1371
+ provider: runtime.provider,
1372
+ api: runtime.api,
1373
+ model: runtime.model,
1374
+ },
1375
+ config,
1376
+ ctx,
1377
+ );
1378
+ if (ctx.hasUI) {
1379
+ ctx.ui.notify(
1380
+ `${COMPACTION_EXTENSION_ID}: native compaction replay failed (${rewrite.reason}); provider request aborted`,
1381
+ "error",
1382
+ );
1383
+ }
1384
+ ctx.abort();
1385
+ return undefined;
1386
+ }
1387
+
1388
+ writeDebugArtifact(
1389
+ "provider-request",
1390
+ {
1391
+ event: "before_provider_request.native-rewrite",
1392
+ provider: runtime.provider,
1393
+ api: runtime.api,
1394
+ model: runtime.model,
1395
+ baseUrl: runtime.baseUrl,
1396
+ compactionEntryId: latestNativeCompactionEntry.id,
1397
+ boundaryIndex: rewrite.segments.boundaryIndex,
1398
+ firstKeptEntryIndex: rewrite.segments.firstKeptEntryIndex,
1399
+ originalInputItems: payload.input.length,
1400
+ rewrittenInputItems: rewrite.rewrittenPayload.input.length,
1401
+ leadingItems: rewrite.segments.leading.length,
1402
+ compactionSummaryItems: rewrite.segments.compactionSummary.length,
1403
+ compactedItems: rewrite.segments.compactedWindow.length,
1404
+ postItems: rewrite.segments.post.length,
1405
+ payload: rewrite.rewrittenPayload,
1406
+ originalPayload: payload,
1407
+ },
1408
+ config,
1409
+ ctx,
1410
+ );
1411
+
1412
+ return rewrite.rewrittenPayload;
1413
+ }
1414
+
1415
+ export default function registerCompactionExtension(
1416
+ pi: ExtensionAPI,
1417
+ overrides: Partial<CompactionDependencies> = {},
1418
+ ) {
1419
+ const loadConfig = overrides.loadConfig ?? loadToolkitConfig;
1420
+ registerToolkitConfigCommand(pi, loadConfig);
1421
+ const piContextHookPatch = installPiContextHookPatch();
1422
+ const contextWindows = overrides.contextWindows ?? new CodexContextWindowManager((ctx, signal, gatewayModels) =>
1423
+ loadHistoryNotesThreadHint(ctx, signal, gatewayModels)
1424
+ );
1425
+ const manualCompact = new ManagedManualCompact(pi, contextWindows);
1426
+ const dependencies: CompactionDependencies = {
1427
+ remoteCompact: executeRemoteV2Compaction,
1428
+ standaloneCompact: executeNativeCompaction,
1429
+ nativeFallback: runNativeFallbackCompaction,
1430
+ contextWindows,
1431
+ ...overrides,
1432
+ manualCompact,
1433
+ loadConfig: () => manualCompact.snapshot ?? loadConfig(),
1434
+ piContextHookPatch: overrides.piContextHookPatch ?? piContextHookPatch,
1435
+ };
1436
+ let tools!: ReturnType<typeof registerContextManagementTools>;
1437
+ let contextWindowReady = false;
1438
+ const isContextRuntimeActive = async (
1439
+ ctx: ExtensionContext,
1440
+ config: CompactionConfig,
1441
+ model = ctx.model,
1442
+ ): Promise<boolean> =>
1443
+ contextWindowReady && tools.isRegistered && await isRemoteContextActive(ctx, config, model);
1444
+ tools = registerContextManagementTools(
1445
+ pi,
1446
+ contextWindows,
1447
+ async (ctx) => {
1448
+ const resolved = contextOperation(dependencies.loadConfig, ctx);
1449
+ assertConfigValid(resolved, "context", "compatibility");
1450
+ return { active: await isContextRuntimeActive(ctx, resolved.config.compaction), gatewayModels: resolved.gatewayModelKeys };
1451
+ },
1452
+ undefined,
1453
+ manualCompact,
1454
+ );
1455
+ const remoteContextActive: RemoteContextActive = isContextRuntimeActive;
1456
+ const syncTools = async (
1457
+ ctx: ExtensionContext,
1458
+ model = ctx.model,
1459
+ options: { notifyWindowFailure?: boolean } = {},
1460
+ resolved = contextOperation(dependencies.loadConfig, ctx, model),
1461
+ ): Promise<ContextToolSyncOutcome> => {
1462
+ // Until this call proves otherwise, do not let a previous session/window
1463
+ // identity make a failed activation look usable.
1464
+ contextWindowReady = false;
1465
+ const config = resolved.config.compaction;
1466
+ const eligible = !resolved.invalidFeatures.some((feature) => ["context", "compatibility"].includes(feature)) && await isRemoteContextActive(ctx, config, model);
1467
+ const toolSync = tools.sync(eligible);
1468
+ const outcome: ContextToolSyncOutcome = {
1469
+ eligible,
1470
+ toolsSynced: toolSync.synced,
1471
+ registrationState: toolSync.registrationState,
1472
+ windowInitialized: false,
1473
+ };
1474
+ if (!eligible || !toolSync.synced) return outcome;
1475
+ // Activation can succeed after `session_start` missed it, because Pi 0.86 may
1476
+ // reject the registration read while a session replacement is still binding.
1477
+ // The window lifecycle has to open on that later activation too, or requests
1478
+ // carry no window metadata and the backend never ingests the turns.
1479
+ try {
1480
+ contextWindows.ensureInitialized(pi, ctx, true);
1481
+ contextWindowReady = true;
1482
+ return { ...outcome, windowInitialized: true };
1483
+ } catch {
1484
+ // Do not leave tools exposed while the request path has no valid window
1485
+ // identity. The next lifecycle hook may retry once Pi is usable again.
1486
+ tools.sync(false);
1487
+ if (options.notifyWindowFailure) notifyRemoteContextFailure(ctx, "malformed-window-state");
1488
+ return outcome;
1489
+ }
1490
+ };
1491
+ pi.on("session_start", async (_event, ctx) => {
1492
+ const resolved = contextOperation(dependencies.loadConfig, ctx);
1493
+ const syncOutcome = await syncTools(ctx, ctx.model, { notifyWindowFailure: true }, resolved);
1494
+ await manualCompact.recover(ctx);
1495
+ const active = syncOutcome.eligible && syncOutcome.toolsSynced && syncOutcome.windowInitialized;
1496
+ const { source } = resolved;
1497
+ const warnings = resolved.issues.map((issue) => `${issue.path}: ${issue.code}`);
1498
+ const config = resolved.config.compaction;
1499
+ if (resolved.invalidFeatures.length > 0) return;
1500
+ if (!config.enabled) return;
1501
+ const contextModel = await resolveContextCapabilityModel(ctx);
1502
+
1503
+ let activationReason: string | undefined;
1504
+ // Only models the built-in Remote Context coverage targets may activate or
1505
+ // warn; everything else silently runs Pi's normal compaction path.
1506
+ if (config.contextManagement === "remote" && isCodexContextModel(contextModel, config) && !active) {
1507
+ if (syncOutcome.registrationState === "excluded") {
1508
+ activationReason = "context-tools-excluded";
1509
+ } else if (syncOutcome.registrationState === "conflict") {
1510
+ activationReason = "tool-name-conflict";
1511
+ notifyRemoteContextFailure(ctx, activationReason);
1512
+ } else if (syncOutcome.registrationState === "unverified" || !syncOutcome.toolsSynced) {
1513
+ // Transient: the runtime has not published or accepted our tools yet. A
1514
+ // later before_agent_start re-verifies, so do not report provider failure.
1515
+ activationReason = "tool-registration-pending";
1516
+ } else if (syncOutcome.eligible && !syncOutcome.windowInitialized) {
1517
+ activationReason = "malformed-window-state";
1518
+ } else {
1519
+ const remoteResolution = await resolveCodexContextProvider(ctx, ctx.model, config.gatewayContextModels);
1520
+ activationReason = remoteResolution.ok ? "codex-context-unavailable" : remoteResolution.reason;
1521
+ notifyRemoteContextFailure(ctx, activationReason);
1522
+ }
1523
+ }
1524
+
1525
+
1526
+ const artifactPath = writeDebugArtifact(
1527
+ "lifecycle",
1528
+ {
1529
+ event: "session_start",
1530
+ config,
1531
+ configSource: source,
1532
+ warnings,
1533
+ activation: {
1534
+ active,
1535
+ contextManagement: config.contextManagement,
1536
+ remoteV2ContextSource: config.remoteV2ContextSource,
1537
+ model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined,
1538
+ ...(activationReason ? { reason: activationReason } : {}),
1539
+ },
1540
+ piContextHookPatch: dependencies.piContextHookPatch,
1541
+ },
1542
+ config,
1543
+ ctx,
1544
+ );
1545
+
1546
+ if (ctx.hasUI && ["info", "debug"].includes(resolved.policy.diagnostics.level) && (config.notifyOnLoad || config.debug)) {
1547
+ ctx.ui.notify(
1548
+ artifactPath
1549
+ ? `${COMPACTION_EXTENSION_ID} loaded • debug artifacts → ${artifactPath}`
1550
+ : `${COMPACTION_EXTENSION_ID} loaded`,
1551
+ "info",
1552
+ );
1553
+ }
1554
+ });
1555
+
1556
+ const getContextToolRegistrationState: ContextToolRegistrationStateReader = () => tools.registrationState;
1557
+ pi.on("context_with_system", (event, ctx) => {
1558
+ manualCompact.guardRequest(ctx);
1559
+ return handleContext(event, ctx, pi, dependencies.loadConfig, contextWindows, remoteContextActive, getContextToolRegistrationState);
1560
+ });
1561
+ pi.on("session_before_compact", (event, ctx) => handleSessionBeforeCompact(event, ctx, dependencies, remoteContextActive, getContextToolRegistrationState));
1562
+ pi.on("session_compact", (_event, ctx) => contextWindows.synchronize(ctx));
1563
+ pi.on("session_compact_failed", async (event, ctx) => {
1564
+ if (event.reason === "manual" && !manualCompact.isMaintenance) await manualCompact.compactFailed(ctx);
1565
+ });
1566
+ pi.on("session_shutdown", async (_event, ctx) => {
1567
+ await manualCompact.shutdown(ctx);
1568
+ contextWindowReady = false;
1569
+ contextWindows.reset();
1570
+ tools.reset();
1571
+ });
1572
+ pi.on("agent_settled", async (_event, ctx) => {
1573
+ // Only the owned manual handoff restores/continues here; Pi owns normal
1574
+ // compaction scheduling. Never compact retired windows on model changes.
1575
+ await manualCompact.settled(ctx);
1576
+ });
1577
+ pi.on("model_select", async (event, ctx) => {
1578
+ manualCompact.modelSelected(event.model, ctx);
1579
+ // Switching into a covered model mid-session must open the window
1580
+ // lifecycle immediately: without an identity the request rewrite skips
1581
+ // window metadata, the backend never ingests those turns, and the first
1582
+ // new_context would trim pre-switch history that no history can recover.
1583
+ // syncTools() activates and initializes the window when the model is covered.
1584
+ const resolved = contextOperation(dependencies.loadConfig, ctx, event.model);
1585
+ await syncTools(ctx, event.model, { notifyWindowFailure: true }, resolved);
1586
+ });
1587
+ pi.on("before_agent_start", async (_event, ctx) => {
1588
+ await syncTools(ctx, ctx.model, { notifyWindowFailure: true });
1589
+ });
1590
+ pi.on("thinking_level_select", (event, ctx) => manualCompact.thinkingSelected(event.level, ctx));
1591
+ pi.on("message_start", (event) => manualCompact.messageStarted(event.message));
1592
+ pi.on("turn_end", (_event, ctx) => manualCompact.turnEnded(ctx));
1593
+ pi.on("tool_call", (event) => manualCompact.guardTool(event.toolName));
1594
+ pi.on("session_tree", async (_event, ctx) => {
1595
+ contextWindows.synchronize(ctx);
1596
+ await manualCompact.recover(ctx);
1597
+ await syncTools(ctx);
1598
+ });
1599
+ pi.on("before_provider_request", (event, ctx) => {
1600
+ manualCompact.guardRequest(ctx);
1601
+ return handleBeforeProviderRequest(event, ctx, dependencies.loadConfig, contextWindows, remoteContextActive, getContextToolRegistrationState);
1602
+ });
1603
+ pi.on("before_provider_headers", async (event, ctx) => {
1604
+ const resolved = contextOperation(dependencies.loadConfig, ctx);
1605
+ requireContextPolicy(resolved, ctx);
1606
+ const config = resolved.config.compaction;
1607
+ const contextModel = await resolveContextCapabilityModel(ctx);
1608
+ if (!isCodexContextModel(contextModel, config)) return;
1609
+ const active = await remoteContextActive(ctx, config, contextModel);
1610
+ if (!active) {
1611
+ const registrationState = getContextToolRegistrationState();
1612
+ if (isExcludedGatewayContext(contextModel, config, registrationState)) return;
1613
+ if (isCodexGatewayModel(contextModel, config.gatewayContextModels)) {
1614
+ const reason = contextManagementFailureReason(registrationState);
1615
+ notifyRemoteContextFailure(ctx, reason);
1616
+ ctx.abort();
1617
+ }
1618
+ return;
1619
+ }
1620
+ const provider = await resolveCodexContextProvider(ctx, ctx.model, config.gatewayContextModels);
1621
+ if (provider.ok && provider.provider.kind === "codex-gateway") {
1622
+ const sessionId = getSessionId(ctx);
1623
+ const gatewayHeaders = codexContextProviderHeaders(provider.provider, {
1624
+ sessionId,
1625
+ clientRequestId: sessionId,
1626
+ });
1627
+ const allowedGatewayHeaders = new Set<string>(CODEX_GATEWAY_FORWARD_HEADERS);
1628
+ for (const existing of Object.keys(event.headers)) {
1629
+ if (!allowedGatewayHeaders.has(existing.toLowerCase())) delete event.headers[existing];
1630
+ }
1631
+ // Keep any caller-supplied version without forcing a Toolkit pin.
1632
+ // The gateway allowlist above already permits it.
1633
+ for (const name of [
1634
+ "authorization",
1635
+ "originator",
1636
+ "user-agent",
1637
+ "session-id",
1638
+ "x-client-request-id",
1639
+ "x-codex-affinity-scope",
1640
+ "x-codex-model",
1641
+ ]) {
1642
+ for (const existing of Object.keys(event.headers)) {
1643
+ if (existing.toLowerCase() === name) delete event.headers[existing];
1644
+ }
1645
+ const value = gatewayHeaders.get(name);
1646
+ if (value) event.headers[name] = value;
1647
+ }
1648
+ }
1649
+ contextWindows.rewriteHeaders(event.headers, ctx);
1650
+ });
1651
+ pi.on("message_end", async (event, ctx) => {
1652
+ rememberPhysicalModel(ctx, event.message);
1653
+ const resolved = contextOperation(dependencies.loadConfig, ctx);
1654
+ requireContextPolicy(resolved, ctx);
1655
+ const config = resolved.config.compaction;
1656
+ const contextModel = await resolveContextCapabilityModel(ctx);
1657
+ if (!isCodexContextModel(contextModel, config) || !tools.isRegistered) return undefined;
1658
+ if (!(await remoteContextActive(ctx, config, contextModel))) return undefined;
1659
+ const message = routeContextNamespaceToolMessage(event.message);
1660
+ return message === event.message ? undefined : { message };
1661
+ });
1662
+ }