@bitkyc08/opencodex 2.36.0-preview.20260829 → 2.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/gui/dist/assets/index-Cy7Z_pl0.css +1 -0
  2. package/gui/dist/assets/index-DO8liQVL.js +112 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +2 -1
  5. package/src/AGENTS.md +2 -1
  6. package/src/adapters/agentrouter.ts +50 -0
  7. package/src/adapters/anthropic.ts +1 -51
  8. package/src/adapters/cursor/call-id.ts +76 -8
  9. package/src/adapters/cursor/checkpoint-store.ts +6 -1
  10. package/src/adapters/cursor/cursor-errors.ts +44 -0
  11. package/src/adapters/cursor/native-exec.ts +13 -0
  12. package/src/adapters/cursor/protobuf-request.ts +651 -29
  13. package/src/adapters/cursor/tool-result-normalize.ts +3 -3
  14. package/src/adapters/cursor/transport-retry.ts +5 -1
  15. package/src/adapters/cursor.ts +15 -1
  16. package/src/adapters/empty-tool-output-annotation.ts +43 -0
  17. package/src/adapters/exec-tool-result-normalize.ts +70 -5
  18. package/src/adapters/google.ts +22 -2
  19. package/src/adapters/kiro.ts +26 -2
  20. package/src/adapters/ollama-native-url.ts +111 -0
  21. package/src/adapters/ollama-native.ts +1131 -0
  22. package/src/adapters/openai-chat.ts +30 -7
  23. package/src/adapters/openai-responses.ts +72 -4
  24. package/src/adapters/registry.ts +7 -0
  25. package/src/adapters/xai-web-search.ts +58 -0
  26. package/src/claude/desktop-3p.ts +21 -1
  27. package/src/claude/desktop-policy.ts +149 -0
  28. package/src/cli/account.ts +16 -2
  29. package/src/cli/claude-desktop.ts +13 -3
  30. package/src/cli/combo.ts +8 -5
  31. package/src/cli/doctor.ts +77 -11
  32. package/src/cli/help.ts +1 -1
  33. package/src/cli/index.ts +16 -0
  34. package/src/cli/models.ts +20 -3
  35. package/src/cli/registry.ts +2 -1
  36. package/src/cli/status.ts +140 -2
  37. package/src/cli/storage.ts +10 -1
  38. package/src/codex/account-runtime-state.ts +39 -5
  39. package/src/codex/account-store.ts +393 -13
  40. package/src/codex/account-usability.ts +11 -4
  41. package/src/codex/app-server-processes.ts +46 -5
  42. package/src/codex/auth-context.ts +160 -32
  43. package/src/codex/catalog/bundled.ts +7 -5
  44. package/src/codex/catalog/metadata.ts +1 -1
  45. package/src/codex/catalog/parsing.ts +57 -1
  46. package/src/codex/catalog/provider-fetch.ts +61 -4
  47. package/src/codex/catalog/sync.ts +4 -3
  48. package/src/codex/convergence.ts +3 -2
  49. package/src/codex/data/upstream-models.json +40 -8
  50. package/src/codex/inject-coordination.ts +111 -14
  51. package/src/codex/integration-record.ts +12 -2
  52. package/src/codex/main-account.ts +225 -1
  53. package/src/codex/model-entitlements.ts +339 -27
  54. package/src/codex/prompt-layers.ts +346 -7
  55. package/src/codex/prompt-text-probe.ts +272 -21
  56. package/src/codex/routing.ts +693 -132
  57. package/src/codex/runtime.ts +12 -0
  58. package/src/codex/subagent-model-fallback.ts +62 -24
  59. package/src/codex/user-identity.ts +33 -25
  60. package/src/combos/index.ts +1 -0
  61. package/src/combos/reset-window.ts +46 -0
  62. package/src/combos/resolve.ts +84 -2
  63. package/src/combos/types.ts +5 -2
  64. package/src/config/atomic-write.ts +104 -22
  65. package/src/config/provider-validation.ts +11 -0
  66. package/src/config.ts +75 -3
  67. package/src/generated/compatibility-version.json +207 -131
  68. package/src/generated/model-metadata.ts +1 -1
  69. package/src/grok/catalog.ts +71 -0
  70. package/src/grok/effort.ts +83 -0
  71. package/src/grok/inject.ts +952 -127
  72. package/src/grok/models.ts +56 -0
  73. package/src/grok/status.ts +21 -8
  74. package/src/grok/sync.ts +10 -18
  75. package/src/images/loop.ts +6 -3
  76. package/src/integrations/native/ownership-preflight.ts +4 -1
  77. package/src/lab/fabric/producer-isolate.ts +36 -3
  78. package/src/lib/destination-policy.ts +93 -7
  79. package/src/lib/redact.ts +6 -1
  80. package/src/lib/shadow-call.ts +38 -3
  81. package/src/lib/test-home-guard.ts +18 -3
  82. package/src/lib/upstream-retry.ts +43 -6
  83. package/src/lib/windows-secret-acl.ts +66 -0
  84. package/src/lib/windows-text.ts +28 -2
  85. package/src/lib/windows-user-principal.ts +35 -23
  86. package/src/oauth/account-quota-rank.ts +107 -0
  87. package/src/oauth/anthropic-routing.ts +125 -30
  88. package/src/oauth/chatgpt.ts +5 -1
  89. package/src/oauth/generic-account-failover.ts +114 -7
  90. package/src/oauth/index.ts +15 -8
  91. package/src/oauth/store.ts +16 -0
  92. package/src/providers/account-quota-disk.ts +79 -0
  93. package/src/providers/command-code-efforts.ts +24 -0
  94. package/src/providers/derive.ts +6 -0
  95. package/src/providers/key-failover.ts +33 -1
  96. package/src/providers/kiro-usage.ts +272 -0
  97. package/src/providers/ollama-show.ts +311 -0
  98. package/src/providers/openai-sidecar.ts +5 -0
  99. package/src/providers/quota-routing-cache.ts +32 -0
  100. package/src/providers/quota-types.ts +36 -0
  101. package/src/providers/quota-wire.ts +102 -0
  102. package/src/providers/quota.ts +208 -147
  103. package/src/providers/registry.ts +68 -8
  104. package/src/providers/slug-codec.ts +12 -4
  105. package/src/providers/vercel-gateway-routing.ts +108 -0
  106. package/src/router.ts +22 -12
  107. package/src/server/auth-cors.ts +26 -0
  108. package/src/server/catalog-download.ts +73 -0
  109. package/src/server/chat-native.ts +12 -2
  110. package/src/server/gui-static.ts +4 -1
  111. package/src/server/index.ts +132 -9
  112. package/src/server/management/agent-settings-routes.ts +38 -5
  113. package/src/server/management/codex-prompt-routes.ts +7 -1
  114. package/src/server/management/combo-routes.ts +10 -1
  115. package/src/server/management/config-routes.ts +9 -1
  116. package/src/server/management/context.ts +5 -0
  117. package/src/server/management/model-routes.ts +16 -6
  118. package/src/server/management/native-integration-routes.ts +12 -17
  119. package/src/server/management/oauth-account-routes.ts +13 -0
  120. package/src/server/management/provider-routes.ts +32 -5
  121. package/src/server/management/routing-profile-routes.ts +15 -0
  122. package/src/server/management/shadow-call-validation.ts +29 -0
  123. package/src/server/management-api.ts +7 -3
  124. package/src/server/request-log.ts +3 -5
  125. package/src/server/responses/agent-task-recovery-cache.ts +8 -0
  126. package/src/server/responses/agent-task-recovery.ts +52 -20
  127. package/src/server/responses/codex-auth-error.ts +26 -0
  128. package/src/server/responses/compact.ts +345 -10
  129. package/src/server/responses/core.ts +736 -108
  130. package/src/server/responses/empty-completion-guard.ts +16 -0
  131. package/src/server/responses/fetch-helpers.ts +42 -0
  132. package/src/server/responses/policy-fallback.ts +11 -6
  133. package/src/server/responses-undeclared-tool-guard.ts +16 -3
  134. package/src/server/startup-health-cache.ts +59 -13
  135. package/src/service-manager-probe.ts +115 -9
  136. package/src/service.ts +139 -40
  137. package/src/storage/cleanup.ts +10 -0
  138. package/src/storage/storage-mutation-coordinator.ts +14 -3
  139. package/src/tray/windows-tray.ps1 +10 -4
  140. package/src/tray/windows.ts +30 -2
  141. package/src/types/config.ts +27 -14
  142. package/src/types/provider.ts +54 -0
  143. package/src/types/tools.ts +13 -3
  144. package/src/types.ts +4 -0
  145. package/src/usage/summary.ts +421 -177
  146. package/src/vision/anthropic-describe.ts +3 -3
  147. package/src/vision/describe.ts +5 -3
  148. package/src/web-search/anthropic-executor.ts +9 -2
  149. package/src/web-search/exa-executor.ts +3 -3
  150. package/src/web-search/executor.ts +8 -3
  151. package/src/web-search/gemini-executor.ts +3 -3
  152. package/src/web-search/loop.ts +11 -3
  153. package/src/web-search/xai-executor.ts +3 -3
  154. package/gui/dist/assets/index-DQ-Ie18T.css +0 -1
  155. package/gui/dist/assets/index-DX0RGj3e.js +0 -112
