pi-openai-toolkit 0.23.1 → 0.24.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 (53) hide show
  1. package/LICENSE +21 -21
  2. package/NOTICE +83 -83
  3. package/README.md +4 -11
  4. package/README.zh.md +4 -11
  5. package/config.schema.json +264 -278
  6. package/docs/configuration.md +304 -298
  7. package/docs/internals.md +98 -181
  8. package/package.json +123 -135
  9. package/src/compact-client.ts +412 -412
  10. package/src/compaction-projection.ts +35 -35
  11. package/src/config/legacy.ts +12 -62
  12. package/src/config/migration.ts +115 -103
  13. package/src/config/notifications.ts +10 -4
  14. package/src/config/policy.ts +9 -10
  15. package/src/config/v2.ts +12 -51
  16. package/src/config.ts +320 -310
  17. package/src/debug.ts +238 -238
  18. package/src/deferred-tool-carryover.ts +282 -282
  19. package/src/details-store.ts +151 -151
  20. package/src/extension-runtime.ts +1095 -1986
  21. package/src/image-generation/extension.ts +189 -189
  22. package/src/image-generation/protocol.ts +579 -579
  23. package/src/image-generation/types.ts +300 -300
  24. package/src/native-fallback.ts +14 -23
  25. package/src/payload-rewrite.ts +515 -515
  26. package/src/pi-context-hook.ts +117 -117
  27. package/src/remote-v2-client.ts +811 -811
  28. package/src/request-context-cache.ts +155 -155
  29. package/src/responses-headers.ts +180 -180
  30. package/src/retired-remote-windows.ts +35 -0
  31. package/src/runtime.ts +875 -865
  32. package/src/serializer.ts +882 -882
  33. package/src/supported-environment.ts +27 -27
  34. package/src/types.ts +541 -565
  35. package/src/web-search/alpha-client.ts +876 -876
  36. package/src/web-search/extension.ts +530 -530
  37. package/src/web-search/payload.ts +299 -299
  38. package/src/web-search/prompt.ts +60 -60
  39. package/src/web-search/types.ts +166 -166
  40. package/tsconfig.check.json +50 -50
  41. package/src/context-management/codex-provider.ts +0 -270
  42. package/src/context-management/history-notes.ts +0 -357
  43. package/src/context-management/manual-compact.ts +0 -599
  44. package/src/context-management/messages.ts +0 -238
  45. package/src/context-management/namespace-tools.ts +0 -426
  46. package/src/context-management/responses-tools.ts +0 -238
  47. package/src/context-management/tool-contract.ts +0 -62
  48. package/src/context-management/tools.ts +0 -371
  49. package/src/context-management/types.ts +0 -168
  50. package/src/context-management/window-budget.ts +0 -83
  51. package/src/context-management/window-bulk.ts +0 -155
  52. package/src/context-management/window-manager.ts +0 -905
  53. package/src/context-management/window-request.ts +0 -125
