@yansigit/opencodex 2.31.1 → 2.31.3

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 (36) hide show
  1. package/gui/dist/assets/{index-DJDp_XER.js → index-Cxt5fZMP.js} +14 -14
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +1 -1
  4. package/src/adapters/base.ts +6 -0
  5. package/src/adapters/command-code-project-context.ts +377 -0
  6. package/src/adapters/command-code.ts +5 -1
  7. package/src/adapters/cursor/cursor-errors.ts +12 -0
  8. package/src/adapters/cursor/live-transport.ts +21 -0
  9. package/src/adapters/cursor/native-exec-bridge.ts +141 -0
  10. package/src/adapters/cursor/thread-continuity.ts +71 -0
  11. package/src/adapters/cursor.ts +90 -28
  12. package/src/adapters/google-http.ts +12 -2
  13. package/src/adapters/google-wire-compiler.ts +91 -2
  14. package/src/adapters/google.ts +83 -15
  15. package/src/config.ts +2 -0
  16. package/src/generated/compatibility-version.json +57 -25
  17. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  18. package/src/oauth/anthropic-routing.ts +129 -219
  19. package/src/oauth/antigravity-routing.ts +165 -0
  20. package/src/oauth/cursor-routing.ts +252 -0
  21. package/src/oauth/index.ts +3 -0
  22. package/src/providers/cursor-pool.ts +3 -3
  23. package/src/routing/account-pool/affinity.ts +125 -0
  24. package/src/routing/account-pool/cooldown.ts +145 -0
  25. package/src/routing/account-pool/index.ts +45 -0
  26. package/src/routing/account-pool/resolve.ts +356 -0
  27. package/src/routing/account-pool/types.ts +32 -0
  28. package/src/routing/compatibility/behavior.ts +3 -0
  29. package/src/server/management/oauth-account-routes.ts +47 -12
  30. package/src/server/responses/core.ts +400 -72
  31. package/src/types/config.ts +8 -0
  32. package/src/types/provider.ts +7 -0
  33. package/src/types/request.ts +13 -3
  34. package/src/usage/log.ts +4 -0
  35. package/src/web-search/gemini-executor.ts +35 -13
  36. package/src/web-search/index.ts +85 -1
@@ -106,12 +106,26 @@ import {
106
106
  rotateAnthropicAccountOn429,
107
107
  } from "../../oauth/anthropic-routing";
108
108
  import {
109
+ antigravity429StickWaitMs,
110
+ antigravitySessionKeyFromParts,
109
111
  bindAntigravityProject,
110
- isAntigravityAccountInCooldown,
111
- nextAntigravityAccount,
112
+ bindAntigravitySessionAffinity,
113
+ resolveAntigravityAccountForSession,
114
+ rotateAntigravityAccountOn429,
112
115
  } from "../../oauth/antigravity-routing";
