pi-openai-codex-compat 0.0.2 → 0.0.4

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 (29) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +86 -38
  3. package/extensions/openai-codex-compat/apply-patch-diff-render.ts +6 -2
  4. package/extensions/openai-codex-compat/apply-patch-engine.ts +89 -5
  5. package/extensions/openai-codex-compat/apply-patch.ts +4 -5
  6. package/extensions/openai-codex-compat/codex-cache-diagnostics.ts +97 -0
  7. package/extensions/openai-codex-compat/codex-cache-key.ts +9 -0
  8. package/extensions/openai-codex-compat/codex-installation.ts +51 -0
  9. package/extensions/openai-codex-compat/codex-metadata.ts +139 -0
  10. package/extensions/openai-codex-compat/codex-protocol.ts +4 -2
  11. package/extensions/openai-codex-compat/codex-provider.ts +708 -128
  12. package/extensions/openai-codex-compat/codex-stream.ts +137 -40
  13. package/extensions/openai-codex-compat/codex-thread-lineage.ts +156 -0
  14. package/extensions/openai-codex-compat/codex-transport.ts +1795 -199
  15. package/extensions/openai-codex-compat/compaction-checkpoint.ts +2 -2
  16. package/extensions/openai-codex-compat/config.ts +15 -2
  17. package/extensions/openai-codex-compat/image-generation-schema.ts +37 -0
  18. package/extensions/openai-codex-compat/image-generation.ts +25 -48
  19. package/extensions/openai-codex-compat/index.ts +13 -0
  20. package/extensions/openai-codex-compat/namespaced-tools.ts +2 -0
  21. package/extensions/openai-codex-compat/output-limit-continuation.ts +151 -0
  22. package/extensions/openai-codex-compat/provider-error.ts +79 -0
  23. package/extensions/openai-codex-compat/remote-compaction.ts +13 -0
  24. package/extensions/openai-codex-compat/request-options.ts +2 -2
  25. package/extensions/openai-codex-compat/responses-lite.ts +147 -0
  26. package/extensions/openai-codex-compat/responses-replay.ts +0 -7
  27. package/extensions/openai-codex-compat/settings-pane.ts +11 -0
  28. package/extensions/openai-codex-compat/web-run.ts +7 -0
  29. package/package.json +2 -1
@@ -17,7 +17,12 @@ import {
17
17
  type SimpleStreamOptions,
18
18
  type Tool,
19
19
  type Usage,
20
+ uuidv7,
20
21
  } from "@earendil-works/pi-ai";
