@f5-sales-demo/pi-ai 22.5.1 → 22.6.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/pi-ai",
4
- "version": "22.5.1",
4
+ "version": "22.6.0",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -46,7 +46,7 @@
46
46
  "@anthropic-ai/sdk": "0.124.0",
47
47
  "@aws-sdk/client-bedrock-runtime": "3.1127.0",
48
48
  "@bufbuild/protobuf": "2.14.1",
49
- "@f5-sales-demo/pi-utils": "22.5.1",
49
+ "@f5-sales-demo/pi-utils": "22.6.0",
50
50
  "@google/genai": "2.21.0",
51
51
  "@sinclair/typebox": "0.34.52",
52
52
  "@smithy/node-http-handler": "4.12.1",
@@ -192,7 +192,7 @@ function getCacheControl(
192
192
  }
193
193
 
194
194
  // Stealth mode: Mimic Claude Code headers and tool prefixing.
195
- export const claudeCodeVersion = "2.1.285";
195
+ export const claudeCodeVersion = "2.1.286";
196
196
  export const claudeToolPrefix: string = "proxy_";
197
197
  export const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
198
198
 
@@ -169,6 +169,7 @@ interface CodexProviderSessionState extends ProviderSessionState {
169
169
  }
170
170
 
171
171
  interface CodexRequestContext {
172
+ model: Model<"openai-codex-responses">;
172
173
  grammarToolInputProperties?: Map<string, string>;
173
174
  apiKey: string;
174
175
  accountId: string;
@@ -187,7 +188,16 @@ interface CodexRequestSetup {
187
188
  requestAbortController: AbortController;
188
189
  }
189
190
 
191
+ interface CodexWebSocketRequestState {
192
+ usedContinuation: boolean;
193
+ connection?: CodexWebSocketConnection;
194
+ }
195
+
190
196
  interface CodexStreamRuntime {
197
+ websocketRequestState?: CodexWebSocketRequestState;
198
+ continuationRecoveryAttempt: number;
199
+ sawResponseAccepted: boolean;
200
+ sawOutputEvent: boolean;
191
201
  grammarToolInputProperties?: Map<string, string>;
192
202
  eventStream: AsyncGenerator<Record<string, unknown>>;
193
203
  requestBodyForState: RequestBody;
@@ -470,6 +480,7 @@ async function buildCodexRequestContext(
470
480
  const replacement = await options?.onPayload?.(transformedBody, model);
471
481
  if (replacement !== undefined) transformedBody = replacement as typeof transformedBody;
472
482
  validateFinalResponsesRequest(model, transformedBody);
483
+ transformedBody = structuredCloneJSON(transformedBody);
473
484
 
474
485
  const requestHeaders = { ...(model.headers ?? {}), ...(options?.headers ?? {}) };
475
486
  const rawRequestDump: RawHttpRequestDump = {
@@ -489,6 +500,33 @@ async function buildCodexRequestContext(
489
500
  }
490
501
  const websocketState =
491
502
  sessionKey && providerSessionState ? getCodexWebSocketSessionState(sessionKey, providerSessionState) : undefined;
503
+
504
+ return {
505
+ model,
506
+ apiKey,
507
+ grammarToolInputProperties: new Map(
508
+ (context.tools ?? []).flatMap(tool =>
509
+ tool.constrainedSampling && tool.constrainedSampling.type === "grammar"
510
+ ? [[tool.name, (tool.parameters as { required?: string[] }).required?.[0] ?? "input"]]
511
+ : [],
512
+ ),
513
+ ),
514
+ accountId,
515
+ baseUrl,
516
+ url,
517
+ requestHeaders,
518
+ providerSessionState,
519
+ websocketState,
520
+ transformedBody,
521
+ rawRequestDump,
522
+ };
523
+ }
524
+
525
+ function applyCodexCachedReasoning(
526
+ model: Model<"openai-codex-responses">,
527
+ transformedBody: RequestBody,
528
+ websocketState: CodexWebSocketSessionState,
529
+ ): void {
492
530
  if (
493
531
  model.compat?.supportsCachedReasoningUpdates &&
494
532
  websocketState?.lastRequest?.reasoning?.effort &&
@@ -520,25 +558,6 @@ async function buildCodexRequestContext(
520
558
  transformedBody.reasoning.effort = previous.reasoning!.effort;
521
559
  }
522
560
  }
523
-
524
- return {
525
- apiKey,
526
- grammarToolInputProperties: new Map(
527
- (context.tools ?? []).flatMap(tool =>
528
- tool.constrainedSampling && tool.constrainedSampling.type === "grammar"
529
- ? [[tool.name, (tool.parameters as { required?: string[] }).required?.[0] ?? "input"]]
530
- : [],
531
- ),
532
- ),
533
- accountId,
534
- baseUrl,
535
- url,
536
- requestHeaders,
537
- providerSessionState,
538
- websocketState,
539
- transformedBody,
540
- rawRequestDump,
541
- };
542
561
  }
543
562
 
544
563
  async function buildTransformedCodexRequestBody(
@@ -626,6 +645,7 @@ async function openInitialCodexEventStream(
626
645
  eventStream: AsyncGenerator<Record<string, unknown>>;
627
646
  requestBodyForState: RequestBody;
628
647
  transport: CodexTransport;
648
+ websocketRequestState?: CodexWebSocketRequestState;
629
649
  }> {
630
650
  const { transformedBody, websocketState } = requestContext;
631
651
  if (websocketState && shouldUseCodexWebSocket(model, websocketState, options?.preferWebsockets)) {
@@ -641,6 +661,7 @@ async function openInitialCodexEventStream(
641
661
  websocketRetries,
642
662
  );
643
663
  } catch (error) {
664
+ if (requestSetup.requestSignal.aborted) throw error;
644
665
  const websocketError = error instanceof Error ? error : new Error(String(error));
645
666
  const isFatal = isCodexWebSocketFatalError(websocketError);
646
667
  const activateFallback = isFatal || websocketRetries >= websocketRetryBudget;
@@ -673,55 +694,73 @@ async function openCodexWebSocketTransport(
673
694
  eventStream: AsyncGenerator<Record<string, unknown>>;
674
695
  requestBodyForState: RequestBody;
675
696
  transport: CodexTransport;
697
+ websocketRequestState?: CodexWebSocketRequestState;
676
698
  }> {
677
- let websocketRequest = buildCodexWebSocketRequest(requestContext.transformedBody, websocketState);
678
- let attach = false;
699
+ const headers = () =>
700
+ createCodexHeaders(
701
+ requestContext.requestHeaders,
702
+ requestContext.accountId,
703
+ requestContext.apiKey,
704
+ options?.sessionId,
705
+ "websocket",
706
+ websocketState,
707
+ );
708
+ let connection = await getOrCreateCodexWebSocketConnection(
709
+ websocketState,
710
+ toWebSocketUrl(requestContext.url),
711
+ headers(),
712
+ requestSetup.requestSignal,
713
+ );
714
+ // Divergent accepted steering belongs to the old socket; reconnect before preparing the request.
679
715
  if (websocketState.acceptedSteering?.length) {
680
- const delta = websocketRequest.previous_response_id
681
- ? (websocketRequest.input as import("./openai-codex/request-transformer").InputItem[])
682
- : undefined;
716
+ const preview = structuredCloneJSON(requestContext.transformedBody);
717
+ applyCodexCachedReasoning(requestContext.model, preview, websocketState);
718
+ const request = buildCodexWebSocketRequest(preview, { ...websocketState });
683
719
  const plan = planSteeredRequest(
684
- delta,
720
+ request.previous_response_id ? (request.input as InputItem[]) : undefined,
685
721
  websocketState.acceptedSteering.flatMap(steer => steer.items),
686
722
  );
687
723
  if (plan.kind === "discard") {
688
- websocketState.connection?.close("steering-diverged");
689
- resetCodexWebSocketAppendState(websocketState);
690
- websocketRequest = buildCodexWebSocketRequest(requestContext.transformedBody, websocketState);
691
- } else if (plan.kind === "attach") attach = true;
692
- else websocketRequest.input = plan.input;
693
- websocketState.acceptedSteering = undefined;
694
- }
695
- const websocketHeaders = createCodexHeaders(
696
- requestContext.requestHeaders,
697
- requestContext.accountId,
698
- requestContext.apiKey,
699
- options?.sessionId,
700
- "websocket",
701
- websocketState,
702
- );
724
+ connection.close("steering-diverged");
725
+ connection = await getOrCreateCodexWebSocketConnection(
726
+ websocketState,
727
+ toWebSocketUrl(requestContext.url),
728
+ headers(),
729
+ requestSetup.requestSignal,
730
+ );
731
+ }
732
+ }
703
733
  const requestBodyForState = structuredCloneJSON(requestContext.transformedBody);
704
- logCodexDebug("codex websocket request", {
705
- url: toWebSocketUrl(requestContext.url),
706
- model: requestContext.transformedBody.model,
707
- reasoningEffort: requestContext.transformedBody.reasoning?.effort ?? null,
708
- headers: redactHeaders(websocketHeaders),
709
- sentTurnStateHeader: websocketHeaders.has(X_CODEX_TURN_STATE_HEADER),
710
- sentModelsEtagHeader: websocketHeaders.has(X_MODELS_ETAG_HEADER),
711
- requestType: websocketRequest.type,
712
- retry,
713
- retryBudget: getCodexWebSocketRetryBudget(),
714
- });
715
- const eventStream = await openCodexWebSocketEventStream(
716
- toWebSocketUrl(requestContext.url),
717
- websocketHeaders,
718
- websocketRequest,
719
- websocketState,
734
+ const websocketRequestState: CodexWebSocketRequestState = { usedContinuation: false, connection };
735
+ const eventStream = connection.streamRequest(
736
+ () => {
737
+ applyCodexCachedReasoning(requestContext.model, requestBodyForState, websocketState);
738
+ const request = buildCodexWebSocketRequest(requestBodyForState, websocketState);
739
+ let attach = false;
740
+ if (websocketState.acceptedSteering?.length) {
741
+ const plan = planSteeredRequest(
742
+ request.previous_response_id ? (request.input as InputItem[]) : undefined,
743
+ websocketState.acceptedSteering.flatMap(steer => steer.items),
744
+ );
745
+ if (plan.kind === "attach") attach = true;
746
+ else if (plan.kind === "create") request.input = plan.input;
747
+ websocketState.acceptedSteering = undefined;
748
+ }
749
+ websocketRequestState.usedContinuation =
750
+ typeof request.previous_response_id === "string" &&
751
+ request.previous_response_id === websocketState.lastResponseId;
752
+ logCodexDebug("codex websocket request", {
753
+ retry,
754
+ retryBudget: getCodexWebSocketRetryBudget(),
755
+ requestType: request.type,
756
+ });
757
+ return { request, attach };
758
+ },
720
759
  requestSetup.requestSignal,
721
760
  options?.liveSteering,
722
- attach,
761
+ websocketState,
723
762
  );
724
- return { eventStream, requestBodyForState, transport: "websocket" };
763
+ return { eventStream, requestBodyForState, transport: "websocket", websocketRequestState };
725
764
  }
726
765
 
727
766
  async function openCodexSseTransport(
@@ -734,6 +773,7 @@ async function openCodexSseTransport(
734
773
  eventStream: AsyncGenerator<Record<string, unknown>>;
735
774
  requestBodyForState: RequestBody;
736
775
  transport: CodexTransport;
776
+ websocketRequestState?: CodexWebSocketRequestState;
737
777
  }> {
738
778
  const eventStream = requestSetup.wrapCodexSseStream(
739
779
  await openCodexSseEventStream(
@@ -762,6 +802,7 @@ async function reopenCodexWebSocketRuntimeStream(
762
802
  state,
763
803
  runtime.websocketStreamRetries,
764
804
  );
805
+ runtime.websocketRequestState = next.websocketRequestState;
765
806
  runtime.eventStream = next.eventStream;
766
807
  runtime.requestBodyForState = next.requestBodyForState;
767
808
  runtime.transport = next.transport;
@@ -774,6 +815,7 @@ async function reopenCodexSseRuntimeStream(
774
815
  state: CodexWebSocketSessionState | undefined,
775
816
  ): Promise<void> {
776
817
  const next = await openCodexSseTransport(context.requestContext, context.requestSetup, context.options, state);
818
+ runtime.websocketRequestState = next.websocketRequestState;
777
819
  runtime.eventStream = next.eventStream;
778
820
  runtime.requestBodyForState = next.requestBodyForState;
779
821
  runtime.transport = next.transport;
@@ -787,8 +829,13 @@ function createCodexStreamRuntime(initial: {
787
829
  requestBodyForState: RequestBody;
788
830
  transport: CodexTransport;
789
831
  websocketState?: CodexWebSocketSessionState;
832
+ websocketRequestState?: CodexWebSocketRequestState;
790
833
  }): CodexStreamRuntime {
791
834
  return {
835
+ websocketRequestState: initial.websocketRequestState,
836
+ continuationRecoveryAttempt: 0,
837
+ sawResponseAccepted: false,
838
+ sawOutputEvent: false,
792
839
  eventStream: initial.eventStream,
793
840
  requestBodyForState: initial.requestBodyForState,
794
841
  transport: initial.transport,
@@ -847,6 +894,14 @@ function handleCodexStreamEvent(args: {
847
894
  const { model, output, stream, runtime, rawEvent } = args;
848
895
  const eventType = typeof rawEvent.type === "string" ? rawEvent.type : "";
849
896
  if (!eventType) return args.firstTokenTime;
897
+ if (
898
+ eventType === "response.created" ||
899
+ eventType === "response.in_progress" ||
900
+ (eventType === "response.failed" && typeof asRecord(rawEvent.response)?.id === "string")
901
+ )
902
+ runtime.sawResponseAccepted = true;
903
+ if (/^response\.(?:output|content|reasoning|function_call|custom_tool_call)/.test(eventType))
904
+ runtime.sawOutputEvent = true;
850
905
 
851
906
  if (eventType !== "response.output_item.added" && typeof rawEvent.output_index === "number") {
852
907
  const slot = runtime.outputSlots.get(rawEvent.output_index);
@@ -1215,7 +1270,14 @@ function handleResponseCreated(
1215
1270
  };
1216
1271
  }
1217
1272
  const state = runtime.websocketState;
1218
- if (runtime.transport === "websocket" && state && typeof response?.id === "string" && response.id.length > 0) {
1273
+ if (
1274
+ runtime.transport === "websocket" &&
1275
+ state &&
1276
+ state.connection === runtime.websocketRequestState?.connection &&
1277
+ state.connection?.isOpen() &&
1278
+ typeof response?.id === "string" &&
1279
+ response.id.length > 0
1280
+ ) {
1219
1281
  state.lastResponseId = response.id;
1220
1282
  }
1221
1283
  }
@@ -1281,7 +1343,12 @@ function handleResponseCompleted(
1281
1343
  }
1282
1344
 
1283
1345
  const state = runtime.websocketState;
1284
- if (runtime.transport === "websocket" && state) {
1346
+ if (
1347
+ runtime.transport === "websocket" &&
1348
+ state &&
1349
+ state.connection === runtime.websocketRequestState?.connection &&
1350
+ state.connection?.isOpen()
1351
+ ) {
1285
1352
  state.lastRequest = structuredCloneJSON(runtime.requestBodyForState);
1286
1353
  if (typeof response?.id === "string" && response.id.length > 0) {
1287
1354
  state.lastResponseId = response.id;
@@ -1309,6 +1376,9 @@ async function recoverCodexStreamError(
1309
1376
  runtime: CodexStreamRuntime,
1310
1377
  error: unknown,
1311
1378
  ): Promise<boolean> {
1379
+ if (isCodexRejectedContinuation(error)) {
1380
+ return tryRecoverCodexContinuation(context, runtime);
1381
+ }
1312
1382
  if (await tryReconnectCodexWebSocketOnConnectionLimit(context, runtime, error)) {
1313
1383
  return true;
1314
1384
  }
@@ -1321,6 +1391,55 @@ async function recoverCodexStreamError(
1321
1391
  return false;
1322
1392
  }
1323
1393
 
1394
+ function isCodexRejectedContinuation(error: unknown): boolean {
1395
+ return (
1396
+ error instanceof CodexProviderStreamError &&
1397
+ (error.code === "previous_response_not_found" ||
1398
+ (error.code === "invalid_request_error" &&
1399
+ (error.param === "previous_response_id" || /\bprevious_response_id\b/i.test(error.providerMessage))))
1400
+ );
1401
+ }
1402
+
1403
+ async function tryRecoverCodexContinuation(
1404
+ context: CodexStreamProcessingContext,
1405
+ runtime: CodexStreamRuntime,
1406
+ ): Promise<boolean> {
1407
+ const state = runtime.websocketState;
1408
+ if (!state || runtime.transport !== "websocket") return false;
1409
+ const eligible =
1410
+ runtime.websocketRequestState?.usedContinuation &&
1411
+ runtime.continuationRecoveryAttempt === 0 &&
1412
+ !runtime.sawResponseAccepted &&
1413
+ !runtime.sawOutputEvent &&
1414
+ !runtime.sawTerminalEvent &&
1415
+ context.output.content.length === 0 &&
1416
+ !context.requestSetup.requestSignal.aborted;
1417
+ state.connection?.close("continuation-rejected");
1418
+ state.connection = undefined;
1419
+ resetCodexWebSocketAppendState(state);
1420
+ resetCodexSessionMetadata(state);
1421
+ if (!eligible) return false;
1422
+ runtime.continuationRecoveryAttempt += 1;
1423
+ logCodexDebug("codex continuation recovery", {
1424
+ reason: "previous_response_unavailable",
1425
+ attempt: runtime.continuationRecoveryAttempt,
1426
+ transport: "websocket",
1427
+ });
1428
+ // Connection failures retain the normal WebSocket retry/SSE fallback policy, with full context.
1429
+ const next = await openInitialCodexEventStream(
1430
+ context.model,
1431
+ context.options,
1432
+ context.requestSetup,
1433
+ context.requestContext,
1434
+ );
1435
+ runtime.eventStream = next.eventStream;
1436
+ runtime.requestBodyForState = next.requestBodyForState;
1437
+ runtime.websocketRequestState = next.websocketRequestState;
1438
+ runtime.transport = next.transport;
1439
+ state.lastTransport = next.transport;
1440
+ return true;
1441
+ }
1442
+
1324
1443
  /**
1325
1444
  * Handles `websocket_connection_limit_reached` errors by closing the stale connection
1326
1445
  * and opening a fresh websocket. If content has already been emitted to the caller,
@@ -1586,6 +1705,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
1586
1705
  options,
1587
1706
  requestSetup,
1588
1707
  requestContext: {
1708
+ model,
1589
1709
  apiKey: "",
1590
1710
  accountId: "",
1591
1711
  baseUrl: model.baseUrl || CODEX_BASE_URL,
@@ -1684,6 +1804,7 @@ function getCodexWebSocketSessionState(
1684
1804
  }
1685
1805
 
1686
1806
  function resetCodexWebSocketAppendState(state: CodexWebSocketSessionState): void {
1807
+ state.acceptedSteering = undefined;
1687
1808
  state.canAppend = false;
1688
1809
  state.lastRequest = undefined;
1689
1810
  state.lastResponseItems = undefined;
@@ -1698,6 +1819,7 @@ function resetCodexSessionMetadata(state: CodexWebSocketSessionState): void {
1698
1819
 
1699
1820
  function recordCodexWebSocketFailure(state: CodexWebSocketSessionState, activateFallback: boolean): void {
1700
1821
  resetCodexWebSocketAppendState(state);
1822
+ resetCodexSessionMetadata(state);
1701
1823
  state.connection?.close("fallback");
1702
1824
  state.connection = undefined;
1703
1825
  state.lastFallbackAt = Date.now();
@@ -1843,6 +1965,7 @@ interface CodexWebSocketConnectionOptions {
1843
1965
  idleTimeoutMs: number;
1844
1966
  firstEventTimeoutMs: number;
1845
1967
  onHandshakeHeaders?: (headers: Headers) => void;
1968
+ onClose?: () => void;
1846
1969
  }
1847
1970
 
1848
1971
  class CodexWebSocketConnection {
@@ -1851,6 +1974,7 @@ class CodexWebSocketConnection {
1851
1974
  #idleTimeoutMs: number;
1852
1975
  #firstEventTimeoutMs: number;
1853
1976
  #onHandshakeHeaders?: (headers: Headers) => void;
1977
+ #onClose?: () => void;
1854
1978
  #socket: WebSocket | null = null;
1855
1979
  #queue: Array<Record<string, unknown> | Error | null> = [];
1856
1980
  #waiters: Array<() => void> = [];
@@ -1864,6 +1988,7 @@ class CodexWebSocketConnection {
1864
1988
  this.#idleTimeoutMs = options.idleTimeoutMs;
1865
1989
  this.#firstEventTimeoutMs = options.firstEventTimeoutMs;
1866
1990
  this.#onHandshakeHeaders = options.onHandshakeHeaders;
1991
+ this.#onClose = options.onClose;
1867
1992
  }
1868
1993
 
1869
1994
  isOpen(): boolean {
@@ -1882,6 +2007,7 @@ class CodexWebSocketConnection {
1882
2007
  this.#socket.close(1000, reason);
1883
2008
  }
1884
2009
  this.#socket = null;
2010
+ this.#onClose?.();
1885
2011
  for (const waiter of this.#steerWaiters.values()) waiter.reject(new Error("Steering socket closed"));
1886
2012
  this.#steerWaiters.clear();
1887
2013
  }
@@ -1929,6 +2055,7 @@ class CodexWebSocketConnection {
1929
2055
  }, CODEX_WEBSOCKET_CONNECT_TIMEOUT_MS);
1930
2056
 
1931
2057
  socket.addEventListener("open", event => {
2058
+ if (this.#socket !== socket) return;
1932
2059
  if (!settled) {
1933
2060
  settled = true;
1934
2061
  clearPending();
@@ -1937,6 +2064,7 @@ class CodexWebSocketConnection {
1937
2064
  }
1938
2065
  });
1939
2066
  socket.addEventListener("error", event => {
2067
+ if (this.#socket !== socket) return;
1940
2068
  const eventRecord = event as unknown as Record<string, unknown>;
1941
2069
  const detail =
1942
2070
  (typeof eventRecord.message === "string" && eventRecord.message) ||
@@ -1952,6 +2080,7 @@ class CodexWebSocketConnection {
1952
2080
  this.#push(error);
1953
2081
  });
1954
2082
  socket.addEventListener("close", event => {
2083
+ if (this.#socket !== socket) return;
1955
2084
  this.close("closed");
1956
2085
  if (!settled) {
1957
2086
  settled = true;
@@ -1963,6 +2092,7 @@ class CodexWebSocketConnection {
1963
2092
  this.#push(null);
1964
2093
  });
1965
2094
  socket.addEventListener("message", event => {
2095
+ if (this.#socket !== socket) return;
1966
2096
  if (typeof event.data !== "string") return;
1967
2097
  try {
1968
2098
  const parsed = JSON.parse(event.data) as Record<string, unknown>;
@@ -2016,11 +2146,10 @@ class CodexWebSocketConnection {
2016
2146
  return promise;
2017
2147
  }
2018
2148
  async *streamRequest(
2019
- request: Record<string, unknown>,
2149
+ prepare: () => { request: Record<string, unknown>; attach: boolean },
2020
2150
  signal?: AbortSignal,
2021
2151
  liveSteering?: import("../types").LiveSteering,
2022
2152
  state?: CodexWebSocketSessionState,
2023
- attach = false,
2024
2153
  ): AsyncGenerator<Record<string, unknown>> {
2025
2154
  if (!this.#socket || this.#socket.readyState !== WebSocket.OPEN) {
2026
2155
  throw createCodexWebSocketTransportError("websocket connection is unavailable");
@@ -2050,7 +2179,9 @@ class CodexWebSocketConnection {
2050
2179
  )
2051
2180
  : undefined;
2052
2181
  try {
2053
- if (!attach) this.#socket.send(JSON.stringify(request));
2182
+ if (signal?.aborted) throw createCodexWebSocketTransportError("request was aborted");
2183
+ const { request, attach } = prepare();
2184
+ if (!attach) this.#socket!.send(JSON.stringify(request));
2054
2185
  let sawFirstEvent = false;
2055
2186
  while (true) {
2056
2187
  const next = await this.#nextMessage(
@@ -2079,7 +2210,7 @@ class CodexWebSocketConnection {
2079
2210
  ) {
2080
2211
  if (pump) {
2081
2212
  const outcome = await pump.finish();
2082
- if (state) state.acceptedSteering = outcome.accepted;
2213
+ if (state && state.connection === this && this.isOpen()) state.acceptedSteering = outcome.accepted;
2083
2214
  if (outcome.uncertain) {
2084
2215
  this.close("steering-uncertain");
2085
2216
  if (state) resetCodexWebSocketAppendState(state);
@@ -2153,16 +2284,25 @@ async function getOrCreateCodexWebSocketConnection(
2153
2284
  }
2154
2285
  state.connection?.close("reconnect");
2155
2286
  resetCodexWebSocketAppendState(state);
2287
+ resetCodexSessionMetadata(state);
2288
+ for (const name of [X_CODEX_TURN_STATE_HEADER, X_MODELS_ETAG_HEADER, X_REASONING_INCLUDED_HEADER])
2289
+ delete headerRecord[name];
2156
2290
  logger.time("codexWs:newSocket");
2157
- state.connection = new CodexWebSocketConnection(url, headerRecord, {
2291
+ const connection = new CodexWebSocketConnection(url, headerRecord, {
2158
2292
  idleTimeoutMs: getCodexWebSocketIdleTimeoutMs(),
2159
2293
  firstEventTimeoutMs: getCodexWebSocketFirstEventTimeoutMs(),
2160
2294
  onHandshakeHeaders: handshakeHeaders => {
2161
- updateCodexSessionMetadataFromHeaders(state, handshakeHeaders);
2295
+ if (state.connection === connection) updateCodexSessionMetadataFromHeaders(state, handshakeHeaders);
2296
+ },
2297
+ onClose: () => {
2298
+ if (state.connection !== connection) return;
2299
+ resetCodexWebSocketAppendState(state);
2300
+ resetCodexSessionMetadata(state);
2162
2301
  },
2163
2302
  });
2164
- await state.connection.connect(signal);
2165
- return state.connection;
2303
+ state.connection = connection;
2304
+ await connection.connect(signal);
2305
+ return connection;
2166
2306
  }
2167
2307
 
2168
2308
  async function openCodexSseEventStream(
@@ -2216,19 +2356,6 @@ async function openCodexSseEventStream(
2216
2356
  return readSseJson<Record<string, unknown>>(response.body, signal);
2217
2357
  }
2218
2358
 
2219
- async function openCodexWebSocketEventStream(
2220
- url: string,
2221
- headers: Headers,
2222
- request: Record<string, unknown>,
2223
- state: CodexWebSocketSessionState,
2224
- signal?: AbortSignal,
2225
- liveSteering?: import("../types").LiveSteering,
2226
- attach = false,
2227
- ): Promise<AsyncGenerator<Record<string, unknown>>> {
2228
- const connection = await getOrCreateCodexWebSocketConnection(state, url, headers, signal);
2229
- return connection.streamRequest(request, signal, liveSteering, state, attach);
2230
- }
2231
-
2232
2359
  function createCodexHeaders(
2233
2360
  initHeaders: Record<string, string> | undefined,
2234
2361
  accountId: string,
@@ -2579,12 +2706,16 @@ function sanitizeProviderFailureCode(value: unknown): string | undefined {
2579
2706
  class CodexProviderStreamError extends Error {
2580
2707
  readonly retryable: boolean;
2581
2708
  readonly code?: string;
2709
+ readonly param?: string;
2710
+ readonly providerMessage: string;
2582
2711
 
2583
- constructor(message: string, retryable: boolean, code?: string) {
2712
+ constructor(message: string, retryable: boolean, code?: string, param?: string, providerMessage = message) {
2584
2713
  super(message);
2585
2714
  this.name = "CodexProviderStreamError";
2586
2715
  this.retryable = retryable;
2587
2716
  this.code = sanitizeProviderFailureCode(code);
2717
+ this.param = param;
2718
+ this.providerMessage = providerMessage;
2588
2719
  }
2589
2720
  }
2590
2721
 
@@ -2595,6 +2726,7 @@ function isRetryableCodexFailureEvent(rawEvent: Record<string, unknown>): boolea
2595
2726
  if (code && CODEX_RETRYABLE_EVENT_CODES.has(code.toLowerCase())) {
2596
2727
  return true;
2597
2728
  }
2729
+ if (code === "invalid_request_error" || code === "previous_response_not_found") return false;
2598
2730
  const message = getString(error?.message) ?? getString(rawEvent.message) ?? getString(response?.message);
2599
2731
  return !!message && CODEX_RETRYABLE_EVENT_MESSAGE.test(message);
2600
2732
  }
@@ -2603,12 +2735,19 @@ function createCodexProviderStreamError(rawEvent: Record<string, unknown>): Code
2603
2735
  const response = asRecord(rawEvent.response);
2604
2736
  const providerError = asRecord(rawEvent.error) ?? (response ? asRecord(response.error) : null);
2605
2737
  const code = getString(providerError?.code) ?? getString(providerError?.type) ?? getString(rawEvent.code) ?? "";
2606
- const message = getString(rawEvent.message) ?? "";
2738
+ const message = getString(providerError?.message) ?? getString(rawEvent.message) ?? "";
2739
+ const param = getString(providerError?.param) ?? getString(rawEvent.param);
2607
2740
  const formattedMessage =
2608
2741
  typeof rawEvent.type === "string" && rawEvent.type === "error"
2609
2742
  ? formatCodexErrorEvent(rawEvent, code, message)
2610
2743
  : (formatCodexFailure(rawEvent) ?? "Codex response failed");
2611
- return new CodexProviderStreamError(formattedMessage, isRetryableCodexFailureEvent(rawEvent), code || undefined);
2744
+ return new CodexProviderStreamError(
2745
+ formattedMessage,
2746
+ isRetryableCodexFailureEvent(rawEvent),
2747
+ code || undefined,
2748
+ param,
2749
+ message,
2750
+ );
2612
2751
  }
2613
2752
 
2614
2753
  function isRetryableCodexProviderError(error: unknown): boolean {