pi-openai-toolkit 0.14.4 → 0.14.6

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 (43) hide show
  1. package/README.md +119 -147
  2. package/README.zh.md +119 -151
  3. package/docs/internals.md +42 -21
  4. package/package.json +127 -127
  5. package/src/auto-mode/classifier.ts +165 -165
  6. package/src/auto-mode/policy.ts +146 -146
  7. package/src/auto-mode/prompt.ts +279 -279
  8. package/src/auto-mode/reviewer.ts +325 -325
  9. package/src/auto-mode/transcript.ts +133 -133
  10. package/src/auto-mode/types.ts +137 -137
  11. package/src/codex-astra/effort-planner.ts +175 -175
  12. package/src/codex-astra/extension.ts +122 -122
  13. package/src/config.ts +621 -592
  14. package/src/context-management/codex-provider.ts +260 -260
  15. package/src/context-management/history-notes.ts +357 -325
  16. package/src/context-management/messages.ts +112 -106
  17. package/src/context-management/namespace-tools.ts +426 -426
  18. package/src/context-management/tool-contract.ts +23 -23
  19. package/src/context-management/tools.ts +306 -311
  20. package/src/context-management/types.ts +168 -161
  21. package/src/context-management/window-budget.ts +83 -83
  22. package/src/context-management/window-manager.ts +60 -51
  23. package/src/context-management/window-request.ts +125 -125
  24. package/src/debug.ts +1 -1
  25. package/src/extension-runtime.ts +34 -9
  26. package/src/image-generation/artifacts.ts +237 -237
  27. package/src/image-generation/client.ts +248 -248
  28. package/src/image-generation/eligibility.ts +18 -18
  29. package/src/image-generation/extension.ts +19 -2
  30. package/src/image-generation/protocol.ts +473 -405
  31. package/src/image-generation/references.ts +194 -194
  32. package/src/image-generation/render.ts +252 -252
  33. package/src/image-generation/service.ts +249 -243
  34. package/src/image-generation/types.ts +189 -184
  35. package/src/model-scope.ts +17 -17
  36. package/src/provider-headers.ts +23 -23
  37. package/src/responses-headers.ts +173 -108
  38. package/src/runtime.ts +513 -477
  39. package/src/types.ts +11 -0
  40. package/src/web-search/extension.ts +80 -80
  41. package/src/web-search/payload.ts +96 -96
  42. package/src/web-search/prompt.ts +20 -20
  43. package/src/web-search/types.ts +45 -45
@@ -31,8 +31,13 @@ import {
31
31
  isNonEmptyString,
32
32
  isRecord,
33
33
  isContextWindowCompactionDetails,
34
+ type NotesCheckpointReceipt,
34
35
  } from "./types";
35
- import { encodeEncryptedOutputForContext, loadHistoryNotesThreadHint } from "./history-notes";
36
+ import {
37
+ encodeEncryptedOutputForContext,
38
+ isSuccessfulHistoryNotesToolResult,
39
+ loadHistoryNotesThreadHint,
40
+ } from "./history-notes";
36
41
  import { rewriteContextNamespaceTools } from "./namespace-tools";
37
42
 