22
+ import {
23
+ codexCacheDiagnosticContext,
24
+ type CodexCacheDiagnosticContext,
25
+ } from "./codex-cache-diagnostics.ts";
21
26
  import {
22
27
  checkpointData,
23
28
  providerHistory,
@@ -34,9 +39,26 @@ import {
34
39
  type JsonRecord,
35
40
  type ResponsesItem,
36
41
  } from "./codex-protocol.ts";
37
- import { processCodexStream } from "./codex-stream.ts";
38
- import { CodexTransport } from "./codex-transport.ts";
39
- import type { CodexCompatConfig, ImageDetail } from "./config.ts";
42
+ import { codexCacheKey } from "./codex-cache-key.ts";
43
+ import { resolveCodexInstallationId } from "./codex-installation.ts";
44
+ import {
45
+ type CodexCompactionMetadata,
46
+ type CodexMetadataIdentity,
47
+ responsesCompactionV2Metadata,
48
+ withCodexRequestMetadata,
49
+ } from "./codex-metadata.ts";
50
+ import { resolveCodexThreadIdentity, type CodexThreadIdentity } from "./codex-thread-lineage.ts";
51
+ import { applyResponsesLite } from "./responses-lite.ts";
52
+ import { processCodexStream, type CodexStreamAttemptState } from "./codex-stream.ts";
53
+ import {
54
+ CodexTransport,
55
+ CodexTurnState,
56
+ validateCodexAuthentication,
57
+ type CodexContinuationHandle,
58
+ type CodexTransportDiagnostic,
59
+ type CodexWebSocketResponseHandle,
60
+ } from "./codex-transport.ts";
61
+ import { DEFAULT_CONFIG, type CodexCompatConfig, type ImageDetail } from "./config.ts";
40
62
  import { nativeResponseData, NATIVE_RESPONSE_ENTRY_TYPE } from "./native-history.ts";
41
63
  import {
42
64
  CODEX_NAMESPACED_TOOL_NAMES,
@@ -49,10 +71,10 @@ import {
49
71
  createGrammarToolInputProperties,
50
72
  type ResponsesItem as SerializedResponsesItem,
51
73
  } from "./vendor/pi-ai/openai-responses-serialization.ts";
74
+ import { formatProviderError } from "./provider-error.ts";
52
75
 
53
76
  const CODEX_PROVIDER = "openai-codex";
54
77
  const CODEX_API = "openai-codex-responses";
55
- const CHECKPOINT_STATUS_ID = "openai-codex-compat-compaction";
56
78
  const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
57
79
 
58
80
  type ConfigResolver = (ctx: ExtensionContext) => CodexCompatConfig;
@@ -78,7 +100,6 @@ type RuntimeScope = {
78
100
  config: CodexCompatConfig;
79
101
  hasUI: boolean;
80
102
  notify(message: string, level: "info" | "warning" | "error"): void;
81
- setStatus(message: string | undefined): void;
82
103
  };
83
104
 
84
105
  type RequestTemplate = {
@@ -88,16 +109,32 @@ type RequestTemplate = {
88
109
  requestOptions: OpenAICodexResponsesOptions;
89
110
  };
90
111
 
112
+ type ActiveAgentTurn = {
113
+ turnId: string;
114
+ startedAtUnixMs: number;
115
+ turnState: CodexTurnState;
116
+ };
117
+
91
118
  type CodexCompat = {
92
119
  supportsToolSearch?: boolean;
93
120
  supportsStrictMode?: boolean;
94
121
  supportsOpenAIGrammarTools?: boolean;
95
122
  };
96
123
 
97
- function clampPromptCacheKey(key: string | undefined): string | undefined {
98
- if (key === undefined) return undefined;
99
- return Array.from(key).slice(0, 64).join("");
100
- }
124
+ type CodexTerminalState = {
125
+ type?: "response.completed" | "response.incomplete" | "response.failed";
126
+ response?: JsonRecord;
127
+ };
128
+
129
+ export type CodexResponseRetryPolicy = {
130
+ maxRetries: number;
131
+ baseDelayMs: number;
132
+ };
133
+
134
+ const DEFAULT_RESPONSE_RETRY_POLICY: CodexResponseRetryPolicy = {
135
+ maxRetries: 5,
136
+ baseDelayMs: 200,
137
+ };
101
138
 
102
139
  function markerSummary(): string {
103
140
  return `OpenAI Codex remote compaction checkpoint (${randomUUID()}).`;
@@ -153,7 +190,7 @@ function nativeOverrideRequired(
153
190
  function captureRawEvents(
154
191
  events: AsyncIterable<JsonRecord>,
155
192
  items: ResponsesItem[],
156
- metadata: { serviceTier?: string },
193
+ terminalState?: CodexTerminalState,
157
194
  ): AsyncIterable<JsonRecord> {
158
195
  return {
159
196
  async *[Symbol.asyncIterator]() {
@@ -162,7 +199,9 @@ function captureRawEvents(
162
199
  items.push(structuredClone(event.item));
163
200
  }
164
201
  if (
165
- (event.type === "response.completed" || event.type === "response.incomplete") &&
202
+ (event.type === "response.completed" ||
203
+ event.type === "response.incomplete" ||
204
+ event.type === "response.failed") &&
166
205
  isObject(event.response) &&
167
206
  Array.isArray(event.response["output"])
168
207
  ) {
@@ -172,11 +211,35 @@ function captureRawEvents(
172
211
  }
173
212
  }
174
213
  if (
175
- (event.type === "response.completed" || event.type === "response.incomplete") &&
176
- isObject(event.response) &&
177
- typeof event.response.service_tier === "string"
214
+ terminalState &&
215
+ (event.type === "response.completed" ||
216
+ event.type === "response.incomplete" ||
217
+ event.type === "response.failed")
178
218
  ) {
179
- metadata.serviceTier = event.response.service_tier;
219
+ terminalState.type = event.type;
220
+ if (isObject(event.response)) {
221
+ terminalState.response = structuredClone(event.response);
222
+ } else {
223
+ delete terminalState.response;
224
+ }
225
+ }
226
+ yield event;
227
+ }
228
+ },
229
+ };
230
+ }
231
+
232
+ function startOnFirstEvent(
233
+ events: AsyncIterable<JsonRecord>,
234
+ onStart: () => void,
235
+ ): AsyncIterable<JsonRecord> {
236
+ return {
237
+ async *[Symbol.asyncIterator]() {
238
+ let started = false;
239
+ for await (const event of events) {
240
+ if (!started) {
241
+ started = true;
242
+ onStart();
180
243
  }
181
244
  yield event;
182
245
  }
@@ -184,6 +247,99 @@ function captureRawEvents(
184
247
  };
185
248
  }
186
249
 
250
+ function clearStreamingScratchState(message: AssistantMessage): void {
251
+ for (const block of message.content) {
252
+ delete (block as { partialJson?: string }).partialJson;
253
+ delete (block as { customInput?: unknown }).customInput;
254
+ }
255
+ }
256
+
257
+ function emptyUsage(): Usage {
258
+ return {
259
+ input: 0,
260
+ output: 0,
261
+ cacheRead: 0,
262
+ cacheWrite: 0,
263
+ totalTokens: 0,
264
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
265
+ };
266
+ }
267
+
268
+ function discardIncompleteAttemptContent(
269
+ message: AssistantMessage,
270
+ attempt: CodexStreamAttemptState,
271
+ ): void {
272
+ const incomplete = [...attempt.startedContentIndexes]
273
+ .filter((index) => !attempt.completedContentIndexes.has(index))
274
+ .sort((left, right) => right - left);
275
+ for (const index of incomplete) message.content.splice(index, 1);
276
+ }
277
+
278
+ function accumulateUsage(previous: Usage, current: Usage): Usage {
279
+ const previousReasoning = previous.reasoning;
280
+ const currentReasoning = current.reasoning;
281
+ return {
282
+ input: previous.input + current.input,
283
+ output: previous.output + current.output,
284
+ cacheRead: previous.cacheRead + current.cacheRead,
285
+ cacheWrite: previous.cacheWrite + current.cacheWrite,
286
+ ...(previousReasoning === undefined && currentReasoning === undefined
287
+ ? {}
288
+ : { reasoning: (previousReasoning ?? 0) + (currentReasoning ?? 0) }),
289
+ totalTokens: previous.totalTokens + current.totalTokens,
290
+ cost: {
291
+ input: previous.cost.input + current.cost.input,
292
+ output: previous.cost.output + current.cost.output,
293
+ cacheRead: previous.cost.cacheRead + current.cost.cacheRead,
294
+ cacheWrite: previous.cost.cacheWrite + current.cost.cacheWrite,
295
+ total: previous.cost.total + current.cost.total,
296
+ },
297
+ };
298
+ }
299
+
300
+ function retryableResponseFailure(response: JsonRecord | undefined): boolean {
301
+ const error = isObject(response?.["error"]) ? response["error"] : undefined;
302
+ const code = typeof error?.["code"] === "string" ? error["code"].toLowerCase() : "";
303
+ return !(
304
+ code === "context_length_exceeded" ||
305
+ code === "insufficient_quota" ||
306
+ code === "usage_not_included" ||
307
+ code === "cyber_policy" ||
308
+ code === "invalid_prompt" ||
309
+ code === "bio_policy"
310
+ );
311
+ }
312
+
313
+ function responseRetryDelayMs(baseDelayMs: number, attempt: number): number {
314
+ if (baseDelayMs <= 0) return 0;
315
+ const exponential = baseDelayMs * 2 ** Math.max(0, attempt - 1);
316
+ return Math.floor(exponential * (0.9 + Math.random() * 0.2));
317
+ }
318
+
319
+ function waitForResponseRetry(milliseconds: number, signal?: AbortSignal): Promise<void> {
320
+ if (signal?.aborted) return Promise.reject(new Error("Request was aborted"));
321
+ if (milliseconds <= 0) return Promise.resolve();
322
+ return new Promise((resolve, reject) => {
323
+ const onAbort = () => {
324
+ clearTimeout(timer);
325
+ reject(new Error("Request was aborted"));
326
+ };
327
+ const timer = setTimeout(() => {
328
+ signal?.removeEventListener("abort", onAbort);
329
+ resolve();
330
+ }, milliseconds);
331
+ signal?.addEventListener("abort", onAbort, { once: true });
332
+ });
333
+ }
334
+
335
+ function continueResponseBody(
336
+ body: JsonRecord,
337
+ responseItems: readonly ResponsesItem[],
338
+ ): JsonRecord | undefined {
339
+ if (!Array.isArray(body.input) || !body.input.every(isResponsesItem)) return undefined;
340
+ return updateInput(body, [...body.input, ...responseItems]);
341
+ }
342
+
187
343
  function updateInput(payload: JsonRecord, input: readonly ResponsesItem[]): JsonRecord {
188
344
  const result: JsonRecord = {
189
345
  ...payload,
@@ -194,14 +350,15 @@ function updateInput(payload: JsonRecord, input: readonly ResponsesItem[]): Json
194
350
  return result;
195
351
  }
196
352
 
197
- function successfulStopReason(
353
+ function assertSuccessfulOutput(
198
354
  message: AssistantMessage,
199
- ): message is AssistantMessage & { stopReason: "stop" | "length" | "toolUse" } {
200
- return (
201
- message.stopReason === "stop" ||
202
- message.stopReason === "length" ||
203
- message.stopReason === "toolUse"
204
- );
355
+ ): asserts message is AssistantMessage & { stopReason: "stop" | "length" | "toolUse" } {
356
+ if (message.stopReason === "pending") {
357
+ throw new Error("Codex stream ended without a stop reason");
358
+ }
359
+ if (message.stopReason === "error" || message.stopReason === "aborted") {
360
+ throw new Error(message.errorMessage || "An unknown error occurred");
361
+ }
205
362
  }
206
363
 
207
364
  function applyServiceTierPricing(
@@ -286,13 +443,38 @@ export class CodexProviderRuntime {
286
443
  readonly transport = new CodexTransport();
287
444
  private readonly scopes = new Map<string, RuntimeScope>();
288
445
  private readonly templates = new Map<string, RequestTemplate>();
446
+ private readonly prewarmedTemplates = new Set<string>();
289
447
  private readonly requestTails = new Map<string, Promise<void>>();
448
+ private readonly activeAgentTurns = new Map<string, ActiveAgentTurn>();
449
+ private readonly windowNumbers = new Map<string, number>();
450
+ private readonly activeThreadIds = new Map<string, string>();
290
451
  private readonly pi: ExtensionAPI;
291
452
  private readonly resolveConfig: ConfigResolver;
453
+ private readonly installationId: string;
454
+ private readonly responseRetryPolicy: CodexResponseRetryPolicy;
292
455
 
293
- constructor(pi: ExtensionAPI, resolveConfig: ConfigResolver) {
456
+ constructor(
457
+ pi: ExtensionAPI,
458
+ resolveConfig: ConfigResolver,
459
+ installationId: string = randomUUID(),
460
+ responseRetryPolicy: Partial<CodexResponseRetryPolicy> = {},
461
+ ) {
294
462
  this.pi = pi;
295
463
  this.resolveConfig = resolveConfig;
464
+ this.installationId = installationId;
465
+ const maxRetries = responseRetryPolicy.maxRetries ?? DEFAULT_RESPONSE_RETRY_POLICY.maxRetries;
466
+ const baseDelayMs =
467
+ responseRetryPolicy.baseDelayMs ?? DEFAULT_RESPONSE_RETRY_POLICY.baseDelayMs;
468
+ if (!Number.isFinite(maxRetries) || maxRetries < 0) {
469
+ throw new Error(`Invalid Codex response maxRetries: ${String(maxRetries)}`);
470
+ }
471
+ if (!Number.isFinite(baseDelayMs) || baseDelayMs < 0) {
472
+ throw new Error(`Invalid Codex response baseDelayMs: ${String(baseDelayMs)}`);
473
+ }
474
+ this.responseRetryPolicy = {
475
+ maxRetries: Math.floor(maxRetries),
476
+ baseDelayMs: Math.floor(baseDelayMs),
477
+ };
296
478
  }
297
479
 
298
480
  captureScope(ctx: ExtensionContext): void {
@@ -308,18 +490,161 @@ export class CodexProviderRuntime {
308
490
  config: this.resolveConfig(ctx),
309
491
  hasUI: ctx.hasUI,
310
492
  notify: (message, level) => ctx.ui.notify(message, level),
311
- setStatus: (message) => ctx.ui.setStatus(CHECKPOINT_STATUS_ID, message),
312
493
  });
313
494
  }
314
495
 
496
+ beginAgentTurn(ctx: ExtensionContext): void {
497
+ this.activeAgentTurns.set(ctx.sessionManager.getSessionId(), {
498
+ turnId: uuidv7(),
499
+ startedAtUnixMs: Date.now(),
500
+ turnState: new CodexTurnState(),
501
+ });
502
+ }
503
+
504
+ endAgentTurn(ctx: ExtensionContext): void {
505
+ this.activeAgentTurns.delete(ctx.sessionManager.getSessionId());
506
+ }
507
+
508
+ updateSessionConfig(sessionId: string, config: CodexCompatConfig): void {
509
+ const scope = this.scopes.get(sessionId);
510
+ if (scope) scope.config = config;
511
+ }
512
+
513
+ private responsesLiteEnabled(sessionId: string | undefined): boolean {
514
+ return (
515
+ (sessionId ? this.scopes.get(sessionId)?.config.responsesLite : undefined) ??
516
+ DEFAULT_CONFIG.responsesLite
517
+ );
518
+ }
519
+
520
+ private agentTurn(sessionId: string | undefined): ActiveAgentTurn {
521
+ return (
522
+ (sessionId ? this.activeAgentTurns.get(sessionId) : undefined) ?? {
523
+ turnId: uuidv7(),
524
+ startedAtUnixMs: Date.now(),
525
+ turnState: new CodexTurnState(),
526
+ }
527
+ );
528
+ }
529
+
530
+ private metadataIdentity(
531
+ metadataSessionId: string | undefined,
532
+ turn?: ActiveAgentTurn,
533
+ runtimeSessionId = metadataSessionId,
534
+ ): CodexMetadataIdentity {
535
+ const thread: Partial<CodexThreadIdentity> = runtimeSessionId
536
+ ? this.threadIdentity(runtimeSessionId)
537
+ : metadataSessionId
538
+ ? { threadId: metadataSessionId }
539
+ : {};
540
+ const windowKey =
541
+ runtimeSessionId && thread.threadId ? `${runtimeSessionId}\0${thread.threadId}` : undefined;
542
+ return {
543
+ installationId: this.installationId,
544
+ ...(thread.threadId ? { threadId: thread.threadId } : {}),
545
+ ...(thread.forkedFromThreadId ? { forkedFromThreadId: thread.forkedFromThreadId } : {}),
546
+ windowNumber: windowKey ? (this.windowNumbers.get(windowKey) ?? 0) : 0,
547
+ ...(turn ? { turnStartedAtUnixMs: turn.startedAtUnixMs } : {}),
548
+ threadSource: "user",
549
+ // Pi extensions execute without Codex's platform sandbox.
550
+ sandbox: "none",
551
+ };
552
+ }
553
+
554
+ private threadIdentity(sessionId: string): CodexThreadIdentity {
555
+ const branch = this.scopes.get(sessionId)?.manager.getBranch() as SessionEntry[] | undefined;
556
+ return resolveCodexThreadIdentity(sessionId, branch ?? []);
557
+ }
558
+
559
+ private clearPrewarmState(sessionId: string): void {
560
+ for (const key of this.prewarmedTemplates) {
561
+ if (key.startsWith(`${sessionId}\0`)) this.prewarmedTemplates.delete(key);
562
+ }
563
+ }
564
+
565
+ private activateThread(sessionId: string | undefined): void {
566
+ if (!sessionId) return;
567
+ const threadId = this.threadIdentity(sessionId).threadId;
568
+ const previous = this.activeThreadIds.get(sessionId);
569
+ if (previous && previous !== threadId) {
570
+ this.transport.close(sessionId);
571
+ this.clearPrewarmState(sessionId);
572
+ }
573
+ this.activeThreadIds.set(sessionId, threadId);
574
+ }
575
+
576
+ private advanceWindow(sessionId: string): void {
577
+ const threadId = this.threadIdentity(sessionId).threadId;
578
+ const key = `${sessionId}\0${threadId}`;
579
+ this.windowNumbers.set(key, (this.windowNumbers.get(key) ?? 0) + 1);
580
+ }
581
+
315
582
  clearSession(sessionId: string): void {
316
583
  this.scopes.delete(sessionId);
317
584
  this.templates.delete(sessionId);
585
+ this.clearPrewarmState(sessionId);
318
586
  this.requestTails.delete(sessionId);
587
+ this.activeAgentTurns.delete(sessionId);
588
+ for (const key of this.windowNumbers.keys()) {
589
+ if (key.startsWith(`${sessionId}\0`)) this.windowNumbers.delete(key);
590
+ }
591
+ this.activeThreadIds.delete(sessionId);
319
592
  this.transport.close(sessionId);
320
593
  }
321
594
 
322
- private async acquireRequest(sessionId: string | undefined): Promise<() => void> {
595
+ private async maybePrewarm(options: {
596
+ model: Model<any>;
597
+ body: JsonRecord;
598
+ fullBody: JsonRecord;
599
+ requestOptions: OpenAICodexResponsesOptions;
600
+ accountId: string;
601
+ diagnostics: CodexTransportDiagnostic[];
602
+ turnState: CodexTurnState;
603
+ cacheDiagnostics: CodexCacheDiagnosticContext;
604
+ }): Promise<void> {
605
+ const sessionId = options.requestOptions.sessionId;
606
+ if (
607
+ !sessionId ||
608
+ options.requestOptions.cacheRetention === "none" ||
609
+ options.requestOptions.transport === "sse" ||
610
+ !Array.isArray(options.body.input) ||
611
+ !Array.isArray(options.fullBody.input)
612
+ ) {
613
+ return;
614
+ }
615
+ const key = `${sessionId}\0${options.model.id}\0${options.cacheDiagnostics.envelope}`;
616
+ if (this.prewarmedTemplates.has(key)) return;
617
+ this.prewarmedTemplates.add(key);
618
+
619
+ const cacheSessionId = codexCacheKey(sessionId);
620
+ const prewarmBody = withCodexRequestMetadata(
621
+ options.body,
622
+ cacheSessionId,
623
+ { kind: "prewarm" },
624
+ "",
625
+ this.metadataIdentity(cacheSessionId, undefined, sessionId),
626
+ );
627
+ try {
628
+ await this.transport.prewarm(options.model, prewarmBody, {
629
+ ...options.requestOptions,
630
+ accountId: options.accountId,
631
+ turnState: options.turnState,
632
+ cacheDiagnostics: options.cacheDiagnostics,
633
+ requestKind: "prewarm",
634
+ onTransportDiagnostic(diagnostic) {
635
+ options.diagnostics.push(diagnostic);
636
+ },
637
+ });
638
+ } catch {
639
+ // Warmup is best-effort. The transport has already activated sticky SSE
640
+ // after exhausting its WebSocket retry budget.
641
+ }
642
+ }
643
+
644
+ private async acquireRequest(
645
+ sessionId: string | undefined,
646
+ signal?: AbortSignal,
647
+ ): Promise<() => void> {
323
648
  if (!sessionId) return () => {};
324
649
  const previous = this.requestTails.get(sessionId) ?? Promise.resolve();
325
650
  let releaseCurrent!: () => void;
@@ -327,11 +652,44 @@ export class CodexProviderRuntime {
327
652
  releaseCurrent = resolve;
328
653
  });
329
654
  this.requestTails.set(sessionId, current);
330
- await previous;
331
- return () => {
655
+ let released = false;
656
+ const release = () => {
657
+ if (released) return;
658
+ released = true;
332
659
  releaseCurrent();
333
660
  if (this.requestTails.get(sessionId) === current) this.requestTails.delete(sessionId);
334
661
  };
662
+
663
+ if (signal?.aborted) {
664
+ void previous.then(release);
665
+ throw new Error("Request was aborted");
666
+ }
667
+
668
+ let onAbort: (() => void) | undefined;
669
+ try {
670
+ await Promise.race([
671
+ previous,
672
+ ...(signal
673
+ ? [
674
+ new Promise<never>((_resolve, reject) => {
675
+ onAbort = () => reject(new Error("Request was aborted"));
676
+ signal.addEventListener("abort", onAbort, { once: true });
677
+ }),
678
+ ]
679
+ : []),
680
+ ]);
681
+ } catch (error) {
682
+ void previous.then(release);
683
+ throw error;
684
+ } finally {
685
+ if (onAbort) signal?.removeEventListener("abort", onAbort);
686
+ }
687
+
688
+ if (signal?.aborted) {
689
+ release();
690
+ throw new Error("Request was aborted");
691
+ }
692
+ return release;
335
693
  }
336
694
 
337
695
  createProvider(base: Provider): Provider {
@@ -358,7 +716,7 @@ export class CodexProviderRuntime {
358
716
  grammarToolInputProperties,
359
717
  deferredTools: splitDeferredTools(context, Boolean(compat?.supportsToolSearch)).deferred,
360
718
  toolOptions: {
361
- strict: null,
719
+ strict: false,
362
720
  supportsStrictMode: compat?.supportsStrictMode ?? true,
363
721
  supportsOpenAIGrammarTools: compat?.supportsOpenAIGrammarTools ?? false,
364
722
  },
@@ -381,32 +739,45 @@ export class CodexProviderRuntime {
381
739
  model: Model<any>,
382
740
  context: Context,
383
741
  options: OpenAICodexResponsesOptions,
384
- sessionId: string | undefined,
742
+ runtimeSessionId: string | undefined,
743
+ cacheSessionId: string | undefined,
385
744
  grammarToolInputProperties: GrammarToolInputProperties,
745
+ turnId: string,
386
746
  ): JsonRecord {
387
747
  const compat = model.compat as CodexCompat | undefined;
388
748
  const toolPlacement = splitDeferredTools(context, Boolean(compat?.supportsToolSearch));
389
- const body: JsonRecord = {
749
+ let body: JsonRecord = {
390
750
  model: model.id,
391
751
  store: false,
392
752
  stream: true,
393
753
  instructions: context.systemPrompt || "You are a helpful assistant.",
394
754
  input: this.wireHistory(
395
755
  model,
396
- Object.assign({}, context, { sessionId }),
756
+ Object.assign({}, context, { sessionId: runtimeSessionId }),
397
757
  grammarToolInputProperties,
398
758
  ),
399
759
  text: { verbosity: options.textVerbosity ?? "low" },
400
760
  include: ["reasoning.encrypted_content"],
401
- prompt_cache_key: sessionId,
761
+ prompt_cache_key: cacheSessionId,
402
762
  tool_choice: options.toolChoice ?? "auto",
403
763
  parallel_tool_calls: true,
764
+ tools: [],
404
765
  };
405
- if (options.temperature !== undefined) body["temperature"] = options.temperature;
766
+ body = withCodexRequestMetadata(
767
+ body,
768
+ cacheSessionId,
769
+ { kind: "turn" },
770
+ turnId,
771
+ this.metadataIdentity(
772
+ cacheSessionId,
773
+ this.activeAgentTurns.get(runtimeSessionId ?? ""),
774
+ runtimeSessionId,
775
+ ),
776
+ );
406
777
  if (options.serviceTier !== undefined) body.service_tier = options.serviceTier;
407
778
  if (toolPlacement.immediate.length > 0) {
408
779
  body.tools = convertResponsesTools(toolPlacement.immediate, {
409
- strict: null,
780
+ strict: false,
410
781
  supportsStrictMode: compat?.supportsStrictMode ?? true,
411
782
  supportsOpenAIGrammarTools: compat?.supportsOpenAIGrammarTools ?? false,
412
783
  namespacedToolNames: CODEX_NAMESPACED_TOOL_NAMES,
@@ -436,24 +807,74 @@ export class CodexProviderRuntime {
436
807
  instructions: string;
437
808
  grammarToolInputProperties: GrammarToolInputProperties;
438
809
  priority: boolean;
810
+ compactionMetadata: CodexCompactionMetadata;
811
+ agentTurn?: ActiveAgentTurn;
812
+ responsesLiteEnabled?: boolean;
439
813
  }): Promise<{ checkpoint: CheckpointData; usage?: Usage }> {
440
814
  const sessionId = options.requestOptions.sessionId;
441
815
  if (!sessionId) throw new Error("Codex compaction requires a Pi session id.");
442
- const payload = remoteCompactionPayload({
443
- template: options.template,
444
- modelId: options.model.id,
445
- history: options.history,
446
- instructions: options.instructions,
447
- sessionId,
448
- priority: options.priority,
449
- });
816
+ this.activateThread(sessionId);
817
+ const agentTurn = options.agentTurn ?? this.agentTurn(sessionId);
818
+ const responsesLiteEnabled =
819
+ options.responsesLiteEnabled ?? this.responsesLiteEnabled(sessionId);
820
+ const accountId = validateCodexAuthentication(options.model, options.requestOptions.apiKey);
821
+ const payload = withCodexRequestMetadata(
822
+ remoteCompactionPayload({
823
+ template: options.template,
824
+ modelId: options.model.id,
825
+ history: options.history,
826
+ instructions: options.instructions,
827
+ sessionId:
828
+ options.requestOptions.cacheRetention === "none" ? undefined : codexCacheKey(sessionId),
829
+ priority: options.priority,
830
+ }),
831
+ options.requestOptions.cacheRetention === "none" ? undefined : codexCacheKey(sessionId),
832
+ { kind: "compaction", compaction: options.compactionMetadata },
833
+ agentTurn.turnId,
834
+ this.metadataIdentity(
835
+ options.requestOptions.cacheRetention === "none" ? undefined : codexCacheKey(sessionId),
836
+ agentTurn,
837
+ sessionId,
838
+ ),
839
+ );
450
840
  const transformed = await options.requestOptions.onPayload?.(payload, options.model);
451
- const request = isObject(transformed) ? transformed : payload;
452
- const compacted = await collectRemoteCompaction(
453
- this.transport.request(options.model, request, options.requestOptions),
454
- options.model,
455
- options.priority,
841
+ const ordinaryRequest = transformed === undefined ? payload : (transformed as JsonRecord);
842
+ const request = applyResponsesLite(ordinaryRequest, options.model.id, responsesLiteEnabled);
843
+ const staticRequest = applyResponsesLite(
844
+ updateInput(ordinaryRequest, []),
845
+ options.model.id,
846
+ responsesLiteEnabled,
847
+ );
848
+ const cacheDiagnostics = codexCacheDiagnosticContext(
849
+ ordinaryRequest,
850
+ request,
851
+ staticRequest,
852
+ options.model.id,
853
+ responsesLiteEnabled,
456
854
  );
855
+ let webSocketResponseHandle: CodexWebSocketResponseHandle | undefined;
856
+ let compacted: Awaited<ReturnType<typeof collectRemoteCompaction>>;
857
+ try {
858
+ compacted = await collectRemoteCompaction(
859
+ this.transport.request(options.model, request, {
860
+ ...options.requestOptions,
861
+ accountId,
862
+ requestKind: "compaction",
863
+ turnState: agentTurn.turnState,
864
+ cacheDiagnostics,
865
+ onWebSocketResponseHandle(handle) {
866
+ webSocketResponseHandle = handle;
867
+ },
868
+ }),
869
+ options.model,
870
+ options.priority,
871
+ );
872
+ } catch (error) {
873
+ if (!options.requestOptions.signal?.aborted) {
874
+ webSocketResponseHandle?.failParsing(error);
875
+ }
876
+ throw error;
877
+ }
457
878
  return {
458
879
  checkpoint: checkpointData(
459
880
  options.model.id,
@@ -471,12 +892,16 @@ export class CodexProviderRuntime {
471
892
  options: OpenAICodexResponsesOptions,
472
893
  body: JsonRecord,
473
894
  grammarToolInputProperties: GrammarToolInputProperties,
895
+ agentTurn: ActiveAgentTurn,
896
+ responsesLiteEnabled: boolean,
474
897
  ): Promise<JsonRecord> {
475
898
  const sessionId = options.sessionId;
476
899
  const scope = sessionId ? this.scopes.get(sessionId) : undefined;
477
900
  const threshold = scope?.config.autoCompactAtPercent;
901
+ if (!sessionId || !scope) {
902
+ return body;
903
+ }
478
904
  if (
479
- !scope ||
480
905
  threshold === undefined ||
481
906
  scope.contextPercent === null ||
482
907
  scope.contextPercent < threshold ||
@@ -515,44 +940,50 @@ export class CodexProviderRuntime {
515
940
  return body;
516
941
  }
517
942
 
518
- scope.setStatus("Codex compacting…");
519
- try {
520
- const compacted = await this.performCompaction({
521
- model,
522
- requestOptions: options,
523
- history: split.history,
524
- postCompactionTail: split.tail,
525
- template: withoutConversationInput(body),
526
- instructions:
527
- typeof body.instructions === "string"
528
- ? body.instructions
529
- : context.systemPrompt || "You are a helpful assistant.",
530
- grammarToolInputProperties,
531
- priority: scope.config.fastMode,
532
- });
533
- const firstKeptEntryId = userEntryAfterLastSampled(branch)?.id ?? scope.manager.getLeafId();
534
- if (!firstKeptEntryId || typeof scope.manager.appendCompaction !== "function") {
535
- throw new Error("Pi's mutable SessionManager is unavailable for percentage compaction.");
536
- }
537
- if (scope.manager.getLeafId() !== scope.leafId) {
538
- throw new Error("Pi's active session branch changed while Codex was compacting.");
539
- }
540
- scope.manager.appendCompaction(
541
- markerSummary(),
542
- firstKeptEntryId,
543
- scope.contextTokens ?? 0,
544
- compacted.checkpoint,
545
- true,
546
- compacted.usage,
547
- );
548
- scope.notify(
549
- `OpenAI Codex context compacted at ${scope.contextPercent.toFixed(1)}% and will continue.`,
550
- "info",
551
- );
552
- return updateInput(body, compacted.checkpoint.history);
553
- } finally {
554
- scope.setStatus(undefined);
943
+ const compacted = await this.performCompaction({
944
+ model,
945
+ requestOptions: options,
946
+ history: split.history,
947
+ postCompactionTail: split.tail,
948
+ template: withoutConversationInput(body),
949
+ instructions:
950
+ typeof body.instructions === "string"
951
+ ? body.instructions
952
+ : context.systemPrompt || "You are a helpful assistant.",
953
+ grammarToolInputProperties,
954
+ priority: scope.config.fastMode,
955
+ compactionMetadata: responsesCompactionV2Metadata("auto", "context_limit", "pre_turn"),
956
+ agentTurn,
957
+ responsesLiteEnabled,
958
+ });
959
+ const firstKeptEntryId = userEntryAfterLastSampled(branch)?.id ?? scope.manager.getLeafId();
960
+ if (!firstKeptEntryId || typeof scope.manager.appendCompaction !== "function") {
961
+ throw new Error("Pi's mutable SessionManager is unavailable for percentage compaction.");
962
+ }
963
+ if (scope.manager.getLeafId() !== scope.leafId) {
964
+ throw new Error("Pi's active session branch changed while Codex was compacting.");
555
965
  }
966
+ scope.manager.appendCompaction(
967
+ markerSummary(),
968
+ firstKeptEntryId,
969
+ scope.contextTokens ?? 0,
970
+ compacted.checkpoint,
971
+ true,
972
+ compacted.usage,
973
+ );
974
+ scope.notify(
975
+ `OpenAI Codex context compacted at ${scope.contextPercent.toFixed(1)}% and will continue.`,
976
+ "info",
977
+ );
978
+ this.advanceWindow(sessionId);
979
+ const cacheSessionId = options.cacheRetention === "none" ? undefined : codexCacheKey(sessionId);
980
+ return withCodexRequestMetadata(
981
+ updateInput(body, compacted.checkpoint.history),
982
+ cacheSessionId,
983
+ { kind: "turn" },
984
+ agentTurn.turnId,
985
+ this.metadataIdentity(cacheSessionId, agentTurn, sessionId),
986
+ );
556
987
  }
557
988
 
558
989
  stream(
@@ -569,21 +1000,20 @@ export class CodexProviderRuntime {
569
1000
  api: CODEX_API,
570
1001
  provider: model.provider,
571
1002
  model: model.id,
572
- usage: {
573
- input: 0,
574
- output: 0,
575
- cacheRead: 0,
576
- cacheWrite: 0,
577
- totalTokens: 0,
578
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
579
- },
1003
+ usage: emptyUsage(),
580
1004
  stopReason: "pending",
581
1005
  timestamp: Date.now(),
582
1006
  };
583
1007
  const runtimeSessionId = requestOptions.sessionId;
584
- const releaseRequest = await this.acquireRequest(runtimeSessionId);
1008
+ let releaseRequest = () => {};
585
1009
  try {
586
- const cacheSessionId = clampPromptCacheKey(runtimeSessionId);
1010
+ const accountId = validateCodexAuthentication(model, requestOptions.apiKey);
1011
+ releaseRequest = await this.acquireRequest(runtimeSessionId, requestOptions.signal);
1012
+ this.activateThread(runtimeSessionId);
1013
+ const cacheSessionId =
1014
+ requestOptions.cacheRetention === "none" ? undefined : codexCacheKey(runtimeSessionId);
1015
+ const agentTurn = this.agentTurn(runtimeSessionId);
1016
+ const responsesLiteEnabled = this.responsesLiteEnabled(runtimeSessionId);
587
1017
  const grammarToolInputProperties = createGrammarToolInputProperties(
588
1018
  context.tools,
589
1019
  (model.compat as CodexCompat | undefined)?.supportsOpenAIGrammarTools ?? false,
@@ -592,11 +1022,13 @@ export class CodexProviderRuntime {
592
1022
  model,
593
1023
  context,
594
1024
  requestOptions,
1025
+ runtimeSessionId,
595
1026
  cacheSessionId,
596
1027
  grammarToolInputProperties,
1028
+ agentTurn.turnId,
597
1029
  );
598
1030
  const transformed = await requestOptions.onPayload?.(body, model);
599
- if (isObject(transformed)) body = transformed;
1031
+ if (transformed !== undefined) body = transformed as JsonRecord;
600
1032
  if (runtimeSessionId) {
601
1033
  this.templates.set(runtimeSessionId, {
602
1034
  modelId: model.id,
@@ -611,31 +1043,151 @@ export class CodexProviderRuntime {
611
1043
  requestOptions,
612
1044
  body,
613
1045
  grammarToolInputProperties,
1046
+ agentTurn,
1047
+ responsesLiteEnabled,
1048
+ );
1049
+ const ordinaryBody = body;
1050
+ const staticBody = applyResponsesLite(
1051
+ updateInput(ordinaryBody, []),
1052
+ model.id,
1053
+ responsesLiteEnabled,
1054
+ );
1055
+ body = applyResponsesLite(ordinaryBody, model.id, responsesLiteEnabled);
1056
+ const cacheDiagnostics = codexCacheDiagnosticContext(
1057
+ ordinaryBody,
1058
+ body,
1059
+ staticBody,
1060
+ model.id,
1061
+ responsesLiteEnabled,
614
1062
  );
615
1063
 
616
1064
  const rawItems: ResponsesItem[] = [];
617
- const responseMetadata: { serviceTier?: string } = {};
618
- stream.push({ type: "start", partial: output });
619
- await processCodexStream(
620
- captureRawEvents(
621
- this.transport.request(model, body, requestOptions),
622
- rawItems,
623
- responseMetadata,
624
- ),
625
- output,
626
- stream,
1065
+ const prewarmDiagnostics: CodexTransportDiagnostic[] = [];
1066
+ await this.maybePrewarm({
627
1067
  model,
628
- grammarToolInputProperties,
629
- );
630
- if (!successfulStopReason(output)) {
631
- throw new Error(output.errorMessage || "Codex stream ended without a successful stop.");
1068
+ body: staticBody,
1069
+ fullBody: body,
1070
+ requestOptions,
1071
+ accountId,
1072
+ diagnostics: prewarmDiagnostics,
1073
+ turnState: agentTurn.turnState,
1074
+ cacheDiagnostics,
1075
+ });
1076
+ if (prewarmDiagnostics.length > 0) output.diagnostics = prewarmDiagnostics;
1077
+ let continuationHandle: CodexContinuationHandle | undefined;
1078
+ let webSocketResponseHandle: CodexWebSocketResponseHandle | undefined;
1079
+ let startEmitted = false;
1080
+ const emitStart = () => {
1081
+ if (startEmitted) return;
1082
+ startEmitted = true;
1083
+ stream.push({ type: "start", partial: output });
1084
+ };
1085
+ const transportRequestOptions = {
1086
+ ...requestOptions,
1087
+ accountId,
1088
+ requestKind: "turn" as const,
1089
+ turnState: agentTurn.turnState,
1090
+ cacheDiagnostics,
1091
+ onContinuationReady(handle: CodexContinuationHandle) {
1092
+ continuationHandle = handle;
1093
+ },
1094
+ onWebSocketResponseHandle(handle: CodexWebSocketResponseHandle) {
1095
+ webSocketResponseHandle = handle;
1096
+ },
1097
+ onTransportStart: emitStart,
1098
+ onTransportDiagnostic(diagnostic: CodexTransportDiagnostic) {
1099
+ output.diagnostics = [...(output.diagnostics ?? []), diagnostic];
1100
+ },
1101
+ };
1102
+ let responseRequests = 0;
1103
+ let responseRetries = 0;
1104
+ while (true) {
1105
+ responseRequests += 1;
1106
+ const attemptItems: ResponsesItem[] = [];
1107
+ const terminalState: CodexTerminalState = {};
1108
+ const attemptState: CodexStreamAttemptState = {
1109
+ startedContentIndexes: new Set(),
1110
+ completedContentIndexes: new Set(),
1111
+ };
1112
+ const usageBeforeAttempt = structuredClone(output.usage);
1113
+ output.usage = emptyUsage();
1114
+ try {
1115
+ await processCodexStream(
1116
+ startOnFirstEvent(
1117
+ captureRawEvents(
1118
+ this.transport.request(model, body, transportRequestOptions),
1119
+ attemptItems,
1120
+ terminalState,
1121
+ ),
1122
+ emitStart,
1123
+ ),
1124
+ output,
1125
+ stream,
1126
+ model,
1127
+ grammarToolInputProperties,
1128
+ {
1129
+ attemptState,
1130
+ applyServiceTierPricing(usage, responseServiceTier) {
1131
+ applyServiceTierPricing(usage, model, body.service_tier, responseServiceTier);
1132
+ },
1133
+ },
1134
+ );
1135
+ } catch (error) {
1136
+ output.usage = usageBeforeAttempt;
1137
+ if (!requestOptions.signal?.aborted) {
1138
+ webSocketResponseHandle?.failParsing(error);
1139
+ }
1140
+ throw error;
1141
+ }
1142
+ output.usage = accumulateUsage(usageBeforeAttempt, output.usage);
1143
+ rawItems.push(...attemptItems.map((item) => structuredClone(item)));
1144
+
1145
+ const nextBody = continueResponseBody(body, attemptItems);
1146
+ const attemptHasToolCall = [...attemptState.completedContentIndexes].some(
1147
+ (index) => output.content[index]?.type === "toolCall",
1148
+ );
1149
+ discardIncompleteAttemptContent(output, attemptState);
1150
+ const retryableTerminal =
1151
+ terminalState.type === "response.incomplete" ||
1152
+ (terminalState.type === "response.failed" &&
1153
+ retryableResponseFailure(terminalState.response));
1154
+ if (
1155
+ retryableTerminal &&
1156
+ nextBody &&
1157
+ responseRetries < this.responseRetryPolicy.maxRetries
1158
+ ) {
1159
+ responseRetries += 1;
1160
+ body = nextBody;
1161
+ output.stopReason = "pending";
1162
+ delete output.errorMessage;
1163
+ delete output.rawStopReason;
1164
+ await waitForResponseRetry(
1165
+ responseRetryDelayMs(this.responseRetryPolicy.baseDelayMs, responseRetries),
1166
+ requestOptions.signal,
1167
+ );
1168
+ continue;
1169
+ }
1170
+
1171
+ if (
1172
+ terminalState.type === "response.completed" &&
1173
+ terminalState.response?.["end_turn"] === false &&
1174
+ !attemptHasToolCall
1175
+ ) {
1176
+ if (!nextBody) {
1177
+ throw new Error(
1178
+ "Codex requested a follow-up response, but its completed output could not be appended to request history.",
1179
+ );
1180
+ }
1181
+ responseRetries = 0;
1182
+ body = nextBody;
1183
+ output.stopReason = "pending";
1184
+ delete output.errorMessage;
1185
+ delete output.rawStopReason;
1186
+ continue;
1187
+ }
1188
+ break;
632
1189
  }
633
- applyServiceTierPricing(
634
- output.usage,
635
- model,
636
- body.service_tier,
637
- responseMetadata.serviceTier,
638
- );
1190
+ if (requestOptions.signal?.aborted) throw new Error("Request was aborted");
639
1191
 
640
1192
  const compat = model.compat as CodexCompat | undefined;
641
1193
  const canonicalContext: Context = {
@@ -652,7 +1204,7 @@ export class CodexProviderRuntime {
652
1204
  deferredTools: splitDeferredTools(context, Boolean(compat?.supportsToolSearch))
653
1205
  .deferred,
654
1206
  toolOptions: {
655
- strict: null,
1207
+ strict: false,
656
1208
  supportsStrictMode: compat?.supportsStrictMode ?? true,
657
1209
  supportsOpenAIGrammarTools: compat?.supportsOpenAIGrammarTools ?? false,
658
1210
  },
@@ -667,19 +1219,37 @@ export class CodexProviderRuntime {
667
1219
  (item) =>
668
1220
  item["type"] !== "function_call_output" && item["type"] !== "custom_tool_call_output",
669
1221
  );
670
- if (rawItems.length > 0 && nativeOverrideRequired(rawItems, canonicalItems)) {
1222
+ const persistNativeItems =
1223
+ rawItems.length > 0 &&
1224
+ (responseRequests > 1 || nativeOverrideRequired(rawItems, canonicalItems));
1225
+ if (persistNativeItems) {
671
1226
  if (!output.responseId) throw new Error("Codex response is missing a response id.");
672
1227
  this.pi.appendEntry(
673
1228
  NATIVE_RESPONSE_ENTRY_TYPE,
674
1229
  nativeResponseData(model.id, output.responseId, rawItems),
675
1230
  );
676
1231
  }
1232
+ try {
1233
+ assertSuccessfulOutput(output);
1234
+ } catch (error) {
1235
+ webSocketResponseHandle?.discard();
1236
+ throw error;
1237
+ }
1238
+ const readyContinuation = continuationHandle;
1239
+ if (
1240
+ responseRequests === 1 &&
1241
+ readyContinuation &&
1242
+ readyContinuation.responseId === output.responseId
1243
+ ) {
1244
+ readyContinuation.replaceResponseItems(persistNativeItems ? rawItems : canonicalItems);
1245
+ }
677
1246
 
678
1247
  stream.push({ type: "done", reason: output.stopReason, message: output });
679
1248
  stream.end();
680
1249
  } catch (error) {
1250
+ clearStreamingScratchState(output);
681
1251
  output.stopReason = requestOptions.signal?.aborted ? "aborted" : "error";
682
- output.errorMessage = error instanceof Error ? error.message : String(error);
1252
+ output.errorMessage = formatProviderError(error);
683
1253
  stream.push({ type: "error", reason: output.stopReason, error: output });
684
1254
  stream.end();
685
1255
  } finally {
@@ -694,6 +1264,7 @@ export class CodexProviderRuntime {
694
1264
  context: Context,
695
1265
  options?: SimpleStreamOptions,
696
1266
  ): AssistantMessageEventStream {
1267
+ if (!options?.apiKey) throw new Error(`No API key for provider: ${model.provider}`);
697
1268
  const effort = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
698
1269
  return this.stream(model, context, {
699
1270
  ...options,
@@ -705,7 +1276,7 @@ export class CodexProviderRuntime {
705
1276
  return this.templates.get(sessionId);
706
1277
  }
707
1278
 
708
- compact(options: {
1279
+ async compact(options: {
709
1280
  model: Model<any>;
710
1281
  requestOptions: OpenAICodexResponsesOptions;
711
1282
  history: ResponsesItem[];
@@ -713,14 +1284,21 @@ export class CodexProviderRuntime {
713
1284
  grammarToolInputProperties: GrammarToolInputProperties;
714
1285
  template: JsonRecord;
715
1286
  priority: boolean;
1287
+ compactionMetadata: CodexCompactionMetadata;
716
1288
  }): Promise<{ checkpoint: CheckpointData; usage?: Usage }> {
717
- return this.acquireRequest(options.requestOptions.sessionId).then(async (release) => {
718
- try {
719
- return await this.performCompaction(options);
720
- } finally {
721
- release();
722
- }
723
- });
1289
+ validateCodexAuthentication(options.model, options.requestOptions.apiKey);
1290
+ const release = await this.acquireRequest(
1291
+ options.requestOptions.sessionId,
1292
+ options.requestOptions.signal,
1293
+ );
1294
+ try {
1295
+ const compacted = await this.performCompaction(options);
1296
+ const sessionId = options.requestOptions.sessionId;
1297
+ if (sessionId) this.advanceWindow(sessionId);
1298
+ return compacted;
1299
+ } finally {
1300
+ release();
1301
+ }
724
1302
  }
725
1303
  }
726
1304
 
@@ -728,7 +1306,7 @@ export function registerCodexProvider(
728
1306
  pi: ExtensionAPI,
729
1307
  resolveConfig: ConfigResolver,
730
1308
  ): CodexProviderRuntime {
731
- const runtime = new CodexProviderRuntime(pi, resolveConfig);
1309
+ const runtime = new CodexProviderRuntime(pi, resolveConfig, resolveCodexInstallationId());
732
1310
  pi.on("session_start", (_event, ctx) => {
733
1311
  const base =
734
1312
  ctx.modelRegistry.getRegisteredNativeProvider(CODEX_PROVIDER) ??
@@ -736,5 +1314,7 @@ export function registerCodexProvider(
736
1314
  if (!base) throw new Error("Pi's built-in OpenAI Codex provider is unavailable.");
737
1315
  pi.registerProvider(runtime.createProvider(base));
738
1316
  });
1317
+ pi.on("agent_start", (_event, ctx) => runtime.beginAgentTurn(ctx));
1318
+ pi.on("agent_end", (_event, ctx) => runtime.endAgentTurn(ctx));
739
1319
  return runtime;
740
1320
  }