@@ -86,7 +86,7 @@ import {
86
86
  import { injectionDebugLog } from "../../lib/injection-debug-log";
87
87
  import { resolveClientRetryAfter } from "../../lib/retry-after";
88
88
  import { enrichOpenCodeZenRateLimitMessage } from "../../providers/opencode-zen-rate-limit";
89
- import { modelInList, namespacedToolName } from "../../types";
89
+ import { CODE_MODE_EXEC_TOOL_NAME, modelInList, namespacedToolName } from "../../types";
90
90
  import type {
91
91
  AdapterEvent,
92
92
  OcxConfig,
@@ -100,8 +100,8 @@ import type {
100
100
  } from "../../types";
101
101
  import {
102
102
  forceRefreshOAuthAccessSnapshot,
103
- getOAuthCredentialApiBaseUrl,
104
103
  getValidAccessTokenForAccount,
104
+ getValidAccessSnapshotForAccount,
105
105
  getValidAccessTokenSnapshot,
106
106
  publicOAuthAuthenticationErrorMessage,
107
107
  type OAuthAccessSnapshot,
@@ -122,11 +122,14 @@ import {
122
122
  import { stampOAuthAccountLabel } from "../../providers/label";
123
123
  import {
124
124
  failoverAccountSnapshot,
125
+ forgetGenericFailoverRoster,
125
126
  GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
126
127
  isGenericFailoverProvider,
127
128
  isGenericOAuthFailoverEnabled,
129
+ preferredInitialAccount,
128
130
  rotateGenericOAuthAccountOn429,
129
131
  } from "../../oauth/generic-account-failover";
132
+ import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
130
133
  import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
131
134
  import { buildImageTool, buildVideoTool, planImageBridge, planVideoBridge, runWithImageBridge, clampImageMaxRounds, IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "../../images";
132
135
  import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
@@ -140,7 +143,7 @@ import {
140
143
  CodexPoolAuthenticationError,
141
144
  CodexThreadAffinityExpiredError,
142
145
  headersForCodexAuthContext,
143
- materializeCodexUpstreamAuth,
146
+ materializeCodexUpstreamAuthAsync,
144
147
  isCodexAuthContextUsable,
145
148
  resolveCodexAuthContext,
146
149
  codexProbeLeaseId,
@@ -155,16 +158,26 @@ import {
155
158
  resolveCodexModelEntitlements,
156
159
  } from "../../codex/model-entitlements";
157
160
  import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
158
- import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
161
+ import {
162
+ MAIN_CODEX_ACCOUNT_ID,
163
+ forceRefreshMainAccountToken,
164
+ type NativeMainRefreshDependencies,
165
+ } from "../../codex/main-account";
159
166
  import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
160
167
  import {
161
168
  computeQuotaCooldown,
162
169
  codexQuotaScopeForModel,
163
170
  formatCodexProviderForLog,
171
+ handOffThreadAffinityGeneration,
164
172
  previewCodexAccountForRequest,
165
173
  recordCodexUpstreamOutcome,
166
174
  type CodexUpstreamOutcome,
167
175
  } from "../../codex/routing";
176
+ import {
177
+ TokenRefreshError,
178
+ forceRefreshCodexPoolToken,
179
+ readCodexAccountRecord,
180
+ } from "../../codex/account-store";
168
181
  import { codexAuthContextLogLabel } from "../../codex/account-label";
169
182
  import {
170
183
  applyUpstreamRecoveryInit,
@@ -172,7 +185,11 @@ import {
172
185
  fetchWithTransientRetry,
173
186
  prepareSameTarget429Wait,
174
187
  } from "../../lib/upstream-retry";
175
- import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "../auth-cors";
188
+ import {
189
+ ForwardAdmissionCredentialError,
190
+ hasForwardableCodexBearer,
191
+ validateForwardAdmissionCredential,
192
+ } from "../auth-cors";
176
193
  import type { DataPlaneAdmission } from "../auth-cors";
177
194
  import { createTranslatorBudget, isTranslatorBudgetExceededError, type TranslatorBudget } from "../../lib/translator-budget";
178
195
  import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
@@ -210,11 +227,18 @@ import {
210
227
  rateLimitRetryDelayMs,
211
228
  rateLimitRetryPolicyFor,
212
229
  rotateProviderTransportOn429,
230
+ transientRetryPolicyFor,
213
231
  } from "../../providers/key-failover";
214
232
  import { shouldAttemptImageTierRetry } from "../image-retry";
215
233
  import { isXaiResponsesDestination, resolveProviderTransport } from "../../providers/xai-transport";
216
234
  import type { WsData } from "../ws-bridge";
217
- import { codexAccountSelectionForTurn, registerTurn, trackStreamLifetime, unregisterTurn } from "../lifecycle";
235
+ import {
236
+ codexAccountSelectionForTurn,
237
+ registerTurn,
238
+ trackStreamLifetime,
239
+ tryClaimNativeMainProfileForTurn,
240
+ unregisterTurn,
241
+ } from "../lifecycle";
218
242
  import { redactSecretString, sanitizeLogMetadataString } from "../../lib/redact";
219
243
  import { readBoundedResponseBody } from "../../lib/bounded-body";
220
244
  import type { AdmissionLease } from "../../lib/admission";
@@ -266,6 +290,7 @@ import {
266
290
  } from "../relay";
267
291
  import {
268
292
  agentTaskRecoveryConfig,
293
+ discardEncryptedAgentTaskRecovery,
269
294
  recoverEncryptedAgentTask,
270
295
  } from "./agent-task-recovery";
271
296
  import { relaySseEagerBounded } from "../relay-eager";
@@ -295,9 +320,9 @@ import { createResponsesModelPayloadRewrite, rewriteResponsesModelJson } from ".
295
320
  import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
296
321
 
297
322
  import { buildToolBridgeMaps, collabSurface, injectDeveloperMessage, multiAgentGuidanceText } from "./collaboration";
298
- import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
323
+ import { mapCodexAuthContextErrorToResponse, nativeMainRefreshFailureResponse } from "./codex-auth-error";
299
324
  import { hasUnreadableEncryptedAgentTask, looksLikeBackendCiphertext, sanitizeEncryptedContentInPlace } from "./encrypted-payload";
300
- import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
325
+ import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel, storedPoolReplayDispatchNotifier } from "./fetch-helpers";
301
326
  import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
302
327
  import {
303
328
  acquireUpstreamHostAdmission,
@@ -327,6 +352,7 @@ import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-sea
327
352
  import {
328
353
  createRoutedNamespaceCallRestoreRewrite,
329
354
  NamespaceToolCollisionError,
355
+ restoreRoutedNamespaceCalls,
330
356
  restoreRoutedNamespaceCallsInJson,
331
357
  type RoutedNamespaceToolAliases,
332
358
  } from "../../responses/namespace-tool-compat";
@@ -347,6 +373,7 @@ import { responsesJsonToSseStream } from "../responses-json-events";
347
373
  import { guardTerminalEventStream } from "./terminal-guard";
348
374
  import {
349
375
  emptyCompletionRetryEnabled,
376
+ emptyCompletionNotice,
350
377
  observeEmptyCompletion,
351
378
  guardEmptyCompletionEventStream,
352
379
  } from "./empty-completion-guard";
@@ -375,6 +402,11 @@ export function sidecarOutcomeRecorder(
375
402
  probeLeaseId: authCtx.probeLeaseId,
376
403
  probeQuotaScope: authCtx.probeQuotaScope,
377
404
  writerGeneration: authCtx.writerGeneration,
405
+ // A vision or web-search sidecar can return 401/403, and that is evidence about the exact
406
+ // stored credential it used. Without the generation it becomes an account-wide quarantine
407
+ // that a replacement inherits (#2892 gap 4). `main-pool` has no stored-record generation, so
408
+ // it keeps the unfenced account-wide semantics.
409
+ ...(authCtx.kind === "pool" ? { credentialGeneration: authCtx.generation } : {}),
378
410
  })
379
411
  : undefined;
380
412
  }
@@ -893,10 +925,20 @@ interface CodexPoolAccountRetryArgs {
893
925
  codexWsRuntimeIdentity?: BunRuntimeGateInput;
894
926
  translatorBudget: TranslatorBudget;
895
927
  turnAdmissionLease?: AdmissionLease;
928
+ resolveCodexModelEntitlements?: typeof resolveCodexModelEntitlements;
896
929
  };
897
930
  firstAuthCtx: Extract<CodexAuthContext, { kind: "pool" | "main-pool" }>;
898
931
  firstResponse: Response;
899
932
  outcomeStatus: number;
933
+ /**
934
+ * Forbid resolving a DIFFERENT account for this retry.
935
+ *
936
+ * Set when a stored Pool 401 already spent this logical request's account budget on its own
937
+ * refresh and replay. The same-account gated-model retry above stays available, because it
938
+ * sends to the account that was already paying; only the alternate-account resolution below is
939
+ * out of budget.
940
+ */
941
+ sameAccountOnly?: boolean;
900
942
  upstream: AbortController;
901
943
  connectMs: number;
902
944
  passthroughEstimate?: number;
@@ -923,6 +965,29 @@ type CodexPoolAccountRetryResult =
923
965
  authCtx: Extract<CodexAuthContext, { kind: "pool" | "main-pool" }>;
924
966
  };
925
967
 
968
+ /** Keep retry-stage entitlement snapshots inside the native-main selection fence. */
969
+ async function resolveCodexRetryModelEntitlements(
970
+ config: OcxConfig,
971
+ resolver: typeof resolveCodexModelEntitlements,
972
+ turnAdmissionLease?: AdmissionLease,
973
+ ): Promise<Awaited<ReturnType<typeof resolveCodexModelEntitlements>>> {
974
+ // The initial auth selection has already released its admission before the first
975
+ // response arrives. Re-enter for every refresh so profile switching cannot overlap
976
+ // credential discovery, and omit main entirely when a drain or recovery owns it.
977
+ const selectionAdmission = codexAccountSelectionForTurn(turnAdmissionLease)?.();
978
+ const nativeMainReadsForbidden = isNativeMainTrafficBlocked()
979
+ || selectionAdmission?.mainProfileDraining === true;
980
+ try {
981
+ return await resolver(config, {
982
+ excludeAccountIds: nativeMainReadsForbidden
983
+ ? new Set([MAIN_CODEX_ACCOUNT_ID])
984
+ : undefined,
985
+ });
986
+ } finally {
987
+ selectionAdmission?.release();
988
+ }
989
+ }
990
+
926
991
  const CODEX_ACCOUNT_GATED_CANONICAL_WIRE_MODELS: ReadonlyMap<string, string> = new Map([
927
992
  // The authenticated catalog currently advertises Daybreak Blue, while successful responses
928
993
  // identify the serving model as gpt-5.6-sol. Sending the selector itself is shard-dependent:
@@ -1011,10 +1076,22 @@ async function retryCodexPoolOnAlternateAccount(
1011
1076
  outcomeStatus, upstream, connectMs, passthroughEstimate, stream,
1012
1077
  } = args;
1013
1078
  const inboundWire = options.inboundWire ?? "responses";
1079
+ const entitlementResolver = options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements;
1014
1080
  let retryAuthCtx: CodexAuthContext | undefined;
1015
1081
  if (outcomeStatus === 400 && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId)) {
1016
1082
  invalidateCodexModelEntitlementsForAccount(firstAuthCtx.accountId);
1017
- const refreshed = await resolveCodexModelEntitlements(config);
1083
+ let refreshed;
1084
+ try {
1085
+ refreshed = await resolveCodexRetryModelEntitlements(
1086
+ config,
1087
+ entitlementResolver,
1088
+ options.turnAdmissionLease,
1089
+ );
1090
+ } catch (error) {
1091
+ await firstResponse.body?.cancel().catch(() => undefined);
1092
+ releaseCodexAuthContextProbeLease(firstAuthCtx);
1093
+ throw error;
1094
+ }
1018
1095
  if (entitledCodexAccountIdsForModel(refreshed, route.modelId)?.has(firstAuthCtx.accountId)) {
1019
1096
  // The authenticated roster still grants this exact model. Retry on the same account:
1020
1097
  // upstream shards can briefly disagree during a gated-model rollout, but a pre-stream 400
@@ -1024,7 +1101,9 @@ async function retryCodexPoolOnAlternateAccount(
1024
1101
  }
1025
1102
  // Exact account selectors may retry the same confirmed account above, but must never resolve
1026
1103
  // an alternate. Quota failures and a refreshed entitlement miss remain terminal.
1027
- if (!retryAuthCtx && firstAuthCtx.fixedAccount) return { kind: "no-alternate" };
1104
+ if (!retryAuthCtx && (firstAuthCtx.fixedAccount || args.sameAccountOnly === true)) {
1105
+ return { kind: "no-alternate" };
1106
+ }
1028
1107
  try {
1029
1108
  retryAuthCtx ??= await resolveCodexAuthContext(
1030
1109
  req.headers,
@@ -1033,16 +1112,22 @@ async function retryCodexPoolOnAlternateAccount(
1033
1112
  {
1034
1113
  excludeAccountId: firstAuthCtx.accountId,
1035
1114
  modelId: route.modelId,
1115
+ requestScopedMainCredential: hasForwardableCodexBearer(req.headers, config),
1036
1116
  beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
1117
+ resolveCodexModelEntitlements: entitlementResolver,
1037
1118
  },
1038
1119
  );
1039
1120
  } catch (error) {
1040
- if (
1121
+ const unexpectedRetryError =
1041
1122
  !(error instanceof CodexPoolAuthenticationError)
1042
1123
  && !(error instanceof CodexAuthContextError)
1043
1124
  && !(error instanceof CodexAccountCooldownError)
1044
- && !(error instanceof CodexMainProfileDrainingError)
1045
- ) throw error;
1125
+ && !(error instanceof CodexMainProfileDrainingError);
1126
+ if (unexpectedRetryError) {
1127
+ await firstResponse.body?.cancel().catch(() => undefined);
1128
+ releaseCodexAuthContextProbeLease(firstAuthCtx);
1129
+ throw error;
1130
+ }
1046
1131
  }
1047
1132
  if (retryAuthCtx?.kind !== "pool" && retryAuthCtx?.kind !== "main-pool") {
1048
1133
  return { kind: "no-alternate" };
@@ -1131,24 +1216,30 @@ async function retryCodexPoolOnAlternateAccount(
1131
1216
  try {
1132
1217
  while (true) {
1133
1218
  noteAttemptSend(logCtx.activeAttempt, passthroughEstimate);
1134
- upstreamResponse = await fetchWithHeaderTimeout(
1135
- request.url,
1136
- {
1137
- method: request.method,
1138
- headers: request.headers,
1139
- body: request.body,
1140
- },
1141
- upstream.signal,
1142
- connectMs,
1143
- stream,
1144
- providerFetch(route.provider, options.codexWsRuntimeIdentity, {
1145
- providerName: route.providerName,
1146
- modelId: route.modelId,
1147
- }),
1148
- // Credential-bearing forward send: never follow a redirect into a
1149
- // dead-host rejection after the credential was seen (#914).
1150
- route.provider.authMode === "forward",
1151
- );
1219
+ try {
1220
+ upstreamResponse = await fetchWithHeaderTimeout(
1221
+ request.url,
1222
+ {
1223
+ method: request.method,
1224
+ headers: request.headers,
1225
+ body: request.body,
1226
+ },
1227
+ upstream.signal,
1228
+ connectMs,
1229
+ stream,
1230
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
1231
+ providerName: route.providerName,
1232
+ modelId: route.modelId,
1233
+ }),
1234
+ // Credential-bearing forward send: never follow a redirect into a
1235
+ // dead-host rejection after the credential was seen (#914).
1236
+ route.provider.authMode === "forward",
1237
+ );
1238
+ } catch (error) {
1239
+ // Only the forward send is a transport boundary. Entitlement resolver throws below are
1240
+ // deliberately outside this catch so programming errors retain their original path.
1241
+ return { kind: "transport", error, authCtx: retryAuthCtx };
1242
+ }
1152
1243
  retrySendCount += 1;
1153
1244
  args.onResponse?.(upstreamResponse, retryAuthCtx, request);
1154
1245
  if (!retrySameConfirmedAccount || retrySendCount >= maxRetrySends) break;
@@ -1158,13 +1249,23 @@ async function retryCodexPoolOnAlternateAccount(
1158
1249
  options.abortSignal,
1159
1250
  )) break;
1160
1251
  invalidateCodexModelEntitlementsForAccount(retryAuthCtx.accountId);
1161
- const refreshed = await resolveCodexModelEntitlements(config);
1252
+ let refreshed: Awaited<ReturnType<typeof resolveCodexModelEntitlements>>;
1253
+ try {
1254
+ refreshed = await resolveCodexRetryModelEntitlements(
1255
+ config,
1256
+ entitlementResolver,
1257
+ options.turnAdmissionLease,
1258
+ );
1259
+ } catch (error) {
1260
+ await upstreamResponse.body?.cancel().catch(() => undefined);
1261
+ await firstResponse.body?.cancel().catch(() => undefined);
1262
+ releaseCodexAuthContextProbeLease(firstAuthCtx);
1263
+ releaseCodexAuthContextProbeLease(retryAuthCtx);
1264
+ throw error;
1265
+ }
1162
1266
  if (!entitledCodexAccountIdsForModel(refreshed, route.modelId)?.has(retryAuthCtx.accountId)) break;
1163
1267
  await upstreamResponse.body?.cancel().catch(() => undefined);
1164
1268
  }
1165
- } catch (error) {
1166
- // Attribute the transport failure to the alternate account (already selected).
1167
- return { kind: "transport", error, authCtx: retryAuthCtx };
1168
1269
  } finally {
1169
1270
  request.releaseBodyObservation?.();
1170
1271
  }
@@ -1235,6 +1336,10 @@ export function codexForwardTerminalOutcomeRecorder(
1235
1336
  probeLeaseId: codexProbeLeaseId(authCtx),
1236
1337
  probeQuotaScope: codexProbeQuotaScope(authCtx),
1237
1338
  writerGeneration: authCtx.writerGeneration,
1339
+ // A mid-stream terminal can carry a semantic 401 long after the credential was
1340
+ // replaced. It is never replayed — the client already saw output — but it must
1341
+ // not retire the replacement either (#2887).
1342
+ ...(authCtx.kind === "pool" ? { credentialGeneration: authCtx.generation } : {}),
1238
1343
  });
1239
1344
  };
1240
1345
  }
@@ -1311,6 +1416,8 @@ export interface HandleResponsesOptions {
1311
1416
  responsesTerminalRepairScheduler?: ResponsesTerminalRepairScheduler;
1312
1417
  /** Internal deterministic runtime-identity seam for Codex upstream WS selection tests. */
1313
1418
  codexWsRuntimeIdentity?: BunRuntimeGateInput;
1419
+ /** Test seam for native main refresh without live OAuth traffic. */
1420
+ nativeMainRefreshDependencies?: NativeMainRefreshDependencies;
1314
1421
  /**
1315
1422
  * When true, body `prompt_cache_key` is a Claude Desktop shared cache cohort
1316
1423
  * (system/tools hash), not a per-session id — do not use it for Anthropic pool affinity.
@@ -1339,11 +1446,14 @@ export interface HandleResponsesOptions {
1339
1446
  sourceBody: unknown;
1340
1447
  previousResponseInputExpanded: boolean;
1341
1448
  providerContinuation: OcxProviderContinuationState | undefined;
1449
+ recoveredPlaintext: boolean;
1342
1450
  };
1343
1451
  /** Internal combo handoff: allow a later same-provider model after a reset-derived 429/402. */
1344
1452
  deferCodexResetDerivedCooldown?: boolean;
1345
1453
  /** 030-owned handoff when a child consumed the original failure under bounds. */
1346
1454
  onConsumedComboFailure?: (failure: ConsumedComboFailure) => void;
1455
+ /** A stored Pool credential was refreshed and its one allowed same-account replay was sent. */
1456
+ onStoredPool401ReplayDispatched?: () => void;
1347
1457
  /** Caller-owned for Chat/Claude replay; omitted only at genuine Responses ingress. */
1348
1458
  translatorBudget?: TranslatorBudget;
1349
1459
  /**
@@ -1592,6 +1702,9 @@ async function resolveResponsesCodexAuth(
1592
1702
  // no-ChatGPT-login install keeps working.
1593
1703
  const substituteMainCredential = options.admission?.source === "bearer"
1594
1704
  && (route.codexAccountMode !== undefined || isCanonicalOpenAiForwardProvider(route.provider));
1705
+ const requestScopedMainCredential = route.codexAccountMode !== undefined
1706
+ && !substituteMainCredential
1707
+ && hasForwardableCodexBearer(req.headers, config);
1595
1708
  if (route.codexAccountMode === "direct" && !substituteMainCredential) {
1596
1709
  validateForwardAdmissionCredential(req.headers, config);
1597
1710
  }
@@ -1601,11 +1714,28 @@ async function resolveResponsesCodexAuth(
1601
1714
  accountId: route.codexAccountId,
1602
1715
  modelId: route.modelId,
1603
1716
  substituteMainCredentialForDirect: substituteMainCredential,
1717
+ requestScopedMainCredential,
1604
1718
  beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
1605
1719
  resolveCodexModelEntitlements: options.resolveCodexModelEntitlements,
1720
+ signal: options.abortSignal,
1721
+ nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
1606
1722
  });
1607
1723
  options.onCodexAuthContextResolved?.(authCtx);
1608
1724
  } else {
1725
+ // A custom-named canonical-forward provider has no Codex account mode, but an
1726
+ // admission bearer still substitutes the stored main credential below. Claim the
1727
+ // same physical profile before synthesizing the main context so transport-based
1728
+ // substitution cannot bypass a switch drain.
1729
+ if (
1730
+ substituteMainCredential
1731
+ && (
1732
+ isNativeMainTrafficBlocked()
1733
+ || !tryClaimNativeMainProfileForTurn(options.turnAdmissionLease)
1734
+ || isNativeMainTrafficBlocked()
1735
+ )
1736
+ ) {
1737
+ throw new CodexMainProfileDrainingError();
1738
+ }
1609
1739
  authCtx = { kind: "main", accountId: null };
1610
1740
  options.onCodexAuthContextResolved?.(undefined);
1611
1741
  }
@@ -1619,7 +1749,11 @@ async function resolveResponsesCodexAuth(
1619
1749
  return {
1620
1750
  ok: true,
1621
1751
  authCtx,
1622
- headers: materializeCodexUpstreamAuth(req.headers, authCtx, { substituteMainCredential }),
1752
+ headers: await materializeCodexUpstreamAuthAsync(req.headers, authCtx, {
1753
+ substituteMainCredential,
1754
+ signal: options.abortSignal,
1755
+ nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
1756
+ }),
1623
1757
  substituteMainCredential,
1624
1758
  };
1625
1759
  } catch (err) {
@@ -1641,6 +1775,137 @@ async function resolveResponsesCodexAuth(
1641
1775
  }
1642
1776
  }
1643
1777
 
1778
+ /**
1779
+ * Terminal means the grant itself is dead and no retry can help. Everything else —
1780
+ * an untyped network failure, a token-endpoint 5xx surfacing as `unknown`, an abort,
1781
+ * refresh capacity, lock contention, a superseded flight — is transient, and treating
1782
+ * it as terminal would quarantine a healthy account on an upstream blip, which is the
1783
+ * defect this path exists to fix (#2887).
1784
+ */
1785
+ function isTerminalPoolRefreshFailure(error: unknown): boolean {
1786
+ return error instanceof TokenRefreshError && (error.reason === "revoked" || error.reason === "expired");
1787
+ }
1788
+
1789
+ /**
1790
+ * One forced refresh and one same-account rebuild for a stored pool credential that
1791
+ * upstream rejected with a pre-stream 401. `quarantine` distinguishes a dead grant,
1792
+ * which must retire the account, from a transient failure, which must not.
1793
+ */
1794
+ async function refreshPoolForwardAuth(args: {
1795
+ req: Request;
1796
+ route: RouteResult;
1797
+ authCtx: CodexAuthContext & { kind: "pool" };
1798
+ substituteMainCredential: boolean;
1799
+ options: HandleResponsesOptions;
1800
+ }): Promise<
1801
+ | { ok: true; authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers }
1802
+ | { ok: false; response: Response; quarantine: boolean; quarantineGeneration?: number }
1803
+ > {
1804
+ const { req, route, authCtx, substituteMainCredential, options } = args;
1805
+ try {
1806
+ const refreshed = await forceRefreshCodexPoolToken(authCtx.accountId, {
1807
+ rejectedGeneration: authCtx.generation,
1808
+ rejectedAccessToken: authCtx.accessToken,
1809
+ signal: options.abortSignal,
1810
+ });
1811
+ if (!refreshed.rotated) {
1812
+ // The store resolved to the same bearer upstream just rejected. Replaying it
1813
+ // would spend another upstream call to earn the identical 401. Upstream can do
1814
+ // this on a SUCCESSFUL response by rotating only the refresh grant, so the
1815
+ // credential generation may already have moved — quarantine has to be fenced on
1816
+ // where the credential actually is, not on the generation we started from.
1817
+ return {
1818
+ ok: false,
1819
+ quarantine: true,
1820
+ quarantineGeneration: refreshed.generation,
1821
+ response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
1822
+ };
1823
+ }
1824
+ // Only a CAS this request performed itself proves the new credential descends from
1825
+ // the rejected one. Somebody else's replacement may be a different identity, and
1826
+ // its affinity must be retired rather than inherited.
1827
+ if (refreshed.selfRefreshed) {
1828
+ handOffThreadAffinityGeneration(authCtx.accountId, authCtx.generation, refreshed.generation);
1829
+ }
1830
+ const refreshedAuthCtx: CodexAuthContext = {
1831
+ ...authCtx,
1832
+ accessToken: refreshed.accessToken,
1833
+ chatgptAccountId: refreshed.chatgptAccountId,
1834
+ generation: refreshed.generation,
1835
+ };
1836
+ const provider = applyCodexAuthContextToProvider(
1837
+ stripCodexRuntimeProviderFields(route.provider),
1838
+ refreshedAuthCtx,
1839
+ route.codexAccountMode,
1840
+ );
1841
+ const headers = await materializeCodexUpstreamAuthAsync(req.headers, refreshedAuthCtx, {
1842
+ substituteMainCredential,
1843
+ signal: options.abortSignal,
1844
+ nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
1845
+ });
1846
+ return { ok: true, authCtx: refreshedAuthCtx, provider, headers };
1847
+ } catch (error) {
1848
+ if (isTerminalPoolRefreshFailure(error)) {
1849
+ return {
1850
+ ok: false,
1851
+ quarantine: true,
1852
+ response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
1853
+ };
1854
+ }
1855
+ const response = formatErrorResponse(
1856
+ 503,
1857
+ "server_busy",
1858
+ "Codex credential refresh did not complete; retry this request",
1859
+ );
1860
+ const headers = new Headers(response.headers);
1861
+ headers.set("Retry-After", "1");
1862
+ return { ok: false, quarantine: false, response: new Response(response.body, { status: response.status, headers }) };
1863
+ }
1864
+ }
1865
+
1866
+ async function refreshNativeMainForwardAuth(args: {
1867
+ req: Request;
1868
+ route: RouteResult;
1869
+ authCtx: CodexAuthContext;
1870
+ substituteMainCredential: boolean;
1871
+ options: HandleResponsesOptions;
1872
+ }): Promise<
1873
+ | { ok: true; authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers }
1874
+ | { ok: false; response: Response }
1875
+ > {
1876
+ const { req, route, authCtx, substituteMainCredential, options } = args;
1877
+ if (authCtx.kind !== "main-pool") {
1878
+ return { ok: false, response: formatErrorResponse(401, "authentication_error", "No native main credential to refresh") };
1879
+ }
1880
+ try {
1881
+ const refreshed = await forceRefreshMainAccountToken(authCtx.accessToken, {
1882
+ signal: options.abortSignal,
1883
+ ...(options.nativeMainRefreshDependencies ?? {}),
1884
+ });
1885
+ if (!refreshed) {
1886
+ return { ok: false, response: formatErrorResponse(401, "authentication_error", "Codex main account needs reauthentication") };
1887
+ }
1888
+ const refreshedAuthCtx: CodexAuthContext = {
1889
+ ...authCtx,
1890
+ accessToken: refreshed.accessToken,
1891
+ chatgptAccountId: refreshed.chatgptAccountId,
1892
+ };
1893
+ const provider = applyCodexAuthContextToProvider(
1894
+ stripCodexRuntimeProviderFields(route.provider),
1895
+ refreshedAuthCtx,
1896
+ route.codexAccountMode,
1897
+ );
1898
+ const headers = await materializeCodexUpstreamAuthAsync(req.headers, refreshedAuthCtx, {
1899
+ substituteMainCredential,
1900
+ signal: options.abortSignal,
1901
+ nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
1902
+ });
1903
+ return { ok: true, authCtx: refreshedAuthCtx, provider, headers };
1904
+ } catch (error) {
1905
+ return { ok: false, response: nativeMainRefreshFailureResponse(error) };
1906
+ }
1907
+ }
1908
+
1644
1909
  async function resolveSubagentFallbackModelEligibility(args: {
1645
1910
  config: OcxConfig;
1646
1911
  fallbackChain: readonly string[] | null;
@@ -1894,6 +2159,7 @@ export async function handleComboResponses(
1894
2159
  providerContinuation: !scopeMismatch && body !== rawBody && requestedPreviousId
1895
2160
  ? previousResponseProviderState(requestedPreviousId)
1896
2161
  : undefined,
2162
+ recoveredPlaintext: false,
1897
2163
  };
1898
2164
  const adoptFailedChildLog = (childLog: RequestLogContext): void => {
1899
2165
  // Attempts remain the complete physical history; the logical row mirrors the most recent
@@ -1923,18 +2189,74 @@ export async function handleComboResponses(
1923
2189
  return false;
1924
2190
  }
1925
2191
  };
2192
+ let comboPayloadReadable = false;
1926
2193
  const payloadEligible = (target: (typeof combo.targets)[number]): boolean =>
1927
- !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
2194
+ comboPayloadReadable || !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
2195
+ const initialNow = Date.now();
2196
+ let pick: ReturnType<typeof pickComboTarget> = null;
1928
2197
 
1929
2198
  if (unreadableEncryptedAgentTask && !combo.targets.some(canDecryptUnreadableAgentTask)) {
1930
- return unreadableEncryptedAgentTaskResponse();
2199
+ const recovery = agentTaskRecoveryConfig(config);
2200
+ if (
2201
+ (options.inboundWire ?? "responses") !== "responses"
2202
+ || !isThreadSpawnRequest(req.headers)
2203
+ || !recovery
2204
+ || options.comboAttempt
2205
+ ) {
2206
+ discardEncryptedAgentTaskRecovery(
2207
+ req,
2208
+ (body as { input?: unknown } | undefined)?.input,
2209
+ config,
2210
+ { parentThreadId: inboundClientThreadId },
2211
+ );
2212
+ return unreadableEncryptedAgentTaskResponse();
2213
+ }
2214
+ pick = pickComboTarget(config, comboId, {
2215
+ eligible: target => !isComboTargetInCooldown(comboId, target, initialNow),
2216
+ });
2217
+ if (!pick) {
2218
+ discardEncryptedAgentTaskRecovery(
2219
+ req,
2220
+ (body as { input?: unknown } | undefined)?.input,
2221
+ config,
2222
+ { parentThreadId: inboundClientThreadId },
2223
+ );
2224
+ return comboUnavailableResponse(`No available targets for combo: ${comboId}`);
2225
+ }
2226
+ let recovered = false;
2227
+ try {
2228
+ recovered = await recoverEncryptedAgentTask(
2229
+ req,
2230
+ (body as { input?: unknown } | undefined)?.input,
2231
+ recovery,
2232
+ config,
2233
+ { parentThreadId: inboundClientThreadId, abortSignal: options.abortSignal },
2234
+ );
2235
+ } catch {
2236
+ recovered = false;
2237
+ }
2238
+ // Recovery has the same in-place input mutation contract as the direct routed path.
2239
+ if (
2240
+ !recovered
2241
+ || hasUnreadableEncryptedAgentTask((body as { input?: unknown } | undefined)?.input)
2242
+ ) {
2243
+ discardEncryptedAgentTaskRecovery(
2244
+ req,
2245
+ (body as { input?: unknown } | undefined)?.input,
2246
+ config,
2247
+ { parentThreadId: inboundClientThreadId },
2248
+ );
2249
+ return unreadableEncryptedAgentTaskResponse();
2250
+ }
2251
+ comboPayloadReadable = true;
2252
+ comboReplaySnapshot.recoveredPlaintext = true;
2253
+ } else {
2254
+ pick = pickComboTarget(config, comboId, {
2255
+ eligible: target => payloadEligible(target)
2256
+ && !isComboTargetInCooldown(comboId, target, initialNow),
2257
+ });
1931
2258
  }
1932
2259
 
1933
- const initialNow = Date.now();
1934
- let pick = pickComboTarget(config, comboId, {
1935
- eligible: target => payloadEligible(target)
1936
- && !isComboTargetInCooldown(comboId, target, initialNow),
1937
- });
1938
2260
  if (!pick) {
1939
2261
  return comboUnavailableResponse(`No available targets for combo: ${comboId}`);
1940
2262
  }
@@ -1988,6 +2310,7 @@ export async function handleComboResponses(
1988
2310
  attemptRetained = true;
1989
2311
  };
1990
2312
  let consumedChildFailure: ConsumedComboFailure | undefined;
2313
+ let storedPool401ReplayDispatched = false;
1991
2314
  const callbackGate = createChildPassthroughCallbackGate(options);
1992
2315
  let response: Response;
1993
2316
  try {
@@ -2014,6 +2337,7 @@ export async function handleComboResponses(
2014
2337
  onCodexAuthContextResolved: value => { resolvedAuth = value; },
2015
2338
  setTerminalOutcomeRecorder: value => { terminalRecorder = value; },
2016
2339
  onConsumedComboFailure: value => { consumedChildFailure = value; },
2340
+ onStoredPool401ReplayDispatched: () => { storedPool401ReplayDispatched = true; },
2017
2341
  onNativePassthroughTerminal: callbackGate.onTerminal,
2018
2342
  onNativePassthroughCancel: callbackGate.onCancel,
2019
2343
  });
@@ -2119,6 +2443,10 @@ export async function handleComboResponses(
2119
2443
  (logCtx.attempts ??= []).push(attempt);
2120
2444
  attemptRetained = true;
2121
2445
  lastFailure = failure.response;
2446
+ if (storedPool401ReplayDispatched) {
2447
+ adoptFailedChildLog(childLog);
2448
+ return lastFailure;
2449
+ }
2122
2450
  if (comboFailureDecision(failure.response.status, failure.classificationText, {
2123
2451
  code: failure.upstreamCode,
2124
2452
  }) === "stop") {
@@ -2335,6 +2663,9 @@ async function handleResponsesInner(
2335
2663
  let toolBridgeMaps: ReturnType<typeof buildToolBridgeMaps>;
2336
2664
  try {
2337
2665
  parsed = parseRequest(body);
2666
+ if (options.comboReplaySnapshot?.recoveredPlaintext) {
2667
+ markBodyNonPersistable(parsed._rawBody);
2668
+ }
2338
2669
  toolBridgeMaps = buildToolBridgeMaps(parsed, translatorBudget);
2339
2670
  if (previousResponseInputExpanded) parsed._previousResponseInputExpanded = true;
2340
2671
  const providerContinuationCandidate = options.comboReplaySnapshot
@@ -2417,41 +2748,42 @@ async function handleResponsesInner(
2417
2748
  logCtx.configuredServiceTier = readConfiguredCodexServiceTier();
2418
2749
  logCtx.configuredSpeedLabel = requestLogSpeedLabel(logCtx.configuredServiceTier);
2419
2750
 
2420
- // Shadow call intercept: rewrite Codex 0.145.0+ helper calls (gpt-5.6-luna).
2421
- // Ancient clients using gpt-5.4-mini remain configurable via sourceModels.
2422
- const _sci = config.shadowCallIntercept;
2423
- if (_sci?.enabled && _sci.model && shouldInterceptShadowCall(
2424
- parsed.modelId,
2425
- _sci.sourceModels,
2426
- )) {
2427
- const _sciOriginal = parsed.modelId;
2428
- parsed.modelId = _sci.model;
2429
- if (parsed._rawBody && typeof parsed._rawBody === "object") {
2430
- (parsed._rawBody as { model?: string }).model = _sci.model;
2431
- }
2432
- // Force effort to low for shadow/helper calls (matching upstream behavior)
2433
- parsed.options.reasoning = "low";
2434
- if (parsed._rawBody && typeof parsed._rawBody === "object") {
2435
- (parsed._rawBody as Record<string, unknown>).reasoning = { effort: "low" };
2436
- }
2437
- // Record the operator-configured prefix that matched, NOT the caller's raw model string.
2438
- // Matching is by prefix, so a caller can append arbitrary text and still intercept; that
2439
- // raw value would then land in usage.jsonl and /api/logs behind a pattern-based redactor
2440
- // that does not recognize every credential family. The prefix is a value the operator
2441
- // configured, so no caller-controlled string is persisted.
2442
- logCtx.shadowCallRewrittenFrom = sanitizeLogMetadataString(
2443
- shadowSourceModelPrefix(_sciOriginal, _sci.sourceModels),
2444
- );
2445
- // Helpers must not resume/append into the parent thread's Cursor conversation.
2446
- parsed._cursorIsolateConversation = true;
2447
- }
2448
- if (parsed._compactionRequest === true) parsed._cursorIsolateConversation = true;
2449
-
2450
2751
  let route: RouteResult;
2451
2752
  try {
2452
- route = options.comboAttempt
2453
- ? routeConcreteModel(config, parsed.modelId)
2454
- : routeModel(config, parsed.modelId, evidenceFromBody(parsed._rawBody));
2753
+ const resolveRoute = (modelId: string) => options.comboAttempt
2754
+ ? routeConcreteModel(config, modelId)
2755
+ : routeModel(config, modelId, evidenceFromBody(parsed._rawBody));
2756
+ const _sci = config.shadowCallIntercept;
2757
+ let shadowRoute: RouteResult | undefined;
2758
+ if (_sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
2759
+ const sourcePrefix = shadowSourceModelPrefix(parsed.modelId, _sci.sourceModels)!;
2760
+ let sourceIdentity = { providerName: OPENAI_CODEX_PROVIDER_ID, modelId: sourcePrefix };
2761
+ try {
2762
+ const resolvedSource = routeConcreteModel(config, parsed.modelId);
2763
+ sourceIdentity = { providerName: resolvedSource.providerName, modelId: sourcePrefix };
2764
+ } catch { /* Native Codex helper calls remain OpenAI-owned without an enabled OpenAI route. */ }
2765
+ const targetRoute = resolveRoute(_sci.model);
2766
+ if (shouldInterceptShadowCall(parsed.modelId, _sci.sourceModels, sourceIdentity, targetRoute)) {
2767
+ const _sciOriginal = parsed.modelId;
2768
+ parsed.modelId = _sci.model;
2769
+ if (parsed._rawBody && typeof parsed._rawBody === "object") {
2770
+ (parsed._rawBody as { model?: string }).model = _sci.model;
2771
+ }
2772
+ // Record the operator-configured prefix that matched, NOT the caller's raw model string.
2773
+ // Matching is by prefix, so a caller can append arbitrary text and still intercept; that
2774
+ // raw value would then land in usage.jsonl and /api/logs behind a pattern-based redactor
2775
+ // that does not recognize every credential family. The prefix is a value the operator
2776
+ // configured, so no caller-controlled string is persisted.
2777
+ logCtx.shadowCallRewrittenFrom = sanitizeLogMetadataString(
2778
+ shadowSourceModelPrefix(_sciOriginal, _sci.sourceModels),
2779
+ );
2780
+ // Helpers must not resume/append into the parent thread's Cursor conversation.
2781
+ parsed._cursorIsolateConversation = true;
2782
+ shadowRoute = targetRoute;
2783
+ }
2784
+ }
2785
+ if (parsed._compactionRequest === true) parsed._cursorIsolateConversation = true;
2786
+ route = shadowRoute ?? resolveRoute(parsed.modelId);
2455
2787
  logCtx.routeDecision = route.routeDecision;
2456
2788
  } catch (err) {
2457
2789
  if (err instanceof NoAvailableComboTargetsError) {
@@ -2530,6 +2862,7 @@ async function handleResponsesInner(
2530
2862
  previewNow,
2531
2863
  codexQuotaScopeForModel(modelId),
2532
2864
  { ...previewSelectionOptions, modelEligibleAccountIds },
2865
+ modelId,
2533
2866
  );
2534
2867
  const previewAccountId = route.codexAccountId ?? subagentFallbackAccountPreview(
2535
2868
  route.modelId,
@@ -2651,6 +2984,7 @@ async function handleResponsesInner(
2651
2984
  previewNow,
2652
2985
  codexQuotaScopeForModel(modelId),
2653
2986
  { ...recoverySelectionOptions, modelEligibleAccountIds },
2987
+ modelId,
2654
2988
  );
2655
2989
  const recoveryPreviewAccountId = subagentFallbackAccountPreview(
2656
2990
  parsed.modelId,
@@ -2831,7 +3165,12 @@ async function handleResponsesInner(
2831
3165
  * rotation rather than send a half-applied identity:
2832
3166
  *
2833
3167
  * - Copilot pins its bearer to an account-scoped regional origin, so transport is re-resolved
2834
- * with the new account's `apiBaseUrl` instead of inheriting the previous account's host.
3168
+ * with the new account's `apiBaseUrl` instead of inheriting the previous account's host. The
3169
+ * snapshot value is RESOLVED first: `rotatedProvider` is a clone of the FAILED account's
3170
+ * provider, so passing a bare `undefined` origin let the transport resolver fall through its
3171
+ * own `?? validateCopilotApiBaseUrl(provider.baseUrl)` step to the previous account's host —
3172
+ * pairing B's bearer with A's accepted origin. Login and refresh always persist a resolved
3173
+ * origin, so this fallback protects malformed or manually seeded credentials.
2835
3174
  * - A Cloud Code Assist provider needs an account-matched project. Antigravity's refresh path
2836
3175
  * tolerates project discovery failing, so a stored account can legitimately have no project;
2837
3176
  * sending that account's bearer with the FAILED account's project is worse than not rotating.
@@ -2844,7 +3183,7 @@ async function handleResponsesInner(
2844
3183
  route.providerName,
2845
3184
  rotatedProvider,
2846
3185
  parsed.options.promptCacheKey,
2847
- snapshot.apiBaseUrl,
3186
+ resolveCopilotApiBaseUrl(snapshot.apiBaseUrl),
2848
3187
  ) as OcxProviderConfig;
2849
3188
  }
2850
3189
  if (snapshot.projectId) rotatedProvider = { ...rotatedProvider, project: snapshot.projectId };
@@ -2888,7 +3227,53 @@ async function handleResponsesInner(
2888
3227
  route.provider = { ...route.provider, apiKey: accessToken };
2889
3228
  logCtx.provider = formatAnthropicProviderForLog("anthropic", selection.accountId, config);
2890
3229
  } else {
2891
- const resolved = await getValidAccessTokenSnapshot(route.providerName);
3230
+ // Prefer the account with known headroom BEFORE the first attempt. Rotation alone
3231
+ // only reacts to a 429, so a turn could open on an account a previous probe already
3232
+ // measured as spent. A null answer means "use the active account", so every provider
3233
+ // without quota evidence keeps the resolution it has today.
3234
+ const preferredAccountId = isGenericFailoverProvider(route.providerName, route.provider)
3235
+ ? preferredInitialAccount(config, route.providerName)
3236
+ : null;
3237
+ // Resolved account-scoped, NOT through failoverAccountSnapshot: that helper marks a
3238
+ // rotation site, and rotation sites must apply their credential through
3239
+ // applyFailoverSnapshot's pairing rules. This is initial resolution — the code below
3240
+ // already pairs the snapshot's Kiro metadata, Copilot origin and Antigravity project
3241
+ // with this same bearer, exactly as it does for the active account.
3242
+ let usedPreferredAccount = preferredAccountId !== null;
3243
+ let resolved: OAuthAccessSnapshot;
3244
+ if (preferredAccountId) {
3245
+ try {
3246
+ // `requireUsableAccount` makes a removed OR reauth-flagged account throw from
3247
+ // inside the resolver's own store read. Without it a revoked account resolves
3248
+ // successfully — its credential is still readable — and the request would
3249
+ // dispatch on an account already known to need a fresh login.
3250
+ resolved = await getValidAccessSnapshotForAccount(
3251
+ route.providerName,
3252
+ preferredAccountId,
3253
+ { requireUsableAccount: true },
3254
+ );
3255
+ } catch {
3256
+ // The roster is read behind a short TTL, so a preferred account can be removed
3257
+ // or flagged for reauth in the window after it was cached. Resolving it then
3258
+ // throws, and a PREFERENCE that turns a healthy request into a 401 is worse
3259
+ // than no preference at all — the active account is still perfectly usable.
3260
+ // Drop the stale roster so the next request re-reads it, and carry on.
3261
+ forgetGenericFailoverRoster(route.providerName);
3262
+ usedPreferredAccount = false;
3263
+ resolved = await getValidAccessTokenSnapshot(route.providerName);
3264
+ }
3265
+ } else {
3266
+ resolved = await getValidAccessTokenSnapshot(route.providerName);
3267
+ }
3268
+ // A Cloud Code Assist account needs its own project. Antigravity's refresh path
3269
+ // tolerates project discovery failing, so a stored account can legitimately have
3270
+ // none — and a PREFERENCE must never turn a working request into an error. Fall
3271
+ // back to the ordinary active-account resolution instead, which is exactly what
3272
+ // would have happened had the preference never existed.
3273
+ if (usedPreferredAccount && route.provider.googleMode === "cloud-code-assist" && !resolved.projectId) {
3274
+ resolved = await getValidAccessTokenSnapshot(route.providerName);
3275
+ usedPreferredAccount = false;
3276
+ }
2892
3277
  replayOAuthCredentialSnapshot = {
2893
3278
  accountId: resolved.accountId,
2894
3279
  generation: resolved.generation,
@@ -2912,9 +3297,19 @@ async function handleResponsesInner(
2912
3297
  // Antigravity (cloud-code-assist) needs the discovered Cloud Code Assist project id in the
2913
3298
  // CCA envelope. Keep it paired with the token snapshot so an account rotation cannot mix
2914
3299
  // a fresh token with project metadata re-read from a different credential generation.
2915
- if (route.provider.googleMode === "cloud-code-assist" && !route.provider.project) {
2916
- const projectId = resolved.projectId;
2917
- if (projectId) route.provider = { ...route.provider, project: projectId };
3300
+ if (route.provider.googleMode === "cloud-code-assist") {
3301
+ // When pre-dispatch chose a DIFFERENT account, the configured project belongs to
3302
+ // the account we did not use, and `!route.provider.project` would skip right past
3303
+ // it — installing B's bearer alongside A's project. That is the #2841 pairing bug
3304
+ // in its original shape, so the preferred-account path replaces the project
3305
+ // unconditionally and refuses to dispatch at all if the chosen account has none.
3306
+ // A project-less preferred account already fell back above, so by here the
3307
+ // preferred path always has one.
3308
+ if (usedPreferredAccount && resolved.projectId) {
3309
+ route.provider = { ...route.provider, project: resolved.projectId };
3310
+ } else if (!route.provider.project && resolved.projectId) {
3311
+ route.provider = { ...route.provider, project: resolved.projectId };
3312
+ }
2918
3313
  }
2919
3314
  }
2920
3315
  } catch (err) {
@@ -2933,7 +3328,9 @@ async function handleResponsesInner(
2933
3328
  route.providerName,
2934
3329
  route.provider,
2935
3330
  parsed.options.promptCacheKey,
2936
- route.providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(route.providerName) : undefined,
3331
+ route.providerName === "github-copilot" && route.provider.authMode === "oauth"
3332
+ ? resolveCopilotApiBaseUrl(sentOAuthSnapshot?.apiBaseUrl)
3333
+ : undefined,
2937
3334
  );
2938
3335
  let adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
2939
3336
  const stripClaudeMainAuth = options.stripClaudeMainAuthForNoncanonicalForward === true
@@ -3172,9 +3569,10 @@ async function handleResponsesInner(
3172
3569
  );
3173
3570
  // Hosted calls the PROVIDER runs itself. Gated on the destination actually being xAI, so a
3174
3571
  // declaration alone cannot buy the exemption on some other upstream that never serves it.
3175
- const providerExecutedCallTypes = isXaiResponsesDestination(route.provider)
3176
- ? collectProviderExecutedCallTypes(clientToolAuthorizationBody)
3177
- : new Set<ProviderExecutedCallType>();
3572
+ // Provider-executed declarations are authorized from the actual outbound body, after the
3573
+ // adapter has applied destination-specific injection and normalization. Client-executed tool
3574
+ // authority remains bounded to the caller-owned catalog above.
3575
+ const providerExecutedCallTypes = new Set<ProviderExecutedCallType>();
3178
3576
  let request: Awaited<ReturnType<typeof adapter.buildRequest>>;
3179
3577
  try {
3180
3578
  request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
@@ -3239,9 +3637,41 @@ async function handleResponsesInner(
3239
3637
  let outboundRequestBody: Record<string, unknown> | undefined;
3240
3638
  const declaredWireToolNames = new Set<string>();
3241
3639
  const declaredNamelessClientCallTypes = new Set<string>();
3640
+ // `buildToolBridgeMaps` creates a bare alias only when the caller selected exactly one
3641
+ // namespaced tool through a bare tool_choice. Restore that request-bounded identity before
3642
+ // authorization checks instead of admitting the bare name into the declared set: for `exec`,
3643
+ // the latter would also authorize the unrelated code-mode helper names.
3644
+ const authorizedBareNamespaceToolAliases: RoutedNamespaceToolAliases = new Map(
3645
+ [...toolBridgeMaps.toolNsMap].flatMap(([alias, identity]) =>
3646
+ alias === identity.name
3647
+ ? [[alias, {
3648
+ namespace: identity.namespace,
3649
+ name: identity.name,
3650
+ kind: identity.freeform ? "custom" as const : "function" as const,
3651
+ }] as const]
3652
+ : []
3653
+ ),
3654
+ );
3655
+ const restoreAuthorizedBareNamespaceToolCalls = (value: unknown): unknown =>
3656
+ restoreRoutedNamespaceCalls(value, authorizedBareNamespaceToolAliases).value;
3242
3657
  let undeclaredToolGuardActive = false;
3243
3658
  const refreshUndeclaredToolGuard = (builtRequest: AdapterRequest): void => {
3244
3659
  outboundRequestBody = parseOutboundRequestBody(builtRequest.body);
3660
+ providerExecutedCallTypes.clear();
3661
+ if (isXaiResponsesDestination(route.provider)) {
3662
+ // Preserve the caller-declared authorization recognized by the original classifier, then
3663
+ // add adapter-injected declarations from the actual current-turn outbound catalog.
3664
+ for (const callType of collectProviderExecutedCallTypes(clientToolAuthorizationBody)) {
3665
+ providerExecutedCallTypes.add(callType);
3666
+ }
3667
+ const currentOutboundCatalog = currentTurnWireToolCatalogBody(
3668
+ outboundRequestBody,
3669
+ replayedInputPrefixLength,
3670
+ );
3671
+ for (const callType of collectProviderExecutedCallTypes(currentOutboundCatalog)) {
3672
+ providerExecutedCallTypes.add(callType);
3673
+ }
3674
+ }
3245
3675
  declaredWireToolNames.clear();
3246
3676
  // With no replay prefix the full outbound body belongs to this turn and its normalized
3247
3677
  // aliases are authoritative. A continuation's outbound body still contains historical
@@ -3267,7 +3697,19 @@ async function handleResponsesInner(
3267
3697
  // however, the parsed maps also contain historical catalog entries, so only the bounded
3268
3698
  // current-turn wire snapshot above may authorize a call.
3269
3699
  if (replayedInputPrefixLength === 0) {
3270
- for (const name of toolBridgeMaps.declaredToolNames) declaredWireToolNames.add(name);
3700
+ for (const name of toolBridgeMaps.declaredToolNames) {
3701
+ // `buildToolBridgeMaps` also aliases a namespaced tool under its bare name when the
3702
+ // caller's `tool_choice` selected it unambiguously, which the bridge needs to route the
3703
+ // call back. For `exec` alone that alias would also switch on nested-helper
3704
+ // normalization and re-authorize `exec_command`/`shell_command`/`apply_patch`, so it is
3705
+ // admitted here only when the caller's own catalog declared a bare `exec`. Selecting an
3706
+ // MCP `exec` is not a declaration of the code-mode shell tool.
3707
+ if (
3708
+ name === CODE_MODE_EXEC_TOOL_NAME
3709
+ && !clientDeclaredWireToolNames.has(CODE_MODE_EXEC_TOOL_NAME)
3710
+ ) continue;
3711
+ declaredWireToolNames.add(name);
3712
+ }
3271
3713
  }
3272
3714
  undeclaredToolGuardActive = (
3273
3715
  declaredWireToolNames.size > 0
@@ -3293,7 +3735,7 @@ async function handleResponsesInner(
3293
3735
  // state for exactly the passthrough traffic the guard deliberately stands down for.
3294
3736
  if (!undeclaredToolGuardActive || inspectionSawUndeclaredTool) return;
3295
3737
  if (undeclaredToolCallName(
3296
- payload,
3738
+ restoreAuthorizedBareNamespaceToolCalls(payload),
3297
3739
  declaredWireToolNames,
3298
3740
  declaredNamelessClientCallTypes,
3299
3741
  providerExecutedCallTypes,
@@ -3305,7 +3747,7 @@ async function handleResponsesInner(
3305
3747
  ? (response: { id?: unknown; output?: unknown; status?: unknown }) => {
3306
3748
  if (inspectionSawUndeclaredTool) return;
3307
3749
  const restoredResponse = restoreRoutedCustomCalls(
3308
- response,
3750
+ restoreAuthorizedBareNamespaceToolCalls(response),
3309
3751
  routedCustomToolNames,
3310
3752
  routedCustomToolRepairNames,
3311
3753
  declaredWireToolNames,
@@ -3453,6 +3895,7 @@ async function handleResponsesInner(
3453
3895
 
3454
3896
  const opaqueBlobRecoveryGuard: OpaqueBlobRecoveryGuard = { attempted: false };
3455
3897
  let oauth401ReplayAttempted = false;
3898
+ let codex401ReplayKind: "main" | "stored" | null = null;
3456
3899
  const rateLimitPolicy = rateLimitRetryPolicyFor(route.provider);
3457
3900
  let rateLimitRetries = 0;
3458
3901
  const rebuildAndRefetch = async (
@@ -3523,6 +3966,102 @@ async function handleResponsesInner(
3523
3966
  // Keep recovery kinds in sync with the generic `recovery:` loop below.
3524
3967
  passthroughRecovery: for (;;) {
3525
3968
 
3969
+ if (
3970
+ upstreamResponse.status === 401
3971
+ && (authCtx.kind === "main-pool" || authCtx.kind === "pool")
3972
+ && usesCodexForwardPoolAuth(authCtx, route.provider)
3973
+ && codex401ReplayKind === null
3974
+ ) {
3975
+ codex401ReplayKind = authCtx.kind === "pool" ? "stored" : "main";
3976
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed */ }
3977
+ const poolAuthCtx = authCtx.kind === "pool" ? authCtx : undefined;
3978
+ const poolReplay = poolAuthCtx
3979
+ ? await refreshPoolForwardAuth({ req, route, authCtx: poolAuthCtx, substituteMainCredential, options })
3980
+ : undefined;
3981
+ const replay = poolReplay
3982
+ ?? await refreshNativeMainForwardAuth({ req, route, authCtx, substituteMainCredential, options });
3983
+ if (!replay.ok) {
3984
+ // Compact already records this; core historically returned without recording,
3985
+ // so a dead grant stayed selectable and every request repeated the same doomed
3986
+ // refresh. Fenced by the generation the 401 belongs to (#2887).
3987
+ if (poolAuthCtx && poolReplay && !poolReplay.ok && poolReplay.quarantine) {
3988
+ recordCodexUpstreamOutcome(config, poolAuthCtx.accountId, 401, {
3989
+ threadId: poolAuthCtx.affinityKey,
3990
+ fixedAccount: poolAuthCtx.fixedAccount,
3991
+ modelId: route.modelId,
3992
+ writerGeneration: poolAuthCtx.writerGeneration,
3993
+ credentialGeneration: poolReplay.quarantineGeneration ?? poolAuthCtx.generation,
3994
+ });
3995
+ }
3996
+ upstream.abort();
3997
+ releaseCodexAuthContextProbeLease(authCtx);
3998
+ return replay.response;
3999
+ }
4000
+ authCtx = replay.authCtx;
4001
+ route.provider = replay.provider;
4002
+ selectedForwardHeaders = replay.headers;
4003
+ const replayAdapter = resolveAdapter(
4004
+ resolveWireProtocolOverride(route.providerName, route.modelId, replay.provider, inboundWire),
4005
+ config.cacheRetention,
4006
+ );
4007
+ if (!("passthrough" in replayAdapter) || !replayAdapter.passthrough) {
4008
+ upstream.abort();
4009
+ return formatErrorResponse(502, "upstream_error", "Native main refresh changed the provider wire unexpectedly");
4010
+ }
4011
+ bindRouteReasoningReplayScope({
4012
+ parsed,
4013
+ providerName: route.providerName,
4014
+ provider: replay.provider,
4015
+ adapterName: replayAdapter.name,
4016
+ codexAuthContext: authCtx,
4017
+ forwardHeaders: selectedForwardHeaders,
4018
+ });
4019
+ logCtx.providerAdapter = replayAdapter.name;
4020
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, replayAdapter.name, logCtx.accountLogLabel);
4021
+ try {
4022
+ request = await replayAdapter.buildRequest(parsed, {
4023
+ headers: selectedForwardHeaders,
4024
+ translatorBudget,
4025
+ });
4026
+ refreshRoutedNamespaceToolAliases(request);
4027
+ recordAdapterReasoning(logCtx, request);
4028
+ recordAdapterTier(logCtx, request);
4029
+ refreshUndeclaredToolGuard(request);
4030
+ noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, "oauth-401");
4031
+ upstreamResponse = await fetchWithHeaderTimeout(
4032
+ request.url,
4033
+ { method: request.method, headers: request.headers, body: request.body },
4034
+ upstream.signal,
4035
+ connectMs,
4036
+ parsed.stream,
4037
+ // The replay-dispatched signal is what bounds the rest of this logical request, so it
4038
+ // has to describe a send that actually happened. fetchWithHeaderTimeout awaits pacing
4039
+ // admission BEFORE calling the executor, so signalling at the call site would spend the
4040
+ // budget even when a rejected pacing wait means nothing reaches the network. Wrapping
4041
+ // the executor moves the signal to the last moment before the send, where a throw from
4042
+ // here on is a genuine transport attempt.
4043
+ storedPoolReplayDispatchNotifier(
4044
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
4045
+ providerName: route.providerName,
4046
+ modelId: route.modelId,
4047
+ }),
4048
+ codex401ReplayKind === "stored" ? options.onStoredPool401ReplayDispatched : undefined,
4049
+ ),
4050
+ route.provider.authMode === "forward",
4051
+ ).then(response => {
4052
+ settleObservedHostResponse();
4053
+ return response;
4054
+ });
4055
+ } catch (err) {
4056
+ return transportFailureResponse(err);
4057
+ } finally {
4058
+ request.releaseBodyObservation?.();
4059
+ }
4060
+ continue passthroughRecovery;
4061
+ }
4062
+
4063
+ if (codex401ReplayKind !== null && upstreamResponse.status === 401) break;
4064
+
3526
4065
  // Native Responses providers return before the generic adapter recovery loop below. Keep
3527
4066
  // their OAuth contract identical: one pre-stream 401 forces a credential refresh and one
3528
4067
  // rebuilt replay. xAI's current subscription models use this branch now that their official
@@ -3555,7 +4094,9 @@ async function handleResponsesInner(
3555
4094
  route.providerName,
3556
4095
  { ...route.provider, apiKey: refreshed.accessToken },
3557
4096
  parsed.options.promptCacheKey,
3558
- route.providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(route.providerName) : undefined,
4097
+ route.providerName === "github-copilot"
4098
+ ? resolveCopilotApiBaseUrl(refreshed.apiBaseUrl)
4099
+ : undefined,
3559
4100
  );
3560
4101
  route.provider = refreshedProvider;
3561
4102
  const refreshedAdapter = resolveAdapter(
@@ -3720,6 +4261,14 @@ async function handleResponsesInner(
3720
4261
  }
3721
4262
 
3722
4263
  if (poolRetryOutcome !== undefined) {
4264
+ // A stored Pool 401 spent this request's account budget on its own refresh and replay, so
4265
+ // nothing afterwards may be paid for out of a DIFFERENT account. One flag carries that,
4266
+ // rather than a status check here as well: a quota failure has no same-account move, so
4267
+ // `sameAccountOnly` makes it terminal by refusing the alternate; the gated-model 400
4268
+ // ladder does have one — retrying the account the refreshed roster still grants — and
4269
+ // keeps it. An earlier revision also broke here on a non-400 outcome, which no test could
4270
+ // justify because this flag already produced the identical result.
4271
+ const storedReplaySpent = codex401ReplayKind === "stored";
3723
4272
  const retry = await retryCodexPoolOnAlternateAccount({
3724
4273
  req,
3725
4274
  config,
@@ -3730,6 +4279,7 @@ async function handleResponsesInner(
3730
4279
  firstAuthCtx: authCtx,
3731
4280
  firstResponse: upstreamResponse,
3732
4281
  outcomeStatus: poolRetryOutcome,
4282
+ sameAccountOnly: storedReplaySpent,
3733
4283
  upstream,
3734
4284
  connectMs,
3735
4285
  passthroughEstimate,
@@ -3845,6 +4395,9 @@ async function handleResponsesInner(
3845
4395
  probeLeaseId: codexProbeLeaseId(authCtx),
3846
4396
  probeQuotaScope: codexProbeQuotaScope(authCtx),
3847
4397
  writerGeneration: authCtx.writerGeneration,
4398
+ // Includes a replay's second 401, which is the case that actually retires the
4399
+ // account — fence it on the credential the request was holding.
4400
+ ...(authCtx.kind === "pool" ? { credentialGeneration: authCtx.generation } : {}),
3848
4401
  });
3849
4402
  }
3850
4403
  }
@@ -3928,6 +4481,9 @@ async function handleResponsesInner(
3928
4481
  routedNamespaceToolAliases.size > 0
3929
4482
  ? createRoutedNamespaceCallRestoreRewrite(routedNamespaceToolAliases)
3930
4483
  : undefined,
4484
+ authorizedBareNamespaceToolAliases.size > 0
4485
+ ? createRoutedNamespaceCallRestoreRewrite(authorizedBareNamespaceToolAliases)
4486
+ : undefined,
3931
4487
  hasResponsesItemIdRepair(repairConfig)
3932
4488
  ? createResponsesItemIdPayloadRewrite(repairConfig!, translatorBudget)
3933
4489
  : undefined,
@@ -4157,8 +4713,12 @@ async function handleResponsesInner(
4157
4713
  restoreImageGenCallsInJson(text, imageGenCallAliases),
4158
4714
  routedNamespaceToolAliases,
4159
4715
  );
4160
- const restored = restoreRoutedCustomCallsInJson(
4716
+ const restoredAuthorizedBareNamespace = restoreRoutedNamespaceCallsInJson(
4161
4717
  restoredNamespace,
4718
+ authorizedBareNamespaceToolAliases,
4719
+ );
4720
+ const restored = restoreRoutedCustomCallsInJson(
4721
+ restoredAuthorizedBareNamespace,
4162
4722
  routedCustomToolNames,
4163
4723
  routedCustomToolRepairNames,
4164
4724
  declaredWireToolNames,
@@ -4484,9 +5044,21 @@ async function handleResponsesInner(
4484
5044
  // through web-search instead of being swallowed. runTurn adapters never enter this branch.
4485
5045
  if (canRunWebSearch && wsPlan) {
4486
5046
  parsed.context.tools = [...(parsed.context.tools ?? []), buildWebSearchTool()];
5047
+ // Resolve the mutable route at send time: a 429 rotation replaces route.provider, so retaining
5048
+ // one pre-rotation providerFetch would keep the old credential and transport pin.
5049
+ const routedProviderFetch = ((input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) =>
5050
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
5051
+ providerName: route.providerName,
5052
+ modelId: route.modelId,
5053
+ })(input, init)) as typeof globalThis.fetch;
4487
5054
  const wsResponse = await runWithWebSearch({
4488
5055
  parsed, adapter,
4489
- incomingMeta: { headers: selectedForwardHeaders, abortSignal: options.abortSignal, translatorBudget },
5056
+ incomingMeta: {
5057
+ headers: selectedForwardHeaders,
5058
+ abortSignal: options.abortSignal,
5059
+ translatorBudget,
5060
+ providerFetch: routedProviderFetch,
5061
+ },
4490
5062
  backend: wsPlan.backend,
4491
5063
  forwardProvider: wsPlan.forwardSidecar?.provider,
4492
5064
  anthropicSidecar: wsPlan.anthropicSidecar,
@@ -4738,10 +5310,7 @@ async function handleResponsesInner(
4738
5310
  // result (#2472). Retrying by default would re-send a turn that may already have had
4739
5311
  // billable side effects, so the honest default is observability, not recovery.
4740
5312
  : observeEmptyCompletion(eventSource, () => {
4741
- console.warn(
4742
- `[opencodex] ${route.providerName}/${route.modelId} completed with no output text `
4743
- + "and no tool call. Set \"emptyCompletionRetry\": true to retry such turns once.",
4744
- );
5313
+ console.warn(emptyCompletionNotice(route.providerName, route.modelId));
4745
5314
  });
4746
5315
  const sseStream = bridgeToResponsesSSE(
4747
5316
  guardedSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
@@ -4936,6 +5505,13 @@ async function handleResponsesInner(
4936
5505
  { headers: { "Content-Type": "application/json" } },
4937
5506
  );
4938
5507
  }
5508
+ // One request-scoped transient-retry budget owner, declared here so BOTH the initial send
5509
+ // and the later recovery refetches (429, key/account rotation, OAuth replay) share it. A
5510
+ // per-leg budget would let a request that recovers several times multiply upstream load.
5511
+ let transientSendsUsed = 0;
5512
+ const noteTransientSends = (used: number): void => { transientSendsUsed += Math.max(0, used); };
5513
+ const remainingTransientSendBudget = (budget: number): number =>
5514
+ Math.max(1, budget - transientSendsUsed);
4939
5515
  try {
4940
5516
  initialRequest = await activeAdapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
4941
5517
  refreshRoutedNamespaceToolAliases(initialRequest);
@@ -4990,7 +5566,13 @@ async function handleResponsesInner(
4990
5566
  // direct Google AI Studio only (Vertex/Antigravity use fetchResponse above). Other
4991
5567
  // adapters keep reset-only retry so combo failover still hops on the first 5xx
4992
5568
  // instead of burning ~1.2s of same-target retries per hop.
4993
- const fetchWithRetryPolicy = route.provider.adapter === "google" ? fetchWithTransientRetry : fetchWithResetRetry;
5569
+ // #2643: an opted-in key-auth openai-chat provider also gets transient-5xx retry. The
5570
+ // legacy direct-Google exception is preserved exactly; every other adapter still keeps
5571
+ // reset-only semantics so combo failover hops on the first 5xx.
5572
+ const transientPolicy = transientRetryPolicyFor(route.provider);
5573
+ const fetchWithRetryPolicy = (route.provider.adapter === "google" || transientPolicy)
5574
+ ? fetchWithTransientRetry
5575
+ : fetchWithResetRetry;
4994
5576
  upstreamResponse = await fetchWithRetryPolicy(
4995
5577
  recovery => {
4996
5578
  noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate, recovery);
@@ -5004,7 +5586,13 @@ async function handleResponsesInner(
5004
5586
  modelId: route.modelId,
5005
5587
  }));
5006
5588
  },
5007
- { abortSignal: upstream.signal, label: safeHostLabel(builtInitialRequest.url) },
5589
+ {
5590
+ abortSignal: upstream.signal,
5591
+ label: safeHostLabel(builtInitialRequest.url),
5592
+ ...(transientPolicy
5593
+ ? { attempts: transientPolicy.attempts, onSendsConsumed: noteTransientSends }
5594
+ : {}),
5595
+ },
5008
5596
  );
5009
5597
  }
5010
5598
  } catch (err) {
@@ -5093,13 +5681,36 @@ async function handleResponsesInner(
5093
5681
  }),
5094
5682
  });
5095
5683
  }
5096
- return await fetchWithHeaderTimeout(retryRequest.url, {
5097
- method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
5098
- }, upstream.signal, connectMs, parsed.stream,
5099
- providerFetch(route.provider, options.codexWsRuntimeIdentity, {
5100
- providerName: route.providerName,
5101
- modelId: route.modelId,
5102
- }));
5684
+ // #2643 review: this leg used to call fetchWithHeaderTimeout directly, so an
5685
+ // opted-in provider's transient-5xx policy applied to the initial send and to
5686
+ // native chat but was silently bypassed here — a 429 that recovered into a
5687
+ // retryable 503 got no retry on the Responses path. Route it through the same
5688
+ // selection, and pass what is LEFT of the request-scoped budget rather than a
5689
+ // fresh one, so a recovery loop cannot multiply total upstream sends.
5690
+ const refetchTransientPolicy = transientRetryPolicyFor(route.provider);
5691
+ const refetchWithPolicy = (route.provider.adapter === "google" || refetchTransientPolicy)
5692
+ ? fetchWithTransientRetry
5693
+ : fetchWithResetRetry;
5694
+ return await refetchWithPolicy(
5695
+ recoveryKind => fetchWithHeaderTimeout(retryRequest.url,
5696
+ applyUpstreamRecoveryInit({
5697
+ method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
5698
+ }, recoveryKind), upstream.signal, connectMs, parsed.stream,
5699
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
5700
+ providerName: route.providerName,
5701
+ modelId: route.modelId,
5702
+ })),
5703
+ {
5704
+ abortSignal: upstream.signal,
5705
+ label: safeHostLabel(retryRequest.url),
5706
+ ...(refetchTransientPolicy
5707
+ ? {
5708
+ attempts: remainingTransientSendBudget(refetchTransientPolicy.attempts),
5709
+ onSendsConsumed: noteTransientSends,
5710
+ }
5711
+ : {}),
5712
+ },
5713
+ );
5103
5714
  } finally {
5104
5715
  retryRequest.releaseBodyObservation?.();
5105
5716
  }
@@ -5142,7 +5753,9 @@ async function handleResponsesInner(
5142
5753
  route.providerName,
5143
5754
  { ...route.provider, apiKey: refreshed.accessToken },
5144
5755
  parsed.options.promptCacheKey,
5145
- route.providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(route.providerName) : undefined,
5756
+ route.providerName === "github-copilot"
5757
+ ? resolveCopilotApiBaseUrl(refreshed.apiBaseUrl)
5758
+ : undefined,
5146
5759
  );
5147
5760
  route.provider = refreshedProvider;
5148
5761
  invalidateSameTargetRequest();
@@ -5505,7 +6118,10 @@ async function handleResponsesInner(
5505
6118
  }
5506
6119
  // Same #1851 scope guard as the initial send: transient-5xx retry only for direct
5507
6120
  // Google AI Studio; every other adapter keeps reset-only semantics here.
5508
- const fetchContinuationWithRetryPolicy = route.provider.adapter === "google" ? fetchWithTransientRetry : fetchWithResetRetry;
6121
+ const continuationTransientPolicy = transientRetryPolicyFor(route.provider);
6122
+ const fetchContinuationWithRetryPolicy = (route.provider.adapter === "google" || continuationTransientPolicy)
6123
+ ? fetchWithTransientRetry
6124
+ : fetchWithResetRetry;
5509
6125
  return await fetchContinuationWithRetryPolicy(
5510
6126
  recovery => {
5511
6127
  noteAttemptSend(logCtx.activeAttempt, continuationEstimate, recovery ?? replayKind);
@@ -5525,7 +6141,19 @@ async function handleResponsesInner(
5525
6141
  }),
5526
6142
  );
5527
6143
  },
5528
- { abortSignal: upstream.signal, label: safeHostLabel(builtContinuationRequest.url) },
6144
+ {
6145
+ abortSignal: upstream.signal,
6146
+ label: safeHostLabel(builtContinuationRequest.url),
6147
+ // Same request-scoped budget as the initial send and the 429/rotation refetches:
6148
+ // a terminal-guard continuation is another leg of ONE request, so handing it a
6149
+ // fresh `attempts` would let one request exceed the configured total-send ceiling.
6150
+ ...(continuationTransientPolicy
6151
+ ? {
6152
+ attempts: remainingTransientSendBudget(continuationTransientPolicy.attempts),
6153
+ onSendsConsumed: noteTransientSends,
6154
+ }
6155
+ : {}),
6156
+ },
5529
6157
  );
5530
6158
  } finally {
5531
6159
  builtContinuationRequest.releaseBodyObservation?.();