113
- import { getAccountCredential, getAccountSet, setActiveAccount } from "../../oauth/store";
114
- import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
116
+ import {
117
+ CURSOR_POOL_MAX_FAILOVERS_PER_REQUEST,
118
+ bindCursorSessionAffinity,
119
+ cursorSessionKeyFromParts,
120
+ formatCursorProviderForLog,
121
+ isCursorAccountPoolActive,
122
+ recordCursorAccountBillingCooldown,
123
+ resolveCursorAccountForSession,
124
+ rotateCursorAccountOn429,
125
+ rotateCursorAccountOnAuth,
126
+ } from "../../oauth/cursor-routing";
127
+ import { getAccountCredential } from "../../oauth/store";
128
+ import { buildWebSearchTool, mediaBridgeWillRun, planWebSearch, resolveCcaInTurnGrounding, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
115
129
  import { buildImageTool, buildVideoTool, planImageBridge, planVideoBridge, runWithImageBridge, clampImageMaxRounds, IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "../../images";
116
130
  import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
117
131
  import { createAdapterEventQueue, preflightAdapterEvents, type AdapterEventQueue } from "../../adapters/run-turn-queue";
@@ -506,7 +520,7 @@ function bindRouteReasoningReplayScope(args: {
506
520
  // seed assigned before route binding. A Cursor conversation must be scoped to the exact
507
521
  // provider/destination/adapter/model/credential that serves it.
508
522
  if (continuationOwner) parsed._cursorIdentityScope = providerContinuationRouteScope(continuationOwner);
509
- else if (!parsed._cursorIdentityScope?.startsWith("cursor-unowned:")) {
523
+ else if (!parsed._cursorIdentityScope?.trim()) {
510
524
  // Prevent the adapter's token-only fallback from recreating a provider-private id after the
511
525
  // route owner failed closed. The sentinel is per parsed request and contains no credential.
512
526
  parsed._cursorIdentityScope = `cursor-unowned:${randomUUID()}`;
@@ -1185,6 +1199,8 @@ export interface HandleResponsesOptions {
1185
1199
  admission?: DataPlaneAdmission;
1186
1200
  /** Called at most once after the complete client body is read and accepted for dispatch. */
1187
1201
  onRequestBodyRead?: () => void;
1202
+ /** Internal combo handoff invoked after the final adapter accepts the parsed request. */
1203
+ onRequestValidated?: () => void;
1188
1204
  forceEmptyResponseId?: boolean;
1189
1205
  abortSignal?: AbortSignal;
1190
1206
  /** One-shot TTFT callback: first non-empty model output observed (WP4). */
@@ -1820,6 +1836,7 @@ export async function handleComboResponses(
1820
1836
  let lastFailure: Response | null = null;
1821
1837
  while (pick) {
1822
1838
  if (options.abortSignal?.aborted) return clientCancelledResponse();
1839
+ const selectedPick = pick;
1823
1840
  const childLog: RequestLogContext = {
1824
1841
  model: pick.target.model,
1825
1842
  provider: pick.target.provider,
@@ -1841,24 +1858,31 @@ export async function handleComboResponses(
1841
1858
  });
1842
1859
  let resolvedAuth: CodexAuthContext | undefined;
1843
1860
  let terminalRecorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined;
1844
- const started = Date.now();
1845
- const attempt = beginRequestAttempt(
1846
- (logCtx.attempts?.length ?? 0) + 1,
1847
- pick.target.provider,
1848
- pick.target.model,
1849
- config.providers[pick.target.provider]!.adapter,
1850
- );
1851
- childLog.activeAttempt = attempt;
1861
+ let started: number | undefined;
1862
+ let attempt: ReturnType<typeof beginRequestAttempt> | undefined;
1863
+ const beginAttempt = (): void => {
1864
+ if (attempt) return;
1865
+ started = Date.now();
1866
+ attempt = beginRequestAttempt(
1867
+ (logCtx.attempts?.length ?? 0) + 1,
1868
+ selectedPick.target.provider,
1869
+ selectedPick.target.model,
1870
+ config.providers[selectedPick.target.provider]!.adapter,
1871
+ );
1872
+ childLog.activeAttempt = attempt;
1873
+ childLog.activeAttemptStartedAt = started;
1874
+ recordAttemptRequestedEffort(childLog);
1875
+ };
1852
1876
  let attemptRetained = false;
1853
1877
  const retainCancelledAttempt = (): void => {
1854
- if (attemptRetained) return;
1878
+ if (attemptRetained || !attempt) return;
1855
1879
  sealRequestAttemptIdentity(
1856
1880
  attempt,
1857
1881
  childLog.provider,
1858
1882
  childLog.providerAdapter ?? attempt.adapter,
1859
1883
  childLog.accountLogLabel,
1860
1884
  );
1861
- finishRequestAttempt(attempt, 499, Date.now() - started, childLog.usage);
1885
+ finishRequestAttempt(attempt, 499, Date.now() - (started ?? Date.now()), childLog.usage);
1862
1886
  (logCtx.attempts ??= []).push(attempt);
1863
1887
  attemptRetained = true;
1864
1888
  };
@@ -1878,10 +1902,11 @@ export async function handleComboResponses(
1878
1902
  comboAttempt: true,
1879
1903
  comboReplaySnapshot,
1880
1904
  deferCodexResetDerivedCooldown,
1905
+ onRequestValidated: beginAttempt,
1881
1906
  // Attempt-relative TTFT is recorded HERE (not via childLog.firstOutputMs — a later
1882
1907
  // Object.assign(logCtx, childLog) would overwrite the request-relative value).
1883
1908
  onFirstOutput: () => {
1884
- if (attempt.firstOutputMs === undefined) {
1909
+ if (attempt && started !== undefined && attempt.firstOutputMs === undefined) {
1885
1910
  attempt.firstOutputMs = Math.max(0, Date.now() - started);
1886
1911
  }
1887
1912
  options.onFirstOutput?.();
@@ -1908,6 +1933,7 @@ export async function handleComboResponses(
1908
1933
  }
1909
1934
 
1910
1935
  if (response.ok) {
1936
+ if (!attempt) return response;
1911
1937
  sealRequestAttemptIdentity(
1912
1938
  attempt,
1913
1939
  childLog.provider,
@@ -1954,20 +1980,22 @@ export async function handleComboResponses(
1954
1980
  retainCancelledAttempt();
1955
1981
  return clientCancelledResponse();
1956
1982
  }
1957
- sealRequestAttemptIdentity(
1958
- attempt,
1959
- childLog.provider,
1960
- childLog.providerAdapter ?? attempt.adapter,
1961
- childLog.accountLogLabel,
1962
- );
1963
- finishRequestAttempt(
1964
- attempt,
1965
- response.status,
1966
- Date.now() - started,
1967
- failure.usage,
1968
- );
1969
- (logCtx.attempts ??= []).push(attempt);
1970
- attemptRetained = true;
1983
+ if (attempt) {
1984
+ sealRequestAttemptIdentity(
1985
+ attempt,
1986
+ childLog.provider,
1987
+ childLog.providerAdapter ?? attempt.adapter,
1988
+ childLog.accountLogLabel,
1989
+ );
1990
+ finishRequestAttempt(
1991
+ attempt,
1992
+ response.status,
1993
+ Date.now() - (started ?? Date.now()),
1994
+ failure.usage,
1995
+ );
1996
+ (logCtx.attempts ??= []).push(attempt);
1997
+ attemptRetained = true;
1998
+ }
1971
1999
  lastFailure = failure.response;
1972
2000
  if (comboFailureDecision(failure.response.status, failure.classificationText, {
1973
2001
  code: failure.upstreamCode,
@@ -1976,7 +2004,7 @@ export async function handleComboResponses(
1976
2004
  return lastFailure;
1977
2005
  }
1978
2006
  console.warn(
1979
- `[combo] ${comboId}: ${targetKey(pick.target)} failed with ${response.status} after ${Date.now() - started}ms`,
2007
+ `[combo] ${comboId}: ${targetKey(pick.target)} failed with ${response.status} after ${started === undefined ? 0 : Date.now() - started}ms`,
1980
2008
  );
1981
2009
  const nextPick = advanceComboAfterFailure(config, pick, {
1982
2010
  retryAfter: failure.retryAfter,
@@ -2589,13 +2617,27 @@ async function handleResponsesInner(
2589
2617
  let anthropicPoolFailovers = 0;
2590
2618
  let antigravityAccountId: string | undefined;
2591
2619
  let antigravityFailovers = 0;
2620
+ let cursorPoolAccountId: string | null = null;
2621
+ let cursorPoolFailovers = 0;
2622
+ const oauthSessionKeyParts = {
2623
+ sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
2624
+ threadIdHeader: req.headers.get("thread-id"),
2625
+ promptCacheKey: typeof parsed.options.promptCacheKey === "string" ? parsed.options.promptCacheKey : null,
2626
+ clientThreadId: typeof parsed._clientThreadId === "string" ? parsed._clientThreadId : null,
2627
+ promptCacheKeyIsSharedCohort: options.promptCacheKeyIsSharedCohort === true,
2628
+ };
2592
2629
  const anthropicSessionKey = route.providerName === "anthropic" && route.provider.authMode === "oauth"
2593
- ? anthropicSessionKeyFromParts({
2594
- sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
2595
- threadIdHeader: req.headers.get("thread-id"),
2596
- promptCacheKey: typeof parsed.options.promptCacheKey === "string" ? parsed.options.promptCacheKey : null,
2630
+ ? anthropicSessionKeyFromParts(oauthSessionKeyParts)
2631
+ : null;
2632
+ const antigravitySessionKey = route.providerName === "google-antigravity"
2633
+ && route.provider.authMode === "oauth"
2634
+ && route.provider.googleMode === "cloud-code-assist"
2635
+ ? antigravitySessionKeyFromParts(oauthSessionKeyParts)
2636
+ : null;
2637
+ const cursorSessionKey = route.providerName === "cursor"
2638
+ && route.provider.authMode === "oauth"
2639
+ ? cursorSessionKeyFromParts({
2597
2640
  clientThreadId: typeof parsed._clientThreadId === "string" ? parsed._clientThreadId : null,
2598
- promptCacheKeyIsSharedCohort: options.promptCacheKeyIsSharedCohort === true,
2599
2641
  })
2600
2642
  : null;
2601
2643
  if (route.provider.authMode === "oauth") {
@@ -2620,48 +2662,88 @@ async function handleResponsesInner(
2620
2662
  promoteAnthropicActiveAccount(selection.accountId);
2621
2663
  route.provider = { ...route.provider, apiKey: accessToken };
2622
2664
  logCtx.provider = formatAnthropicProviderForLog("anthropic", selection.accountId, config);
2623
- } else {
2624
- let resolved = await getValidAccessTokenSnapshot(route.providerName);
2625
- let skippedAntigravityCooldown = false;
2626
- if (route.providerName === "google-antigravity"
2627
- && route.provider.googleMode === "cloud-code-assist"
2628
- && isAntigravityAccountInCooldown(resolved.accountId)) {
2629
- const accountIds = getAccountSet("google-antigravity")?.accounts.map(account => account.id) ?? [];
2630
- const nextAccountId = nextAntigravityAccount(accountIds, resolved.accountId);
2631
- if (!nextAccountId) {
2632
- return formatErrorResponse(429, "rate_limit_error", "All Google Antigravity OAuth accounts are temporarily unavailable");
2665
+ } else if (
2666
+ route.providerName === "google-antigravity"
2667
+ && route.provider.googleMode === "cloud-code-assist"
2668
+ ) {
2669
+ const selection = resolveAntigravityAccountForSession(antigravitySessionKey);
2670
+ if (!selection.accountId) {
2671
+ if (selection.reason === "all-cooled") {
2672
+ return formatErrorResponse(
2673
+ 429,
2674
+ "rate_limit_error",
2675
+ "All Google Antigravity OAuth accounts are temporarily unavailable",
2676
+ );
2633
2677
  }
2634
- const accessToken = await getValidAccessTokenForAccount("google-antigravity", nextAccountId);
2635
- const nextCredential = getAccountCredential("google-antigravity", nextAccountId);
2678
+ return formatErrorResponse(
2679
+ 401,
2680
+ "authentication_error",
2681
+ "No eligible Google Antigravity OAuth account available",
2682
+ );
2683
+ }
2684
+ let resolved = await getValidAccessTokenSnapshot(route.providerName);
2685
+ if (selection.accountId !== resolved.accountId) {
2686
+ const accessToken = await getValidAccessTokenForAccount("google-antigravity", selection.accountId);
2687
+ const nextCredential = getAccountCredential("google-antigravity", selection.accountId);
2636
2688
  resolved = {
2637
2689
  ...resolved,
2638
- accountId: nextAccountId,
2690
+ accountId: selection.accountId,
2639
2691
  accessToken,
2640
2692
  // Always replace; omitting a missing id would keep the previous account's projectId.
2641
2693
  projectId: nextCredential?.projectId,
2642
2694
  };
2643
- skippedAntigravityCooldown = true;
2644
2695
  }
2645
2696
  replayOAuthCredentialSnapshot = {
2646
2697
  accountId: resolved.accountId,
2647
2698
  generation: resolved.generation,
2648
2699
  };
2649
- if (skippedAntigravityCooldown) replayOAuthCredentialSnapshot = undefined;
2650
- if (isOAuth401ReplayProvider) sentOAuthSnapshot = resolved;
2700
+ if (selection.reason === "failover") replayOAuthCredentialSnapshot = undefined;
2651
2701
  route.provider = { ...route.provider, apiKey: resolved.accessToken };
2652
- if (route.providerName === "google-antigravity" && route.provider.googleMode === "cloud-code-assist") {
2653
- antigravityAccountId = resolved.accountId;
2654
- // Always overwrite `project` from the credential in use. A missing id fails closed
2655
- // so a rotated account cannot inherit the previous account's Cloud Code Assist project.
2656
- const bound = bindAntigravityProject(route.provider, resolved.projectId);
2657
- if (!bound.ok) {
2658
- return formatErrorResponse(bound.status, bound.type, bound.message);
2659
- }
2660
- route.provider = bound.provider;
2661
- if (skippedAntigravityCooldown) {
2662
- void setActiveAccount("google-antigravity", resolved.accountId).catch(() => { /* best-effort promotion */ });
2702
+ antigravityAccountId = selection.accountId;
2703
+ bindAntigravitySessionAffinity(antigravitySessionKey, selection.accountId);
2704
+ const bound = bindAntigravityProject(route.provider, resolved.projectId);
2705
+ if (!bound.ok) {
2706
+ return formatErrorResponse(bound.status, bound.type, bound.message);
2707
+ }
2708
+ route.provider = bound.provider;
2709
+ } else if (
2710
+ route.providerName === "cursor"
2711
+ && route.provider.authMode === "oauth"
2712
+ && isCursorAccountPoolActive(config)
2713
+ ) {
2714
+ const selection = resolveCursorAccountForSession(cursorSessionKey, config);
2715
+ if (!selection.accountId) {
2716
+ if (selection.reason === "all-cooled") {
2717
+ return formatErrorResponse(
2718
+ 429,
2719
+ "rate_limit_error",
2720
+ "All Cursor OAuth accounts are temporarily rate-limited",
2721
+ );
2663
2722
  }
2723
+ return formatErrorResponse(401, "authentication_error", "No eligible Cursor OAuth account available");
2724
+ }
2725
+ let resolved = await getValidAccessTokenSnapshot("cursor");
2726
+ if (selection.accountId !== resolved.accountId) {
2727
+ const accessToken = await getValidAccessTokenForAccount("cursor", selection.accountId);
2728
+ resolved = { ...resolved, accountId: selection.accountId, accessToken };
2664
2729
  }
2730
+ cursorPoolAccountId = selection.accountId;
2731
+ parsed._cursorIdentityScope = selection.accountId;
2732
+ bindCursorSessionAffinity(cursorSessionKey, selection.accountId);
2733
+ replayOAuthCredentialSnapshot = {
2734
+ accountId: resolved.accountId,
2735
+ generation: resolved.generation,
2736
+ };
2737
+ route.provider = { ...route.provider, apiKey: resolved.accessToken };
2738
+ logCtx.provider = formatCursorProviderForLog("cursor", selection.accountId);
2739
+ } else {
2740
+ const resolved = await getValidAccessTokenSnapshot(route.providerName);
2741
+ replayOAuthCredentialSnapshot = {
2742
+ accountId: resolved.accountId,
2743
+ generation: resolved.generation,
2744
+ };
2745
+ if (isOAuth401ReplayProvider) sentOAuthSnapshot = resolved;
2746
+ route.provider = { ...route.provider, apiKey: resolved.accessToken };
2665
2747
  if (route.providerName === "kiro") {
2666
2748
  // `{}` is intentional: this is an account-scoped request with no stored routing metadata.
2667
2749
  // Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
@@ -2719,6 +2801,16 @@ async function handleResponsesInner(
2719
2801
  logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
2720
2802
  }
2721
2803
  logCtx.providerAdapter = adapter.name;
2804
+ try {
2805
+ adapter.validateRequest?.(parsed);
2806
+ } catch (err) {
2807
+ return formatErrorResponse(
2808
+ 400,
2809
+ "invalid_request_error",
2810
+ redactSecretString(err instanceof Error ? err.message : String(err)),
2811
+ );
2812
+ }
2813
+ options.onRequestValidated?.();
2722
2814
  // Ordinary requests receive one durable attempt only after their final initial
2723
2815
  // adapter is resolved. Combo children own their attempt and retries keep it.
2724
2816
  if (!options.comboAttempt && !logCtx.activeAttempt) {
@@ -3969,13 +4061,23 @@ async function handleResponsesInner(
3969
4061
  // - non-runTurn: web-search wins over image when both eligible (documented priority)
3970
4062
  // - runTurn: image bridge may run (it supports runTurn); web-search is skipped so runTurn
3971
4063
  // can proceed for web-search-only turns
3972
- const wsPlan = !routedCompaction
3973
- ? planWebSearch(config, parsed, false, route.provider, route.modelId, openAiSidecar)
3974
- : undefined;
3975
4064
  const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined;
3976
4065
  const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
3977
- const canRunWebSearch = !!wsPlan && !adapter.runTurn;
3978
- if ((imgPlan || vidPlan) && canRunWebSearch) {
4066
+ const hasMediaPlan = !!(imgPlan || vidPlan);
4067
+ // A media plan is not enough to suppress Gemini 2.x grounding: both bridges inject tools only
4068
+ // on streaming turns. This is the potential-injection value used to resolve sidecar precedence.
4069
+ const mediaMayInject = mediaBridgeWillRun(hasMediaPlan, false, !!adapter.runTurn, parsed.stream);
4070
+ const wsPlan = !routedCompaction
4071
+ ? planWebSearch(config, parsed, false, route.provider, route.modelId, openAiSidecar, mediaMayInject)
4072
+ : undefined;
4073
+ const webSearchWinsMedia = !!wsPlan && !adapter.runTurn;
4074
+ const mediaWillRun = mediaBridgeWillRun(hasMediaPlan, !!wsPlan, !!adapter.runTurn, parsed.stream);
4075
+ const ccaInTurnGrounding = !routedCompaction
4076
+ ? resolveCcaInTurnGrounding(config, parsed, false, route.provider, route.modelId, mediaWillRun)
4077
+ : undefined;
4078
+ if (ccaInTurnGrounding) parsed._ccaInTurnGrounding = ccaInTurnGrounding;
4079
+ const canRunWebSearch = webSearchWinsMedia && !ccaInTurnGrounding;
4080
+ if (hasMediaPlan && webSearchWinsMedia) {
3979
4081
  // Web search takes priority when both are active — the media bridge cannot run
3980
4082
  // alongside runWithWebSearch. Surface a runtime signal so the user knows their
3981
4083
  // configured video/image bridge was skipped for this turn, rather than silently
@@ -3983,7 +4085,7 @@ async function handleResponsesInner(
3983
4085
  if (vidPlan) console.warn("[videos] video bridge skipped: web search is active for this turn");
3984
4086
  if (imgPlan) console.warn("[images] image bridge skipped: web search is active for this turn");
3985
4087
  }
3986
- if ((imgPlan || vidPlan) && (!wsPlan || adapter.runTurn)) {
4088
+ if (mediaWillRun || (imgPlan && !parsed.stream && !webSearchWinsMedia)) {
3987
4089
  // The image bridge detects a hosted image_generation tool and requires streaming.
3988
4090
  // The video bridge activates from config and injects a tool — it also needs streaming
3989
4091
  // (the loop returns SSE). For video-only (no imgPlan) on a non-streaming request, skip
@@ -4782,8 +4884,29 @@ async function handleResponsesInner(
4782
4884
  && antigravityAccountId
4783
4885
  && antigravityFailovers < 3
4784
4886
  ) {
4785
- const accountIds = getAccountSet("google-antigravity")?.accounts.map(account => account.id) ?? [];
4786
- const nextAccountId = nextAntigravityAccount(accountIds, antigravityAccountId);
4887
+ const stickWaitMs = antigravity429StickWaitMs(antigravityAccountId);
4888
+ if (stickWaitMs !== null) {
4889
+ await Bun.sleep(stickWaitMs);
4890
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
4891
+ invalidateSameTargetRequest();
4892
+ activeAdapter = resolveAdapter(
4893
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
4894
+ config.cacheRetention,
4895
+ );
4896
+ bindRouteReasoningReplayScope({
4897
+ parsed,
4898
+ providerName: route.providerName,
4899
+ provider: route.provider,
4900
+ adapterName: activeAdapter.name,
4901
+ codexAuthContext: authCtx,
4902
+ forwardHeaders: selectedForwardHeaders,
4903
+ });
4904
+ const result = await rebuildAndRefetch("rate-limit-429");
4905
+ if ("failed" in result) return result.failed;
4906
+ upstreamResponse = result;
4907
+ continue;
4908
+ }
4909
+ const nextAccountId = rotateAntigravityAccountOn429(antigravityAccountId, antigravitySessionKey);
4787
4910
  if (!nextAccountId) break;
4788
4911
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
4789
4912
  try {
@@ -4813,7 +4936,6 @@ async function handleResponsesInner(
4813
4936
  codexAuthContext: authCtx,
4814
4937
  forwardHeaders: selectedForwardHeaders,
4815
4938
  });
4816
- void setActiveAccount("google-antigravity", nextAccountId).catch(() => { /* best-effort promotion */ });
4817
4939
  const result = await rebuildAndRefetch("rate-limit-429");
4818
4940
  if ("failed" in result) return result.failed;
4819
4941
  upstreamResponse = result;
@@ -4821,6 +4943,105 @@ async function handleResponsesInner(
4821
4943
  break;
4822
4944
  }
4823
4945
  }
4946
+ if (
4947
+ upstreamResponse.status === 402
4948
+ && cursorPoolAccountId
4949
+ && isCursorAccountPoolActive(config)
4950
+ ) {
4951
+ recordCursorAccountBillingCooldown(
4952
+ cursorPoolAccountId,
4953
+ upstreamResponse.headers.get("retry-after"),
4954
+ );
4955
+ }
4956
+ while (
4957
+ (upstreamResponse.status === 401 || upstreamResponse.status === 403)
4958
+ && route.providerName === "cursor"
4959
+ && route.provider.authMode === "oauth"
4960
+ && cursorPoolAccountId
4961
+ && isCursorAccountPoolActive(config)
4962
+ && cursorPoolFailovers < CURSOR_POOL_MAX_FAILOVERS_PER_REQUEST
4963
+ ) {
4964
+ const nextAccountId = rotateCursorAccountOnAuth(
4965
+ config,
4966
+ cursorPoolAccountId,
4967
+ cursorSessionKey,
4968
+ );
4969
+ if (!nextAccountId) break;
4970
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
4971
+ try {
4972
+ const accessToken = await getValidAccessTokenForAccount("cursor", nextAccountId);
4973
+ cursorPoolAccountId = nextAccountId;
4974
+ cursorPoolFailovers += 1;
4975
+ parsed._cursorIdentityScope = nextAccountId;
4976
+ route.provider = { ...route.provider, apiKey: accessToken };
4977
+ replayOAuthCredentialSnapshot = undefined;
4978
+ invalidateSameTargetRequest();
4979
+ activeAdapter = resolveAdapter(
4980
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
4981
+ config.cacheRetention,
4982
+ );
4983
+ bindRouteReasoningReplayScope({
4984
+ parsed,
4985
+ providerName: route.providerName,
4986
+ provider: route.provider,
4987
+ adapterName: activeAdapter.name,
4988
+ codexAuthContext: authCtx,
4989
+ forwardHeaders: selectedForwardHeaders,
4990
+ });
4991
+ logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
4992
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
4993
+ const result = await rebuildAndRefetch("cursor-oauth-auth");
4994
+ if ("failed" in result) return result.failed;
4995
+ upstreamResponse = result;
4996
+ } catch {
4997
+ break;
4998
+ }
4999
+ }
5000
+ while (
5001
+ upstreamResponse.status === 429
5002
+ && route.providerName === "cursor"
5003
+ && route.provider.authMode === "oauth"
5004
+ && cursorPoolAccountId
5005
+ && isCursorAccountPoolActive(config)
5006
+ && cursorPoolFailovers < CURSOR_POOL_MAX_FAILOVERS_PER_REQUEST
5007
+ ) {
5008
+ const nextAccountId = rotateCursorAccountOn429(
5009
+ config,
5010
+ cursorPoolAccountId,
5011
+ upstreamResponse.headers.get("retry-after"),
5012
+ cursorSessionKey,
5013
+ );
5014
+ if (!nextAccountId) break;
5015
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
5016
+ try {
5017
+ const accessToken = await getValidAccessTokenForAccount("cursor", nextAccountId);
5018
+ cursorPoolAccountId = nextAccountId;
5019
+ cursorPoolFailovers += 1;
5020
+ parsed._cursorIdentityScope = nextAccountId;
5021
+ route.provider = { ...route.provider, apiKey: accessToken };
5022
+ replayOAuthCredentialSnapshot = undefined;
5023
+ invalidateSameTargetRequest();
5024
+ activeAdapter = resolveAdapter(
5025
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
5026
+ config.cacheRetention,
5027
+ );
5028
+ bindRouteReasoningReplayScope({
5029
+ parsed,
5030
+ providerName: route.providerName,
5031
+ provider: route.provider,
5032
+ adapterName: activeAdapter.name,
5033
+ codexAuthContext: authCtx,
5034
+ forwardHeaders: selectedForwardHeaders,
5035
+ });
5036
+ logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
5037
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
5038
+ const result = await rebuildAndRefetch("cursor-oauth-429");
5039
+ if ("failed" in result) return result.failed;
5040
+ upstreamResponse = result;
5041
+ } catch {
5042
+ break;
5043
+ }
5044
+ }
4824
5045
  // Unknown provenance is deliberately fail-soft in pre-flight: after a restart, TTL expiry,
4825
5046
  // or LRU eviction, a valid same-backend blob must survive. A decoder's own 4xx identity is
4826
5047
  // the missing authoritative signal. Rebuild once through the same sanitation path used by a
@@ -5155,6 +5376,113 @@ async function handleResponsesInner(
5155
5376
  }
5156
5377
  }
5157
5378
  }
5379
+ if (
5380
+ response.status === 402
5381
+ && cursorPoolAccountId
5382
+ && isCursorAccountPoolActive(config)
5383
+ ) {
5384
+ recordCursorAccountBillingCooldown(
5385
+ cursorPoolAccountId,
5386
+ response.headers.get("retry-after"),
5387
+ );
5388
+ }
5389
+ if (
5390
+ (response.status === 401 || response.status === 403)
5391
+ && route.providerName === "cursor"
5392
+ && route.provider.authMode === "oauth"
5393
+ && cursorPoolAccountId
5394
+ && isCursorAccountPoolActive(config)
5395
+ && cursorPoolFailovers < CURSOR_POOL_MAX_FAILOVERS_PER_REQUEST
5396
+ ) {
5397
+ const nextAccountId = rotateCursorAccountOnAuth(
5398
+ config,
5399
+ cursorPoolAccountId,
5400
+ cursorSessionKey,
5401
+ );
5402
+ if (nextAccountId) {
5403
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
5404
+ try {
5405
+ const accessToken = await getValidAccessTokenForAccount("cursor", nextAccountId);
5406
+ cursorPoolAccountId = nextAccountId;
5407
+ cursorPoolFailovers += 1;
5408
+ parsed._cursorIdentityScope = nextAccountId;
5409
+ route.provider = { ...route.provider, apiKey: accessToken };
5410
+ replayOAuthCredentialSnapshot = undefined;
5411
+ invalidateSameTargetRequest();
5412
+ activeAdapter = resolveAdapter(
5413
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
5414
+ config.cacheRetention,
5415
+ );
5416
+ bindRouteReasoningReplayScope({
5417
+ parsed: nextParsed,
5418
+ providerName: route.providerName,
5419
+ provider: route.provider,
5420
+ adapterName: activeAdapter.name,
5421
+ });
5422
+ bindRouteReasoningReplayScope({
5423
+ parsed,
5424
+ providerName: route.providerName,
5425
+ provider: route.provider,
5426
+ adapterName: activeAdapter.name,
5427
+ });
5428
+ logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
5429
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
5430
+ nextContinuationRecoveryKind = "cursor-oauth-auth";
5431
+ continue;
5432
+ } catch {
5433
+ // fall through to emit continuation error below
5434
+ }
5435
+ }
5436
+ }
5437
+ if (
5438
+ response.status === 429
5439
+ && route.providerName === "cursor"
5440
+ && route.provider.authMode === "oauth"
5441
+ && cursorPoolAccountId
5442
+ && isCursorAccountPoolActive(config)
5443
+ && cursorPoolFailovers < CURSOR_POOL_MAX_FAILOVERS_PER_REQUEST
5444
+ ) {
5445
+ const nextAccountId = rotateCursorAccountOn429(
5446
+ config,
5447
+ cursorPoolAccountId,
5448
+ response.headers.get("retry-after"),
5449
+ cursorSessionKey,
5450
+ );
5451
+ if (nextAccountId) {
5452
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
5453
+ try {
5454
+ const accessToken = await getValidAccessTokenForAccount("cursor", nextAccountId);
5455
+ cursorPoolAccountId = nextAccountId;
5456
+ cursorPoolFailovers += 1;
5457
+ parsed._cursorIdentityScope = nextAccountId;
5458
+ route.provider = { ...route.provider, apiKey: accessToken };
5459
+ replayOAuthCredentialSnapshot = undefined;
5460
+ invalidateSameTargetRequest();
5461
+ activeAdapter = resolveAdapter(
5462
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
5463
+ config.cacheRetention,
5464
+ );
5465
+ bindRouteReasoningReplayScope({
5466
+ parsed: nextParsed,
5467
+ providerName: route.providerName,
5468
+ provider: route.provider,
5469
+ adapterName: activeAdapter.name,
5470
+ });
5471
+ bindRouteReasoningReplayScope({
5472
+ parsed,
5473
+ providerName: route.providerName,
5474
+ provider: route.provider,
5475
+ adapterName: activeAdapter.name,
5476
+ });
5477
+ logCtx.provider = formatCursorProviderForLog("cursor", nextAccountId);
5478
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
5479
+ nextContinuationRecoveryKind = "cursor-oauth-429";
5480
+ continue;
5481
+ } catch {
5482
+ // fall through to emit continuation error below
5483
+ }
5484
+ }
5485
+ }
5158
5486
  if (shouldAttemptImageTierRetry({
5159
5487
  status: response.status,
5160
5488
  adapterName: activeAdapter.name,
@@ -601,6 +601,14 @@ export interface OcxConfig {
601
601
  /** Successful new-session binds retained on one round-robin selection. Default 1; range 1..100. */
602
602
  stickyLimit?: number;
603
603
  };
604
+ /**
605
+ * Opt-in Cursor OAuth account pool. Default OFF.
606
+ * Sticky `_clientThreadId` affinity + bounded 429/auth failover when ≥2 OAuth accounts exist.
607
+ * Does not wire weighted-round-robin `CursorCredentialRouter`.
608
+ */
609
+ cursorAccountPool?: {
610
+ enabled?: boolean;
611
+ };
604
612
  /** Virtual `combo/<id>` models spanning concrete provider/model targets (issue #133). */
605
613
  combos?: Record<string, OcxComboConfig>;
606
614
  /**
@@ -169,6 +169,13 @@ export interface OcxProviderConfig {
169
169
  * version here instead of waiting for a code change. Absent uses the adapter's current default.
170
170
  */
171
171
  commandCodeVersion?: string;
172
+ /**
173
+ * Command Code OAuth `/alpha/generate` project-context envelope. When `"on"`, the adapter
174
+ * fills `memory`, `taste`, and `skills` from bounded files under `process.cwd()`. Absent or
175
+ * `"off"` keeps the empty envelope (`memory: ""`, `taste: null`, `skills: null`). Does not
176
+ * enable taste learning (`x-taste-learning` stays `"false"`).
177
+ */
178
+ projectContext?: "off" | "on";
172
179
  /**
173
180
  * Responses upstream that stores nothing server-side (DeepSeek documents "the API
174
181
  * is stateless"). Stateful request parameters are dropped, `store` is pinned false,