@bitkyc08/opencodex 2.47.0-preview.20260908 → 2.48.0-preview.20260908

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.
@@ -15,6 +15,7 @@ import { nativeContextLimits } from "../../codex/catalog";
15
15
  import { describeUpstreamConnectFailure } from "./upstream-error";
16
16
  import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
17
17
  import { applyAccountQuotaFromUpstreamHeaders as applyCapturedCodexQuota } from "../../codex/quota";
18
+ import { isCodexAccountGenerationLive } from "../../codex/account-store";
18
19
  import { isCodexWsQuotaObservedResponse } from "./ws-upstream";
19
20
  import {
20
21
  multiAgentGuidanceEnabled,
@@ -58,6 +59,10 @@ import {
58
59
  providerContinuationRouteScope,
59
60
  sameProviderContinuationOwner,
60
61
  } from "../../responses/provider-continuation";
62
+ import {
63
+ rememberComboForLane,
64
+ recallComboForLane,
65
+ } from "./combo-session-recall";
61
66
  import {
62
67
  comboRouteDecisionTrace,
63
68
  NoEligiblePolicyCandidateError,
@@ -78,6 +83,7 @@ import {
78
83
  comboRequestHasImageInput,
79
84
  concreteComboRequestBody,
80
85
  getCombo,
86
+ resolveComboId,
81
87
  isComboTargetInCooldown,
82
88
  NoAvailableComboTargetsError,
83
89
  noteComboSuccess,
@@ -310,6 +316,7 @@ import {
310
316
  consumeForInspection,
311
317
  consumeForResponseLogMetadata,
312
318
  createSseInspector,
319
+ terminalStatusFromParsed,
313
320
  isEagerRelaySseResponse,
314
321
  isNativePassthroughSseResponse,
315
322
  markEagerRelaySseResponse,
@@ -340,6 +347,7 @@ import {
340
347
  } from "../responses-item-id-repair";
341
348
  import {
342
349
  createReasoningSummaryChannelPayloadRewrite,
350
+ rewriteReasoningSummaryInJson,
343
351
  rewriteReasoningSummaryInJsonString,
344
352
  routeUsesContentChannelReasoning,
345
353
  } from "../responses-reasoning-summary-rewrite";
@@ -1004,8 +1012,12 @@ export function usesCodexForwardPoolAuth(
1004
1012
  function codexWsQuotaObserver(authCtx: CodexAuthContext, provider: OcxProviderConfig): CodexWsQuotaObserver | undefined {
1005
1013
  if (!isCanonicalOpenAiForwardProvider(provider) || !usesCodexForwardPoolAuth(authCtx, provider)) return undefined;
1006
1014
  const { accountId, writerGeneration } = authCtx;
1015
+ const credentialGeneration = authCtx.kind === "pool" ? authCtx.generation : undefined;
1007
1016
  const mainWriter = authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined;
1008
- return headers => applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter);
1017
+ return headers => {
1018
+ if (credentialGeneration !== undefined && !isCodexAccountGenerationLive(accountId, credentialGeneration)) return;
1019
+ applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter);
1020
+ };
1009
1021
  }
1010
1022
 
1011
1023
  export function preAuthUpstreamHostCircuitKey(
@@ -1661,6 +1673,8 @@ export interface HandleResponsesOptions {
1661
1673
  onCodexAuthContextResolved?: (context: CodexAuthContext | undefined) => void;
1662
1674
  /** Internal deterministic seam for account-gated native fallback tests. */
1663
1675
  resolveCodexModelEntitlements?: typeof resolveCodexModelEntitlements;
1676
+ /** Internal: validated final client-visible model, after completed terminal success only. */
1677
+ onResponseComplete?: (model: string) => void;
1664
1678
  recordTerminalOutcomes?: boolean;
1665
1679
  setTerminalOutcomeRecorder?: (recorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined) => void;
1666
1680
  onNativePassthroughTerminal?: (status: ResponsesTerminalStatus) => void;
@@ -1861,34 +1875,54 @@ export function createChildPassthroughCallbackGate(options: HandleResponsesOptio
1861
1875
  let state: "pending" | "committed" | "discarded" = "pending";
1862
1876
  let pending: Pending | undefined;
1863
1877
  let accepted = false;
1878
+ let pendingModel: string | undefined;
1879
+ let completionAccepted = false;
1880
+ let completionRejected = false;
1864
1881
  const publish = (value: Pending): void => {
1865
1882
  if (value.kind === "terminal") options.onNativePassthroughTerminal?.(value.status);
1866
1883
  else options.onNativePassthroughCancel?.();
1867
1884
  };
1885
+ const publishCompletion = (): void => {
1886
+ if (state !== "committed" || completionRejected || pendingModel === undefined) return;
1887
+ const model = pendingModel;
1888
+ pendingModel = undefined;
1889
+ options.onResponseComplete?.(model);
1890
+ };
1868
1891
  const receive = (value: Pending): void => {
1869
1892
  if (state === "discarded" || accepted) return;
1870
1893
  accepted = true;
1894
+ if (value.kind === "cancel" || value.status !== "completed") {
1895
+ completionRejected = true;
1896
+ pendingModel = undefined;
1897
+ }
1871
1898
  if (state === "committed") return publish(value);
1872
1899
  pending ??= value;
1873
1900
  };
1874
1901
  return {
1875
1902
  onTerminal: (status: ResponsesTerminalStatus) => receive({ kind: "terminal", status }),
1876
1903
  onCancel: () => receive({ kind: "cancel" }),
1904
+ onResponseComplete: (model: string) => {
1905
+ if (state === "discarded" || completionRejected || completionAccepted || !model.trim()) return;
1906
+ completionAccepted = true;
1907
+ pendingModel = model;
1908
+ publishCompletion();
1909
+ },
1877
1910
  commit: () => {
1878
1911
  if (state !== "pending") return;
1879
1912
  state = "committed";
1880
1913
  if (pending) publish(pending);
1881
1914
  pending = undefined;
1915
+ publishCompletion();
1882
1916
  },
1883
1917
  discard: () => {
1884
1918
  state = "discarded";
1885
1919
  pending = undefined;
1920
+ pendingModel = undefined;
1886
1921
  },
1887
1922
  };
1888
1923
  }
1889
1924
 
1890
1925
 
1891
-
1892
1926
  export function buildComboChildHeaders(parentHeaders: HeadersInit): Headers {
1893
1927
  const childHeaders = new Headers(parentHeaders);
1894
1928
  // Combo children re-serialize already-decoded JSON. Keeping transport metadata from
@@ -2689,9 +2723,22 @@ export async function handleComboResponses(
2689
2723
  (logCtx.attempts ??= []).push(attempt);
2690
2724
  attemptRetained = true;
2691
2725
  };
2726
+ const completedTarget = { provider: pick.target.provider, model: pick.target.model };
2727
+ const writerGeneration = pick.writerGeneration;
2692
2728
  let consumedChildFailure: ConsumedComboFailure | undefined;
2693
2729
  const callbackGate = createChildPassthroughCallbackGate({
2694
2730
  ...options,
2731
+ onResponseComplete: model => {
2732
+ // The live config can change while the child is streaming. Never retain credentials.
2733
+ const currentCombo = getCombo(config, comboId);
2734
+ const provider = config.providers[completedTarget.provider];
2735
+ if (Object.hasOwn(config.providers, completedTarget.provider)
2736
+ && provider && provider.disabled !== true
2737
+ && currentCombo?.targets.some(target => targetKey(target) === targetKey(completedTarget))) {
2738
+ rememberComboForLane(sessionLaneIdFromRequest(req.headers), comboId, completedTarget, model, writerGeneration);
2739
+ }
2740
+ options.onResponseComplete?.(model);
2741
+ },
2695
2742
  onNativePassthroughTerminal: status => {
2696
2743
  // A committed stream can acquire terminal metadata after preflight copied
2697
2744
  // the child log. Publish it before the outer logger finalizes, but only
@@ -2732,6 +2779,7 @@ export async function handleComboResponses(
2732
2779
  onStoredPool401ReplayDispatched: () => { storedPool401ReplayDispatched = true; },
2733
2780
  onNativePassthroughTerminal: callbackGate.onTerminal,
2734
2781
  onNativePassthroughCancel: callbackGate.onCancel,
2782
+ onResponseComplete: callbackGate.onResponseComplete,
2735
2783
  });
2736
2784
  } catch (error) {
2737
2785
  callbackGate.discard();
@@ -3104,6 +3152,23 @@ async function handleResponsesInner(
3104
3152
  effort: comboEffortRow.effort,
3105
3153
  };
3106
3154
  }
3155
+ // Compaction may send the last client-visible bare model after a combo switch.
3156
+ // Configured selectors take precedence; otherwise recall before combo dispatch (#3891).
3157
+ if (!options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)) {
3158
+ const rawModel = (body as { model?: unknown }).model;
3159
+ const rawInput = (body as { input?: unknown }).input;
3160
+ const isCompactionTrigger = Array.isArray(rawInput)
3161
+ && rawInput.some((item: unknown) =>
3162
+ typeof item === "object" && item !== null && (item as { type?: string }).type === "compaction_trigger");
3163
+ if (typeof rawModel === "string" && !rawModel.includes("/") && isCompactionTrigger
3164
+ && !comboRows.fastRow && !comboEffortRow
3165
+ && !resolveComboId(config, rawModel)) {
3166
+ const recalledComboId = recallComboForLane(config, sessionLaneIdFromRequest(req.headers), rawModel);
3167
+ if (recalledComboId) {
3168
+ (body as Record<string, unknown>).model = `combo/${recalledComboId}`;
3169
+ }
3170
+ }
3171
+ }
3107
3172
  const comboId = !options.comboAttempt ? comboIdFromRawBody(body, config) : null;
3108
3173
  if (comboId && Object.hasOwn(config.combos ?? {}, comboId)) {
3109
3174
  options.onRequestBodyRead?.();
@@ -3595,14 +3660,16 @@ async function handleResponsesInner(
3595
3660
  // The canonical ChatGPT backend rejects previous_response_id, so a local replay miss leaves no
3596
3661
  // safe way to recover the omitted history. Fail before auth, adapter construction, or upstream
3597
3662
  // I/O instead of stripping the id and silently forwarding a context-free delta (#702).
3663
+ // Codex recognizes previous_response_not_found on WebSocket errors and reconnects with its
3664
+ // full input. A generic invalid_request_error instead terminates the task after cache expiry.
3598
3665
  if (
3599
3666
  hasUnexpandedPreviousResponse
3600
3667
  && isCanonicalOpenAiForwardProvider(route.provider)
3601
3668
  ) {
3602
3669
  return formatErrorResponse(
3603
3670
  400,
3604
- "invalid_request_error",
3605
- "OpenAI forward continuation state is unavailable or expired; start a new session instead of reusing this previous_response_id.",
3671
+ "previous_response_not_found",
3672
+ "OpenAI forward continuation state is unavailable or expired; resend the full conversation without previous_response_id.",
3606
3673
  );
3607
3674
  }
3608
3675
 
@@ -4305,6 +4372,17 @@ async function handleResponsesInner(
4305
4372
  }
4306
4373
 
4307
4374
  const recordTerminalOutcomes = options.recordTerminalOutcomes !== false;
4375
+ let responseCompletionNotified = false;
4376
+ let responseCompletionCancelled = false;
4377
+ const cancelResponseCompletion = (): void => { responseCompletionCancelled = true; };
4378
+ const notifyResponseComplete = (response: { status?: unknown; model?: unknown }): void => {
4379
+ if (responseCompletionNotified || responseCompletionCancelled
4380
+ || options.abortSignal?.aborted || req.signal.aborted
4381
+ || response.status !== "completed"
4382
+ || typeof response.model !== "string" || !response.model.trim()) return;
4383
+ responseCompletionNotified = true;
4384
+ options.onResponseComplete?.(response.model);
4385
+ };
4308
4386
 
4309
4387
  const continuationStateForResponse = (
4310
4388
  emitted?: OcxProviderContinuationState,
@@ -4582,9 +4660,26 @@ async function handleResponsesInner(
4582
4660
  // check sees nothing undeclared, and the refused turn enters continuation state anyway. So the
4583
4661
  // rejection is sticky for the whole turn, set from every parsed payload on the inspection side.
4584
4662
  let inspectionSawUndeclaredTool = false;
4663
+ let inspectedTerminal: ResponsesTerminalStatus | null = null;
4664
+ let inspectedCompletionSeen = false;
4665
+ let firstTerminalAllowsRecall = false;
4585
4666
  const passiveQuotaObserved = hasPassiveAccountQuota(route.providerName)
4586
4667
  && route.provider.authMode === "oauth";
4587
4668
  const noteInspectedPayload = (payload: unknown) => {
4669
+ // First terminal stays authoritative even in metadata-only inspection, which
4670
+ // intentionally continues parsing after a failed/incomplete terminal.
4671
+ const terminal = terminalStatusFromParsed(payload);
4672
+ if (inspectedTerminal === null && terminal !== null) {
4673
+ inspectedTerminal = terminal;
4674
+ // The client boundary accepts a terminal by event type, even without a
4675
+ // response object. Such a terminal must permanently decline recall.
4676
+ if (terminal === "completed" && payload && typeof payload === "object"
4677
+ && "response" in payload && payload.response && typeof payload.response === "object"
4678
+ && !Array.isArray(payload.response) && "model" in payload.response) {
4679
+ firstTerminalAllowsRecall = typeof payload.response.model === "string"
4680
+ && payload.response.model.trim().length > 0;
4681
+ }
4682
+ }
4588
4683
  // Meta reports subscription usage ONLY as an in-stream event; there is no endpoint
4589
4684
  // to poll (003 §E probed 17 paths, all 404). Observed here rather than behind a
4590
4685
  // dedicated inspector handler because onParsedPayload already reaches every
@@ -4606,8 +4701,7 @@ async function handleResponsesInner(
4606
4701
  // Gated on the same flag as the guard itself: with no readable catalog (or a forward-auth
4607
4702
  // provider) every name looks undeclared, and flipping this would stop recording continuation
4608
4703
  // state for exactly the passthrough traffic the guard deliberately stands down for.
4609
- if (!undeclaredToolGuardActive || inspectionSawUndeclaredTool) return;
4610
- if (undeclaredToolCallName(
4704
+ if (undeclaredToolGuardActive && !inspectionSawUndeclaredTool && undeclaredToolCallName(
4611
4705
  restoreAuthorizedBareNamespaceToolCalls(payload),
4612
4706
  declaredWireToolNames,
4613
4707
  declaredNamelessClientCallTypes,
@@ -4615,33 +4709,61 @@ async function handleResponsesInner(
4615
4709
  ) !== undefined) {
4616
4710
  inspectionSawUndeclaredTool = true;
4617
4711
  }
4712
+ // The snapshot callback opts the inspector into output reconstruction. Compaction
4713
+ // has no continuation cache, so use the parsed terminal here without adding retention.
4714
+ if (!rememberPassthroughResponse && payload && typeof payload === "object"
4715
+ && "type" in payload && payload.type === "response.completed"
4716
+ && "response" in payload && payload.response && typeof payload.response === "object"
4717
+ && !Array.isArray(payload.response)) {
4718
+ rememberPassthroughResponseChecked(payload.response as Record<string, unknown>);
4719
+ }
4618
4720
  };
4619
- const rememberPassthroughResponseChecked = rememberPassthroughResponse
4620
- ? (response: { id?: unknown; output?: unknown; status?: unknown }) => {
4621
- if (inspectionSawUndeclaredTool) return;
4622
- const restored = restoreRoutedCustomCalls(
4623
- restoreAuthorizedBareNamespaceToolCalls(restoreRoutedNamespaceCalls(response, routedNamespaceToolAliases).value),
4624
- routedCustomToolNames,
4625
- routedCustomToolRepairNames,
4721
+ const rememberPassthroughResponseChecked = (
4722
+ response: { id?: unknown; output?: unknown; status?: unknown; model?: unknown },
4723
+ ) => {
4724
+ if (inspectionSawUndeclaredTool) return;
4725
+ const restored = restoreRoutedCustomCalls(
4726
+ restoreAuthorizedBareNamespaceToolCalls(restoreRoutedNamespaceCalls(response, routedNamespaceToolAliases).value),
4727
+ routedCustomToolNames,
4728
+ routedCustomToolRepairNames,
4729
+ declaredWireToolNames,
4730
+ ).value;
4731
+ const restoredResponse = (functionRepairSchemas.size > 0
4732
+ ? JSON.parse(normalizeFunctionCompletionJson(JSON.stringify(restored)))
4733
+ : restored) as { id?: unknown; output?: unknown; status?: unknown };
4734
+ // Replay overlap compares the items the client echoes, including visible reasoning shape.
4735
+ const replayResponse = parsed.options.hideThinkingSummary !== true
4736
+ && routeUsesContentChannelReasoning(route.provider, route.modelId)
4737
+ ? rewriteReasoningSummaryInJson(restoredResponse) as typeof restoredResponse
4738
+ : restoredResponse;
4739
+ if (
4740
+ undeclaredToolGuardActive
4741
+ && undeclaredToolCallNameInResponse(
4742
+ restoredResponse,
4626
4743
  declaredWireToolNames,
4627
- ).value;
4628
- const restoredResponse = (functionRepairSchemas.size > 0
4629
- ? JSON.parse(normalizeFunctionCompletionJson(JSON.stringify(restored)))
4630
- : restored) as { id?: unknown; output?: unknown; status?: unknown };
4631
- if (
4632
- undeclaredToolGuardActive
4633
- && undeclaredToolCallNameInResponse(
4634
- restoredResponse,
4635
- declaredWireToolNames,
4636
- declaredNamelessClientCallTypes,
4637
- providerExecutedCallTypes,
4638
- ) !== undefined
4639
- ) {
4640
- return;
4744
+ declaredNamelessClientCallTypes,
4745
+ providerExecutedCallTypes,
4746
+ ) !== undefined
4747
+ ) {
4748
+ return;
4749
+ }
4750
+ rememberPassthroughResponse?.(replayResponse);
4751
+ const firstCompletion = !inspectedCompletionSeen;
4752
+ inspectedCompletionSeen = true;
4753
+ if (firstCompletion && (inspectedTerminal === null || firstTerminalAllowsRecall)) {
4754
+ // A model-less first completion permanently declines recall; later terminal
4755
+ // frames are hidden by the client boundary and cannot supply its identity.
4756
+ // Native inspection sees the pre-rewrite model. Only an actual terminal
4757
+ // model can seed recall; an absent model never falls back to the pick.
4758
+ if (typeof response.model === "string" && response.model.trim()) {
4759
+ notifyResponseComplete({
4760
+ status: response.status,
4761
+ model: parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId
4762
+ ? parsed._responseModelId : response.model,
4763
+ });
4641
4764
  }
4642
- rememberPassthroughResponse(restoredResponse);
4643
4765
  }
4644
- : undefined;
4766
+ };
4645
4767
  recordAdapterReasoning(logCtx, request);
4646
4768
  recordAdapterTier(logCtx, request);
4647
4769
  const actualHostKey = upstreamHostHealthKey(
@@ -5649,7 +5771,7 @@ async function handleResponsesInner(
5649
5771
  const inspector = createSseInspector({
5650
5772
  onTerminal: reportNativeTerminal,
5651
5773
  logCtx,
5652
- onCompletedResponse: rememberPassthroughResponseChecked,
5774
+ onCompletedResponse: rememberPassthroughResponse ? rememberPassthroughResponseChecked : undefined,
5653
5775
  onParsedPayload: noteInspectedPayload,
5654
5776
  onFirstOutput: options.onFirstOutput,
5655
5777
  pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
@@ -5679,7 +5801,10 @@ async function handleResponsesInner(
5679
5801
  reportNativeTerminal("failed", 502);
5680
5802
  }
5681
5803
  },
5682
- onClientCancel: () => options.onNativePassthroughCancel?.(),
5804
+ onClientCancel: () => {
5805
+ responseCompletionCancelled = true;
5806
+ options.onNativePassthroughCancel?.();
5807
+ },
5683
5808
  onDone: () => unregisterTurn(turnAc),
5684
5809
  }, {
5685
5810
  clientGoneSignal: options.abortSignal,
@@ -5741,8 +5866,11 @@ async function handleResponsesInner(
5741
5866
  turnAc.signal,
5742
5867
  () => unregisterTurn(turnAc),
5743
5868
  logCtx,
5744
- () => options.onNativePassthroughCancel?.(),
5745
- rememberPassthroughResponseChecked,
5869
+ () => {
5870
+ responseCompletionCancelled = true;
5871
+ options.onNativePassthroughCancel?.();
5872
+ },
5873
+ rememberPassthroughResponse ? rememberPassthroughResponseChecked : undefined,
5746
5874
  options.onFirstOutput,
5747
5875
  inspectionConsumerOptions,
5748
5876
  );
@@ -5752,7 +5880,7 @@ async function handleResponsesInner(
5752
5880
  logCtx,
5753
5881
  turnAc.signal,
5754
5882
  () => unregisterTurn(turnAc),
5755
- rememberPassthroughResponseChecked,
5883
+ rememberPassthroughResponse ? rememberPassthroughResponseChecked : undefined,
5756
5884
  options.onFirstOutput,
5757
5885
  inspectionConsumerOptions,
5758
5886
  );
@@ -5767,7 +5895,10 @@ async function handleResponsesInner(
5767
5895
  const clientBody = relaySseWithFailedTail(
5768
5896
  rewrittenBody,
5769
5897
  upstream,
5770
- reason => clientGone.abort(reason),
5898
+ reason => {
5899
+ responseCompletionCancelled = true;
5900
+ clientGone.abort(reason);
5901
+ },
5771
5902
  { upstreamError: logCtx.upstreamError },
5772
5903
  );
5773
5904
  return markNativePassthroughSseResponse(new Response(clientBody, {
@@ -5847,13 +5978,11 @@ async function handleResponsesInner(
5847
5978
  }
5848
5979
  }
5849
5980
  commitReasoningReplayServingRoute();
5850
- if (rememberPassthroughResponseChecked) {
5851
- try {
5852
- rememberPassthroughResponseChecked(
5853
- JSON.parse(text) as { id?: unknown; output?: unknown; status?: unknown },
5854
- );
5855
- } catch { /* non-JSON despite content-type; recording is best-effort */ }
5856
- }
5981
+ try {
5982
+ rememberPassthroughResponseChecked(
5983
+ JSON.parse(text) as { id?: unknown; output?: unknown; status?: unknown; model?: unknown },
5984
+ );
5985
+ } catch { /* non-JSON despite content-type; recording is best-effort */ }
5857
5986
  // #875: the transport-neutral reliability policy forced a bounded JSON
5858
5987
  // upstream for a client that asked for SSE. Reframe the completed JSON
5859
5988
  // as the canonical terminal SSE sequence (created → output_item.done →
@@ -6187,10 +6316,12 @@ async function handleResponsesInner(
6187
6316
  continuationStateForResponse(providerState),
6188
6317
  responseStateOptions(adapterNeedsForcedContinuation(adapter.name)),
6189
6318
  );
6319
+ notifyResponseComplete(response);
6190
6320
  },
6191
6321
  });
6192
6322
  if (imgResponse.body) {
6193
6323
  const imgTurnAc = new AbortController();
6324
+ imgTurnAc.signal.addEventListener("abort", cancelResponseCompletion, { once: true });
6194
6325
  return new Response(trackStreamLifetime(imgResponse.body, imgTurnAc, undefined, options.turnAdmissionLease), {
6195
6326
  status: imgResponse.status,
6196
6327
  headers: imgResponse.headers,
@@ -6261,12 +6392,16 @@ async function handleResponsesInner(
6261
6392
  streamRoutedModelOutput: wsPlan.streamRoutedModelOutput,
6262
6393
  on429: rotateSidecarProviderOn429,
6263
6394
  retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
6264
- onCompletedResponse: commitReasoningReplayServingRoute,
6395
+ onCompletedResponse: response => {
6396
+ commitReasoningReplayServingRoute();
6397
+ notifyResponseComplete(response);
6398
+ },
6265
6399
  });
6266
6400
  // Register the sidecar stream as an active turn so drainAndShutdown waits for (or aborts)
6267
6401
  // in-flight web-search turns instead of skipping them during graceful shutdown.
6268
6402
  if (wsResponse.body) {
6269
6403
  const wsTurnAc = new AbortController();
6404
+ wsTurnAc.signal.addEventListener("abort", cancelResponseCompletion, { once: true });
6270
6405
  return new Response(trackStreamLifetime(wsResponse.body, wsTurnAc, undefined, options.turnAdmissionLease), {
6271
6406
  status: wsResponse.status,
6272
6407
  headers: wsResponse.headers,
@@ -6490,6 +6625,7 @@ async function handleResponsesInner(
6490
6625
  const sseStream = bridgeToResponsesSSE(
6491
6626
  guardedSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
6492
6627
  () => {
6628
+ cancelResponseCompletion();
6493
6629
  runTurnAbort.abort();
6494
6630
  queue.close();
6495
6631
  }, 2_000,
@@ -6526,6 +6662,7 @@ async function handleResponsesInner(
6526
6662
  responseStateOptions(adapterNeedsForcedContinuation(adapter.name)),
6527
6663
  );
6528
6664
  }
6665
+ notifyResponseComplete(response);
6529
6666
  },
6530
6667
  },
6531
6668
  );
@@ -6602,6 +6739,7 @@ async function handleResponsesInner(
6602
6739
  if (adapterResponseReachedServingTerminal(events, json)) {
6603
6740
  commitReasoningReplayServingRoute();
6604
6741
  }
6742
+ notifyResponseComplete(json);
6605
6743
  return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
6606
6744
  }
6607
6745
 
@@ -6649,10 +6787,11 @@ async function handleResponsesInner(
6649
6787
  toolBridgeMaps.toolNsMap,
6650
6788
  toolBridgeMaps.freeformToolNames,
6651
6789
  toolBridgeMaps.toolSearchToolNames,
6652
- undefined,
6790
+ cancelResponseCompletion,
6653
6791
  2_000,
6654
6792
  {
6655
6793
  translatorBudget,
6794
+ onCompletedResponse: notifyResponseComplete,
6656
6795
  ...(options.forceEmptyResponseId ? { responseId: "" } : {}),
6657
6796
  ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
6658
6797
  },
@@ -6673,12 +6812,9 @@ async function handleResponsesInner(
6673
6812
  },
6674
6813
  );
6675
6814
  }
6676
- return new Response(
6677
- JSON.stringify(buildResponseJSON(terminalEvents, parsed._responseModelId ?? parsed.modelId, {
6678
- translatorBudget,
6679
- })),
6680
- { headers: { "Content-Type": "application/json" } },
6681
- );
6815
+ const json = buildResponseJSON(terminalEvents, parsed._responseModelId ?? parsed.modelId, { translatorBudget });
6816
+ notifyResponseComplete(json);
6817
+ return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
6682
6818
  }
6683
6819
  // One request-scoped transient-retry budget owner, declared here so BOTH the initial send
6684
6820
  // and the later recovery refetches (429, key/account rotation, OAuth replay) share it. A
@@ -7660,7 +7796,7 @@ async function handleResponsesInner(
7660
7796
  const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
7661
7797
  const sseStream = bridgeToResponsesSSE(
7662
7798
  guardedEventStream, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
7663
- () => upstream.abort(), 2_000,
7799
+ () => { cancelResponseCompletion(); upstream.abort(); }, 2_000,
7664
7800
  {
7665
7801
  translatorBudget,
7666
7802
  replayCacheScope: parsed._reasoningReplayScope,
@@ -7695,6 +7831,7 @@ async function handleResponsesInner(
7695
7831
  responseStateOptions(activeAdapter.name === "kiro"),
7696
7832
  );
7697
7833
  }
7834
+ notifyResponseComplete(response);
7698
7835
  },
7699
7836
  },
7700
7837
  );
@@ -7774,6 +7911,7 @@ async function handleResponsesInner(
7774
7911
  if (adapterResponseReachedServingTerminal(events, json)) {
7775
7912
  commitReasoningReplayServingRoute();
7776
7913
  }
7914
+ notifyResponseComplete(json);
7777
7915
  return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
7778
7916
  }
7779
7917
 
@@ -469,8 +469,8 @@ export interface OcxConfig {
469
469
  */
470
470
  syncCodexSubagentDefaults?: boolean;
471
471
  /**
472
- * Optional reasoning effort the delegation prompt tells the agent to pass in spawn_agent calls
473
- * (`reasoning_effort` argument). Only meaningful while `injectionModel` is set; validated against
472
+ * Optional reasoning effort reported as advisory metadata in v2 sub-agent guidance.
473
+ * It does not prescribe spawn overrides. Only meaningful while `injectionModel` is set; validated against
474
474
  * the Codex ladder (src/reasoning-effort.ts CODEX_REASONING_LEVELS) at the API boundary.
475
475
  */
476
476
  injectionEffort?: string;
@@ -513,7 +513,7 @@ export interface OcxConfig {
513
513
  streamMode?: "auto" | "legacy-tee" | "eager-relay";
514
514
  /**
515
515
  * Custom override for the injected v2 multi-agent guidance body (the text inside
516
- * the <multi_agent_mode> tags). After guidance is enabled and the v2 surface and
516
+ * the <opencodex_subagent_guidance> tags). After guidance is enabled and the v2 surface and
517
517
  * catalog-state gates pass, a configured injectionModel is sufficient to render it;
518
518
  * otherwise an eligible roster or fallback is required. Placeholders: `{{model}}` -> the
519
519
  * effective preferred model for the request (a bare native model is account-qualified