@@ -1,1986 +1,1095 @@
1
- import { isDeepStrictEqual } from "node:util";
2
- import type { AgentMessage } from "@earendil-works/pi-agent-core";
3
- import { declarationsEqual, getCurrentSystemMessage, getCurrentTools } from "@earendil-works/pi-ai";
4
- import { convertResponsesTools } from "./context-management/responses-tools";
5
- import type {
6
- BeforeProviderRequestEvent,
7
- ContextWithSystemEvent,
8
- CompactionResult,
9
- ExtensionAPI,
10
- ExtensionContext,
11
- SessionBeforeCompactEvent,
12
- } from "@earendil-works/pi-coding-agent";
13
- import { assertConfigValid, loadToolkitConfig, resolveToolkitConfig, type ResolvedToolkitConfig } from "./config";
14
- import { notifyConfigIssues } from "./config/notifications";
15
- import { registerToolkitConfigCommand } from "./config-command";
16
- import {
17
- codexContextProviderHeaders,
18
- resolveCodexContextProvider,
19
- isCodexGatewayModel,
20
- isNativeCodexModel,
21
- } from "./context-management/codex-provider";
22
- import { rewriteContextNamespaceCalls, rewriteContextNamespaceTools, routeContextNamespaceToolMessage } from "./context-management/namespace-tools";
23
- import { rewriteEncryptedToolOutputs } from "./context-management/window-request";
24
- import { loadHistoryNotesThreadHint } from "./context-management/history-notes";
25
- import { CodexContextWindowManager } from "./context-management/window-manager";
26
- import { ManagedManualCompact, RemoteManualCompact, type RemoteManualCompactOperation } from "./context-management/manual-compact";
27
- import { isSystemTranscriptMessage } from "./context-management/messages";
28
- import { isRecord } from "./context-management/types";
29
- import {
30
- registerContextManagementTools,
31
- type ContextToolRegistrationState,
32
- } from "./context-management/tools";
33
- import { writeDebugArtifact, writeReplayFailureArtifact } from "./debug";
34
- import {
35
- COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
36
- COMPACTION_CHECKPOINT_CONTEXT_EDITED,
37
- COMPACTION_PROJECTION_UNAVAILABLE,
38
- COMPACTION_SESSION_CONTEXT_UNAVAILABLE,
39
- COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
40
- hasVerifiedCompactionInputProvenance,
41
- type UnprojectedCompactionReason,
42
- } from "./compaction-projection";
43
- import { CODEX_GATEWAY_FORWARD_HEADERS } from "./responses-headers";
44
- import { executeNativeCompaction, type NativeCompactionClientResult } from "./compact-client";
45
- import { qualifiesForStandaloneCompaction, type CompactionErrorInfo } from "./compaction-errors";
46
- import { resolveLatestNativeCompactionEntry } from "./details-store";
47
- import {
48
- getPiContextHookProjector,
49
- installPiContextHookPatch,
50
- reportPiContextHookFailure,
51
- type PiContextHookPatchResult,
52
- } from "./pi-context-hook";
53
- import { runNativeFallbackCompaction } from "./native-fallback";
54
- import {
55
- rewriteResponsesPayloadWithNativeReplay,
56
- hasInvalidatedCompactionContext,
57
- removeNativeCompactionRetainedMessages,
58
- serializeLiveTailToResponsesInputWithLimits,
59
- } from "./payload-rewrite";
60
- import { getCompactionRequestExtras, rememberRequestContext } from "./request-context-cache";
61
- import { resolveWebSearchRoute } from "./web-search/types";
62
- import { executeRemoteV2Compaction } from "./remote-v2-client";
63
- import {
64
- rememberPhysicalModel,
65
- resolveEffectiveModel,
66
- resolveNativeCompactionEnvironment,
67
- resolveRemoteCompactionExecution,
68
- isStandaloneCompactionEligible,
69
- parseModelSpec,
70
- type RemoteCompactionExecution,
71
- } from "./runtime";
72
- import {
73
- serializeMessagesToCompactRequestWithLimits,
74
- serializeMessagesToResponsesInputWithLimits,
75
- type NativeCompactionRequestBody,
76
- type ResponsesInputItem,
77
- } from "./serializer";
78
- import {
79
- createNativeCompactionDetails,
80
- createNativeCompactionResult,
81
- COMPACTION_EXTENSION_ID,
82
- LEGACY_NATIVE_COMPACTION_STRATEGY,
83
- REMOTE_V2_COMPACTION_STRATEGY,
84
- getLatestDeferredToolCarryover,
85
- getRemoteV2InputProvenance,
86
- isNativeCompactionDetails,
87
- type CompactionConfig,
88
- type DeferredToolCarryoverV1,
89
- type NativeCompactionDetails,
90
- type NativeCompactionEntry,
91
- type NativeCompactionRequestMeta,
92
- } from "./types";
93
-
94
- type CompactionContextProjection = (
95
- messages: readonly AgentMessage[],
96
- ctx: ExtensionContext,
97
- ) => readonly AgentMessage[] | undefined | Promise<readonly AgentMessage[] | undefined>;
98
-
99
- function contextOperation(loadConfig: typeof loadToolkitConfig, ctx: ExtensionContext, model = ctx.model): ResolvedToolkitConfig {
100
- const loaded = loadConfig();
101
- const resolved = resolveToolkitConfig(loaded, model);
102
- notifyConfigIssues(ctx, resolved);
103
- return resolved;
104
- }
105
-
106
- function requireContextPolicy(resolved: ResolvedToolkitConfig, ctx: ExtensionContext): void {
107
- try {
108
- assertConfigValid(resolved, "context", "compatibility", "diagnostics");
109
- } catch (error) {
110
- if (typeof ctx.abort === "function") ctx.abort();
111
- throw error;
112
- }
113
- }
114
-
115
- function compactGatewayModels(resolved: ResolvedToolkitConfig): readonly string[] {
116
- return resolved.format === "v2" || resolved.config.compaction.contextManagement === "remote"
117
- ? resolved.gatewayModelKeys : [];
118
- }
119
-
120
- type CompactionDependencies = {
121
- loadConfig: typeof loadToolkitConfig;
122
- remoteCompact: typeof executeRemoteV2Compaction;
123
- standaloneCompact: typeof executeNativeCompaction;
124
- nativeFallback: typeof runNativeFallbackCompaction;
125
- contextWindows: CodexContextWindowManager;
126
- manualCompact?: ManagedManualCompact;
127
- remoteManualCompact: RemoteManualCompact;
128
- /** Test seam for the same ordered projection exposed by the Pi host patch. */
129
- projectCompactionContext?: CompactionContextProjection;
130
- piContextHookPatch: PiContextHookPatchResult;
131
- };
132
-
133
- type RemoteContextActive = (
134
- ctx: ExtensionContext,
135
- config: CompactionConfig,
136
- model?: ExtensionContext["model"],
137
- ) => Promise<boolean>;
138
- type ContextToolRegistrationStateReader = () => ContextToolRegistrationState;
139
-
140
- type ContextToolSyncOutcome = {
141
- eligible: boolean;
142
- toolsSynced: boolean;
143
- registrationState: ContextToolRegistrationState;
144
- windowInitialized: boolean;
145
- };
146
-
147
- type ResponsesCompactOutcome =
148
- | { outcome: "success"; compaction: CompactionResult<NativeCompactionDetails> }
149
- | { outcome: "aborted" }
150
- | { outcome: "failed"; remoteFailure?: CompactionErrorInfo & { reason: string; status?: number }; protocol?: string }
151
- | {
152
- outcome: "unprojected-input";
153
- reason: UnprojectedCompactionReason;
154
- };
155
-
156
- function buildCompactionRequestMeta(event: SessionBeforeCompactEvent): NativeCompactionRequestMeta {
157
- return {
158
- tokensBefore: event.preparation.tokensBefore,
159
- previousSummaryPresent: Boolean(event.preparation.previousSummary),
160
- };
161
- }
162
-
163
- function getCurrentModelDebugInfo(ctx: ExtensionContext) {
164
- return ctx.model
165
- ? {
166
- provider: ctx.model.provider,
167
- id: ctx.model.id,
168
- }
169
- : undefined;
170
- }
171
-
172
- function getCompactionIdentityDebugInfo(entry: { details?: unknown } | undefined) {
173
- return isNativeCompactionDetails(entry?.details)
174
- ? {
175
- provider: entry.details.provider,
176
- api: entry.details.api,
177
- model: entry.details.model,
178
- baseUrl: entry.details.baseUrl,
179
- compactionModel: entry.details.compactionModel,
180
- }
181
- : undefined;
182
- }
183
-
184
- function getSessionId(ctx: ExtensionContext): string | undefined {
185
- try {
186
- return ctx.sessionManager.getSessionId();
187
- } catch {
188
- return undefined;
189
- }
190
- }
191
-
192
- function notifyWarning(ctx: ExtensionContext, message: string): void {
193
- if (ctx.hasUI) {
194
- ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: ${message}`, "warning");
195
- }
196
- }
197
-
198
- async function resolveContextCapabilityModel(
199
- ctx: ExtensionContext,
200
- model: ExtensionContext["model"] = ctx.model,
201
- ): Promise<ExtensionContext["model"]> {
202
- const effective = await resolveEffectiveModel(ctx, model);
203
- return effective.ok ? effective.model : undefined;
204
- }
205
-
206
- async function isRemoteContextActive(
207
- ctx: ExtensionContext,
208
- config: CompactionConfig,
209
- model: ExtensionContext["model"] = ctx.model,
210
- ): Promise<boolean> {
211
- if (!config.enabled || config.contextManagement !== "remote") return false;
212
- const resolution = await resolveCodexContextProvider(ctx, model, config.gatewayContextModels);
213
- return resolution.ok;
214
- }
215
-
216
- function isCodexContextModel(
217
- model: ExtensionContext["model"] | undefined,
218
- config: CompactionConfig,
219
- ): boolean {
220
- return config.contextManagement === "remote"
221
- && (isNativeCodexModel(model) || isCodexGatewayModel(model, config.gatewayContextModels));
222
- }
223
-
224
- function isExcludedGatewayContext(
225
- model: ExtensionContext["model"] | undefined,
226
- config: CompactionConfig,
227
- registrationState: ContextToolRegistrationState,
228
- ): boolean {
229
- return registrationState === "excluded" && isCodexGatewayModel(model, config.gatewayContextModels);
230
- }
231
-
232
- function contextManagementFailureReason(registrationState: ContextToolRegistrationState): string {
233
- switch (registrationState) {
234
- case "excluded":
235
- return "context-tools-excluded";
236
- case "conflict":
237
- return "tool-name-conflict";
238
- case "unverified":
239
- return "tool-registration-pending";
240
- case "verified":
241
- return "context-management-unavailable";
242
- }
243
- }
244
-
245
- function notifyRemoteContextFailure(ctx: ExtensionContext, reason: string): void {
246
- if (ctx.hasUI) {
247
- ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: Remote Context management inactive (${reason})`, "warning");
248
- }
249
- }
250
-
251
- /**
252
- * Surface every window trim that had to be refused to keep the tool loadout intact.
253
- *
254
- * The refusal itself is safe: the model keeps the previous turns instead of losing its
255
- * tools. It is still a continuity incident, so it is recorded once per window and reason
256
- * instead of being allowed to pass silently.
257
- */
258
- function reportProjectionDiagnostics(
259
- contextWindows: CodexContextWindowManager,
260
- config: CompactionConfig,
261
- ctx: ExtensionContext,
262
- ): void {
263
- const diagnostics = contextWindows.takeProjectionDiagnostics();
264
- if (diagnostics.length === 0) return;
265
- writeDebugArtifact(
266
- "compaction-event",
267
- { event: "context.projection.tool_loadout_shrink", diagnostics },
268
- config,
269
- ctx,
270
- );
271
- if (ctx.hasUI) {
272
- ctx.ui.notify(
273
- `${COMPACTION_EXTENSION_ID}: kept previous context to preserve tools (${diagnostics.join("; ")})`,
274
- "warning",
275
- );
276
- }
277
- }
278
-
279
- function cloneOpaqueWindow(window: readonly unknown[]): unknown[] {
280
- return window.map((item) => structuredClone(item));
281
- }
282
-
283
- function buildCompactionInstructions(systemPrompt: string, customInstructions?: string): string {
284
- const guidance = customInstructions?.trim();
285
- if (!guidance) {
286
- return systemPrompt;
287
- }
288
-
289
- return `${systemPrompt}\n\nAdditional user guidance for this manual /compact request:\n${guidance}`;
290
- }
291
-
292
- function findLatestProjectedCompactionSummaryIndex(
293
- messages: readonly AgentMessage[],
294
- summary: string,
295
- ): number {
296
- for (let index = messages.length - 1; index >= 0; index -= 1) {
297
- const message = messages[index];
298
- if (message?.role === "compactionSummary" && message.summary === summary) {
299
- return index;
300
- }
301
- }
302
-
303
- return -1;
304
- }
305
-
306
- function readSessionContextMessages(ctx: ExtensionContext): AgentMessage[] | undefined {
307
- try {
308
- const projection = ctx.sessionManager.buildSessionProjection();
309
- if (!projection || !Array.isArray(projection.messages)) return undefined;
310
- return structuredClone(projection.messages);
311
- } catch {
312
- return undefined;
313
- }
314
- }
315
-
316
- type CompactionProjectionResult =
317
- | { ok: true; messages: readonly AgentMessage[] }
318
- | { ok: false; reason: typeof COMPACTION_PROJECTION_UNAVAILABLE | typeof COMPACTION_SESSION_CONTEXT_UNAVAILABLE };
319
-
320
- async function projectSessionContextForCompaction(
321
- ctx: ExtensionContext,
322
- projectCompactionContext: CompactionContextProjection | undefined,
323
- canonicalMessages?: readonly AgentMessage[],
324
- ): Promise<CompactionProjectionResult> {
325
- const messages = canonicalMessages ? structuredClone([...canonicalMessages]) : readSessionContextMessages(ctx);
326
- if (!messages) {
327
- return { ok: false, reason: COMPACTION_SESSION_CONTEXT_UNAVAILABLE };
328
- }
329
-
330
- const projector = projectCompactionContext ?? getPiContextHookProjector(ctx);
331
- if (!projector) {
332
- return { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
333
- }
334
-
335
- try {
336
- const projected = await projector(messages, ctx);
337
- return Array.isArray(projected)
338
- ? { ok: true, messages: projected }
339
- : { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
340
- } catch {
341
- return { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
342
- }
343
- }
344
-
345
- function getLegacySessionContextMessages(
346
- event: SessionBeforeCompactEvent,
347
- ctx: ExtensionContext,
348
- ): AgentMessage[] {
349
- return readSessionContextMessages(ctx) ?? [
350
- ...event.preparation.messagesToSummarize,
351
- ...event.preparation.turnPrefixMessages,
352
- ];
353
- }
354
-
355
- type ScopedResponsesCompactInput = {
356
- request: NativeCompactionRequestBody;
357
- firstKeptEntryId: string;
358
- deferredToolCarryover?: DeferredToolCarryoverV1;
359
- requestSource: "session-context" | "non-native-session-context" | "latest-native-replay";
360
- isCurrent: () => boolean;
361
- };
362
-
363
- /** Reject incomplete/ambiguous tool batches rather than let serialization invent results. */
364
- function requireCompleteCompactionToolPairs(messages: readonly AgentMessage[]): ReadonlyMap<string, string | undefined> {
365
- const calls = new Map<string, string | undefined>();
366
- const pending = new Set<string>();
367
- for (const message of messages) {
368
- if (message.role === "assistant") {
369
- if (pending.size) throw new Error("compaction-tool-pair-mismatch");
370
- for (const block of message.content) {
371
- if (block.type !== "toolCall") continue;
372
- const callId = block.id.split("|")[0];
373
- if (!callId || calls.has(callId)) throw new Error("compaction-tool-pair-mismatch");
374
- calls.set(callId, block.namespace);
375
- pending.add(block.id);
376
- }
377
- } else if (message.role === "toolResult") {
378
- if (!pending.delete(message.toolCallId)) throw new Error("compaction-tool-pair-mismatch");
379
- } else if (pending.size && !isSystemTranscriptMessage(message)) {
380
- throw new Error("compaction-tool-pair-mismatch");
381
- }
382
- }
383
- if (pending.size) throw new Error("compaction-tool-pair-mismatch");
384
- return calls;
385
- }
386
-
387
- /** Serialization must preserve every verified pair, with unique provider call IDs. */
388
- function requireCompactionWireToolPairs(input: readonly unknown[], namespaces: ReadonlyMap<string, string | undefined>): void {
389
- const calls = new Set<string>();
390
- const pending = new Set<string>();
391
- for (const item of input) {
392
- if (!isRecord(item)) throw new Error("compaction-tool-pair-mismatch");
393
- if (item.type === "function_call") {
394
- if (typeof item.call_id !== "string" || !namespaces.has(item.call_id) || calls.has(item.call_id)) {
395
- throw new Error("compaction-tool-pair-mismatch");
396
- }
397
- calls.add(item.call_id);
398
- pending.add(item.call_id);
399
- } else if (item.type === "function_call_output") {
400
- if (typeof item.call_id !== "string" || !pending.delete(item.call_id)) throw new Error("compaction-tool-pair-mismatch");
401
- }
402
- }
403
- if (pending.size || calls.size !== namespaces.size) throw new Error("compaction-tool-pair-mismatch");
404
- }
405
-
406
- function requireCompactionHeadAndRetention(
407
- canonical: readonly AgentMessage[],
408
- projected: readonly AgentMessage[],
409
- retained: readonly AgentMessage[],
410
- ): void {
411
- const before = getCurrentTools(canonical);
412
- const after = getCurrentTools(projected);
413
- if (!getCurrentSystemMessage(projected) || before.length !== after.length
414
- || before.some((tool, index) => !after[index] || !declarationsEqual(tool, after[index]!))) {
415
- throw new Error("compaction-system-tool-head-lost");
416
- }
417
- // These exact source contributions will accompany the new summary in Pi.
418
- // If a hook rewrites/omits one, future strict retained-copy removal cannot be proven.
419
- let cursor = 0;
420
- for (const message of retained) {
421
- if (message.role === "custom" || message.role === "compactionSummary" || isSystemTranscriptMessage(message)) continue;
422
- const index = projected.findIndex((candidate, index) => index >= cursor && isDeepStrictEqual(candidate, message));
423
- if (index < 0) throw new Error("compaction-retained-context-mismatch");
424
- cursor = index + 1;
425
- }
426
- }
427
-
428
- /** Apply only pure wire adapters, never ordinary turn/history-ingest hooks. */
429
- function adaptCompactionWireInput(input: ResponsesInputItem[], namespaces: ReadonlyMap<string, string | undefined>): ResponsesInputItem[] {
430
- // The shared legacy serializer omits namespace. Restore it from the verified
431
- // canonical call before applying the existing context-router wire adapter.
432
- const namespaced = input.map((item: unknown) => {
433
- if (!isRecord(item) || item.type !== "function_call" || typeof item.call_id !== "string") return item;
434
- const namespace = namespaces.get(item.call_id);
435
- return namespace ? { ...item, namespace } : item;
436
- });
437
- const adapted = rewriteEncryptedToolOutputs(rewriteContextNamespaceCalls({ input: namespaced }));
438
- if (!isRecord(adapted) || !Array.isArray(adapted.input)) throw new Error("compaction-wire-adaptation-failed");
439
- // The adapters preserve the validated array shape, with native encrypted outputs.
440
- return adapted.input as ResponsesInputItem[];
441
- }
442
-
443
- async function prepareActiveRemoteCompaction(
444
- event: SessionBeforeCompactEvent,
445
- ctx: ExtensionContext,
446
- config: CompactionConfig,
447
- execution: RemoteCompactionExecution,
448
- dependencies: CompactionDependencies,
449
- isCurrent: () => boolean,
450
- ): Promise<ScopedResponsesCompactInput> {
451
- const windows = dependencies.contextWindows;
452
- const prepared = windows.prepareRemoteCompaction(event, ctx);
453
- const branch = ctx.sessionManager.getBranch();
454
- const provenance = getRemoteV2InputProvenance(config.remoteV2ContextSource);
455
- let checkpoint: NativeCompactionEntry | undefined;
456
- let requestSource: ScopedResponsesCompactInput["requestSource"] = "session-context";
457
- if (prepared.owningCompaction) {
458
- const latest = resolveLatestNativeCompactionEntry(branch, { baseUrl: execution.consumer.baseUrl });
459
- if (latest.ok) {
460
- if (latest.entry.id !== prepared.owningCompaction.id) throw new Error("compaction-owner-mismatch");
461
- if (hasInvalidatedCompactionContext(branch, latest.entry.id)) throw new Error(COMPACTION_CHECKPOINT_CONTEXT_EDITED);
462
- if (!hasVerifiedCompactionInputProvenance(latest.entry.details, provenance)) throw new Error(COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE);
463
- checkpoint = latest.entry;
464
- requestSource = "latest-native-replay";
465
- } else {
466
- if (latest.reason !== "latest-compaction-not-native" || !config.allowCompactionContinuityBreak) throw new Error("compaction-continuity-unavailable");
467
- requestSource = "non-native-session-context";
468
- }
469
- }
470
- let messages = prepared.messages;
471
- if (config.remoteV2ContextSource === "pi-context-hook") {
472
- const projection = await windows.withCompactionProjection(() => projectSessionContextForCompaction(
473
- ctx, dependencies.projectCompactionContext, messages,
474
- ));
475
- if (!isCurrent()) throw new Error("compaction-operation-superseded");
476
- if (!projection.ok) throw new Error(projection.reason);
477
- messages = windows.projectCompactionContext(projection.messages);
478
- }
479
- if (!isCurrent()) throw new Error("compaction-operation-superseded");
480
- requireCompactionHeadAndRetention(prepared.messages, messages, prepared.retainedMessages);
481
- if (checkpoint) {
482
- // The synthetic Toolkit hook deliberately leaves these copies alone. Match
483
- // exactly once, BEFORE encrypted-output markers change the canonical values.
484
- const pruned = removeNativeCompactionRetainedMessages({
485
- messages, branchEntries: branch, compactionEntry: checkpoint, expectedInputProvenance: provenance,
486
- });
487
- if (!pruned.ok) throw new Error(pruned.reason);
488
- messages = pruned.messages;
489
- }
490
- const namespaces = requireCompleteCompactionToolPairs(messages);
491
- const tools = getCurrentTools(messages);
492
- const model = execution.compactor.currentModel;
493
- const compat = isRecord(model.compat) ? model.compat : {};
494
- const nativeCodex = model.api === "openai-codex-responses";
495
- const encoded = windows.projectCompactionContext(messages, true);
496
- let input: ResponsesInputItem[];
497
- if (checkpoint) {
498
- const summaryIndex = findLatestProjectedCompactionSummaryIndex(encoded, checkpoint.summary);
499
- if (summaryIndex < 0 || encoded.filter((message) => message.role === "compactionSummary").length !== 1) {
500
- throw new Error(COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE);
501
- }
502
- const leading = await serializeMessagesToResponsesInputWithLimits(model, encoded.slice(0, summaryIndex));
503
- if (!isCurrent()) throw new Error("compaction-operation-superseded");
504
- const tail = await serializeMessagesToResponsesInputWithLimits(model, encoded.slice(summaryIndex + 1), { firstSystemMessageIsUpdate: true });
505
- requireCompactionWireToolPairs([...leading, ...tail], namespaces);
506
- const details = checkpoint.details;
507
- if (!details) throw new Error("compaction-owner-mismatch");
508
- input = [...adaptCompactionWireInput(leading, namespaces), ...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]), ...adaptCompactionWireInput(tail, namespaces)];
509
- } else {
510
- const serialized = await serializeMessagesToResponsesInputWithLimits(model, encoded);
511
- requireCompactionWireToolPairs(serialized, namespaces);
512
- input = adaptCompactionWireInput(serialized, namespaces);
513
- }
514
- if (!isCurrent()) throw new Error("compaction-operation-superseded");
515
- const toolPayload = rewriteContextNamespaceTools({ tools: convertResponsesTools(tools, {
516
- ...(nativeCodex ? { strict: null } : {}),
517
- supportsStrictMode: typeof compat.supportsStrictMode === "boolean" ? compat.supportsStrictMode : nativeCodex,
518
- supportsOpenAIGrammarTools: compat.supportsOpenAIGrammarTools === true,
519
- }) }, { encrypted: true });
520
- if (!isRecord(toolPayload) || !Array.isArray(toolPayload.tools)) throw new Error("compaction-wire-adaptation-failed");
521
- // Current cache-stack state is authoritative; never borrow deferred metadata
522
- // from an unrelated pre-window checkpoint. The canonical head proves which
523
- // of those schemas are still live, including tools activated before rollover.
524
- const activation = getLatestDeferredToolCarryover(branch) ?? checkpoint?.details?.deferredToolCarryover;
525
- const names = new Set(tools.map((tool) => tool.name));
526
- const deferredToolCarryover = activation ? { ...activation, toolNames: activation.toolNames.filter((name) => names.has(name)) } : undefined;
527
- return {
528
- request: { model: execution.compactor.requestModel ?? execution.compactor.model, input,
529
- instructions: buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions), tools: toolPayload.tools },
530
- firstKeptEntryId: prepared.firstKeptEntryId,
531
- deferredToolCarryover: deferredToolCarryover?.toolNames.length ? deferredToolCarryover : undefined,
532
- requestSource, isCurrent,
533
- };
534
- }
535
-
536
- async function runResponsesNativeCompact(
537
- event: SessionBeforeCompactEvent,
538
- ctx: ExtensionContext,
539
- config: CompactionConfig,
540
- execution: RemoteCompactionExecution,
541
- remoteCompact: typeof executeRemoteV2Compaction,
542
- projectCompactionContext: CompactionContextProjection | undefined,
543
- /** Only ordinary compaction supplies this; managed recovery remains V2-only. */
544
- standaloneCompact?: typeof executeNativeCompaction,
545
- /** Active Remote manual input is strict/current-window-only and has no fallback. */
546
- scopedInput?: ScopedResponsesCompactInput,
547
- ): Promise<ResponsesCompactOutcome> {
548
- const { consumer, compactor } = execution;
549
- const instructions = buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions);
550
- const branchEntries = event.branchEntries ?? ctx.sessionManager.getBranch();
551
- const deferredToolCarryover = scopedInput ? scopedInput.deferredToolCarryover : getLatestDeferredToolCarryover(branchEntries);
552
- const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
553
- baseUrl: consumer.baseUrl,
554
- });
555
-
556
- const inputProvenance = getRemoteV2InputProvenance(config.remoteV2ContextSource);
557
- let requestSource: "session-context" | "non-native-session-context" | "latest-native-replay";
558
- let request: NativeCompactionRequestBody;
559
- if (scopedInput) {
560
- request = scopedInput.request;
561
- requestSource = scopedInput.requestSource;
562
- } else if (latestNativeCompaction.ok) {
563
- if (hasInvalidatedCompactionContext(branchEntries, latestNativeCompaction.entry.id)) {
564
- return { outcome: "unprojected-input", reason: COMPACTION_CHECKPOINT_CONTEXT_EDITED };
565
- }
566
- const details = latestNativeCompaction.entry.details;
567
- if (!details) {
568
- return { outcome: "failed" };
569
- }
570
- if (!hasVerifiedCompactionInputProvenance(details, inputProvenance)) {
571
- return {
572
- outcome: "unprojected-input",
573
- reason: COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
574
- };
575
- }
576
- requestSource = "latest-native-replay";
577
-
578
- if (config.remoteV2ContextSource === "pi-context-hook") {
579
- const projection = await projectSessionContextForCompaction(ctx, projectCompactionContext);
580
- if (!projection.ok) {
581
- return { outcome: "unprojected-input", reason: projection.reason };
582
- }
583
- const summaryIndex = findLatestProjectedCompactionSummaryIndex(
584
- projection.messages,
585
- latestNativeCompaction.entry.summary,
586
- );
587
- if (summaryIndex < 0) {
588
- return {
589
- outcome: "unprojected-input",
590
- reason: COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
591
- };
592
- }
593
- const input: ResponsesInputItem[] = [
594
- ...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
595
- ...(await serializeMessagesToResponsesInputWithLimits(
596
- compactor.currentModel,
597
- [...projection.messages.slice(summaryIndex + 1)],
598
- { firstSystemMessageIsUpdate: true },
599
- )),
600
- ];
601
- request = {
602
- model: compactor.requestModel ?? compactor.model,
603
- input,
604
- instructions,
605
- };
606
- } else {
607
- const liveTailEntries = branchEntries.slice(latestNativeCompaction.index + 1);
608
- const input: ResponsesInputItem[] = [
609
- ...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
610
- ...(await serializeLiveTailToResponsesInputWithLimits({ model: compactor.currentModel, entries: liveTailEntries })),
611
- ];
612
- request = {
613
- model: compactor.requestModel ?? compactor.model,
614
- input,
615
- instructions,
616
- };
617
- }
618
- } else if (
619
- latestNativeCompaction.reason === "no-compaction" ||
620
- (latestNativeCompaction.reason === "latest-compaction-not-native" &&
621
- config.allowCompactionContinuityBreak)
622
- ) {
623
- requestSource =
624
- latestNativeCompaction.reason === "no-compaction" ? "session-context" : "non-native-session-context";
625
- if (config.remoteV2ContextSource === "pi-context-hook") {
626
- const projection = await projectSessionContextForCompaction(ctx, projectCompactionContext);
627
- if (!projection.ok) {
628
- return { outcome: "unprojected-input", reason: projection.reason };
629
- }
630
- const serialized = await serializeMessagesToCompactRequestWithLimits({
631
- model: compactor.currentModel,
632
- messages: [...projection.messages],
633
- instructions,
634
- });
635
- request = { ...serialized, model: compactor.requestModel ?? serialized.model };
636
- } else {
637
- const serialized = await serializeMessagesToCompactRequestWithLimits({
638
- model: compactor.currentModel,
639
- messages: getLegacySessionContextMessages(event, ctx),
640
- instructions,
641
- });
642
- request = { ...serialized, model: compactor.requestModel ?? serialized.model };
643
- }
644
- } else {
645
- writeDebugArtifact(
646
- "compaction-event",
647
- {
648
- event: "session_before_compact.remote-v2-skip",
649
- reason: latestNativeCompaction.reason,
650
- consumer: {
651
- provider: consumer.provider,
652
- api: consumer.api,
653
- model: consumer.model,
654
- baseUrl: consumer.baseUrl,
655
- },
656
- compactor: {
657
- provider: compactor.provider,
658
- api: compactor.api,
659
- model: compactor.model,
660
- baseUrl: compactor.baseUrl,
661
- },
662
- latestCompactionIndex: latestNativeCompaction.latestCompactionIndex,
663
- latestCompactionIdentity: getCompactionIdentityDebugInfo(latestNativeCompaction.latestCompaction),
664
- },
665
- config,
666
- ctx,
667
- );
668
- return { outcome: "failed" };
669
- }
670
-
671
- // Mirror the latest codex_rs CompactionInput fields captured from the most
672
- // recent live provider request for this model (tools, reasoning, etc.).
673
- const extras = getCompactionRequestExtras({
674
- provider: consumer.provider,
675
- api: consumer.api,
676
- model: consumer.requestModel ?? consumer.model,
677
- baseUrl: consumer.baseUrl,
678
- sessionId: getSessionId(ctx),
679
- }, compactor.currentModel);
680
- if (extras) {
681
- request = { ...request, ...extras, ...(scopedInput ? { tools: scopedInput.request.tools } : {}) };
682
- }
683
-
684
- if (event.signal?.aborted || (scopedInput && !scopedInput.isCurrent())) return { outcome: "aborted" };
685
- let protocol: "remote-v2" | "standalone-compact" = "remote-v2";
686
- let compactResult: Awaited<ReturnType<typeof executeRemoteV2Compaction>> | NativeCompactionClientResult = await remoteCompact({
687
- runtime: compactor,
688
- request,
689
- signal: event.signal,
690
- settings: config,
691
- context: ctx,
692
- });
693
-
694
- if (scopedInput && !scopedInput.isCurrent()) return { outcome: "aborted" };
695
- if (!compactResult.ok) {
696
- if (compactResult.reason === "aborted" || event.signal?.aborted) return { outcome: "aborted" };
697
- const tryStandalone = !!standaloneCompact && isStandaloneCompactionEligible(compactor) && qualifiesForStandaloneCompaction(compactResult);
698
- writeDebugArtifact("compaction-event", {
699
- event: "session_before_compact.remote-v2-failure",
700
- protocol,
701
- reason: compactResult.reason,
702
- status: compactResult.status,
703
- errorCode: compactResult.errorCode,
704
- errorType: compactResult.errorType,
705
- errorParam: compactResult.errorParam,
706
- errorMessage: compactResult.errorMessage,
707
- nextStep: tryStandalone ? "standalone-compact" : scopedInput ? "stop-and-preserve" : "native-summary",
708
- }, config, ctx);
709
- if (tryStandalone && standaloneCompact) {
710
- if (event.signal?.aborted) return { outcome: "aborted" };
711
- protocol = "standalone-compact";
712
- compactResult = await standaloneCompact({ runtime: compactor, request, signal: event.signal, settings: config, context: ctx });
713
- }
714
- if (!compactResult.ok) {
715
- if (compactResult.reason === "aborted" || event.signal?.aborted) return { outcome: "aborted" };
716
- const remoteFailure = {
717
- reason: compactResult.reason,
718
- status: compactResult.status,
719
- errorCode: compactResult.errorCode,
720
- errorType: compactResult.errorType,
721
- errorParam: compactResult.errorParam,
722
- errorMessage: compactResult.errorMessage,
723
- };
724
- if (protocol === "standalone-compact") writeDebugArtifact("compaction-event", {
725
- event: "session_before_compact.standalone-compact-failure", protocol, ...remoteFailure, nextStep: "native-summary",
726
- }, config, ctx);
727
- return { outcome: "failed", remoteFailure, protocol };
728
- }
729
- }
730
-
731
- let details: NativeCompactionDetails;
732
- try {
733
- details = createNativeCompactionDetails({
734
- strategy: protocol === "standalone-compact" ? LEGACY_NATIVE_COMPACTION_STRATEGY : REMOTE_V2_COMPACTION_STRATEGY,
735
- provider: consumer.provider,
736
- api: consumer.api,
737
- model: consumer.model,
738
- baseUrl: consumer.baseUrl,
739
- compactionModel: {
740
- provider: compactor.provider,
741
- api: compactor.api,
742
- model: compactor.model,
743
- baseUrl: compactor.baseUrl,
744
- },
745
- deferredToolCarryover,
746
- compactedWindow: compactResult.compactedWindow,
747
- compactResponseId: compactResult.compactResponseId,
748
- createdAt: compactResult.createdAt,
749
- requestMeta: buildCompactionRequestMeta(event),
750
- inputProvenance,
751
- });
752
- } catch (error) {
753
- writeDebugArtifact(
754
- "compaction-event",
755
- {
756
- event: "session_before_compact.invalid-native-details",
757
- reason: error instanceof Error ? error.message : String(error),
758
- consumer: {
759
- provider: consumer.provider,
760
- api: consumer.api,
761
- model: consumer.model,
762
- baseUrl: consumer.baseUrl,
763
- },
764
- compactor: {
765
- provider: compactor.provider,
766
- api: compactor.api,
767
- model: compactor.model,
768
- baseUrl: compactor.baseUrl,
769
- },
770
- },
771
- config,
772
- ctx,
773
- );
774
- return { outcome: "failed", remoteFailure: { reason: "invalid-native-details" }, protocol };
775
- }
776
-
777
- const compaction = createNativeCompactionResult({
778
- firstKeptEntryId: scopedInput?.firstKeptEntryId ?? event.preparation.firstKeptEntryId,
779
- tokensBefore: event.preparation.tokensBefore,
780
- details,
781
- });
782
-
783
- writeDebugArtifact(
784
- "compaction-event",
785
- {
786
- event: `session_before_compact.${protocol}-success`,
787
- protocol,
788
- consumer: {
789
- provider: consumer.provider,
790
- api: consumer.api,
791
- model: consumer.model,
792
- baseUrl: consumer.baseUrl,
793
- },
794
- compactor: {
795
- provider: compactor.provider,
796
- api: compactor.api,
797
- model: compactor.model,
798
- baseUrl: compactor.baseUrl,
799
- },
800
- requestSource,
801
- remoteV2ContextSource: config.remoteV2ContextSource,
802
- inputProvenance,
803
- requestInputItems: request.input.length,
804
- requestExtras: extras ? Object.keys(extras) : [],
805
- compactResponseId: compactResult.compactResponseId,
806
- compactedItems: compactResult.compactedWindow.length,
807
- firstKeptEntryId: compaction.firstKeptEntryId,
808
- },
809
- config,
810
- ctx,
811
- );
812
-
813
- if (scopedInput && !scopedInput.isCurrent()) return { outcome: "aborted" };
814
- return { outcome: "success", compaction };
815
- }
816
-
817
- type ContextUnavailableCompactionResult =
818
- | { compaction: CompactionResult<unknown> }
819
- | { cancel: true };
820
-
821
- async function runUnavailableContextCompaction(
822
- event: SessionBeforeCompactEvent,
823
- ctx: ExtensionContext,
824
- dependencies: CompactionDependencies,
825
- resolved: ResolvedToolkitConfig,
826
- config: CompactionConfig,
827
- registrationState: ContextToolRegistrationState,
828
- ): Promise<ContextUnavailableCompactionResult> {
829
- const contextReason = contextManagementFailureReason(registrationState);
830
- const remoteModelSpec = config.remoteCompactModel?.trim();
831
- let remoteFailure: string | undefined;
832
-
833
- if (remoteModelSpec) {
834
- const resolution = await resolveRemoteCompactionExecution(
835
- ctx,
836
- {
837
- enabled: config.enabled,
838
- responsesApis: config.responsesApis,
839
- codexGatewayModels: compactGatewayModels(resolved),
840
- },
841
- remoteModelSpec,
842
- );
843
- if (resolution.ok) {
844
- const outcome = await runResponsesNativeCompact(
845
- event,
846
- ctx,
847
- config,
848
- resolution.execution,
849
- dependencies.remoteCompact,
850
- dependencies.projectCompactionContext,
851
- );
852
- if (outcome.outcome === "success") {
853
- writeDebugArtifact(
854
- "compaction-event",
855
- {
856
- event: "session_before_compact.context-fallback-success",
857
- contextReason,
858
- registrationState,
859
- compactor: remoteModelSpec,
860
- },
861
- config,
862
- ctx,
863
- );
864
- return { compaction: outcome.compaction };
865
- }
866
- if (outcome.outcome === "aborted" || outcome.outcome === "unprojected-input") {
867
- return { cancel: true };
868
- }
869
- remoteFailure = "remote-v2-failed";
870
- } else {
871
- remoteFailure = resolution.reason;
872
- writeDebugArtifact(
873
- "compaction-event",
874
- {
875
- event: "session_before_compact.context-fallback-unavailable",
876
- contextReason,
877
- registrationState,
878
- reason: resolution.reason,
879
- modelSpec: remoteModelSpec,
880
- errorMessage: resolution.errorMessage,
881
- },
882
- config,
883
- ctx,
884
- );
885
- }
886
- } else {
887
- remoteFailure = "missing-remote-compaction-model";
888
- }
889
-
890
- const nativeModelSpec = config.nativeFallback.model?.trim();
891
- if (config.nativeFallback.enabled && nativeModelSpec) {
892
- const fallback = await dependencies.nativeFallback({
893
- ctx,
894
- event,
895
- config,
896
- modelSpec: nativeModelSpec,
897
- });
898
- if (fallback.ok) {
899
- writeDebugArtifact(
900
- "compaction-event",
901
- {
902
- event: "session_before_compact.context-fallback-native-success",
903
- contextReason,
904
- registrationState,
905
- remoteFailure,
906
- fallbackModel: nativeModelSpec,
907
- },
908
- config,
909
- ctx,
910
- );
911
- return { compaction: fallback.result };
912
- }
913
- writeDebugArtifact(
914
- "compaction-event",
915
- {
916
- event: "session_before_compact.context-fallback-native-failure",
917
- contextReason,
918
- registrationState,
919
- remoteFailure,
920
- fallbackReason: fallback.reason,
921
- fallbackModel: nativeModelSpec,
922
- errorMessage: fallback.errorMessage,
923
- },
924
- config,
925
- ctx,
926
- );
927
- } else {
928
- writeDebugArtifact(
929
- "compaction-event",
930
- {
931
- event: "session_before_compact.context-fallback-cancelled",
932
- contextReason,
933
- registrationState,
934
- remoteFailure,
935
- reason: config.nativeFallback.enabled ? "missing-native-fallback-model" : "native-fallback-disabled",
936
- },
937
- config,
938
- ctx,
939
- );
940
- }
941
-
942
- return { cancel: true };
943
- }
944
-
945
- const ACTIVE_REMOTE_PREPARATION_REASONS = new Set([
946
- "unsafe-compaction-window-boundary", "compaction-window-boundary-missing", "compaction-system-tool-head-lost",
947
- "compaction-branch-changed", "compaction-source-projection-unavailable", "unverified-window-cleanup",
948
- "compaction-owner-missing", "unsafe-compaction-retained-start", "compaction-owner-mismatch",
949
- "compaction-continuity-unavailable", "compaction-operation-superseded", "compaction-tool-pair-mismatch",
950
- "compaction-retained-context-mismatch", "compaction-wire-adaptation-failed", "compaction-boundary-not-found",
951
- "first-kept-entry-not-found", "compaction-summary-not-found", "retained-context-mismatch", "unverified-compaction-input",
952
- COMPACTION_CHECKPOINT_CONTEXT_EDITED, COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
953
- COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE, COMPACTION_PROJECTION_UNAVAILABLE, COMPACTION_SESSION_CONTEXT_UNAVAILABLE,
954
- ]);
955
-
956
- async function runActiveRemoteManualCompact(
957
- event: SessionBeforeCompactEvent,
958
- ctx: ExtensionContext,
959
- dependencies: CompactionDependencies,
960
- resolved: ResolvedToolkitConfig,
961
- operation: RemoteManualCompactOperation | undefined,
962
- ): Promise<{ cancel: true } | { compaction: CompactionResult<NativeCompactionDetails> }> {
963
- const lifecycle = dependencies.remoteManualCompact;
964
- const config = resolved.config.compaction;
965
- const stop = (reason: string): { cancel: true } => {
966
- // No exception text, provider body, auth or opaque content crosses this boundary.
967
- lifecycle.cancel(operation);
968
- writeDebugArtifact("compaction-event", {
969
- event: "session_before_compact.remote-window-manual-stopped", reason, nextStep: "stop-and-preserve",
970
- }, config, ctx);
971
- notifyWarning(ctx, `Remote /compact stopped (${reason}); existing context preserved. No summary fallback or automatic continuation was started.`);
972
- return { cancel: true };
973
- };
974
- const isCurrent = () => !!operation && lifecycle.current(operation, ctx);
975
- try {
976
- if (!operation || dependencies.manualCompact?.busy || !isCurrent()) return stop("compaction-operation-superseded");
977
- // Producer policy and auth are resolved once from the initiating document.
978
- if (resolved.format === "v2" && config.remoteCompactModel) {
979
- const spec = parseModelSpec(config.remoteCompactModel);
980
- if (spec) {
981
- const producerPolicy = resolveToolkitConfig(resolved.snapshot, { provider: spec.provider, id: spec.modelId });
982
- notifyConfigIssues(ctx, producerPolicy);
983
- if (producerPolicy.invalidFeatures.includes("compatibility")) return stop("invalid-producer-compatibility");
984
- }
985
- }
986
- const resolution = await resolveRemoteCompactionExecution(ctx, {
987
- enabled: config.enabled, responsesApis: config.responsesApis, codexGatewayModels: compactGatewayModels(resolved),
988
- }, config.remoteCompactModel);
989
- if (!isCurrent()) return stop("compaction-operation-superseded");
990
- if (!resolution.ok) return stop(resolution.reason);
991
- const scopedEvent = { ...event, signal: operation.signal };
992
- const input = await prepareActiveRemoteCompaction(scopedEvent, ctx, config, resolution.execution, dependencies, isCurrent);
993
- if (!isCurrent()) return stop("compaction-operation-superseded");
994
- const outcome = await runResponsesNativeCompact(scopedEvent, ctx, config, resolution.execution,
995
- dependencies.remoteCompact, dependencies.projectCompactionContext, undefined, input);
996
- if (!isCurrent()) return stop("compaction-operation-superseded");
997
- if (outcome.outcome !== "success") return stop(outcome.outcome === "unprojected-input" ? outcome.reason
998
- : outcome.outcome === "failed" ? outcome.remoteFailure?.reason ?? "remote-compaction-failed" : "aborted");
999
- const result = lifecycle.arm(operation, ctx, outcome.compaction);
1000
- return result ?? stop("compaction-operation-superseded");
1001
- } catch (error) {
1002
- return stop(error instanceof Error && ACTIVE_REMOTE_PREPARATION_REASONS.has(error.message)
1003
- ? error.message : "remote-compaction-failed");
1004
- }
1005
- }
1006
-
1007
- async function handleSessionBeforeCompact(
1008
- event: SessionBeforeCompactEvent,
1009
- ctx: ExtensionContext,
1010
- dependencies: CompactionDependencies,
1011
- remoteContextActive: RemoteContextActive,
1012
- getContextToolRegistrationState: ContextToolRegistrationStateReader,
1013
- ) {
1014
- const resolved = contextOperation(dependencies.loadConfig, ctx);
1015
- if (resolved.invalidFeatures.some((feature) => ["context", "compatibility", "diagnostics"].includes(feature))) return { cancel: true };
1016
- const config = resolved.config.compaction;
1017
- if (config.enabled) {
1018
- writeDebugArtifact(
1019
- "compaction-event",
1020
- {
1021
- event: "session_before_compact",
1022
- customInstructions: event.customInstructions,
1023
- preparation: {
1024
- tokensBefore: event.preparation.tokensBefore,
1025
- firstKeptEntryId: event.preparation.firstKeptEntryId,
1026
- previousSummaryPresent: Boolean(event.preparation.previousSummary),
1027
- messagesToSummarizeCount: event.preparation.messagesToSummarize.length,
1028
- turnPrefixMessagesCount: event.preparation.turnPrefixMessages.length,
1029
- },
1030
- },
1031
- config,
1032
- ctx,
1033
- );
1034
- }
1035
-
1036
- if (event.signal.aborted) {
1037
- return { cancel: true };
1038
- }
1039
-
1040
- // Window ownership survives model-policy changes, including Pi/disabled mode.
1041
- // Resolve it before any ordinary or default compactor can receive this input.
1042
- try {
1043
- dependencies.contextWindows.synchronize(ctx);
1044
- } catch {
1045
- notifyRemoteContextFailure(ctx, "malformed-window-state");
1046
- return { cancel: true };
1047
- }
1048
-
1049
- // Capture intent BEFORE capability/auth awaits so selection/navigation/work
1050
- // during resolution cannot silently become the owner of an older /compact.
1051
- let manualOperation: RemoteManualCompactOperation | undefined;
1052
- if (event.reason === "manual" && !dependencies.manualCompact?.isMaintenance && config.enabled && config.contextManagement === "remote") {
1053
- try { manualOperation = dependencies.remoteManualCompact.begin(event, ctx, resolved); } catch { /* active route stops below */ }
1054
- }
1055
-
1056
- // Active and inactive covered Codex consumers retain their existing routing.
1057
- const contextModel = config.enabled && config.contextManagement === "remote"
1058
- ? await resolveContextCapabilityModel(ctx)
1059
- : undefined;
1060
- if (manualOperation && !dependencies.remoteManualCompact.current(manualOperation, ctx)) {
1061
- dependencies.remoteManualCompact.cancel(manualOperation);
1062
- return { cancel: true };
1063
- }
1064
- if (isCodexContextModel(contextModel, config)) {
1065
- const active = await remoteContextActive(ctx, config, contextModel);
1066
- // New work may make a formerly active runtime look unavailable. That is
1067
- // supersession, not permission to start the inactive-runtime fallback.
1068
- if (manualOperation && !dependencies.remoteManualCompact.current(manualOperation, ctx)) {
1069
- dependencies.remoteManualCompact.cancel(manualOperation);
1070
- return { cancel: true };
1071
- }
1072
- if (active) {
1073
- try {
1074
- if (event.reason === "manual" && !dependencies.manualCompact?.isMaintenance) {
1075
- return await runActiveRemoteManualCompact(event, ctx, dependencies, resolved, manualOperation);
1076
- }
1077
- return dependencies.contextWindows.prepareCompaction(event);
1078
- } catch {
1079
- notifyRemoteContextFailure(ctx, "malformed-window-state");
1080
- return { cancel: true };
1081
- }
1082
- }
1083
-
1084
- dependencies.remoteManualCompact.cancel(manualOperation);
1085
- const registrationState = getContextToolRegistrationState();
1086
- const failureReason = contextManagementFailureReason(registrationState);
1087
- if (failureReason !== "context-tools-excluded") {
1088
- notifyRemoteContextFailure(ctx, failureReason);
1089
- }
1090
- return runUnavailableContextCompaction(
1091
- event,
1092
- ctx,
1093
- dependencies,
1094
- resolved,
1095
- config,
1096
- registrationState,
1097
- );
1098
- }
1099
-
1100
- dependencies.remoteManualCompact.cancel(manualOperation);
1101
- try {
1102
- const detached = dependencies.contextWindows.prepareDetachedCompaction(event);
1103
- // A local consumer cannot summarize history owned by a Remote window,
1104
- // even after switching modes or disabling Toolkit compaction.
1105
- if (detached) return detached;
1106
- // A boundary already persisted by an earlier detach still owns this session.
1107
- // Retired Remote history is outside Pi's effective projection at that point, so
1108
- // ordinary compaction may continue; record why it no longer detaches.
1109
- const ownedIdentity = dependencies.contextWindows.currentIdentity();
1110
- if (ownedIdentity) {
1111
- writeDebugArtifact(
1112
- "compaction-event",
1113
- {
1114
- event: "session_before_compact.detached-already-owned",
1115
- windowId: ownedIdentity.currentWindowId,
1116
- superseded: dependencies.contextWindows.hasSupersededWindow(),
1117
- },
1118
- config,
1119
- ctx,
1120
- );
1121
- }
1122
- } catch {
1123
- notifyRemoteContextFailure(ctx, "malformed-window-state");
1124
- return { cancel: true };
1125
- }
1126
- if (!config.enabled) return undefined;
1127
-
1128
- // Resolve producer protocol policy from this operation's document, not a later disk read.
1129
- if (resolved.format === "v2" && config.remoteCompactModel) {
1130
- const producer = parseModelSpec(config.remoteCompactModel);
1131
- if (producer) {
1132
- const producerPolicy = resolveToolkitConfig(resolved.snapshot, { provider: producer.provider, id: producer.modelId });
1133
- notifyConfigIssues(ctx, producerPolicy);
1134
- if (producerPolicy.invalidFeatures.includes("compatibility")) return { cancel: true };
1135
- }
1136
- }
1137
- // Branch 1: Responses-family APIs use remote_compaction_v2 on the normal Responses stream.
1138
- let remoteAttempted = false;
1139
- const resolution = await resolveRemoteCompactionExecution(
1140
- ctx,
1141
- {
1142
- enabled: config.enabled,
1143
- responsesApis: config.responsesApis,
1144
- codexGatewayModels: compactGatewayModels(resolved),
1145
- },
1146
- config.remoteCompactModel,
1147
- );
1148
- if (resolution.ok) {
1149
- remoteAttempted = true;
1150
- const responsesOutcome = await runResponsesNativeCompact(
1151
- event,
1152
- ctx,
1153
- config,
1154
- resolution.execution,
1155
- dependencies.remoteCompact,
1156
- dependencies.projectCompactionContext,
1157
- dependencies.standaloneCompact,
1158
- );
1159
- if (responsesOutcome.outcome === "success") {
1160
- return { compaction: responsesOutcome.compaction };
1161
- }
1162
- if (responsesOutcome.outcome === "aborted") {
1163
- return { cancel: true };
1164
- }
1165
- if (responsesOutcome.outcome === "unprojected-input") {
1166
- const message = responsesOutcome.reason === COMPACTION_CHECKPOINT_CONTEXT_EDITED
1167
- ? "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."
1168
- : responsesOutcome.reason === COMPACTION_PROJECTION_UNAVAILABLE
1169
- ? "Pi's ordered context-hook projection is unavailable; Remote V2 was not sent raw session history."
1170
- : responsesOutcome.reason === COMPACTION_SESSION_CONTEXT_UNAVAILABLE
1171
- ? "Pi did not provide the current session context required by its context-hook projection."
1172
- : responsesOutcome.reason === COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE
1173
- ? "Pi's projected context did not include the current compaction summary anchor."
1174
- : "The latest opaque checkpoint has no marker for the configured Remote V2 context source, or was created in the other mode.";
1175
- writeDebugArtifact(
1176
- "compaction-event",
1177
- {
1178
- event: "session_before_compact.remote-v2-unprojected-input",
1179
- reason: responsesOutcome.reason,
1180
- contextSource: config.remoteV2ContextSource,
1181
- inputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
1182
- piContextHookPatch: dependencies.piContextHookPatch,
1183
- message,
1184
- },
1185
- config,
1186
- ctx,
1187
- );
1188
- notifyWarning(
1189
- ctx,
1190
- `Remote V2 compaction cancelled (${responsesOutcome.reason}); ${message} The existing checkpoint and session history were left unchanged.`,
1191
- );
1192
- return { cancel: true };
1193
- }
1194
- // Only an actual exhausted remote attempt warns, never an input/routing skip.
1195
- if (event.signal?.aborted) return { cancel: true };
1196
- if (responsesOutcome.remoteFailure) {
1197
- const failure = responsesOutcome.remoteFailure;
1198
- writeDebugArtifact("compaction-event", {
1199
- event: "session_before_compact.remote-to-native-summary",
1200
- protocol: responsesOutcome.protocol,
1201
- ...failure,
1202
- nextStep: "native-summary",
1203
- }, config, ctx);
1204
- 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}` : ""}`);
1205
- }
1206
- } else {
1207
- writeDebugArtifact(
1208
- "compaction-event",
1209
- {
1210
- event: "session_before_compact.remote-v2-unavailable",
1211
- reason: resolution.reason,
1212
- provider: resolution.provider,
1213
- api: resolution.api,
1214
- model: resolution.model,
1215
- baseUrl: resolution.baseUrl,
1216
- modelSpec: resolution.modelSpec,
1217
- errorMessage: resolution.errorMessage,
1218
- },
1219
- config,
1220
- ctx,
1221
- );
1222
- const expectedConsumerCapabilityMiss =
1223
- resolution.reason === "unsupported-api" && resolution.modelSpec === undefined;
1224
- if (config.remoteCompactModel && !expectedConsumerCapabilityMiss) {
1225
- notifyWarning(
1226
- ctx,
1227
- `remote compaction model "${config.remoteCompactModel}" unusable (${resolution.reason}); using the native fallback chain`,
1228
- );
1229
- }
1230
- }
1231
-
1232
- // Branch 2: run pi's native compaction method. A failed remote request is compacted by the
1233
- // remote producer itself; a model that cannot use remote v2 at all uses nativeFallback.model.
1234
- const fallbackModelSpec = remoteAttempted ? config.remoteCompactModel : config.nativeFallback.model;
1235
- const fallback = await dependencies.nativeFallback({ ctx, event, config, modelSpec: fallbackModelSpec });
1236
- if (fallback.ok) {
1237
- if (ctx.hasUI) {
1238
- ctx.ui.notify(
1239
- `${COMPACTION_EXTENSION_ID}: compacted with ${fallback.model.provider}/${fallback.model.id} (native method)`,
1240
- "info",
1241
- );
1242
- }
1243
- writeDebugArtifact(
1244
- "compaction-event",
1245
- {
1246
- event: "session_before_compact.fallback-success",
1247
- model: fallback.model,
1248
- },
1249
- config,
1250
- ctx,
1251
- );
1252
- return { compaction: fallback.result };
1253
- }
1254
-
1255
- if (fallback.reason === "aborted") {
1256
- return { cancel: true };
1257
- }
1258
-
1259
- writeDebugArtifact(
1260
- "compaction-event",
1261
- {
1262
- event: "session_before_compact.fallback-skip",
1263
- reason: fallback.reason,
1264
- modelSpec: fallback.modelSpec,
1265
- errorMessage: fallback.errorMessage,
1266
- },
1267
- config,
1268
- ctx,
1269
- );
1270
-
1271
- // Intentional pi-default paths: feature disabled, nothing configured, or it matches the current one.
1272
- const intentionalSkip =
1273
- fallback.reason === "disabled" ||
1274
- fallback.reason === "no-model-configured" ||
1275
- fallback.reason === "same-as-current-model";
1276
- if (!intentionalSkip) {
1277
- // An explicit summary model is authoritative: the active model may be
1278
- // unable to summarize at all. Preserve context on failure, never reroute.
1279
- notifyWarning(
1280
- ctx,
1281
- `compaction model "${fallback.modelSpec ?? fallbackModelSpec}" failed (${fallback.reason}${fallback.errorMessage ? `: ${fallback.errorMessage}` : ""}); compaction cancelled, context preserved; not switching to the current model`,
1282
- );
1283
- return { cancel: true };
1284
- }
1285
-
1286
- // Branch 3: pi's default native compaction with the current model.
1287
- return undefined;
1288
- }
1289
-
1290
- function rejectInheritedCheckpointReplay(
1291
- ctx: ExtensionContext,
1292
- config: CompactionConfig,
1293
- compactionEntryId: string,
1294
- reason: string,
1295
- ): undefined {
1296
- writeReplayFailureArtifact({ reason, compactionEntryId }, config, ctx);
1297
- reportPiContextHookFailure(ctx, `replay-failed:${reason}`);
1298
- if (ctx.hasUI) ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: inherited checkpoint replay failed (${reason}); request aborted`, "error");
1299
- ctx.abort();
1300
- return undefined;
1301
- }
1302
-
1303
- async function handleContextInternal(
1304
- event: ContextWithSystemEvent,
1305
- ctx: ExtensionContext,
1306
- pi: ExtensionAPI,
1307
- loadConfig: typeof loadToolkitConfig,
1308
- contextWindows: CodexContextWindowManager,
1309
- remoteContextActive: RemoteContextActive,
1310
- getContextToolRegistrationState: ContextToolRegistrationStateReader,
1311
- ) {
1312
- const resolved = contextOperation(loadConfig, ctx);
1313
- requireContextPolicy(resolved, ctx);
1314
- const config = resolved.config.compaction;
1315
- try {
1316
- contextWindows.synchronize(ctx);
1317
- } catch {
1318
- notifyRemoteContextFailure(ctx, "malformed-window-state");
1319
- reportPiContextHookFailure(ctx, "malformed-window-state");
1320
- ctx.abort();
1321
- return undefined;
1322
- }
1323
-
1324
- let localMessages: AgentMessage[] | undefined;
1325
- let remoteReplayProjection = false;
1326
- // Disabled and intentionally downgraded runtimes never enter opaque replay,
1327
- // but still honor a window already owned by this session.
1328
- let localProjectionOnly = !config.enabled;
1329
- if (config.enabled && config.contextManagement === "remote") {
1330
- const contextModel = await resolveContextCapabilityModel(ctx);
1331
- const remoteActive = await remoteContextActive(ctx, config, contextModel);
1332
- if (remoteActive) {
1333
- try {
1334
- // Measure the provider-visible current window, not Pi's durable transcript.
1335
- // The latter still contains retired remote windows and can be inflated by
1336
- // an upstream error even though this request only carries the live window.
1337
- let projected = contextWindows.project(event.messages, "remote", false);
1338
- if (contextWindows.isPreparingCompaction) {
1339
- // Synthetic preparation is read-only. The caller owns the one strict
1340
- // retained-copy match, then encrypted wire adaptation after all hooks.
1341
- return { messages: projected };
1342
- }
1343
- // Projection may observe the exact queued rollover before Pi persists
1344
- // it. Decide replay ownership from this request, not the old branch.
1345
- const ownsWindow = contextWindows.hasEffectiveWindow() || !contextWindows.currentIdentity();
1346
- if (ownsWindow) projected = contextWindows.project(projected, "remote");
1347
- contextWindows.recordBudget(
1348
- pi,
1349
- ctx,
1350
- true,
1351
- config.contextReminderThresholdPercent,
1352
- );
1353
- reportProjectionDiagnostics(contextWindows, config, ctx);
1354
- if (ownsWindow) {
1355
- return projected.length === event.messages.length && projected.every((message, index) => message === event.messages[index])
1356
- ? undefined
1357
- : { messages: projected };
1358
- }
1359
- localMessages = projected;
1360
- remoteReplayProjection = true;
1361
- } catch (error) {
1362
- notifyRemoteContextFailure(ctx, "malformed-window-state");
1363
- reportPiContextHookFailure(ctx, "malformed-window-state");
1364
- ctx.abort();
1365
- return undefined;
1366
- }
1367
- }
1368
-
1369
- // An unavailable covered runtime must not silently become an ordinary
1370
- // replay consumer. Explicit host exclusion remains the one intentional
1371
- // downgrade; active Remote can replay a newer inherited checkpoint.
1372
- if (!remoteActive && isCodexContextModel(contextModel, config)) {
1373
- const registrationState = getContextToolRegistrationState();
1374
- if (!isExcludedGatewayContext(contextModel, config, registrationState)
1375
- && isCodexGatewayModel(contextModel, config.gatewayContextModels)) {
1376
- const reason = contextManagementFailureReason(registrationState);
1377
- notifyRemoteContextFailure(ctx, reason);
1378
- reportPiContextHookFailure(ctx, reason);
1379
- ctx.abort();
1380
- return undefined;
1381
- }
1382
- localProjectionOnly = true;
1383
- }
1384
- }
1385
-
1386
- if (!remoteReplayProjection) {
1387
- try {
1388
- const projected = contextWindows.project(event.messages, "local");
1389
- localMessages = projected;
1390
- if (localProjectionOnly || contextWindows.hasEffectiveWindow()) {
1391
- // A persisted boundary owns earlier history regardless of the selected
1392
- // mode. Neither disabled policy nor ordinary replay may expand it.
1393
- return projected.length === event.messages.length && projected.every((message, index) => message === event.messages[index])
1394
- ? undefined
1395
- : { messages: projected };
1396
- }
1397
- } catch (error) {
1398
- notifyRemoteContextFailure(ctx, "malformed-window-state");
1399
- reportPiContextHookFailure(ctx, error instanceof Error ? error.message : String(error));
1400
- ctx.abort();
1401
- return undefined;
1402
- }
1403
- }
1404
-
1405
- // Without an effective Remote boundary, the latest checkpoint owns replay.
1406
- // This also covers a newer checkpoint superseding a historical window.
1407
- const visibleMessages = localMessages ?? contextWindows.project(event.messages, "off");
1408
- const finishProjection = (messages: AgentMessage[]) => {
1409
- // Compare canonical retained copies before Remote encrypted-output encoding.
1410
- const projected = remoteReplayProjection ? contextWindows.project(messages, "remote") : messages;
1411
- return projected.length === event.messages.length && projected.every((message, index) => message === event.messages[index])
1412
- ? undefined : { messages: projected };
1413
- };
1414
- const replayEvent = visibleMessages === event.messages ? event : { ...event, messages: visibleMessages };
1415
- const branchEntries = ctx.sessionManager.getBranch();
1416
- const inherited = config.contextManagement === "remote" && contextWindows.hasSupersededWindow()
1417
- ? resolveLatestNativeCompactionEntry(branchEntries) : undefined;
1418
- const inheritedCheckpoint = inherited?.ok ? inherited.entry : undefined;
1419
- const resolution = await resolveNativeCompactionEnvironment(ctx, {
1420
- enabled: config.enabled,
1421
- // Disabling checkpoint generation cannot turn an inherited opaque
1422
- // checkpoint into a usable text summary. Replay checks transport support.
1423
- responsesApis: inheritedCheckpoint ? undefined : config.responsesApis,
1424
- codexGatewayModels: compactGatewayModels(resolved),
1425
- });
1426
- if (!resolution.ok) {
1427
- return inheritedCheckpoint
1428
- ? rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, resolution.reason)
1429
- : finishProjection(visibleMessages);
1430
- }
1431
- const latest = resolveLatestNativeCompactionEntry(branchEntries, { baseUrl: resolution.runtime.baseUrl });
1432
- if (!latest.ok) {
1433
- return inheritedCheckpoint
1434
- ? rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, latest.reason)
1435
- : finishProjection(visibleMessages);
1436
- }
1437
- const result = removeNativeCompactionRetainedMessages({
1438
- messages: replayEvent.messages,
1439
- branchEntries,
1440
- compactionEntry: latest.entry,
1441
- expectedInputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
1442
- });
1443
- if (!result.ok) {
1444
- writeReplayFailureArtifact({ reason: result.reason, compactionEntryId: latest.entry.id }, config, ctx);
1445
- if (ctx.hasUI) ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: replay failed (${result.reason}); request aborted`, "error");
1446
- reportPiContextHookFailure(ctx, `replay-failed:${result.reason}`);
1447
- ctx.abort();
1448
- return undefined;
1449
- }
1450
- return finishProjection(result.messages);
1451
- }
1452
-
1453
- /**
1454
- * Pi's emitContext() catches handler exceptions. Re-throw as usual for Pi, but
1455
- * also report the failure so the compaction projector cannot mistake the
1456
- * caught exception for a successful projection.
1457
- */
1458
- async function handleContext(
1459
- event: ContextWithSystemEvent,
1460
- ctx: ExtensionContext,
1461
- pi: ExtensionAPI,
1462
- loadConfig: typeof loadToolkitConfig,
1463
- contextWindows: CodexContextWindowManager,
1464
- remoteContextActive: RemoteContextActive,
1465
- getContextToolRegistrationState: ContextToolRegistrationStateReader,
1466
- ) {
1467
- try {
1468
- return await handleContextInternal(event, ctx, pi, loadConfig, contextWindows, remoteContextActive, getContextToolRegistrationState);
1469
- } catch (error) {
1470
- reportPiContextHookFailure(ctx, error instanceof Error ? error.message : String(error));
1471
- throw error;
1472
- }
1473
- }
1474
-
1475
- async function handleBeforeProviderRequest(
1476
- event: BeforeProviderRequestEvent,
1477
- ctx: ExtensionContext,
1478
- loadConfig: typeof loadToolkitConfig,
1479
- contextWindows: CodexContextWindowManager,
1480
- remoteContextActive: RemoteContextActive,
1481
- getContextToolRegistrationState: ContextToolRegistrationStateReader,
1482
- ) {
1483
- const resolved = contextOperation(loadConfig, ctx);
1484
- requireContextPolicy(resolved, ctx);
1485
- const toolkitConfig = resolved.config;
1486
- const config = toolkitConfig.compaction;
1487
- try {
1488
- contextWindows.synchronize(ctx);
1489
- } catch {
1490
- notifyRemoteContextFailure(ctx, "malformed-window-state");
1491
- ctx.abort();
1492
- return undefined;
1493
- }
1494
- if (!config.enabled) {
1495
- return undefined;
1496
- }
1497
-
1498
- const contextModel = config.contextManagement === "remote"
1499
- ? await resolveContextCapabilityModel(ctx)
1500
- : undefined;
1501
- const remoteActive = config.contextManagement === "remote" && await remoteContextActive(ctx, config, contextModel);
1502
- let remotePayload: unknown;
1503
- let activeWindowPayload = false;
1504
- if (remoteActive) {
1505
- try {
1506
- remotePayload = contextWindows.rewritePayload(event.payload, ctx);
1507
- activeWindowPayload = contextWindows.hasEffectiveWindow() || !contextWindows.currentIdentity();
1508
- event = { ...event, payload: remotePayload };
1509
- } catch {
1510
- notifyRemoteContextFailure(ctx, "malformed-request-state");
1511
- ctx.abort();
1512
- return undefined;
1513
- }
1514
- }
1515
- if (!remoteActive && isCodexContextModel(contextModel, config)) {
1516
- const registrationState = getContextToolRegistrationState();
1517
- // A readable empty registry is an intentional host exclusion. Do not
1518
- // rewrite gateway identity or abort an ordinary child-session request.
1519
- if (isExcludedGatewayContext(contextModel, config, registrationState)) return undefined;
1520
- // Keep Codex Remote mutually exclusive with the legacy replay pipeline
1521
- // when authentication or tool ownership is unavailable. Gateway traffic
1522
- // cannot continue as an ordinary Responses request because that would
1523
- // silently lose the required CPA session/account selection.
1524
- if (isCodexGatewayModel(contextModel, config.gatewayContextModels)) {
1525
- const reason = contextManagementFailureReason(registrationState);
1526
- notifyRemoteContextFailure(ctx, reason);
1527
- ctx.abort();
1528
- }
1529
- return undefined;
1530
- }
1531
-
1532
- // Do not reintroduce a pre-window opaque checkpoint after local projection.
1533
- // Request affinity/encrypted-output rewriting is reserved for active Remote.
1534
- if (contextWindows.hasEffectiveWindow() && !activeWindowPayload) return undefined;
1535
-
1536
- const branchEntries = ctx.sessionManager.getBranch();
1537
- const inherited = !activeWindowPayload && config.contextManagement === "remote" && contextWindows.hasSupersededWindow()
1538
- ? resolveLatestNativeCompactionEntry(branchEntries) : undefined;
1539
- const inheritedCheckpoint = inherited?.ok ? inherited.entry : undefined;
1540
- const resolution = await resolveNativeCompactionEnvironment(
1541
- ctx,
1542
- {
1543
- enabled: config.enabled,
1544
- responsesApis: inheritedCheckpoint ? undefined : config.responsesApis,
1545
- codexGatewayModels: compactGatewayModels(resolved),
1546
- },
1547
- event.payload,
1548
- );
1549
- if (resolution.ok === false) {
1550
- if (inheritedCheckpoint) return rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, resolution.reason);
1551
- writeDebugArtifact(
1552
- "provider-request",
1553
- {
1554
- event: "before_provider_request.skip",
1555
- reason: resolution.reason,
1556
- provider: resolution.provider,
1557
- api: resolution.api,
1558
- model: resolution.model,
1559
- baseUrl: resolution.baseUrl,
1560
- errorMessage: resolution.errorMessage,
1561
- currentModel: getCurrentModelDebugInfo(ctx),
1562
- payload: event.payload,
1563
- },
1564
- config,
1565
- ctx,
1566
- );
1567
- return remotePayload;
1568
- }
1569
-
1570
- const runtime = resolution.runtime;
1571
- const payload = runtime.payload;
1572
- if (!payload) {
1573
- return undefined;
1574
- }
1575
-
1576
- // Capture compact-relevant request fields (tools, reasoning, ...) for the next
1577
- // synthetic compact request using the active consumer's effective runtime identity.
1578
- // This hook runs before the separate Web Search transform, so injected native search
1579
- // tools are not copied into remote_compaction_v2.
1580
- const webSearchRoute = resolveWebSearchRoute({ model: ctx.model, config: toolkitConfig.webSearch });
1581
- rememberRequestContext(
1582
- payload,
1583
- {
1584
- provider: runtime.provider,
1585
- api: runtime.api,
1586
- model: runtime.requestModel ?? runtime.model,
1587
- baseUrl: runtime.baseUrl,
1588
- sessionId: getSessionId(ctx),
1589
- },
1590
- {
1591
- excludeWebSearchTools: webSearchRoute.route === "standalone-alpha",
1592
- },
1593
- );
1594
-
1595
- // Capture is pre-Web-Search metadata only. An effective Remote window still
1596
- // owns this live payload and must NEVER enter ordinary opaque replay below.
1597
- if (activeWindowPayload) return remotePayload;
1598
-
1599
- const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
1600
- baseUrl: runtime.baseUrl,
1601
- });
1602
- if (!latestNativeCompaction.ok) {
1603
- if (inheritedCheckpoint) return rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, latestNativeCompaction.reason);
1604
- writeDebugArtifact(
1605
- "provider-request",
1606
- {
1607
- event: "before_provider_request.no-native-compaction",
1608
- reason: latestNativeCompaction.reason,
1609
- provider: runtime.provider,
1610
- api: runtime.api,
1611
- model: runtime.model,
1612
- baseUrl: runtime.baseUrl,
1613
- branchEntries: branchEntries.length,
1614
- latestCompactionIndex: latestNativeCompaction.latestCompactionIndex,
1615
- latestCompactionIdentity: getCompactionIdentityDebugInfo(latestNativeCompaction.latestCompaction),
1616
- payload,
1617
- },
1618
- config,
1619
- ctx,
1620
- );
1621
- return remotePayload;
1622
- }
1623
-
1624
- const latestNativeCompactionEntry = latestNativeCompaction.entry;
1625
- const rewrite = rewriteResponsesPayloadWithNativeReplay({
1626
- model: runtime.currentModel,
1627
- payload,
1628
- branchEntries,
1629
- compactionEntry: latestNativeCompactionEntry,
1630
- expectedInputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
1631
- });
1632
- if (!rewrite.ok) {
1633
- writeDebugArtifact(
1634
- "provider-request",
1635
- {
1636
- event: "before_provider_request.rewrite-failed",
1637
- reason: rewrite.reason,
1638
- provider: runtime.provider,
1639
- api: runtime.api,
1640
- model: runtime.model,
1641
- baseUrl: runtime.baseUrl,
1642
- compactionEntryId: latestNativeCompactionEntry.id,
1643
- parity: rewrite.parity,
1644
- payload,
1645
- },
1646
- config,
1647
- ctx,
1648
- );
1649
-
1650
- // Fail loud instead of letting Pi send the sentinel-only payload: the
1651
- // compacted history would be silently lost while the request still succeeds.
1652
- // A forced redacted failure record is written even when logProviderPayloads
1653
- // is disabled so the incident is diagnosable without leaking content.
1654
- writeReplayFailureArtifact(
1655
- {
1656
- reason: rewrite.reason,
1657
- parity: rewrite.parity,
1658
- compactionEntryId: latestNativeCompactionEntry.id,
1659
- provider: runtime.provider,
1660
- api: runtime.api,
1661
- model: runtime.model,
1662
- },
1663
- config,
1664
- ctx,
1665
- );
1666
- if (ctx.hasUI) {
1667
- ctx.ui.notify(
1668
- `${COMPACTION_EXTENSION_ID}: native compaction replay failed (${rewrite.reason}); provider request aborted`,
1669
- "error",
1670
- );
1671
- }
1672
- ctx.abort();
1673
- return undefined;
1674
- }
1675
-
1676
- writeDebugArtifact(
1677
- "provider-request",
1678
- {
1679
- event: "before_provider_request.native-rewrite",
1680
- provider: runtime.provider,
1681
- api: runtime.api,
1682
- model: runtime.model,
1683
- baseUrl: runtime.baseUrl,
1684
- compactionEntryId: latestNativeCompactionEntry.id,
1685
- boundaryIndex: rewrite.segments.boundaryIndex,
1686
- firstKeptEntryIndex: rewrite.segments.firstKeptEntryIndex,
1687
- originalInputItems: payload.input.length,
1688
- rewrittenInputItems: rewrite.rewrittenPayload.input.length,
1689
- leadingItems: rewrite.segments.leading.length,
1690
- compactionSummaryItems: rewrite.segments.compactionSummary.length,
1691
- compactedItems: rewrite.segments.compactedWindow.length,
1692
- postItems: rewrite.segments.post.length,
1693
- payload: rewrite.rewrittenPayload,
1694
- originalPayload: payload,
1695
- },
1696
- config,
1697
- ctx,
1698
- );
1699
-
1700
- return rewrite.rewrittenPayload;
1701
- }
1702
-
1703
- export default function registerCompactionExtension(
1704
- pi: ExtensionAPI,
1705
- overrides: Partial<CompactionDependencies> = {},
1706
- ) {
1707
- const loadConfig = overrides.loadConfig ?? loadToolkitConfig;
1708
- registerToolkitConfigCommand(pi, loadConfig);
1709
- const piContextHookPatch = installPiContextHookPatch();
1710
- const contextWindows = overrides.contextWindows ?? new CodexContextWindowManager((ctx, signal, gatewayModels) =>
1711
- loadHistoryNotesThreadHint(ctx, signal, gatewayModels)
1712
- );
1713
- const manualCompact = new ManagedManualCompact(pi, contextWindows);
1714
- const remoteManualCompact = new RemoteManualCompact(pi, contextWindows);
1715
- const dependencies: CompactionDependencies = {
1716
- remoteCompact: executeRemoteV2Compaction,
1717
- standaloneCompact: executeNativeCompaction,
1718
- nativeFallback: runNativeFallbackCompaction,
1719
- contextWindows,
1720
- ...overrides,
1721
- manualCompact,
1722
- remoteManualCompact,
1723
- loadConfig: () => remoteManualCompact.snapshot ?? manualCompact.snapshot ?? loadConfig(),
1724
- piContextHookPatch: overrides.piContextHookPatch ?? piContextHookPatch,
1725
- };
1726
- let tools!: ReturnType<typeof registerContextManagementTools>;
1727
- let contextWindowReady = false;
1728
- const isContextRuntimeActive = async (
1729
- ctx: ExtensionContext,
1730
- config: CompactionConfig,
1731
- model = ctx.model,
1732
- ): Promise<boolean> =>
1733
- contextWindowReady && tools.isRegistered && await isRemoteContextActive(ctx, config, model);
1734
- tools = registerContextManagementTools(
1735
- pi,
1736
- contextWindows,
1737
- async (ctx) => {
1738
- const resolved = contextOperation(dependencies.loadConfig, ctx);
1739
- assertConfigValid(resolved, "context", "compatibility");
1740
- return { active: await isContextRuntimeActive(ctx, resolved.config.compaction), gatewayModels: resolved.gatewayModelKeys };
1741
- },
1742
- undefined,
1743
- manualCompact,
1744
- );
1745
- const remoteContextActive: RemoteContextActive = isContextRuntimeActive;
1746
- const syncTools = async (
1747
- ctx: ExtensionContext,
1748
- model = ctx.model,
1749
- options: { notifyWindowFailure?: boolean } = {},
1750
- resolved = contextOperation(dependencies.loadConfig, ctx, model),
1751
- ): Promise<ContextToolSyncOutcome> => {
1752
- // Until this call proves otherwise, do not let a previous session/window
1753
- // identity make a failed activation look usable.
1754
- contextWindowReady = false;
1755
- const config = resolved.config.compaction;
1756
- const eligible = !resolved.invalidFeatures.some((feature) => ["context", "compatibility"].includes(feature)) && await isRemoteContextActive(ctx, config, model);
1757
- const toolSync = tools.sync(eligible);
1758
- const outcome: ContextToolSyncOutcome = {
1759
- eligible,
1760
- toolsSynced: toolSync.synced,
1761
- registrationState: toolSync.registrationState,
1762
- windowInitialized: false,
1763
- };
1764
- if (!eligible || !toolSync.synced) return outcome;
1765
- // Activation can succeed after `session_start` missed it, because Pi 0.86 may
1766
- // reject the registration read while a session replacement is still binding.
1767
- // The window lifecycle has to open on that later activation too, or requests
1768
- // carry no window metadata and the backend never ingests the turns.
1769
- try {
1770
- contextWindows.ensureInitialized(pi, ctx, true);
1771
- contextWindowReady = true;
1772
- return { ...outcome, windowInitialized: true };
1773
- } catch {
1774
- // Do not leave tools exposed while the request path has no valid window
1775
- // identity. The next lifecycle hook may retry once Pi is usable again.
1776
- tools.sync(false);
1777
- if (options.notifyWindowFailure) notifyRemoteContextFailure(ctx, "malformed-window-state");
1778
- return outcome;
1779
- }
1780
- };
1781
- pi.on("session_start", async (_event, ctx) => {
1782
- remoteManualCompact.invalidate();
1783
- const resolved = contextOperation(dependencies.loadConfig, ctx);
1784
- const syncOutcome = await syncTools(ctx, ctx.model, { notifyWindowFailure: true }, resolved);
1785
- await manualCompact.recover(ctx);
1786
- const active = syncOutcome.eligible && syncOutcome.toolsSynced && syncOutcome.windowInitialized;
1787
- const { source } = resolved;
1788
- const warnings = resolved.issues.map((issue) => `${issue.path}: ${issue.code}`);
1789
- const config = resolved.config.compaction;
1790
- if (resolved.invalidFeatures.length > 0) return;
1791
- if (!config.enabled) return;
1792
- const contextModel = await resolveContextCapabilityModel(ctx);
1793
-
1794
- let activationReason: string | undefined;
1795
- // Only models the built-in Remote Context coverage targets may activate or
1796
- // warn; everything else silently runs Pi's normal compaction path.
1797
- if (config.contextManagement === "remote" && isCodexContextModel(contextModel, config) && !active) {
1798
- if (syncOutcome.registrationState === "excluded") {
1799
- activationReason = "context-tools-excluded";
1800
- } else if (syncOutcome.registrationState === "conflict") {
1801
- activationReason = "tool-name-conflict";
1802
- notifyRemoteContextFailure(ctx, activationReason);
1803
- } else if (syncOutcome.registrationState === "unverified" || !syncOutcome.toolsSynced) {
1804
- // Transient: the runtime has not published or accepted our tools yet. A
1805
- // later before_agent_start re-verifies, so do not report provider failure.
1806
- activationReason = "tool-registration-pending";
1807
- } else if (syncOutcome.eligible && !syncOutcome.windowInitialized) {
1808
- activationReason = "malformed-window-state";
1809
- } else {
1810
- const remoteResolution = await resolveCodexContextProvider(ctx, ctx.model, config.gatewayContextModels);
1811
- activationReason = remoteResolution.ok ? "codex-context-unavailable" : remoteResolution.reason;
1812
- notifyRemoteContextFailure(ctx, activationReason);
1813
- }
1814
- }
1815
-
1816
-
1817
- const artifactPath = writeDebugArtifact(
1818
- "lifecycle",
1819
- {
1820
- event: "session_start",
1821
- config,
1822
- configSource: source,
1823
- warnings,
1824
- activation: {
1825
- active,
1826
- contextManagement: config.contextManagement,
1827
- remoteV2ContextSource: config.remoteV2ContextSource,
1828
- model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined,
1829
- ...(activationReason ? { reason: activationReason } : {}),
1830
- },
1831
- piContextHookPatch: dependencies.piContextHookPatch,
1832
- },
1833
- config,
1834
- ctx,
1835
- );
1836
-
1837
- if (ctx.hasUI && ["info", "debug"].includes(resolved.policy.diagnostics.level) && (config.notifyOnLoad || config.debug)) {
1838
- ctx.ui.notify(
1839
- artifactPath
1840
- ? `${COMPACTION_EXTENSION_ID} loaded • debug artifacts → ${artifactPath}`
1841
- : `${COMPACTION_EXTENSION_ID} loaded`,
1842
- "info",
1843
- );
1844
- }
1845
- });
1846
-
1847
- const getContextToolRegistrationState: ContextToolRegistrationStateReader = () => tools.registrationState;
1848
- pi.on("context_with_system", (event, ctx) => {
1849
- manualCompact.guardRequest(ctx);
1850
- return handleContext(event, ctx, pi, dependencies.loadConfig, contextWindows, remoteContextActive, getContextToolRegistrationState);
1851
- });
1852
- pi.on("session_before_compact", async (event, ctx) => {
1853
- remoteManualCompact.invalidate();
1854
- try {
1855
- return await handleSessionBeforeCompact(event, ctx, dependencies, remoteContextActive, getContextToolRegistrationState);
1856
- } catch {
1857
- // Pi treats a rejected hook as absent and can invoke its implicit summary.
1858
- // Unexpected preparation/service failures must never authorize that work.
1859
- try { notifyWarning(ctx, "Compaction stopped; existing context preserved. Resume manually."); } catch { /* stale UI */ }
1860
- return { cancel: true };
1861
- }
1862
- });
1863
- pi.on("session_compact", (event, ctx) => {
1864
- contextWindows.synchronize(ctx);
1865
- if (event.reason === "manual" && event.fromExtension) remoteManualCompact.persisted(ctx);
1866
- else remoteManualCompact.invalidate();
1867
- });
1868
- pi.on("session_compact_failed", async (event, ctx) => {
1869
- remoteManualCompact.invalidate();
1870
- if (event.reason === "manual" && !manualCompact.isMaintenance) await manualCompact.compactFailed(ctx);
1871
- });
1872
- pi.on("session_shutdown", async (_event, ctx) => {
1873
- remoteManualCompact.invalidate();
1874
- await manualCompact.shutdown(ctx);
1875
- contextWindowReady = false;
1876
- contextWindows.reset();
1877
- tools.reset();
1878
- });
1879
- pi.on("agent_settled", async (_event, ctx) => {
1880
- // Only the owned manual handoff restores/continues here; Pi owns normal
1881
- // compaction scheduling. Never compact retired windows on model changes.
1882
- await manualCompact.settled(ctx);
1883
- });
1884
- pi.on("model_select", async (event, ctx) => {
1885
- remoteManualCompact.invalidate();
1886
- manualCompact.modelSelected(event.model, ctx);
1887
- // Switching into a covered model mid-session must open the window
1888
- // lifecycle immediately: without an identity the request rewrite skips
1889
- // window metadata, the backend never ingests those turns, and the first
1890
- // new_context would trim pre-switch history that no history can recover.
1891
- // syncTools() activates and initializes the window when the model is covered.
1892
- const resolved = contextOperation(dependencies.loadConfig, ctx, event.model);
1893
- await syncTools(ctx, event.model, { notifyWindowFailure: true }, resolved);
1894
- });
1895
- pi.on("before_agent_start", async (_event, ctx) => {
1896
- remoteManualCompact.invalidate();
1897
- await syncTools(ctx, ctx.model, { notifyWindowFailure: true });
1898
- });
1899
- pi.on("agent_start", () => remoteManualCompact.invalidate());
1900
- pi.on("input", () => { remoteManualCompact.invalidate(); });
1901
- pi.on("thinking_level_select", (event, ctx) => {
1902
- remoteManualCompact.invalidate();
1903
- manualCompact.thinkingSelected(event.level, ctx);
1904
- });
1905
- pi.on("message_start", (event) => {
1906
- remoteManualCompact.invalidate();
1907
- manualCompact.messageStarted(event.message);
1908
- });
1909
- pi.on("turn_end", (_event, ctx) => manualCompact.turnEnded(ctx));
1910
- pi.on("tool_call", (event) => {
1911
- remoteManualCompact.invalidate();
1912
- return manualCompact.guardTool(event.toolName);
1913
- });
1914
- pi.on("session_before_switch", () => remoteManualCompact.invalidate());
1915
- pi.on("session_before_fork", () => remoteManualCompact.invalidate());
1916
- pi.on("session_before_tree", () => remoteManualCompact.invalidate());
1917
- pi.on("session_tree", async (_event, ctx) => {
1918
- remoteManualCompact.invalidate();
1919
- contextWindows.synchronize(ctx);
1920
- await manualCompact.recover(ctx);
1921
- await syncTools(ctx);
1922
- });
1923
- pi.on("before_provider_request", (event, ctx) => {
1924
- manualCompact.guardRequest(ctx);
1925
- return handleBeforeProviderRequest(event, ctx, dependencies.loadConfig, contextWindows, remoteContextActive, getContextToolRegistrationState);
1926
- });
1927
- pi.on("before_provider_headers", async (event, ctx) => {
1928
- const resolved = contextOperation(dependencies.loadConfig, ctx);
1929
- requireContextPolicy(resolved, ctx);
1930
- const config = resolved.config.compaction;
1931
- const contextModel = await resolveContextCapabilityModel(ctx);
1932
- if (!isCodexContextModel(contextModel, config)) return;
1933
- const active = await remoteContextActive(ctx, config, contextModel);
1934
- if (!active) {
1935
- const registrationState = getContextToolRegistrationState();
1936
- if (isExcludedGatewayContext(contextModel, config, registrationState)) return;
1937
- if (isCodexGatewayModel(contextModel, config.gatewayContextModels)) {
1938
- const reason = contextManagementFailureReason(registrationState);
1939
- notifyRemoteContextFailure(ctx, reason);
1940
- ctx.abort();
1941
- }
1942
- return;
1943
- }
1944
- const provider = await resolveCodexContextProvider(ctx, ctx.model, config.gatewayContextModels);
1945
- if (provider.ok && provider.provider.kind === "codex-gateway") {
1946
- const sessionId = getSessionId(ctx);
1947
- const gatewayHeaders = codexContextProviderHeaders(provider.provider, {
1948
- sessionId,
1949
- clientRequestId: sessionId,
1950
- });
1951
- const allowedGatewayHeaders = new Set<string>(CODEX_GATEWAY_FORWARD_HEADERS);
1952
- for (const existing of Object.keys(event.headers)) {
1953
- if (!allowedGatewayHeaders.has(existing.toLowerCase())) delete event.headers[existing];
1954
- }
1955
- // Keep any caller-supplied version without forcing a Toolkit pin.
1956
- // The gateway allowlist above already permits it.
1957
- for (const name of [
1958
- "authorization",
1959
- "originator",
1960
- "user-agent",
1961
- "session-id",
1962
- "x-client-request-id",
1963
- "x-codex-affinity-scope",
1964
- "x-codex-model",
1965
- ]) {
1966
- for (const existing of Object.keys(event.headers)) {
1967
- if (existing.toLowerCase() === name) delete event.headers[existing];
1968
- }
1969
- const value = gatewayHeaders.get(name);
1970
- if (value) event.headers[name] = value;
1971
- }
1972
- }
1973
- contextWindows.rewriteHeaders(event.headers, ctx);
1974
- });
1975
- pi.on("message_end", async (event, ctx) => {
1976
- rememberPhysicalModel(ctx, event.message);
1977
- const resolved = contextOperation(dependencies.loadConfig, ctx);
1978
- requireContextPolicy(resolved, ctx);
1979
- const config = resolved.config.compaction;
1980
- const contextModel = await resolveContextCapabilityModel(ctx);
1981
- if (!isCodexContextModel(contextModel, config) || !tools.isRegistered) return undefined;
1982
- if (!(await remoteContextActive(ctx, config, contextModel))) return undefined;
1983
- const message = routeContextNamespaceToolMessage(event.message);
1984
- return message === event.message ? undefined : { message };
1985
- });
1986
- }
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
3
+ import type {
4
+ BeforeProviderRequestEvent,
5
+ ContextWithSystemEvent,
6
+ CompactionResult,
7
+ ExtensionAPI,
8
+ ExtensionContext,
9
+ SessionBeforeCompactEvent,
10
+ } from "@earendil-works/pi-coding-agent";
11
+ import { assertConfigValid, loadToolkitConfig, resolveToolkitConfig, type ResolvedToolkitConfig } from "./config";
12
+ import { notifyConfigIssues } from "./config/notifications";
13
+ import { registerToolkitConfigCommand } from "./config-command";
14
+ import { writeDebugArtifact, writeReplayFailureArtifact } from "./debug";
15
+ import {
16
+ COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
17
+ COMPACTION_CHECKPOINT_CONTEXT_EDITED,
18
+ COMPACTION_PROJECTION_UNAVAILABLE,
19
+ COMPACTION_SESSION_CONTEXT_UNAVAILABLE,
20
+ COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
21
+ hasVerifiedCompactionInputProvenance,
22
+ type UnprojectedCompactionReason,
23
+ } from "./compaction-projection";
24
+ import { buildResponsesRequestHeaders, CODEX_GATEWAY_FORWARD_HEADERS } from "./responses-headers";
25
+ import { isExactModelAllowed } from "./model-scope";
26
+ import { detectRetiredRemoteWindows, RETIRED_REMOTE_WINDOWS_MESSAGE } from "./retired-remote-windows";
27
+ import { executeNativeCompaction, type NativeCompactionClientResult } from "./compact-client";
28
+ import { qualifiesForStandaloneCompaction, type CompactionErrorInfo } from "./compaction-errors";
29
+ import { resolveLatestNativeCompactionEntry } from "./details-store";
30
+ import {
31
+ getPiContextHookProjector,
32
+ installPiContextHookPatch,
33
+ reportPiContextHookFailure,
34
+ type PiContextHookPatchResult,
35
+ } from "./pi-context-hook";
36
+ import { runNativeFallbackCompaction } from "./native-fallback";
37
+ import {
38
+ rewriteResponsesPayloadWithNativeReplay,
39
+ hasInvalidatedCompactionContext,
40
+ removeNativeCompactionRetainedMessages,
41
+ serializeLiveTailToResponsesInputWithLimits,
42
+ } from "./payload-rewrite";
43
+ import { getCompactionRequestExtras, rememberRequestContext } from "./request-context-cache";
44
+ import { resolveWebSearchRoute } from "./web-search/types";
45
+ import { executeRemoteV2Compaction } from "./remote-v2-client";
46
+ import {
47
+ registerPhysicalModelObserver,
48
+ resolveNativeCompactionEnvironment,
49
+ resolveRemoteCompactionExecution,
50
+ isStandaloneCompactionEligible,
51
+ parseModelSpec,
52
+ type RemoteCompactionExecution,
53
+ } from "./runtime";
54
+ import {
55
+ serializeMessagesToCompactRequestWithLimits,
56
+ serializeMessagesToResponsesInputWithLimits,
57
+ type NativeCompactionRequestBody,
58
+ type ResponsesInputItem,
59
+ } from "./serializer";
60
+ import {
61
+ createNativeCompactionDetails,
62
+ createNativeCompactionResult,
63
+ COMPACTION_EXTENSION_ID,
64
+ LEGACY_NATIVE_COMPACTION_STRATEGY,
65
+ REMOTE_V2_COMPACTION_STRATEGY,
66
+ getLatestDeferredToolCarryover,
67
+ getRemoteV2InputProvenance,
68
+ isNativeCompactionDetails,
69
+ type CompactionConfig,
70
+ type NativeCompactionDetails,
71
+ type NativeCompactionRequestMeta,
72
+ } from "./types";
73
+
74
+ type CompactionContextProjection = (
75
+ messages: readonly AgentMessage[],
76
+ ctx: ExtensionContext,
77
+ ) => readonly AgentMessage[] | undefined | Promise<readonly AgentMessage[] | undefined>;
78
+
79
+ function contextOperation(loadConfig: typeof loadToolkitConfig, ctx: ExtensionContext, model = ctx.model): ResolvedToolkitConfig {
80
+ const loaded = loadConfig();
81
+ const resolved = resolveToolkitConfig(loaded, model);
82
+ notifyConfigIssues(ctx, resolved);
83
+ return resolved;
84
+ }
85
+
86
+ function requireContextPolicy(resolved: ResolvedToolkitConfig, ctx: ExtensionContext): void {
87
+ try {
88
+ assertConfigValid(resolved, "context", "compatibility", "diagnostics");
89
+ } catch (error) {
90
+ if (typeof ctx.abort === "function") ctx.abort();
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ type CompactionDependencies = {
96
+ loadConfig: typeof loadToolkitConfig;
97
+ remoteCompact: typeof executeRemoteV2Compaction;
98
+ standaloneCompact: typeof executeNativeCompaction;
99
+ nativeFallback: typeof runNativeFallbackCompaction;
100
+ /** Revalidate operation ownership after awaits and at result consumption. */
101
+ isCurrentCompaction?: () => boolean;
102
+ /** Test seam for the same ordered projection exposed by the Pi host patch. */
103
+ projectCompactionContext?: CompactionContextProjection;
104
+ piContextHookPatch: PiContextHookPatchResult;
105
+ };
106
+
107
+ // Notices are bounded, but every boundary freshly reads the current branch.
108
+ const retirementNotices = new WeakMap<object, string>();
109
+ function retirementReason(ctx: ExtensionContext): string | undefined {
110
+ try { return detectRetiredRemoteWindows(ctx.sessionManager.getBranch()); }
111
+ catch { return "branch-unavailable"; }
112
+ }
113
+
114
+ function refuseRetiredSession(ctx: ExtensionContext, request = false): boolean {
115
+ const reason = retirementReason(ctx);
116
+ const owner = ctx.sessionManager;
117
+ if (!reason) {
118
+ retirementNotices.delete(owner);
119
+ return false;
120
+ }
121
+ const fingerprint = `${getSessionId(ctx) ?? ""}:${reason}`;
122
+ if (ctx.hasUI && retirementNotices.get(owner) !== fingerprint) {
123
+ retirementNotices.set(owner, fingerprint);
124
+ try {
125
+ ctx.ui.notify(
126
+ reason === "branch-unavailable"
127
+ ? `${COMPACTION_EXTENSION_ID}: Cannot verify the active session branch; request blocked. Reopen the session before continuing.`
128
+ : `${COMPACTION_EXTENSION_ID}: ${RETIRED_REMOTE_WINDOWS_MESSAGE}`,
129
+ "error",
130
+ );
131
+ } catch {
132
+ // A detached UI must not prevent abort or turn compact refusal into fallback.
133
+ }
134
+ }
135
+ if (request) {
136
+ reportPiContextHookFailure(ctx, `retired-remote-windows:${reason}`);
137
+ ctx.abort();
138
+ }
139
+ return true;
140
+ }
141
+
142
+ type ResponsesCompactOutcome =
143
+ | { outcome: "success"; compaction: CompactionResult<NativeCompactionDetails> }
144
+ | { outcome: "aborted" }
145
+ | { outcome: "failed"; remoteFailure?: CompactionErrorInfo & { reason: string; status?: number }; protocol?: string }
146
+ | {
147
+ outcome: "unprojected-input";
148
+ reason: UnprojectedCompactionReason;
149
+ };
150
+
151
+ function buildCompactionRequestMeta(event: SessionBeforeCompactEvent): NativeCompactionRequestMeta {
152
+ return {
153
+ tokensBefore: event.preparation.tokensBefore,
154
+ previousSummaryPresent: Boolean(event.preparation.previousSummary),
155
+ };
156
+ }
157
+
158
+ function getCurrentModelDebugInfo(ctx: ExtensionContext) {
159
+ return ctx.model
160
+ ? {
161
+ provider: ctx.model.provider,
162
+ id: ctx.model.id,
163
+ }
164
+ : undefined;
165
+ }
166
+
167
+ function getCompactionIdentityDebugInfo(entry: { details?: unknown } | undefined) {
168
+ return isNativeCompactionDetails(entry?.details)
169
+ ? {
170
+ provider: entry.details.provider,
171
+ api: entry.details.api,
172
+ model: entry.details.model,
173
+ baseUrl: entry.details.baseUrl,
174
+ compactionModel: entry.details.compactionModel,
175
+ }
176
+ : undefined;
177
+ }
178
+
179
+ function getSessionId(ctx: ExtensionContext): string | undefined {
180
+ try {
181
+ return ctx.sessionManager.getSessionId();
182
+ } catch {
183
+ return undefined;
184
+ }
185
+ }
186
+
187
+ function notifyWarning(ctx: ExtensionContext, message: string): void {
188
+ if (ctx.hasUI) {
189
+ ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: ${message}`, "warning");
190
+ }
191
+ }
192
+
193
+ function cloneOpaqueWindow(window: readonly unknown[]): unknown[] {
194
+ return window.map((item) => structuredClone(item));
195
+ }
196
+
197
+ function buildCompactionInstructions(systemPrompt: string, customInstructions?: string): string {
198
+ const guidance = customInstructions?.trim();
199
+ if (!guidance) {
200
+ return systemPrompt;
201
+ }
202
+
203
+ return `${systemPrompt}\n\nAdditional user guidance for this manual /compact request:\n${guidance}`;
204
+ }
205
+
206
+ function findLatestProjectedCompactionSummaryIndex(
207
+ messages: readonly AgentMessage[],
208
+ summary: string,
209
+ ): number {
210
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
211
+ const message = messages[index];
212
+ if (message?.role === "compactionSummary" && message.summary === summary) {
213
+ return index;
214
+ }
215
+ }
216
+
217
+ return -1;
218
+ }
219
+
220
+ function readSessionContextMessages(ctx: ExtensionContext): AgentMessage[] | undefined {
221
+ try {
222
+ const projection = ctx.sessionManager.buildSessionProjection();
223
+ if (!projection || !Array.isArray(projection.messages)) return undefined;
224
+ return structuredClone(projection.messages);
225
+ } catch {
226
+ return undefined;
227
+ }
228
+ }
229
+
230
+ type CompactionProjectionResult =
231
+ | { ok: true; messages: readonly AgentMessage[] }
232
+ | { ok: false; reason: typeof COMPACTION_PROJECTION_UNAVAILABLE | typeof COMPACTION_SESSION_CONTEXT_UNAVAILABLE };
233
+
234
+ async function projectSessionContextForCompaction(
235
+ ctx: ExtensionContext,
236
+ projectCompactionContext: CompactionContextProjection | undefined,
237
+ ): Promise<CompactionProjectionResult> {
238
+ const messages = readSessionContextMessages(ctx);
239
+ if (!messages) {
240
+ return { ok: false, reason: COMPACTION_SESSION_CONTEXT_UNAVAILABLE };
241
+ }
242
+
243
+ const projector = projectCompactionContext ?? getPiContextHookProjector(ctx);
244
+ if (!projector) {
245
+ return { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
246
+ }
247
+
248
+ try {
249
+ const projected = await projector(messages, ctx);
250
+ return Array.isArray(projected)
251
+ ? { ok: true, messages: projected }
252
+ : { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
253
+ } catch {
254
+ return { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
255
+ }
256
+ }
257
+
258
+ function getLegacySessionContextMessages(
259
+ event: SessionBeforeCompactEvent,
260
+ ctx: ExtensionContext,
261
+ ): AgentMessage[] {
262
+ return readSessionContextMessages(ctx) ?? [
263
+ ...event.preparation.messagesToSummarize,
264
+ ...event.preparation.turnPrefixMessages,
265
+ ];
266
+ }
267
+
268
+ async function runResponsesNativeCompact(
269
+ event: SessionBeforeCompactEvent,
270
+ ctx: ExtensionContext,
271
+ config: CompactionConfig,
272
+ execution: RemoteCompactionExecution,
273
+ remoteCompact: typeof executeRemoteV2Compaction,
274
+ projectCompactionContext: CompactionContextProjection | undefined,
275
+ standaloneCompact: typeof executeNativeCompaction,
276
+ isCurrent: () => boolean,
277
+ ): Promise<ResponsesCompactOutcome> {
278
+ const { consumer, compactor } = execution;
279
+ const instructions = buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions);
280
+ const branchEntries = event.branchEntries ?? ctx.sessionManager.getBranch();
281
+ const deferredToolCarryover = getLatestDeferredToolCarryover(branchEntries);
282
+ const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
283
+ baseUrl: consumer.baseUrl,
284
+ });
285
+
286
+ const inputProvenance = getRemoteV2InputProvenance(config.remoteV2ContextSource);
287
+ let requestSource: "session-context" | "non-native-session-context" | "latest-native-replay";
288
+ let request: NativeCompactionRequestBody;
289
+ if (latestNativeCompaction.ok) {
290
+ if (hasInvalidatedCompactionContext(branchEntries, latestNativeCompaction.entry.id)) {
291
+ return { outcome: "unprojected-input", reason: COMPACTION_CHECKPOINT_CONTEXT_EDITED };
292
+ }
293
+ const details = latestNativeCompaction.entry.details;
294
+ if (!details) {
295
+ return { outcome: "failed" };
296
+ }
297
+ if (!hasVerifiedCompactionInputProvenance(details, inputProvenance)) {
298
+ return {
299
+ outcome: "unprojected-input",
300
+ reason: COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
301
+ };
302
+ }
303
+ requestSource = "latest-native-replay";
304
+
305
+ if (config.remoteV2ContextSource === "pi-context-hook") {
306
+ const projection = await projectSessionContextForCompaction(ctx, projectCompactionContext);
307
+ if (!projection.ok) {
308
+ return { outcome: "unprojected-input", reason: projection.reason };
309
+ }
310
+ const summaryIndex = findLatestProjectedCompactionSummaryIndex(
311
+ projection.messages,
312
+ latestNativeCompaction.entry.summary,
313
+ );
314
+ if (summaryIndex < 0) {
315
+ return {
316
+ outcome: "unprojected-input",
317
+ reason: COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
318
+ };
319
+ }
320
+ const input: ResponsesInputItem[] = [
321
+ ...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
322
+ ...(await serializeMessagesToResponsesInputWithLimits(
323
+ compactor.currentModel,
324
+ [...projection.messages.slice(summaryIndex + 1)],
325
+ { firstSystemMessageIsUpdate: true },
326
+ )),
327
+ ];
328
+ request = {
329
+ model: compactor.requestModel ?? compactor.model,
330
+ input,
331
+ instructions,
332
+ };
333
+ } else {
334
+ const liveTailEntries = branchEntries.slice(latestNativeCompaction.index + 1);
335
+ const input: ResponsesInputItem[] = [
336
+ ...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
337
+ ...(await serializeLiveTailToResponsesInputWithLimits({ model: compactor.currentModel, entries: liveTailEntries })),
338
+ ];
339
+ request = {
340
+ model: compactor.requestModel ?? compactor.model,
341
+ input,
342
+ instructions,
343
+ };
344
+ }
345
+ } else if (
346
+ latestNativeCompaction.reason === "no-compaction" ||
347
+ (latestNativeCompaction.reason === "latest-compaction-not-native" &&
348
+ config.allowCompactionContinuityBreak)
349
+ ) {
350
+ requestSource =
351
+ latestNativeCompaction.reason === "no-compaction" ? "session-context" : "non-native-session-context";
352
+ if (config.remoteV2ContextSource === "pi-context-hook") {
353
+ const projection = await projectSessionContextForCompaction(ctx, projectCompactionContext);
354
+ if (!projection.ok) {
355
+ return { outcome: "unprojected-input", reason: projection.reason };
356
+ }
357
+ const serialized = await serializeMessagesToCompactRequestWithLimits({
358
+ model: compactor.currentModel,
359
+ messages: [...projection.messages],
360
+ instructions,
361
+ });
362
+ request = { ...serialized, model: compactor.requestModel ?? serialized.model };
363
+ } else {
364
+ const serialized = await serializeMessagesToCompactRequestWithLimits({
365
+ model: compactor.currentModel,
366
+ messages: getLegacySessionContextMessages(event, ctx),
367
+ instructions,
368
+ });
369
+ request = { ...serialized, model: compactor.requestModel ?? serialized.model };
370
+ }
371
+ } else {
372
+ writeDebugArtifact(
373
+ "compaction-event",
374
+ {
375
+ event: "session_before_compact.remote-v2-skip",
376
+ reason: latestNativeCompaction.reason,
377
+ consumer: {
378
+ provider: consumer.provider,
379
+ api: consumer.api,
380
+ model: consumer.model,
381
+ baseUrl: consumer.baseUrl,
382
+ },
383
+ compactor: {
384
+ provider: compactor.provider,
385
+ api: compactor.api,
386
+ model: compactor.model,
387
+ baseUrl: compactor.baseUrl,
388
+ },
389
+ latestCompactionIndex: latestNativeCompaction.latestCompactionIndex,
390
+ latestCompactionIdentity: getCompactionIdentityDebugInfo(latestNativeCompaction.latestCompaction),
391
+ },
392
+ config,
393
+ ctx,
394
+ );
395
+ return { outcome: "failed" };
396
+ }
397
+
398
+ // Mirror the latest codex_rs CompactionInput fields captured from the most
399
+ // recent live provider request for this model (tools, reasoning, etc.).
400
+ const extras = getCompactionRequestExtras({
401
+ provider: consumer.provider,
402
+ api: consumer.api,
403
+ model: consumer.requestModel ?? consumer.model,
404
+ baseUrl: consumer.baseUrl,
405
+ sessionId: getSessionId(ctx),
406
+ }, compactor.currentModel);
407
+ if (extras) {
408
+ request = { ...request, ...extras };
409
+ }
410
+
411
+ if (event.signal?.aborted || !isCurrent() || refuseRetiredSession(ctx)) return { outcome: "aborted" };
412
+ let protocol: "remote-v2" | "standalone-compact" = "remote-v2";
413
+ let compactResult: Awaited<ReturnType<typeof executeRemoteV2Compaction>> | NativeCompactionClientResult = await remoteCompact({
414
+ runtime: compactor,
415
+ request,
416
+ signal: event.signal,
417
+ settings: config,
418
+ context: ctx,
419
+ });
420
+
421
+ if (!isCurrent() || refuseRetiredSession(ctx)) return { outcome: "aborted" };
422
+ if (!compactResult.ok) {
423
+ if (compactResult.reason === "aborted" || event.signal?.aborted) return { outcome: "aborted" };
424
+ const tryStandalone = !!standaloneCompact && isStandaloneCompactionEligible(compactor) && qualifiesForStandaloneCompaction(compactResult);
425
+ writeDebugArtifact("compaction-event", {
426
+ event: "session_before_compact.remote-v2-failure",
427
+ protocol,
428
+ reason: compactResult.reason,
429
+ status: compactResult.status,
430
+ errorCode: compactResult.errorCode,
431
+ errorType: compactResult.errorType,
432
+ errorParam: compactResult.errorParam,
433
+ errorMessage: compactResult.errorMessage,
434
+ nextStep: tryStandalone ? "standalone-compact" : "native-summary",
435
+ }, config, ctx);
436
+ if (tryStandalone && standaloneCompact) {
437
+ if (event.signal?.aborted || !isCurrent() || refuseRetiredSession(ctx)) return { outcome: "aborted" };
438
+ protocol = "standalone-compact";
439
+ compactResult = await standaloneCompact({ runtime: compactor, request, signal: event.signal, settings: config, context: ctx });
440
+ }
441
+ if (!compactResult.ok) {
442
+ if (compactResult.reason === "aborted" || event.signal?.aborted) return { outcome: "aborted" };
443
+ const remoteFailure = {
444
+ reason: compactResult.reason,
445
+ status: compactResult.status,
446
+ errorCode: compactResult.errorCode,
447
+ errorType: compactResult.errorType,
448
+ errorParam: compactResult.errorParam,
449
+ errorMessage: compactResult.errorMessage,
450
+ };
451
+ if (protocol === "standalone-compact") writeDebugArtifact("compaction-event", {
452
+ event: "session_before_compact.standalone-compact-failure", protocol, ...remoteFailure, nextStep: "native-summary",
453
+ }, config, ctx);
454
+ return { outcome: "failed", remoteFailure, protocol };
455
+ }
456
+ }
457
+
458
+ let details: NativeCompactionDetails;
459
+ try {
460
+ details = createNativeCompactionDetails({
461
+ strategy: protocol === "standalone-compact" ? LEGACY_NATIVE_COMPACTION_STRATEGY : REMOTE_V2_COMPACTION_STRATEGY,
462
+ provider: consumer.provider,
463
+ api: consumer.api,
464
+ model: consumer.model,
465
+ baseUrl: consumer.baseUrl,
466
+ compactionModel: {
467
+ provider: compactor.provider,
468
+ api: compactor.api,
469
+ model: compactor.model,
470
+ baseUrl: compactor.baseUrl,
471
+ },
472
+ deferredToolCarryover,
473
+ compactedWindow: compactResult.compactedWindow,
474
+ compactResponseId: compactResult.compactResponseId,
475
+ createdAt: compactResult.createdAt,
476
+ requestMeta: buildCompactionRequestMeta(event),
477
+ inputProvenance,
478
+ });
479
+ } catch (error) {
480
+ writeDebugArtifact(
481
+ "compaction-event",
482
+ {
483
+ event: "session_before_compact.invalid-native-details",
484
+ reason: error instanceof Error ? error.message : String(error),
485
+ consumer: {
486
+ provider: consumer.provider,
487
+ api: consumer.api,
488
+ model: consumer.model,
489
+ baseUrl: consumer.baseUrl,
490
+ },
491
+ compactor: {
492
+ provider: compactor.provider,
493
+ api: compactor.api,
494
+ model: compactor.model,
495
+ baseUrl: compactor.baseUrl,
496
+ },
497
+ },
498
+ config,
499
+ ctx,
500
+ );
501
+ return { outcome: "failed", remoteFailure: { reason: "invalid-native-details" }, protocol };
502
+ }
503
+
504
+ const compaction = createNativeCompactionResult({
505
+ firstKeptEntryId: event.preparation.firstKeptEntryId,
506
+ tokensBefore: event.preparation.tokensBefore,
507
+ details,
508
+ });
509
+
510
+ writeDebugArtifact(
511
+ "compaction-event",
512
+ {
513
+ event: `session_before_compact.${protocol}-success`,
514
+ protocol,
515
+ consumer: {
516
+ provider: consumer.provider,
517
+ api: consumer.api,
518
+ model: consumer.model,
519
+ baseUrl: consumer.baseUrl,
520
+ },
521
+ compactor: {
522
+ provider: compactor.provider,
523
+ api: compactor.api,
524
+ model: compactor.model,
525
+ baseUrl: compactor.baseUrl,
526
+ },
527
+ requestSource,
528
+ remoteV2ContextSource: config.remoteV2ContextSource,
529
+ inputProvenance,
530
+ requestInputItems: request.input.length,
531
+ requestExtras: extras ? Object.keys(extras) : [],
532
+ compactResponseId: compactResult.compactResponseId,
533
+ compactedItems: compactResult.compactedWindow.length,
534
+ firstKeptEntryId: compaction.firstKeptEntryId,
535
+ },
536
+ config,
537
+ ctx,
538
+ );
539
+
540
+ if (!isCurrent() || refuseRetiredSession(ctx)) return { outcome: "aborted" };
541
+ return { outcome: "success", compaction };
542
+ }
543
+
544
+ async function handleSessionBeforeCompact(
545
+ event: SessionBeforeCompactEvent,
546
+ ctx: ExtensionContext,
547
+ dependencies: CompactionDependencies,
548
+ ) {
549
+ if (refuseRetiredSession(ctx)) return { cancel: true };
550
+ const resolved = contextOperation(dependencies.loadConfig, ctx);
551
+ if (resolved.invalidFeatures.some((feature) => ["context", "compatibility", "diagnostics"].includes(feature))) return { cancel: true };
552
+ const config = resolved.config.compaction;
553
+ if (config.enabled) {
554
+ writeDebugArtifact(
555
+ "compaction-event",
556
+ {
557
+ event: "session_before_compact",
558
+ customInstructions: event.customInstructions,
559
+ preparation: {
560
+ tokensBefore: event.preparation.tokensBefore,
561
+ firstKeptEntryId: event.preparation.firstKeptEntryId,
562
+ previousSummaryPresent: Boolean(event.preparation.previousSummary),
563
+ messagesToSummarizeCount: event.preparation.messagesToSummarize.length,
564
+ turnPrefixMessagesCount: event.preparation.turnPrefixMessages.length,
565
+ },
566
+ },
567
+ config,
568
+ ctx,
569
+ );
570
+ }
571
+
572
+ if (event.signal.aborted) {
573
+ return { cancel: true };
574
+ }
575
+
576
+ const isCurrent = dependencies.isCurrentCompaction ?? (() => true);
577
+ if (!isCurrent()) return { cancel: true };
578
+ if (!config.enabled || resolved.policy.context.mode !== "remote-compaction") return undefined;
579
+
580
+ // Resolve producer protocol policy from this operation's document, not a later disk read.
581
+ if (resolved.format === "v2" && config.remoteCompactModel) {
582
+ const producer = parseModelSpec(config.remoteCompactModel);
583
+ if (producer) {
584
+ const producerPolicy = resolveToolkitConfig(resolved.snapshot, { provider: producer.provider, id: producer.modelId });
585
+ notifyConfigIssues(ctx, producerPolicy);
586
+ if (producerPolicy.invalidFeatures.includes("compatibility")) return { cancel: true };
587
+ }
588
+ }
589
+ // Branch 1: Responses-family APIs use remote_compaction_v2 on the normal Responses stream.
590
+ let remoteAttempted = false;
591
+ const resolution = await resolveRemoteCompactionExecution(
592
+ ctx,
593
+ {
594
+ enabled: config.enabled,
595
+ responsesApis: config.responsesApis,
596
+ codexGatewayModels: resolved.gatewayModelKeys,
597
+ },
598
+ config.remoteCompactModel,
599
+ );
600
+ if (!isCurrent() || event.signal.aborted || refuseRetiredSession(ctx)) return { cancel: true };
601
+ if (resolution.ok) {
602
+ remoteAttempted = true;
603
+ const responsesOutcome = await runResponsesNativeCompact(
604
+ event,
605
+ ctx,
606
+ config,
607
+ resolution.execution,
608
+ dependencies.remoteCompact,
609
+ dependencies.projectCompactionContext,
610
+ dependencies.standaloneCompact,
611
+ isCurrent,
612
+ );
613
+ if (!isCurrent() || refuseRetiredSession(ctx)) return { cancel: true };
614
+ if (responsesOutcome.outcome === "success") {
615
+ return { compaction: responsesOutcome.compaction };
616
+ }
617
+ if (responsesOutcome.outcome === "aborted") {
618
+ return { cancel: true };
619
+ }
620
+ if (responsesOutcome.outcome === "unprojected-input") {
621
+ const message = responsesOutcome.reason === COMPACTION_CHECKPOINT_CONTEXT_EDITED
622
+ ? "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."
623
+ : responsesOutcome.reason === COMPACTION_PROJECTION_UNAVAILABLE
624
+ ? "Pi's ordered context-hook projection is unavailable; Remote V2 was not sent raw session history."
625
+ : responsesOutcome.reason === COMPACTION_SESSION_CONTEXT_UNAVAILABLE
626
+ ? "Pi did not provide the current session context required by its context-hook projection."
627
+ : responsesOutcome.reason === COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE
628
+ ? "Pi's projected context did not include the current compaction summary anchor."
629
+ : "The latest opaque checkpoint has no marker for the configured Remote V2 context source, or was created in the other mode.";
630
+ writeDebugArtifact(
631
+ "compaction-event",
632
+ {
633
+ event: "session_before_compact.remote-v2-unprojected-input",
634
+ reason: responsesOutcome.reason,
635
+ contextSource: config.remoteV2ContextSource,
636
+ inputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
637
+ piContextHookPatch: dependencies.piContextHookPatch,
638
+ message,
639
+ },
640
+ config,
641
+ ctx,
642
+ );
643
+ notifyWarning(
644
+ ctx,
645
+ `Remote V2 compaction cancelled (${responsesOutcome.reason}); ${message} The existing checkpoint and session history were left unchanged.`,
646
+ );
647
+ return { cancel: true };
648
+ }
649
+ // Only an actual exhausted remote attempt warns, never an input/routing skip.
650
+ if (event.signal?.aborted) return { cancel: true };
651
+ if (responsesOutcome.remoteFailure) {
652
+ const failure = responsesOutcome.remoteFailure;
653
+ writeDebugArtifact("compaction-event", {
654
+ event: "session_before_compact.remote-to-native-summary",
655
+ protocol: responsesOutcome.protocol,
656
+ ...failure,
657
+ nextStep: "native-summary",
658
+ }, config, ctx);
659
+ 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}` : ""}`);
660
+ }
661
+ } else {
662
+ writeDebugArtifact(
663
+ "compaction-event",
664
+ {
665
+ event: "session_before_compact.remote-v2-unavailable",
666
+ reason: resolution.reason,
667
+ provider: resolution.provider,
668
+ api: resolution.api,
669
+ model: resolution.model,
670
+ baseUrl: resolution.baseUrl,
671
+ modelSpec: resolution.modelSpec,
672
+ errorMessage: resolution.errorMessage,
673
+ },
674
+ config,
675
+ ctx,
676
+ );
677
+ const expectedConsumerCapabilityMiss =
678
+ resolution.reason === "unsupported-api" && resolution.modelSpec === undefined;
679
+ if (config.remoteCompactModel && !expectedConsumerCapabilityMiss) {
680
+ notifyWarning(
681
+ ctx,
682
+ `remote compaction model "${config.remoteCompactModel}" unusable (${resolution.reason}); using the native fallback chain`,
683
+ );
684
+ }
685
+ }
686
+
687
+ // Branch 2: run pi's native compaction method. A failed remote request is compacted by the
688
+ // remote producer itself; a model that cannot use remote v2 at all uses nativeFallback.model.
689
+ const fallbackModelSpec = remoteAttempted ? config.remoteCompactModel : config.nativeFallback.model;
690
+ if (!isCurrent() || event.signal.aborted || refuseRetiredSession(ctx)) return { cancel: true };
691
+ const fallback = await dependencies.nativeFallback({ ctx, event, config, modelSpec: fallbackModelSpec, isCurrent });
692
+ if (!isCurrent() || refuseRetiredSession(ctx)) return { cancel: true };
693
+ if (fallback.ok) {
694
+ if (ctx.hasUI) {
695
+ ctx.ui.notify(
696
+ `${COMPACTION_EXTENSION_ID}: compacted with ${fallback.model.provider}/${fallback.model.id} (native method)`,
697
+ "info",
698
+ );
699
+ }
700
+ writeDebugArtifact(
701
+ "compaction-event",
702
+ {
703
+ event: "session_before_compact.fallback-success",
704
+ model: fallback.model,
705
+ },
706
+ config,
707
+ ctx,
708
+ );
709
+ return { compaction: fallback.result };
710
+ }
711
+
712
+ if (fallback.reason === "aborted") {
713
+ return { cancel: true };
714
+ }
715
+
716
+ writeDebugArtifact(
717
+ "compaction-event",
718
+ {
719
+ event: "session_before_compact.fallback-skip",
720
+ reason: fallback.reason,
721
+ modelSpec: fallback.modelSpec,
722
+ errorMessage: fallback.errorMessage,
723
+ },
724
+ config,
725
+ ctx,
726
+ );
727
+
728
+ // Intentional pi-default paths: feature disabled, nothing configured, or it matches the current one.
729
+ const intentionalSkip =
730
+ fallback.reason === "disabled" ||
731
+ fallback.reason === "no-model-configured" ||
732
+ fallback.reason === "same-as-current-model";
733
+ if (!intentionalSkip) {
734
+ // An explicit summary model is authoritative: the active model may be
735
+ // unable to summarize at all. Preserve context on failure, never reroute.
736
+ notifyWarning(
737
+ ctx,
738
+ `compaction model "${fallback.modelSpec ?? fallbackModelSpec}" failed (${fallback.reason}${fallback.errorMessage ? `: ${fallback.errorMessage}` : ""}); compaction cancelled, context preserved; not switching to the current model`,
739
+ );
740
+ return { cancel: true };
741
+ }
742
+
743
+ return undefined;
744
+ }
745
+
746
+ async function handleContextInternal(
747
+ event: ContextWithSystemEvent,
748
+ ctx: ExtensionContext,
749
+ loadConfig: typeof loadToolkitConfig,
750
+ ) {
751
+ if (refuseRetiredSession(ctx, true)) return undefined;
752
+ const resolved = contextOperation(loadConfig, ctx);
753
+ requireContextPolicy(resolved, ctx);
754
+ const config = resolved.config.compaction;
755
+ if (!config.enabled) return undefined;
756
+ const branchEntries = ctx.sessionManager.getBranch();
757
+ const resolution = await resolveNativeCompactionEnvironment(ctx, {
758
+ enabled: config.enabled,
759
+ responsesApis: config.responsesApis,
760
+ codexGatewayModels: resolved.gatewayModelKeys,
761
+ });
762
+ if (refuseRetiredSession(ctx, true)) return undefined;
763
+ if (!resolution.ok) return undefined;
764
+ const latest = resolveLatestNativeCompactionEntry(branchEntries, { baseUrl: resolution.runtime.baseUrl });
765
+ if (!latest.ok) return undefined;
766
+ const result = removeNativeCompactionRetainedMessages({
767
+ messages: event.messages,
768
+ branchEntries,
769
+ compactionEntry: latest.entry,
770
+ expectedInputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
771
+ });
772
+ if (!result.ok) {
773
+ writeReplayFailureArtifact({ reason: result.reason, compactionEntryId: latest.entry.id }, config, ctx);
774
+ if (ctx.hasUI) ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: replay failed (${result.reason}); request aborted`, "error");
775
+ reportPiContextHookFailure(ctx, `replay-failed:${result.reason}`);
776
+ ctx.abort();
777
+ return undefined;
778
+ }
779
+ return result.messages.length === event.messages.length && result.messages.every((message, index) => message === event.messages[index])
780
+ ? undefined : { messages: result.messages };
781
+ }
782
+
783
+ /**
784
+ * Pi's emitContext() catches handler exceptions. Re-throw as usual for Pi, but
785
+ * also report the failure so the compaction projector cannot mistake the
786
+ * caught exception for a successful projection.
787
+ */
788
+ async function handleContext(
789
+ event: ContextWithSystemEvent,
790
+ ctx: ExtensionContext,
791
+ loadConfig: typeof loadToolkitConfig,
792
+ ) {
793
+ try {
794
+ return await handleContextInternal(event, ctx, loadConfig);
795
+ } catch (error) {
796
+ reportPiContextHookFailure(ctx, error instanceof Error ? error.message : String(error));
797
+ throw error;
798
+ }
799
+ }
800
+
801
+ async function handleBeforeProviderRequest(
802
+ event: BeforeProviderRequestEvent,
803
+ ctx: ExtensionContext,
804
+ loadConfig: typeof loadToolkitConfig,
805
+ ) {
806
+ if (refuseRetiredSession(ctx, true)) return undefined;
807
+ const resolved = contextOperation(loadConfig, ctx);
808
+ requireContextPolicy(resolved, ctx);
809
+ const toolkitConfig = resolved.config;
810
+ const config = toolkitConfig.compaction;
811
+ if (!config.enabled) return undefined;
812
+ const resolution = await resolveNativeCompactionEnvironment(ctx, {
813
+ enabled: config.enabled,
814
+ responsesApis: config.responsesApis,
815
+ codexGatewayModels: resolved.gatewayModelKeys,
816
+ }, event.payload);
817
+ if (refuseRetiredSession(ctx, true)) return undefined;
818
+ const branchEntries = ctx.sessionManager.getBranch();
819
+ if (resolution.ok === false) {
820
+ writeDebugArtifact(
821
+ "provider-request",
822
+ {
823
+ event: "before_provider_request.skip",
824
+ reason: resolution.reason,
825
+ provider: resolution.provider,
826
+ api: resolution.api,
827
+ model: resolution.model,
828
+ baseUrl: resolution.baseUrl,
829
+ errorMessage: resolution.errorMessage,
830
+ currentModel: getCurrentModelDebugInfo(ctx),
831
+ payload: event.payload,
832
+ },
833
+ config,
834
+ ctx,
835
+ );
836
+ return undefined;
837
+ }
838
+
839
+ const runtime = resolution.runtime;
840
+ const payload = runtime.payload;
841
+ if (!payload) {
842
+ return undefined;
843
+ }
844
+
845
+ // Capture compact-relevant request fields (tools, reasoning, ...) for the next
846
+ // synthetic compact request using the active consumer's effective runtime identity.
847
+ // This hook runs before the separate Web Search transform, so injected native search
848
+ // tools are not copied into remote_compaction_v2.
849
+ const webSearchRoute = resolveWebSearchRoute({ model: ctx.model, config: toolkitConfig.webSearch });
850
+ rememberRequestContext(
851
+ payload,
852
+ {
853
+ provider: runtime.provider,
854
+ api: runtime.api,
855
+ model: runtime.requestModel ?? runtime.model,
856
+ baseUrl: runtime.baseUrl,
857
+ sessionId: getSessionId(ctx),
858
+ },
859
+ {
860
+ excludeWebSearchTools: webSearchRoute.route === "standalone-alpha",
861
+ },
862
+ );
863
+
864
+ const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
865
+ baseUrl: runtime.baseUrl,
866
+ });
867
+ if (!latestNativeCompaction.ok) {
868
+ writeDebugArtifact(
869
+ "provider-request",
870
+ {
871
+ event: "before_provider_request.no-native-compaction",
872
+ reason: latestNativeCompaction.reason,
873
+ provider: runtime.provider,
874
+ api: runtime.api,
875
+ model: runtime.model,
876
+ baseUrl: runtime.baseUrl,
877
+ branchEntries: branchEntries.length,
878
+ latestCompactionIndex: latestNativeCompaction.latestCompactionIndex,
879
+ latestCompactionIdentity: getCompactionIdentityDebugInfo(latestNativeCompaction.latestCompaction),
880
+ payload,
881
+ },
882
+ config,
883
+ ctx,
884
+ );
885
+ return undefined;
886
+ }
887
+
888
+ const latestNativeCompactionEntry = latestNativeCompaction.entry;
889
+ const rewrite = rewriteResponsesPayloadWithNativeReplay({
890
+ model: runtime.currentModel,
891
+ payload,
892
+ branchEntries,
893
+ compactionEntry: latestNativeCompactionEntry,
894
+ expectedInputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
895
+ });
896
+ if (!rewrite.ok) {
897
+ writeDebugArtifact(
898
+ "provider-request",
899
+ {
900
+ event: "before_provider_request.rewrite-failed",
901
+ reason: rewrite.reason,
902
+ provider: runtime.provider,
903
+ api: runtime.api,
904
+ model: runtime.model,
905
+ baseUrl: runtime.baseUrl,
906
+ compactionEntryId: latestNativeCompactionEntry.id,
907
+ parity: rewrite.parity,
908
+ payload,
909
+ },
910
+ config,
911
+ ctx,
912
+ );
913
+
914
+ // Fail loud instead of letting Pi send the sentinel-only payload: the
915
+ // compacted history would be silently lost while the request still succeeds.
916
+ // A forced redacted failure record is written even when logProviderPayloads
917
+ // is disabled so the incident is diagnosable without leaking content.
918
+ writeReplayFailureArtifact(
919
+ {
920
+ reason: rewrite.reason,
921
+ parity: rewrite.parity,
922
+ compactionEntryId: latestNativeCompactionEntry.id,
923
+ provider: runtime.provider,
924
+ api: runtime.api,
925
+ model: runtime.model,
926
+ },
927
+ config,
928
+ ctx,
929
+ );
930
+ if (ctx.hasUI) {
931
+ ctx.ui.notify(
932
+ `${COMPACTION_EXTENSION_ID}: native compaction replay failed (${rewrite.reason}); provider request aborted`,
933
+ "error",
934
+ );
935
+ }
936
+ ctx.abort();
937
+ return undefined;
938
+ }
939
+
940
+ writeDebugArtifact(
941
+ "provider-request",
942
+ {
943
+ event: "before_provider_request.native-rewrite",
944
+ provider: runtime.provider,
945
+ api: runtime.api,
946
+ model: runtime.model,
947
+ baseUrl: runtime.baseUrl,
948
+ compactionEntryId: latestNativeCompactionEntry.id,
949
+ boundaryIndex: rewrite.segments.boundaryIndex,
950
+ firstKeptEntryIndex: rewrite.segments.firstKeptEntryIndex,
951
+ originalInputItems: payload.input.length,
952
+ rewrittenInputItems: rewrite.rewrittenPayload.input.length,
953
+ leadingItems: rewrite.segments.leading.length,
954
+ compactionSummaryItems: rewrite.segments.compactionSummary.length,
955
+ compactedItems: rewrite.segments.compactedWindow.length,
956
+ postItems: rewrite.segments.post.length,
957
+ payload: rewrite.rewrittenPayload,
958
+ originalPayload: payload,
959
+ },
960
+ config,
961
+ ctx,
962
+ );
963
+
964
+ return rewrite.rewrittenPayload;
965
+ }
966
+
967
+ export default function registerCompactionExtension(
968
+ pi: ExtensionAPI,
969
+ overrides: Partial<CompactionDependencies> = {},
970
+ ) {
971
+ const loadConfig = overrides.loadConfig ?? loadToolkitConfig;
972
+ registerToolkitConfigCommand(pi, loadConfig);
973
+ registerPhysicalModelObserver(pi);
974
+ const piContextHookPatch = installPiContextHookPatch();
975
+ let compactionGeneration = 0;
976
+ const invalidateCompaction = () => { compactionGeneration++; };
977
+ const dependencies: CompactionDependencies = {
978
+ remoteCompact: executeRemoteV2Compaction,
979
+ standaloneCompact: executeNativeCompaction,
980
+ nativeFallback: runNativeFallbackCompaction,
981
+ ...overrides,
982
+ loadConfig,
983
+ piContextHookPatch: overrides.piContextHookPatch ?? piContextHookPatch,
984
+ };
985
+ pi.on("session_start", (_event, ctx) => {
986
+ invalidateCompaction();
987
+ if (refuseRetiredSession(ctx)) return;
988
+ const resolved = contextOperation(loadConfig, ctx);
989
+ const config = resolved.config.compaction;
990
+ if (resolved.invalidFeatures.length || !config.enabled) return;
991
+ const artifactPath = writeDebugArtifact("lifecycle", {
992
+ event: "session_start", config, configSource: resolved.source,
993
+ warnings: resolved.issues.map((issue) => `${issue.path}: ${issue.code}`),
994
+ piContextHookPatch: dependencies.piContextHookPatch,
995
+ }, config, ctx);
996
+ if (ctx.hasUI && ["info", "debug"].includes(resolved.policy.diagnostics.level) && (config.notifyOnLoad || config.debug)) {
997
+ ctx.ui.notify(artifactPath ? `${COMPACTION_EXTENSION_ID} loaded • debug artifacts → ${artifactPath}` : `${COMPACTION_EXTENSION_ID} loaded`, "info");
998
+ }
999
+ });
1000
+ pi.on("session_tree", (_event, ctx) => {
1001
+ invalidateCompaction();
1002
+ refuseRetiredSession(ctx);
1003
+ });
1004
+ pi.on("context_with_system", (event, ctx) => handleContext(event, ctx, loadConfig));
1005
+ pi.on("before_provider_request", (event, ctx) => handleBeforeProviderRequest(event, ctx, loadConfig));
1006
+ pi.on("session_before_compact", async (event, ctx) => {
1007
+ invalidateCompaction();
1008
+ if (refuseRetiredSession(ctx)) return { cancel: true };
1009
+ try {
1010
+ const generation = compactionGeneration;
1011
+ const branch = structuredClone(ctx.sessionManager.getBranch());
1012
+ const sessionId = getSessionId(ctx);
1013
+ const model = ctx.model ? { ...ctx.model } : undefined;
1014
+ const thinking = ctx.thinkingLevel;
1015
+ // Clients classify aborts. This fence handles stale ownership: a
1016
+ // validated V2 terminal still wins over cleanup-triggered cancellation.
1017
+ const isCurrent = () => {
1018
+ try {
1019
+ return generation === compactionGeneration
1020
+ && getSessionId(ctx) === sessionId && isDeepStrictEqual(branch, ctx.sessionManager.getBranch())
1021
+ && isDeepStrictEqual(model, ctx.model) && thinking === ctx.thinkingLevel
1022
+ && !retirementReason(ctx)
1023
+ && !(event.reason === "manual" && typeof ctx.hasPendingMessages === "function" && ctx.hasPendingMessages());
1024
+ } catch { return false; }
1025
+ };
1026
+ const result = await handleSessionBeforeCompact(event, ctx, { ...dependencies, isCurrentCompaction: isCurrent });
1027
+ if (!isCurrent()) return { cancel: true };
1028
+ if (result && "compaction" in result && result.compaction) {
1029
+ // Pi consumes results after later awaited observers. Never persist a
1030
+ // stale candidate or drain/consume the host's queues.
1031
+ return { compaction: result.compaction, get cancel() { return !isCurrent(); } };
1032
+ }
1033
+ if (result === undefined) {
1034
+ // Default summarization starts only after later observers finish too.
1035
+ // Keep the host handoff revocable without claiming its compaction.
1036
+ return { get cancel() { return !isCurrent(); } };
1037
+ }
1038
+ return result;
1039
+ } catch {
1040
+ // Pi can treat a rejected hook as absent and run its implicit summary.
1041
+ try { notifyWarning(ctx, "Compaction stopped; existing context preserved. Resume manually."); } catch { /* stale UI */ }
1042
+ return { cancel: true };
1043
+ }
1044
+ });
1045
+ pi.on("session_compact", invalidateCompaction);
1046
+ pi.on("session_compact_failed", invalidateCompaction);
1047
+ pi.on("session_shutdown", invalidateCompaction);
1048
+ pi.on("model_select", invalidateCompaction);
1049
+ pi.on("before_agent_start", invalidateCompaction);
1050
+ pi.on("agent_start", invalidateCompaction);
1051
+ pi.on("input", invalidateCompaction);
1052
+ pi.on("thinking_level_select", invalidateCompaction);
1053
+ pi.on("message_start", invalidateCompaction);
1054
+ pi.on("tool_call", invalidateCompaction);
1055
+ pi.on("session_before_switch", invalidateCompaction);
1056
+ pi.on("session_before_fork", invalidateCompaction);
1057
+ pi.on("session_before_tree", invalidateCompaction);
1058
+
1059
+ // Exact gateway compatibility is shared transport policy, independent of
1060
+ // compaction enablement. No window identities or ingest headers are generated.
1061
+ pi.on("before_provider_headers", async (event, ctx) => {
1062
+ if (refuseRetiredSession(ctx, true)) return;
1063
+ const resolved = contextOperation(loadConfig, ctx);
1064
+ requireContextPolicy(resolved, ctx);
1065
+ if (!resolved.gatewayModelKeys.length) return;
1066
+ const resolution = await resolveNativeCompactionEnvironment(ctx, {
1067
+ responsesApis: ["openai-responses"], codexGatewayModels: resolved.gatewayModelKeys,
1068
+ });
1069
+ if (refuseRetiredSession(ctx, true)) return;
1070
+ if (!resolution.ok) {
1071
+ const selected = isExactModelAllowed(ctx.model, resolved.gatewayModelKeys);
1072
+ const physical = resolution.provider && resolution.model
1073
+ ? isExactModelAllowed({ provider: resolution.provider, id: resolution.model }, resolved.gatewayModelKeys) : false;
1074
+ if (selected || physical) {
1075
+ notifyWarning(ctx, "Configured gateway transport is unavailable; provider request aborted.");
1076
+ ctx.abort();
1077
+ }
1078
+ return;
1079
+ }
1080
+ const runtime = resolution.runtime;
1081
+ if (!runtime.codexAffinity) return;
1082
+ const gatewayHeaders = buildResponsesRequestHeaders(runtime, { accept: "text/event-stream" });
1083
+ const allowed = new Set<string>(CODEX_GATEWAY_FORWARD_HEADERS);
1084
+ for (const existing of Object.keys(event.headers)) {
1085
+ if (!allowed.has(existing.toLowerCase())) delete event.headers[existing];
1086
+ }
1087
+ for (const name of ["authorization", "originator", "user-agent", "session-id", "x-client-request-id", "x-codex-affinity-scope", "x-codex-model"]) {
1088
+ for (const existing of Object.keys(event.headers)) {
1089
+ if (existing.toLowerCase() === name) delete event.headers[existing];
1090
+ }
1091
+ const value = gatewayHeaders.get(name);
1092
+ if (value) event.headers[name] = value;
1093
+ }
1094
+ });
1095
+ }