38
43
  interface StartContextWindowOptions {
@@ -171,6 +176,9 @@ export class CodexContextWindowManager {
171
176
  this.rolloverPending = true;
172
177
  try {
173
178
  const current = this.identity;
179
+ const checkpoint = current
180
+ ? findLatestNotesCheckpointSinceBoundary(ctx.sessionManager.getBranch(), this.sessionId)
181
+ : undefined;
174
182
  let threadHint: string | undefined;
175
183
  if (current) {
176
184
  try {
@@ -189,7 +197,7 @@ export class CodexContextWindowManager {
189
197
  windowNumber: current.windowNumber + 1,
190
198
  }
191
199
  : { firstWindowId: currentWindowId, currentWindowId, windowNumber: 0 };
192
- this.sendWindowMessage(pi, ctx, next, options, threadHint);
200
+ this.sendWindowMessage(pi, ctx, next, options, threadHint, checkpoint);
193
201
  return true;
194
202
  } catch (error) {
195
203
  this.rolloverPending = false;
@@ -289,10 +297,11 @@ export class CodexContextWindowManager {
289
297
  identity: ContextWindowIdentity,
290
298
  options: StartContextWindowOptions,
291
299
  threadHint?: string,
300
+ checkpoint?: NotesCheckpointReceipt,
292
301
  ): void {
293
302
  sendContextWindowMessage(
294
303
  pi,
295
- renderContextWindowMessage(identity, threadHint),
304
+ renderContextWindowMessage(identity, threadHint, checkpoint),
296
305
  "window",
297
306
  identity,
298
307
  { triggerTurn: options.triggerTurn, sessionId: ctx.sessionManager.getSessionId() },
@@ -329,20 +338,7 @@ export function hasAssistantUsageSinceWindowBoundary(
329
338
  entries: readonly SessionEntry[],
330
339
  sessionId?: string,
331
340
  ): boolean {
332
- let boundaryIndex = -1;
333
- for (let index = entries.length - 1; index >= 0; index -= 1) {
334
- const entry = entries[index]!;
335
- if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
336
- if (!couldBelongToSession(entry.details, sessionId)) continue;
337
- if (!isCodexContextManagementMessageDetails(entry.details)) {
338
- throw new Error("Malformed persisted Codex context-window message");
339
- }
340
- if (!matchesSession(entry.details.sessionId, sessionId)) continue;
341
- if (entry.details.contextManagement.kind === "window") {
342
- boundaryIndex = index;
343
- break;
344
- }
345
- }
341
+ const boundaryIndex = findLatestWindowBoundaryIndex(entries, sessionId);
346
342
  if (boundaryIndex < 0) return false;
347
343
  for (let index = boundaryIndex + 1; index < entries.length; index += 1) {
348
344
  const entry = entries[index]!;
@@ -363,29 +359,18 @@ export function hasAssistantUsageSinceWindowBoundary(
363
359
  }
364
360
 
365
361
  /**
366
- * Whether the branch contains a successful notes append/write result after the
367
- * latest window boundary. Reads and failed writes never count as checkpoints.
362
+ * Return the most recent successful notes write after the latest window
363
+ * boundary. The receipt is derived from persisted session entries so it
364
+ * survives restarts and can be used for an explicit rollover handoff.
368
365
  */
369
- export function findNotesCheckpointSinceBoundary(
366
+ export function findLatestNotesCheckpointSinceBoundary(
370
367
  entries: readonly SessionEntry[],
371
368
  sessionId?: string,
372
- ): boolean {
373
- let boundaryIndex = -1;
374
- for (let index = entries.length - 1; index >= 0; index -= 1) {
375
- const entry = entries[index]!;
376
- if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
377
- if (!couldBelongToSession(entry.details, sessionId)) continue;
378
- if (!isCodexContextManagementMessageDetails(entry.details)) {
379
- throw new Error("Malformed persisted Codex context-window message");
380
- }
381
- if (!matchesSession(entry.details.sessionId, sessionId)) continue;
382
- if (entry.details.contextManagement.kind === "window") {
383
- boundaryIndex = index;
384
- break;
385
- }
386
- }
387
- if (boundaryIndex < 0) return false;
388
- const checkpointCalls = new Set<string>();
369
+ ): NotesCheckpointReceipt | undefined {
370
+ const boundaryIndex = findLatestWindowBoundaryIndex(entries, sessionId);
371
+ if (boundaryIndex < 0) return undefined;
372
+ const checkpointCalls = new Map<string, NotesCheckpointReceipt>();
373
+ let latest: NotesCheckpointReceipt | undefined;
389
374
  for (let index = boundaryIndex + 1; index < entries.length; index += 1) {
390
375
  const entry = entries[index]!;
391
376
  if (entry.type !== "message") continue;
@@ -393,28 +378,52 @@ export function findNotesCheckpointSinceBoundary(
393
378
  if (message.role === "assistant") {
394
379
  const parts = Array.isArray(message.content) ? message.content : [];
395
380
  for (const part of parts) {
396
- if (!isRecord(part) || part.type !== "toolCall") continue;
397
- // Match by call id only; the provider-side namespace rewrite never
398
- // affects the names persisted in the Pi session branch.
399
- if (part.name !== "notes") continue;
381
+ if (!isRecord(part) || part.type !== "toolCall" || part.name !== "notes") continue;
400
382
  const args = isRecord(part.arguments) ? part.arguments : undefined;
401
383
  const action = typeof args?.action === "string" ? args.action : "";
402
- if (typeof part.id === "string" && NOTES_CHECKPOINT_ACTIONS.has(action)) {
403
- checkpointCalls.add(part.id);
384
+ const path = isNonEmptyString(args?.path) ? args.path : undefined;
385
+ if (isNonEmptyString(part.id) && path !== undefined && NOTES_CHECKPOINT_ACTIONS.has(action)) {
386
+ checkpointCalls.set(part.id, { path, toolCallId: part.id });
404
387
  }
405
388
  }
406
389
  continue;
407
390
  }
408
- if (message.role === "toolResult" && checkpointCalls.has(message.toolCallId)) {
409
- if (message.isError) {
410
- checkpointCalls.delete(message.toolCallId);
411
- continue;
412
- }
413
- const details = message.details;
414
- if (isRecord(details) && isRecord(details.codexHistoryNotes)) return true;
391
+ if (message.role !== "toolResult" || message.toolName !== "notes") continue;
392
+ const receipt = checkpointCalls.get(message.toolCallId);
393
+ if (!receipt) continue;
394
+ checkpointCalls.delete(message.toolCallId);
395
+ if (message.isError !== false || !isSuccessfulHistoryNotesToolResult(message.details)) continue;
396
+ latest = receipt;
397
+ }
398
+ return latest;
399
+ }
400
+
401
+ /**
402
+ * Whether the branch contains a successful notes append/write result after the
403
+ * latest window boundary. Reads and failed writes never count as checkpoints.
404
+ */
405
+ export function findNotesCheckpointSinceBoundary(
406
+ entries: readonly SessionEntry[],
407
+ sessionId?: string,
408
+ ): boolean {
409
+ return findLatestNotesCheckpointSinceBoundary(entries, sessionId) !== undefined;
410
+ }
411
+
412
+ function findLatestWindowBoundaryIndex(
413
+ entries: readonly SessionEntry[],
414
+ sessionId?: string,
415
+ ): number {
416
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
417
+ const entry = entries[index]!;
418
+ if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
419
+ if (!couldBelongToSession(entry.details, sessionId)) continue;
420
+ if (!isCodexContextManagementMessageDetails(entry.details)) {
421
+ throw new Error("Malformed persisted Codex context-window message");
415
422
  }
423
+ if (!matchesSession(entry.details.sessionId, sessionId)) continue;
424
+ if (entry.details.contextManagement.kind === "window") return index;
416
425
  }
417
- return false;
426
+ return -1;
418
427
  }
419
428
 
420
429
  export function findLatestWindowBoundaryEntry(
@@ -1,125 +1,125 @@
1
- import type { ProviderHeaders } from "@earendil-works/pi-ai";
2
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import type { ContextWindowIdentity } from "./types";
4
- import { CODEX_ENCRYPTED_OUTPUT_CONTEXT_MARKER } from "./history-notes";
5
- import { rewriteContextNamespaceCalls } from "./namespace-tools";
6
-
7
- export type ContextWindowRequestMetadata = {
8
- session_id: string;
9
- thread_id: string;
10
- agent_name: "/root";
11
- window_id: string;
12
- window_number: number;
13
- context_window_id: string;
14
- request_kind: "turn";
15
- history_ingest_requested: true;
16
- };
17
-
18
- export function buildContextWindowRequestMetadata(
19
- ctx: Pick<ExtensionContext, "sessionManager">,
20
- identity: ContextWindowIdentity,
21
- ): ContextWindowRequestMetadata {
22
- const sessionId = ctx.sessionManager.getSessionId();
23
- return {
24
- session_id: sessionId,
25
- thread_id: sessionId,
26
- agent_name: "/root",
27
- window_id: `${sessionId}:${identity.windowNumber}`,
28
- window_number: identity.windowNumber,
29
- context_window_id: identity.currentWindowId,
30
- request_kind: "turn",
31
- history_ingest_requested: true,
32
- };
33
- }
34
-
35
- export function rewriteWindowPayload(
36
- payload: unknown,
37
- ctx: Pick<ExtensionContext, "sessionManager">,
38
- identity: ContextWindowIdentity | undefined,
39
- ): unknown {
40
- if (!identity || !isResponsesPayload(payload)) return payload;
41
- const clientMetadata = payload.client_metadata;
42
- if (clientMetadata !== undefined && !isRecord(clientMetadata)) return payload;
43
- const existingTurnMetadata = parseTurnMetadata(clientMetadata?.["x-codex-turn-metadata"]);
44
- if (clientMetadata?.["x-codex-turn-metadata"] !== undefined && !existingTurnMetadata) return payload;
45
- const metadata = buildContextWindowRequestMetadata(ctx, identity);
46
- const turnMetadata = { ...existingTurnMetadata, ...metadata };
47
- const rewritten: Record<string, unknown> = {
48
- ...payload,
49
- client_metadata: {
50
- ...(clientMetadata ?? {}),
51
- "x-codex-window-id": metadata.window_id,
52
- "x-codex-turn-metadata": JSON.stringify(turnMetadata),
53
- },
54
- };
55
- return rewriteEncryptedToolOutputs(rewriteContextNamespaceCalls(rewritten));
56
- }
57
-
58
- export function rewriteEncryptedToolOutputs(payload: unknown): unknown {
59
- if (!isRecord(payload) || !Array.isArray(payload.input)) return payload;
60
- let changed = false;
61
- const input = payload.input.map((item) => {
62
- if (!isRecord(item) || item.type !== "function_call_output") return item;
63
- const output = item.output;
64
- if (typeof output === "string") {
65
- const encrypted = decodeEncryptedOutputContext(output);
66
- if (encrypted === undefined) return item;
67
- changed = true;
68
- return { ...item, output: [{ type: "encrypted_content", encrypted_content: encrypted }] };
69
- }
70
- if (!Array.isArray(output)) return item;
71
- let outputChanged = false;
72
- const rewrittenOutput = output.map((part) => {
73
- if (!isRecord(part) || part.type !== "input_text" || typeof part.text !== "string") return part;
74
- const encrypted = decodeEncryptedOutputContext(part.text);
75
- if (encrypted === undefined) return part;
76
- outputChanged = true;
77
- return { type: "encrypted_content", encrypted_content: encrypted };
78
- });
79
- if (!outputChanged) return item;
80
- changed = true;
81
- return { ...item, output: rewrittenOutput };
82
- });
83
- return changed ? { ...payload, input } : payload;
84
- }
85
-
86
- function decodeEncryptedOutputContext(value: string): string | undefined {
87
- if (!value.startsWith(CODEX_ENCRYPTED_OUTPUT_CONTEXT_MARKER)) return undefined;
88
- try {
89
- const decoded: unknown = JSON.parse(value.slice(CODEX_ENCRYPTED_OUTPUT_CONTEXT_MARKER.length));
90
- if (typeof decoded !== "string" || decoded.length === 0) throw new Error("invalid encrypted output marker");
91
- return decoded;
92
- } catch {
93
- throw new Error("Malformed encrypted history/notes output marker");
94
- }
95
- }
96
-
97
- function parseTurnMetadata(value: unknown): Record<string, unknown> | undefined {
98
- if (value === undefined) return {};
99
- if (typeof value !== "string") return undefined;
100
- try {
101
- const parsed: unknown = JSON.parse(value);
102
- return isRecord(parsed) ? parsed : undefined;
103
- } catch {
104
- return undefined;
105
- }
106
- }
107
-
108
- function isResponsesPayload(value: unknown): value is Record<string, unknown> & { model: string; input: unknown[] } {
109
- return isRecord(value) && typeof value.model === "string" && Array.isArray(value.input);
110
- }
111
-
112
- export function rewriteWindowHeaders(
113
- headers: ProviderHeaders,
114
- ctx: Pick<ExtensionContext, "sessionManager">,
115
- identity: ContextWindowIdentity | undefined,
116
- ): void {
117
- if (!identity) return;
118
- const metadata = buildContextWindowRequestMetadata(ctx, identity);
119
- headers["x-codex-window-id"] = metadata.window_id;
120
- headers["x-codex-turn-metadata"] = JSON.stringify(metadata);
121
- }
122
-
123
- function isRecord(value: unknown): value is Record<string, unknown> {
124
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
125
- }
1
+ import type { ProviderHeaders } from "@earendil-works/pi-ai";
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import type { ContextWindowIdentity } from "./types";
4
+ import { CODEX_ENCRYPTED_OUTPUT_CONTEXT_MARKER } from "./history-notes";
5
+ import { rewriteContextNamespaceCalls } from "./namespace-tools";
6
+
7
+ export type ContextWindowRequestMetadata = {
8
+ session_id: string;
9
+ thread_id: string;
10
+ agent_name: "/root";
11
+ window_id: string;
12
+ window_number: number;
13
+ context_window_id: string;
14
+ request_kind: "turn";
15
+ history_ingest_requested: true;
16
+ };
17
+
18
+ export function buildContextWindowRequestMetadata(
19
+ ctx: Pick<ExtensionContext, "sessionManager">,
20
+ identity: ContextWindowIdentity,
21
+ ): ContextWindowRequestMetadata {
22
+ const sessionId = ctx.sessionManager.getSessionId();
23
+ return {
24
+ session_id: sessionId,
25
+ thread_id: sessionId,
26
+ agent_name: "/root",
27
+ window_id: `${sessionId}:${identity.windowNumber}`,
28
+ window_number: identity.windowNumber,
29
+ context_window_id: identity.currentWindowId,
30
+ request_kind: "turn",
31
+ history_ingest_requested: true,
32
+ };
33
+ }
34
+
35
+ export function rewriteWindowPayload(
36
+ payload: unknown,
37
+ ctx: Pick<ExtensionContext, "sessionManager">,
38
+ identity: ContextWindowIdentity | undefined,
39
+ ): unknown {
40
+ if (!identity || !isResponsesPayload(payload)) return payload;
41
+ const clientMetadata = payload.client_metadata;
42
+ if (clientMetadata !== undefined && !isRecord(clientMetadata)) return payload;
43
+ const existingTurnMetadata = parseTurnMetadata(clientMetadata?.["x-codex-turn-metadata"]);
44
+ if (clientMetadata?.["x-codex-turn-metadata"] !== undefined && !existingTurnMetadata) return payload;
45
+ const metadata = buildContextWindowRequestMetadata(ctx, identity);
46
+ const turnMetadata = { ...existingTurnMetadata, ...metadata };
47
+ const rewritten: Record<string, unknown> = {
48
+ ...payload,
49
+ client_metadata: {
50
+ ...(clientMetadata ?? {}),
51
+ "x-codex-window-id": metadata.window_id,
52
+ "x-codex-turn-metadata": JSON.stringify(turnMetadata),
53
+ },
54
+ };
55
+ return rewriteEncryptedToolOutputs(rewriteContextNamespaceCalls(rewritten));
56
+ }
57
+
58
+ export function rewriteEncryptedToolOutputs(payload: unknown): unknown {
59
+ if (!isRecord(payload) || !Array.isArray(payload.input)) return payload;
60
+ let changed = false;
61
+ const input = payload.input.map((item) => {
62
+ if (!isRecord(item) || item.type !== "function_call_output") return item;
63
+ const output = item.output;
64
+ if (typeof output === "string") {
65
+ const encrypted = decodeEncryptedOutputContext(output);
66
+ if (encrypted === undefined) return item;
67
+ changed = true;
68
+ return { ...item, output: [{ type: "encrypted_content", encrypted_content: encrypted }] };
69
+ }
70
+ if (!Array.isArray(output)) return item;
71
+ let outputChanged = false;
72
+ const rewrittenOutput = output.map((part) => {
73
+ if (!isRecord(part) || part.type !== "input_text" || typeof part.text !== "string") return part;
74
+ const encrypted = decodeEncryptedOutputContext(part.text);
75
+ if (encrypted === undefined) return part;
76
+ outputChanged = true;
77
+ return { type: "encrypted_content", encrypted_content: encrypted };
78
+ });
79
+ if (!outputChanged) return item;
80
+ changed = true;
81
+ return { ...item, output: rewrittenOutput };
82
+ });
83
+ return changed ? { ...payload, input } : payload;
84
+ }
85
+
86
+ function decodeEncryptedOutputContext(value: string): string | undefined {
87
+ if (!value.startsWith(CODEX_ENCRYPTED_OUTPUT_CONTEXT_MARKER)) return undefined;
88
+ try {
89
+ const decoded: unknown = JSON.parse(value.slice(CODEX_ENCRYPTED_OUTPUT_CONTEXT_MARKER.length));
90
+ if (typeof decoded !== "string" || decoded.length === 0) throw new Error("invalid encrypted output marker");
91
+ return decoded;
92
+ } catch {
93
+ throw new Error("Malformed encrypted history/notes output marker");
94
+ }
95
+ }
96
+
97
+ function parseTurnMetadata(value: unknown): Record<string, unknown> | undefined {
98
+ if (value === undefined) return {};
99
+ if (typeof value !== "string") return undefined;
100
+ try {
101
+ const parsed: unknown = JSON.parse(value);
102
+ return isRecord(parsed) ? parsed : undefined;
103
+ } catch {
104
+ return undefined;
105
+ }
106
+ }
107
+
108
+ function isResponsesPayload(value: unknown): value is Record<string, unknown> & { model: string; input: unknown[] } {
109
+ return isRecord(value) && typeof value.model === "string" && Array.isArray(value.input);
110
+ }
111
+
112
+ export function rewriteWindowHeaders(
113
+ headers: ProviderHeaders,
114
+ ctx: Pick<ExtensionContext, "sessionManager">,
115
+ identity: ContextWindowIdentity | undefined,
116
+ ): void {
117
+ if (!identity) return;
118
+ const metadata = buildContextWindowRequestMetadata(ctx, identity);
119
+ headers["x-codex-window-id"] = metadata.window_id;
120
+ headers["x-codex-turn-metadata"] = JSON.stringify(metadata);
121
+ }
122
+
123
+ function isRecord(value: unknown): value is Record<string, unknown> {
124
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
125
+ }
package/src/debug.ts CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  type RedactOptions,
15
15
  } from "./types";
16
16
 
17
- const CRITICAL_KEY_RE = /(authorization|api[-_]?key|token|credential|oauth|auth|account[-_]?id|encrypted[_-]?(content|output))/i;
17
+ const CRITICAL_KEY_RE = /(authorization|api[-_]?key|token|credential|oauth|auth|account[-_]?id|cookie|set[-_]?cookie|response(?:text)?|completed|encrypted[_-]?(content|output))/i;
18
18
  const SENSITIVE_KEY_RE = /(authorization|api[-_]?key|token|secret|password|cookie|set-cookie|signature|credential|oauth|auth|account[-_]?id|encrypted[_-]?(content|output))/i;
19
19
  const BEARER_RE = /\bBearer\s+[A-Za-z0-9._\-+/=]+/gi;
20
20
  const OPENAI_KEY_RE = /\bsk-[A-Za-z0-9\-_]+\b/g;
@@ -18,6 +18,7 @@ import { loadHistoryNotesThreadHint } from "./context-management/history-notes";
18
18
  import { CodexContextWindowManager } from "./context-management/window-manager";
19
19
  import { registerContextManagementTools } from "./context-management/tools";
20
20
  import { writeDebugArtifact, writeReplayFailureArtifact } from "./debug";
21
+ import { CODEX_GATEWAY_FORWARD_HEADERS } from "./responses-headers";
21
22
  import { resolveLatestNativeCompactionEntry } from "./details-store";
22
23
  import { runNativeFallbackCompaction } from "./native-fallback";
23
24
  import {
@@ -398,6 +399,7 @@ async function handleSessionBeforeCompact(
398
399
  {
399
400
  enabled: config.enabled,
400
401
  responsesApis: config.responsesApis,
402
+ codexGatewayModels: config.contextManagement === "remote" ? config.gatewayContextModels : [],
401
403
  },
402
404
  config.remoteCompactModel,
403
405
  );
@@ -536,9 +538,15 @@ async function handleContext(
536
538
  }
537
539
 
538
540
  // An eligible Codex Remote session is never allowed to fall through to the
539
- // older compaction-replay path. Keep its internal markers hidden, but let
540
- // Pi's normal request construction handle the inactive session.
541
+ // older compaction-replay path. Gateway traffic must also fail closed when
542
+ // its transport capability is unavailable; otherwise Pi could send an
543
+ // ordinary unscoped request and lose CPA OAuth/session affinity.
541
544
  if (isCodexContextModel(ctx.model, config)) {
545
+ if (isCodexGatewayModel(ctx.model, config.gatewayContextModels)) {
546
+ notifyRemoteContextFailure(ctx, "codex-context-unavailable");
547
+ ctx.abort();
548
+ return undefined;
549
+ }
542
550
  const visibleMessages = contextWindows.project(event.messages, "off");
543
551
  return visibleMessages.length === event.messages.length && visibleMessages.every((message, index) => message === event.messages[index])
544
552
  ? undefined
@@ -554,6 +562,7 @@ async function handleContext(
554
562
  const resolution = await resolveNativeCompactionEnvironment(ctx, {
555
563
  enabled: config.enabled,
556
564
  responsesApis: config.responsesApis,
565
+ codexGatewayModels: config.contextManagement === "remote" ? config.gatewayContextModels : [],
557
566
  });
558
567
  if (!resolution.ok) return undefined;
559
568
  const branchEntries = ctx.sessionManager.getBranch();
@@ -601,8 +610,14 @@ async function handleBeforeProviderRequest(
601
610
  }
602
611
  }
603
612
  if (isCodexContextModel(ctx.model, config)) {
604
- // Keep Codex Remote mutually exclusive with the legacy replay
605
- // pipeline when authentication or tool ownership is unavailable.
613
+ // Keep Codex Remote mutually exclusive with the legacy replay pipeline
614
+ // when authentication or tool ownership is unavailable. Gateway traffic
615
+ // cannot continue as an ordinary Responses request because that would
616
+ // silently lose the required CPA session/account selection.
617
+ if (isCodexGatewayModel(ctx.model, config.gatewayContextModels)) {
618
+ notifyRemoteContextFailure(ctx, "codex-context-unavailable");
619
+ ctx.abort();
620
+ }
606
621
  return undefined;
607
622
  }
608
623
 
@@ -611,6 +626,7 @@ async function handleBeforeProviderRequest(
611
626
  {
612
627
  enabled: config.enabled,
613
628
  responsesApis: config.responsesApis,
629
+ codexGatewayModels: config.contextManagement === "remote" ? config.gatewayContextModels : [],
614
630
  },
615
631
  event.payload,
616
632
  );
@@ -877,7 +893,14 @@ export default function registerCompactionExtension(
877
893
  pi.on("before_provider_headers", async (event, ctx) => {
878
894
  const config = dependencies.loadConfig().config.compaction;
879
895
  if (!isCodexContextModel(ctx.model, config)) return;
880
- if (!(await remoteContextActive(ctx, config))) return;
896
+ const active = await remoteContextActive(ctx, config);
897
+ if (!active) {
898
+ if (isCodexGatewayModel(ctx.model, config.gatewayContextModels)) {
899
+ notifyRemoteContextFailure(ctx, "codex-context-unavailable");
900
+ ctx.abort();
901
+ }
902
+ return;
903
+ }
881
904
  const provider = await resolveCodexContextProvider(ctx, ctx.model, config.gatewayContextModels);
882
905
  if (provider.ok && provider.provider.kind === "codex-gateway") {
883
906
  const sessionId = getSessionId(ctx);
@@ -885,6 +908,10 @@ export default function registerCompactionExtension(
885
908
  sessionId,
886
909
  clientRequestId: sessionId,
887
910
  });
911
+ const allowedGatewayHeaders = new Set<string>(CODEX_GATEWAY_FORWARD_HEADERS);
912
+ for (const existing of Object.keys(event.headers)) {
913
+ if (!allowedGatewayHeaders.has(existing.toLowerCase())) delete event.headers[existing];
914
+ }
888
915
  for (const name of [
889
916
  "authorization",
890
917
  "originator",
@@ -901,15 +928,13 @@ export default function registerCompactionExtension(
901
928
  const value = gatewayHeaders.get(name);
902
929
  if (value) event.headers[name] = value;
903
930
  }
904
- for (const existing of Object.keys(event.headers)) {
905
- if (["cookie", "chatgpt-account-id", "x-api-key"].includes(existing.toLowerCase())) delete event.headers[existing];
906
- }
907
931
  }
908
932
  contextWindows.rewriteHeaders(event.headers, ctx);
909
933
  });
910
- pi.on("message_end", (event, ctx) => {
934
+ pi.on("message_end", async (event, ctx) => {
911
935
  const config = dependencies.loadConfig().config.compaction;
912
936
  if (!isCodexContextModel(ctx.model, config) || !tools.isRegistered) return undefined;
937
+ if (!(await remoteContextActive(ctx, config))) return undefined;
913
938
  const message = routeContextNamespaceToolMessage(event.message);
914
939
  return message === event.message ? undefined : { message };
915
940
  });