@bitkyc08/opencodex 2.69.0 → 2.70.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 (147) hide show
  1. package/gui/dist/assets/App-_MrbTrBe.js +51 -0
  2. package/gui/dist/assets/{Tray-uqKRqfvw.js → Tray-5PuUalET.js} +1 -1
  3. package/gui/dist/assets/index-DKrO2eF3.js +86 -0
  4. package/gui/dist/assets/quota-summary-BCLivc0Q.js +1 -0
  5. package/gui/dist/index.html +1 -1
  6. package/package.json +1 -1
  7. package/src/adapters/anthropic-model-contract.ts +119 -0
  8. package/src/adapters/anthropic.ts +14 -83
  9. package/src/adapters/base.ts +2 -0
  10. package/src/adapters/command-code-project-context.ts +542 -0
  11. package/src/adapters/command-code-tool-text.ts +99 -67
  12. package/src/adapters/command-code.ts +5 -1
  13. package/src/adapters/cursor/catalog.ts +11 -0
  14. package/src/adapters/cursor/effort-map.ts +3 -0
  15. package/src/adapters/devin/cloud-direct/catalog.ts +91 -0
  16. package/src/adapters/devin/cloud-direct/chat.ts +78 -48
  17. package/src/adapters/devin/cloud-direct/tool-schema.ts +121 -0
  18. package/src/adapters/devin/context-overflow.ts +75 -0
  19. package/src/adapters/devin/live-models.ts +174 -13
  20. package/src/adapters/devin/reasoning-signature.ts +100 -0
  21. package/src/adapters/devin.ts +310 -117
  22. package/src/adapters/kiro/payload.ts +10 -10
  23. package/src/adapters/kiro-images.ts +58 -7
  24. package/src/adapters/openai-chat/passthrough.ts +3 -1
  25. package/src/adapters/openai-chat.ts +2 -1
  26. package/src/adapters/openai-responses/passthrough.ts +2 -1
  27. package/src/bridge/response-json.ts +15 -1
  28. package/src/bridge/sse.ts +3 -1
  29. package/src/claude/intercept/model-bindings.ts +1 -0
  30. package/src/cli/account-extended.ts +31 -1
  31. package/src/cli/account.ts +3 -0
  32. package/src/cli/capabilities.ts +13 -0
  33. package/src/cli/client-start-fence.ts +36 -0
  34. package/src/cli/dispatch.ts +3 -2
  35. package/src/cli/index.ts +98 -117
  36. package/src/cli/provider-runtime.ts +23 -2
  37. package/src/cli/provider.ts +3 -0
  38. package/src/cli/resolve.ts +18 -0
  39. package/src/cli/start-owner-fence.ts +30 -0
  40. package/src/cli/stop-approval.ts +13 -1
  41. package/src/cli/stop-restore.ts +73 -0
  42. package/src/cli/tray-proxy.ts +221 -18
  43. package/src/cli/version-skew.ts +25 -23
  44. package/src/client/runtime.ts +8 -4
  45. package/src/codex/auth-api/account-list.ts +11 -5
  46. package/src/codex/auth-api/login-flow.ts +5 -3
  47. package/src/codex/auth-api/main-account-probe.ts +186 -110
  48. package/src/codex/auth-api/pool-mode-gate.ts +61 -7
  49. package/src/codex/auth-api/pool-quota-probe.ts +142 -90
  50. package/src/codex/auth-api/reset-credit-service.ts +1 -1
  51. package/src/codex/catalog/gather-capture.ts +1 -0
  52. package/src/codex/catalog/model-hints.ts +3 -3
  53. package/src/codex/catalog/provider-models.ts +2 -0
  54. package/src/codex/low-quota-events.ts +28 -0
  55. package/src/codex/low-quota-observer.ts +23 -0
  56. package/src/codex/low-quota-protection.ts +272 -0
  57. package/src/codex/quota-query-backoff.ts +148 -0
  58. package/src/codex/quota.ts +7 -2
  59. package/src/config/diagnostics.ts +10 -1
  60. package/src/config/load-degrade.ts +17 -0
  61. package/src/config/mutation-lock.ts +3 -4
  62. package/src/config/process-state.ts +1 -1
  63. package/src/config/provider-validation.ts +22 -0
  64. package/src/config/schema/blocked-model-redirects.ts +17 -0
  65. package/src/config/schema/config-schema.ts +4 -1
  66. package/src/config/schema/leaf-validators.ts +33 -2
  67. package/src/config/serving-runtimes.ts +412 -0
  68. package/src/generated/compatibility-version.json +211 -115
  69. package/src/generated/model-metadata.ts +3 -3
  70. package/src/images/loop.ts +3 -0
  71. package/src/lib/errors.ts +2 -1
  72. package/src/lib/handled-signal-exit.ts +23 -0
  73. package/src/lib/request-failure-model.ts +1 -0
  74. package/src/lib/self-launch-argv.ts +9 -0
  75. package/src/lib/shadow-call.ts +5 -2
  76. package/src/lib/strict-semver.ts +26 -0
  77. package/src/lib/transient-service-path.ts +16 -0
  78. package/src/lib/windows-service-wrappers.ts +10 -6
  79. package/src/lib/winsw.ts +3 -1
  80. package/src/oauth/anthropic-model-routes.ts +105 -0
  81. package/src/oauth/anthropic-routing.ts +85 -48
  82. package/src/oauth/devin.ts +135 -5
  83. package/src/oauth/index.ts +20 -3
  84. package/src/oauth/kiro-terminal-failover.ts +23 -9
  85. package/src/oauth/pool-settings-capability.ts +10 -2
  86. package/src/oauth/store.ts +1 -1
  87. package/src/protocols/encoders/adapter-events.ts +3 -1
  88. package/src/providers/github-copilot-context.ts +27 -0
  89. package/src/providers/kiro-models.ts +4 -0
  90. package/src/providers/model-rename-fields.ts +3 -0
  91. package/src/providers/quota/account-cache.ts +8 -3
  92. package/src/providers/quota/devin.ts +286 -0
  93. package/src/providers/quota/vendor-probes-oauth.ts +6 -2
  94. package/src/providers/quota.ts +1 -1
  95. package/src/providers/registry/entries-core.ts +1 -1
  96. package/src/providers/registry/model-seeds.ts +5 -2
  97. package/src/responses/compaction-images.ts +27 -0
  98. package/src/router.ts +104 -16
  99. package/src/routing/analytics.ts +1 -0
  100. package/src/server/auth-cors.ts +9 -1
  101. package/src/server/background-lifecycle.ts +21 -10
  102. package/src/server/chat-completions.ts +2 -1
  103. package/src/server/chat-native.ts +1 -0
  104. package/src/server/index.ts +4 -8
  105. package/src/server/inference/client-encoder-delivery.ts +3 -0
  106. package/src/server/management/context.ts +3 -0
  107. package/src/server/management/low-quota-routes.ts +14 -0
  108. package/src/server/management/oauth-account-routes.ts +21 -1
  109. package/src/server/management/provider-overwrite-carry.ts +79 -6
  110. package/src/server/management/provider-routes.ts +41 -28
  111. package/src/server/management/route-registry.ts +1 -0
  112. package/src/server/management-api.ts +7 -0
  113. package/src/server/responses/adapter-continuation.ts +3 -0
  114. package/src/server/responses/adapter-delivery.ts +3 -0
  115. package/src/server/responses/adapter-dispatch.ts +20 -4
  116. package/src/server/responses/antigravity-validation-refusal.ts +20 -0
  117. package/src/server/responses/core-auth.ts +1 -0
  118. package/src/server/responses/core-codex-account.ts +1 -0
  119. package/src/server/responses/core-normalize.ts +4 -0
  120. package/src/server/responses/passthrough-dispatch.ts +8 -1
  121. package/src/server/responses/request-sidecar-auth.ts +2 -0
  122. package/src/server/responses/request-transport.ts +27 -10
  123. package/src/server/responses/response-effects.ts +4 -1
  124. package/src/server/responses/run-turn-execution.ts +126 -42
  125. package/src/server/responses/sidecar-execution.ts +4 -1
  126. package/src/service/diagnostics.ts +3 -1
  127. package/src/service/guarded-manager-target.ts +39 -12
  128. package/src/service/launchd.ts +14 -6
  129. package/src/service/repair.ts +6 -1
  130. package/src/service/service-child-ownership.ts +168 -0
  131. package/src/service/state.ts +22 -4
  132. package/src/service/systemd.ts +2 -2
  133. package/src/service/windows-guarded-manager.ts +530 -0
  134. package/src/service/windows-taskxml.ts +2 -2
  135. package/src/service.ts +2 -0
  136. package/src/types/config.ts +26 -0
  137. package/src/types/provider.ts +12 -0
  138. package/src/types/request.ts +2 -0
  139. package/src/types.ts +1 -0
  140. package/src/usage/expected-prices.ts +13 -0
  141. package/src/usage/telemetry-contract.ts +1 -0
  142. package/src/vision/anthropic-describe.ts +2 -1
  143. package/src/web-search/anthropic-executor.ts +2 -1
  144. package/src/web-search/loop.ts +1 -0
  145. package/gui/dist/assets/App-DDbgOTh6.js +0 -51
  146. package/gui/dist/assets/index-CuaBJkG4.js +0 -86
  147. package/gui/dist/assets/quota-summary-COppQSVq.js +0 -1
@@ -1,3 +1,10 @@
1
+ import { fetchCodexUsage } from "../quota-query-backoff";
2
+ import type { CodexUsageOwner } from "../quota-query-backoff";
3
+ import { loadConfig } from "../../config";
4
+ import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
5
+ import { providerCodexAccountMode } from "../../providers/registry";
6
+ import { isSelectableCodexPoolAccount } from "../account-id";
7
+ import type { OcxConfig } from "../../types";
1
8
  import { parseMainPolicyUsageQuota, parseUsageQuota, setAccountQuotaFromParsed } from "../quota";
2
9
  import type { StoredAccountQuota, WhamUsageResponse } from "../quota";
3
10
  import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
@@ -15,7 +22,7 @@ import { nonEmptyPlan } from "./runtime-config";
15
22
  import { tryAcquireNativeMainProfileClaim } from "../native-main-admission";
16
23
  import { WHAM_REQUEST_TIMEOUT_MS } from "../quota-recovery-timing";
17
24
  import { withNativeMainCredentialClaim, isNativeMainClaimUnavailable } from "./http";
18
- import { MAIN_TERMINAL_AUTH_CODES, readMainAuthErrorCode, nextQuotaDispatchSequence, isQuotaDispatchCurrent, publishQuotaDispatch } from "./pool-quota-probe";
25
+ import { MAIN_TERMINAL_AUTH_CODES, readMainAuthErrorCode, currentQuotaDispatchSequence, nextQuotaDispatchSequence, isQuotaDispatchCurrent, publishQuotaDispatch } from "./pool-quota-probe";
19
26
 
20
27
  /**
21
28
  * Last reset-credit count this process parsed for the main account, tagged with the
@@ -52,6 +59,14 @@ export function mainResetCreditsForCurrentIdentity(): number | undefined {
52
59
 
53
60
  export const MAIN_CACHE_TTL = 5 * 60_000;
54
61
 
62
+ function sharedMainQuotaPacingEnabled(config: OcxConfig): boolean {
63
+ const openai = config.providers[OPENAI_CODEX_PROVIDER_ID];
64
+ if (openai?.disabled === true || openai?.codexAccountMode === "direct") return false;
65
+ return (openai !== undefined && isCanonicalOpenAiForwardProvider(openai)
66
+ && providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, openai) === "pool")
67
+ || (config.codexAccounts ?? []).some(isSelectableCodexPoolAccount);
68
+ }
69
+
55
70
  /**
56
71
  * A WHAM 401 is not itself proof the local credential died. Upstream edges can
57
72
  * transiently reject a still-valid access token (region/anti-abuse/rotation
@@ -80,6 +95,8 @@ export interface MainResetQuotaProof {
80
95
 
81
96
  export interface MainAccountInfoFetchResult {
82
97
  info: MainAccountInfo;
98
+ /** Parsed ordinary info from a stale credential; callers must not display it as shared state. */
99
+ infoUnpublished?: true;
83
100
  resetRecoveryProof?: MainResetQuotaProof & { dispatchSequence: number };
84
101
  /** Ephemeral result of this attempt, omitted when no WHAM request was made. */
85
102
  quotaRefresh?: CodexQuotaRefreshOutcome;
@@ -91,22 +108,25 @@ export interface MainAccountInfoFetchResult {
91
108
  hasCredential: boolean;
92
109
  /** Main identity generation captured while the native-main claim was held. */
93
110
  identityGeneration?: number;
94
- /** Present only when this call freshly parsed a WHAM usage response. */
111
+ /** Freshly parsed usage from the current credential; stale ordinary return values are excluded. */
95
112
  freshQuota?: Omit<StoredAccountQuota, "updatedAt">;
96
- /** Present only when this call's WHAM response included `rate_limit_reset_credits.available_count`. */
113
+ /** Current-credential response's `rate_limit_reset_credits.available_count`, when present. */
97
114
  freshResetCredits?: number;
98
115
  }
99
116
 
100
117
  export interface MainAccountInfoSnapshot {
101
118
  info: MainAccountInfo;
119
+ /** Ordinary info that was never published and must not become provider quota. */
120
+ infoUnpublished?: true;
102
121
  mainIdentityGeneration: number;
103
122
  quotaRefresh?: CodexQuotaRefreshOutcome;
104
123
  }
105
124
 
106
- export async function fetchMainAccountInfoSnapshot(forceRefresh = false): Promise<MainAccountInfoSnapshot> {
107
- const result = await fetchMainAccountInfoAttempt(forceRefresh, 1);
125
+ export async function fetchMainAccountInfoSnapshot(forceRefresh = false, config?: OcxConfig): Promise<MainAccountInfoSnapshot> {
126
+ const result = await fetchMainAccountInfoAttempt(forceRefresh, 1, undefined, false, forceRefresh, false, config);
108
127
  return {
109
128
  info: result.info,
129
+ ...(result.infoUnpublished ? { infoUnpublished: true as const } : {}),
110
130
  ...(result.quotaRefresh && result.quotaRefreshGeneration !== undefined
111
131
  && isMainAccountIdentityGenerationLive(result.quotaRefreshGeneration)
112
132
  ? { quotaRefresh: result.quotaRefresh } : {}),
@@ -114,8 +134,8 @@ export async function fetchMainAccountInfoSnapshot(forceRefresh = false): Promis
114
134
  };
115
135
  }
116
136
 
117
- export async function fetchMainAccountInfo(forceRefresh = false): Promise<MainAccountInfo> {
118
- return (await fetchMainAccountInfoSnapshot(forceRefresh)).info;
137
+ export async function fetchMainAccountInfo(forceRefresh = false, config?: OcxConfig): Promise<MainAccountInfo> {
138
+ return (await fetchMainAccountInfoSnapshot(forceRefresh, config)).info;
119
139
  }
120
140
 
121
141
  export const EMPTY_MAIN_ACCOUNT_INFO: MainAccountInfo = { email: null, plan: null, quota: null };
@@ -125,12 +145,13 @@ export async function retryMainAccountInfoIfIdentityChanged(
125
145
  retriesRemaining: number,
126
146
  nativeMainLease: AdmissionLease,
127
147
  explicitRefresh: boolean,
148
+ paced: boolean,
128
149
  ): Promise<MainAccountInfoFetchResult | null> {
129
150
  const currentAccountId = getMainChatgptAccountId();
130
151
  if (currentAccountId === null || currentAccountId === requestAccountId) return null;
131
152
  reconcileMainCodexAccountRuntimeState();
132
153
  return retriesRemaining > 0
133
- ? fetchMainAccountInfoWhileOwned(true, retriesRemaining - 1, nativeMainLease, explicitRefresh)
154
+ ? fetchMainAccountInfoWhileOwned(true, retriesRemaining - 1, nativeMainLease, explicitRefresh, false, paced)
134
155
  : { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
135
156
  }
136
157
 
@@ -140,6 +161,8 @@ export async function fetchMainAccountInfoAttempt(
140
161
  existingNativeMainLease?: AdmissionLease,
141
162
  nativeMainSharedClaimHeld = false,
142
163
  explicitRefresh: boolean = forceRefresh,
164
+ postReset = false,
165
+ config?: OcxConfig,
143
166
  ): Promise<MainAccountInfoFetchResult> {
144
167
  const nativeMainLease = existingNativeMainLease ?? tryAcquireNativeMainProfileClaim();
145
168
  if (!nativeMainLease) {
@@ -151,8 +174,9 @@ export async function fetchMainAccountInfoAttempt(
151
174
  };
152
175
  }
153
176
  try {
177
+ const paced = sharedMainQuotaPacingEnabled(config ?? loadConfig());
154
178
  const operation = async () => ({
155
- ...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease, explicitRefresh),
179
+ ...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease, explicitRefresh, postReset, paced),
156
180
  identityGeneration: captureMainAccountIdentityGeneration(),
157
181
  });
158
182
  if (nativeMainSharedClaimHeld) return await operation();
@@ -174,6 +198,11 @@ export async function fetchMainAccountInfoAttempt(
174
198
  }
175
199
  }
176
200
 
201
+ /**
202
+ * Read native-main usage while ownership is held, publishing only current credential evidence.
203
+ * A replaced same-account bearer may return its parsed ordinary info without mutating shared
204
+ * state or supplying recovery proof. Conflicting identities and stale errors return cached info.
205
+ */
177
206
  export async function fetchMainAccountInfoWhileOwned(
178
207
  forceRefresh: boolean,
179
208
  retriesRemaining: number,
@@ -185,6 +214,9 @@ export async function fetchMainAccountInfoWhileOwned(
185
214
  * promoting a background poll into operator intent below.
186
215
  */
187
216
  explicitRefresh: boolean = forceRefresh,
217
+ /** Reset-credit consume needs a fresh post-spend observation, not an earlier read. */
218
+ postReset = false,
219
+ paced = true,
188
220
  ): Promise<MainAccountInfoFetchResult> {
189
221
  const writerGeneration = captureConfigGeneration();
190
222
  reconcileMainCodexAccountRuntimeState();
@@ -212,120 +244,164 @@ export async function fetchMainAccountInfoWhileOwned(
212
244
  ? observeMainQuotaCredential(tokens.access_token, tokens.account_id)
213
245
  : undefined;
214
246
  const mainQuotaCredentialGeneration = getMainQuotaCredentialGeneration();
247
+ /** A disk replacement may have no second probe to advance the credential generation. */
248
+ const credentialIsCurrent = (): boolean => {
249
+ const current = readCodexTokensResult(undefined, { bounded: true });
250
+ if (current.status !== "ok") return false;
251
+ const effectiveAccountId = extractAccountId(current.tokens.id_token, current.tokens.access_token)
252
+ ?? (current.tokens.account_id || null);
253
+ if (effectiveAccountId !== current.tokens.account_id || effectiveAccountId !== requestAccountId) return false;
254
+ observeMainQuotaCredential(current.tokens.access_token, current.tokens.account_id);
255
+ return mainQuotaWriter !== undefined
256
+ && isMainQuotaWriterLive(mainQuotaWriter)
257
+ && mainQuotaCredentialGeneration === getMainQuotaCredentialGeneration()
258
+ && matchesMainQuotaCredential(tokens.access_token, tokens.account_id);
259
+ };
215
260
  // Keep diagnostics separate from authentication and freshness policy. Never serialize errors.
216
261
  const quotaSignal = AbortSignal.timeout(WHAM_REQUEST_TIMEOUT_MS);
217
262
  let quotaPhase: "request" | "body" | "decode" | "publish" = "request";
218
263
  let quotaRefreshGeneration = captureMainAccountIdentityGeneration();
219
- try {
220
- const dispatchSequence = nextQuotaDispatchSequence();
221
- const resp = await fetch("https://chatgpt.com/backend-api/wham/usage", {
222
- headers: { Authorization: `Bearer ${tokens.access_token}`, "ChatGPT-Account-Id": tokens.account_id },
223
- signal: quotaSignal,
224
- });
225
- quotaPhase = "publish";
226
- if (!resp.ok) {
227
- const terminalAuthFailure = await isTerminalMainAuthResponse(resp, isMainAccountTokenVerifiablyLive());
228
- const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
264
+ const readState: { owner?: CodexUsageOwner<MainAccountInfoFetchResult>; usable: boolean } = { usable: false };
265
+ const read = async (): Promise<MainAccountInfoFetchResult> => {
266
+ try {
267
+ let dispatchSequence = 0;
268
+ const baseKey = `main:${writerGeneration}:${mainQuotaCredentialGeneration}`;
269
+ const resetEpoch = postReset ? `:post-reset:${currentQuotaDispatchSequence()}` : "";
270
+ const admission = await fetchCodexUsage<MainAccountInfoFetchResult>(
271
+ `${baseKey}${resetEpoch}`, {
272
+ headers: { Authorization: `Bearer ${tokens.access_token}`, "ChatGPT-Account-Id": tokens.account_id },
273
+ signal: quotaSignal,
274
+ }, () => { dispatchSequence = nextQuotaDispatchSequence(); },
275
+ paced ? { pacingKey: baseKey } : { unpaced: true });
276
+ if (!admission) return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
277
+ credentialChecked: true, hasCredential: true };
278
+ if (admission.kind === "joined") {
279
+ const current = credentialIsCurrent() && writerGeneration === captureConfigGeneration();
280
+ if (!current) return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
281
+ credentialChecked: true, hasCredential: true };
282
+ if (explicitRefresh && (admission.result.quotaRefresh?.status === "ok"
283
+ || admission.result.quotaRefresh?.status === "not_reported")) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
284
+ return admission.result;
285
+ }
286
+ readState.owner = admission;
287
+ const resp = admission.response;
288
+ quotaPhase = "publish";
289
+ if (!resp.ok) {
290
+ const terminalAuthFailure = await isTerminalMainAuthResponse(resp, isMainAccountTokenVerifiablyLive());
291
+ const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
292
+ if (retried) return retried;
293
+ if (!isQuotaDispatchCurrent(dispatchSequence) || !credentialIsCurrent()) {
294
+ return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
295
+ credentialChecked: true, hasCredential: true };
296
+ }
297
+ if (terminalAuthFailure) {
298
+ // Account for this attempt's own synchronous invalidation, never prior external drift.
299
+ const diagnosticStillLive = isMainAccountIdentityGenerationLive(quotaRefreshGeneration);
300
+ clearMainAccountInfoCache();
301
+ if (diagnosticStillLive) quotaRefreshGeneration = captureMainAccountIdentityGeneration();
302
+ markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
303
+ }
304
+ return {
305
+ info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true,
306
+ quotaRefresh: { status: "http_error", httpStatus: resp.status },
307
+ quotaRefreshGeneration,
308
+ };
309
+ }
310
+ quotaPhase = "body";
311
+ const data = (await resp.json()) as WhamUsageResponse;
312
+ quotaPhase = "publish";
313
+ const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
229
314
  if (retried) return retried;
315
+ quotaPhase = "decode";
316
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
317
+ throw new Error("Invalid WHAM usage object");
318
+ }
319
+ // A newer published response wins over this attempt, including its returned display info.
230
320
  if (!isQuotaDispatchCurrent(dispatchSequence)) {
231
321
  return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
232
322
  credentialChecked: true, hasCredential: true };
233
323
  }
234
- if (terminalAuthFailure) {
235
- // Account for this attempt's own synchronous invalidation, never prior external drift.
236
- const diagnosticStillLive = isMainAccountIdentityGenerationLive(quotaRefreshGeneration);
237
- clearMainAccountInfoCache();
238
- if (diagnosticStillLive) quotaRefreshGeneration = captureMainAccountIdentityGeneration();
239
- markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
324
+ quotaPhase = "publish";
325
+ // A delayed response from a replaced bearer cannot revoke a newer Reserve grant,
326
+ // even in the same workspace or after an A→B→A credential transition.
327
+ if (credentialIsCurrent()) {
328
+ observeMainReserveRevocation(data, mainQuotaWriter);
329
+ }
330
+ quotaPhase = "decode";
331
+ const plan = nonEmptyPlan(data.plan_type) ?? nonEmptyPlan(cached?.plan) ?? nonEmptyPlan(getMainAccountPlan());
332
+ const usage = { ...data, ...(plan ? { plan_type: plan } : {}) };
333
+ const quota = parseUsageQuota(usage);
334
+ const policyQuota = parseMainPolicyUsageQuota(usage);
335
+ quotaPhase = "publish";
336
+ const result = {
337
+ email: data.email ?? null,
338
+ plan,
339
+ quota,
340
+ ts: Date.now(),
341
+ };
342
+ if (!credentialIsCurrent()) {
343
+ // Preserve same-identity ordinary info, but never publish stale evidence or state.
344
+ if (mainQuotaWriter && isMainQuotaWriterLive(mainQuotaWriter)) {
345
+ return { info: result, infoUnpublished: true, credentialChecked: true, hasCredential: true };
346
+ }
347
+ return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
348
+ credentialChecked: true, hasCredential: true };
349
+ }
350
+ const freshResetCredits = quota?.resetCredits;
351
+ // Tag the count with the identity it was read from, so a later response that omits the
352
+ // summary can restore the badge without ever crossing an account boundary.
353
+ rememberMainResetCredits(requestAccountId, freshResetCredits);
354
+ setMainAccountInfoCache(result);
355
+ // Only an explicit refresh may retract a reauth quarantine. A 200 from
356
+ // /wham/usage proves the token authenticates to the usage endpoint; it does not
357
+ // prove the account can serve Responses traffic, which is a different backend path
358
+ // and still answers 403 for a workspace the token may no longer select (#327).
359
+ // Letting the background poll clear the flag put such an account straight back into
360
+ // rotation: the next request failed the same way and re-marked it, so needsReauth
361
+ // never settled and the dashboard kept showing nothing — the symptom #327 reported.
362
+ // An explicit refresh is an operator asking to re-evaluate, normally right after
363
+ // signing in again, so it stays authoritative.
364
+ if (explicitRefresh) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
365
+ // Mirror main quota + plan into the shared stores so the rotation engine can
366
+ // score and auto-switch the main account exactly like a pool account (Option A).
367
+ setMainAccountPlan(result.plan);
368
+ if (result.quota) {
369
+ setAccountQuotaFromParsed(MAIN_CODEX_ACCOUNT_ID, result.quota, writerGeneration, mainQuotaWriter, policyQuota);
240
370
  }
371
+ publishQuotaDispatch(dispatchSequence);
372
+ readState.usable = quota !== null;
373
+ return {
374
+ info: result,
375
+ quotaRefresh: { status: quota ? "ok" : "not_reported" },
376
+ quotaRefreshGeneration,
377
+ credentialChecked: true,
378
+ hasCredential: true,
379
+ ...(quota ? { freshQuota: quota } : {}),
380
+ ...(quota && mainQuotaWriter && credentialIsCurrent()
381
+ ? { resetRecoveryProof: { writer: mainQuotaWriter, credentialGeneration: mainQuotaCredentialGeneration, dispatchSequence } }
382
+ : {}),
383
+ ...(freshResetCredits !== undefined ? { freshResetCredits } : {}),
384
+ };
385
+ } catch (error) {
386
+ const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
387
+ if (retried) return retried;
388
+ if (!credentialIsCurrent()) {
389
+ return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
390
+ credentialChecked: true, hasCredential: true };
391
+ }
392
+ let status: CodexQuotaRefreshOutcome["status"] = "internal_error";
393
+ if ((quotaPhase === "request" || quotaPhase === "body") && quotaSignal.aborted) status = "timeout";
394
+ else if (quotaPhase === "request") status = "network_error";
395
+ else if (quotaPhase === "body") status = error instanceof SyntaxError ? "invalid_response" : "network_error";
396
+ else if (quotaPhase === "decode") status = "invalid_response";
241
397
  return {
242
398
  info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true,
243
- quotaRefresh: { status: "http_error", httpStatus: resp.status },
399
+ quotaRefresh: { status },
244
400
  quotaRefreshGeneration,
245
401
  };
246
402
  }
247
- quotaPhase = "body";
248
- const data = (await resp.json()) as WhamUsageResponse;
249
- quotaPhase = "publish";
250
- const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
251
- if (retried) return retried;
252
- quotaPhase = "decode";
253
- if (data === null || typeof data !== "object" || Array.isArray(data)) {
254
- throw new Error("Invalid WHAM usage object");
255
- }
256
- // Check after body/retry awaits and before any cache, credits, policy or
257
- // Reserve publication. Returning cached state supplies no fresh recovery proof.
258
- if (!isQuotaDispatchCurrent(dispatchSequence)) {
259
- return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
260
- credentialChecked: true, hasCredential: true };
261
- }
262
- quotaPhase = "publish";
263
- // A delayed response from a replaced bearer cannot revoke a newer Reserve grant,
264
- // even in the same workspace or after an A→B→A credential transition.
265
- if (mainQuotaCredentialGeneration === getMainQuotaCredentialGeneration()
266
- && matchesMainQuotaCredential(tokens.access_token, tokens.account_id)) {
267
- observeMainReserveRevocation(data, mainQuotaWriter);
268
- }
269
- quotaPhase = "decode";
270
- const plan = nonEmptyPlan(data.plan_type) ?? nonEmptyPlan(cached?.plan) ?? nonEmptyPlan(getMainAccountPlan());
271
- const usage = { ...data, ...(plan ? { plan_type: plan } : {}) };
272
- const quota = parseUsageQuota(usage);
273
- const policyQuota = parseMainPolicyUsageQuota(usage);
274
- quotaPhase = "publish";
275
- const freshResetCredits = quota?.resetCredits;
276
- // Tag the count with the identity it was read from, so a later response that omits the
277
- // summary can restore the badge without ever crossing an account boundary.
278
- rememberMainResetCredits(requestAccountId, freshResetCredits);
279
- const result = {
280
- email: data.email ?? null,
281
- plan,
282
- quota,
283
- ts: Date.now(),
284
- };
285
- setMainAccountInfoCache(result);
286
- // Only an explicit refresh may retract a reauth quarantine. A 200 from
287
- // /wham/usage proves the token authenticates to the usage endpoint; it does not
288
- // prove the account can serve Responses traffic, which is a different backend path
289
- // and still answers 403 for a workspace the token may no longer select (#327).
290
- // Letting the background poll clear the flag put such an account straight back into
291
- // rotation: the next request failed the same way and re-marked it, so needsReauth
292
- // never settled and the dashboard kept showing nothing — the symptom #327 reported.
293
- // An explicit refresh is an operator asking to re-evaluate, normally right after
294
- // signing in again, so it stays authoritative.
295
- if (explicitRefresh) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
296
- // Mirror main quota + plan into the shared stores so the rotation engine can
297
- // score and auto-switch the main account exactly like a pool account (Option A).
298
- setMainAccountPlan(result.plan);
299
- if (result.quota) {
300
- setAccountQuotaFromParsed(MAIN_CODEX_ACCOUNT_ID, result.quota, writerGeneration, mainQuotaWriter, policyQuota);
301
- }
302
- publishQuotaDispatch(dispatchSequence);
303
- return {
304
- info: result,
305
- quotaRefresh: { status: quota ? "ok" : "not_reported" },
306
- quotaRefreshGeneration,
307
- credentialChecked: true,
308
- hasCredential: true,
309
- ...(quota ? { freshQuota: quota } : {}),
310
- ...(quota && mainQuotaWriter && isMainQuotaWriterLive(mainQuotaWriter)
311
- && mainQuotaCredentialGeneration === getMainQuotaCredentialGeneration()
312
- && matchesMainQuotaCredential(tokens.access_token, tokens.account_id)
313
- ? { resetRecoveryProof: { writer: mainQuotaWriter, credentialGeneration: mainQuotaCredentialGeneration, dispatchSequence } }
314
- : {}),
315
- ...(freshResetCredits !== undefined ? { freshResetCredits } : {}),
316
- };
317
- } catch (error) {
318
- const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
319
- if (retried) return retried;
320
- let status: CodexQuotaRefreshOutcome["status"] = "internal_error";
321
- if ((quotaPhase === "request" || quotaPhase === "body") && quotaSignal.aborted) status = "timeout";
322
- else if (quotaPhase === "request") status = "network_error";
323
- else if (quotaPhase === "body") status = error instanceof SyntaxError ? "invalid_response" : "network_error";
324
- else if (quotaPhase === "decode") status = "invalid_response";
325
- return {
326
- info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true,
327
- quotaRefresh: { status },
328
- quotaRefreshGeneration,
329
- };
330
- }
403
+ };
404
+ let outcome: MainAccountInfoFetchResult | undefined;
405
+ try { outcome = await read(); return outcome; }
406
+ finally { readState.owner?.settle(readState.usable, outcome); }
331
407
  }
@@ -1,3 +1,4 @@
1
+ import { nextCodexUsageQueryAt, nextQuotaQueryDelay, pruneRemovedCodexPoolUsageAccounts } from "../quota-query-backoff";
1
2
  import { CODEX_PRIORITY_FAILBACK_REFRESH_MS } from "../account-priority";
2
3
  import { codexQuotaHasFreshUsage } from "../quota-observation-freshness";
3
4
  import { getCodexAccountCredential, getValidCodexToken, readCodexAccountRecord } from "../account-store";
@@ -8,7 +9,7 @@ import { readCodexTokens } from "../auth-collision";
8
9
  import { isAccountNeedsReauth, markAccountNeedsReauth } from "../account-runtime-state";
9
10
  import { getValidMainAccountToken, MainAccountTokenRefreshError, MAIN_CODEX_ACCOUNT_ID, getMainAccountPlan } from "../main-account";
10
11
  import { captureConfigGeneration, registerStateSweepAfterTick } from "../../lib/state-store-sweeper";
11
- import { captureMainAccountIdentityGeneration, isMainAccountIdentityGenerationLive } from "../main-account-cache";
12
+ import { observeMainQuotaCredential, getMainQuotaCredentialGeneration, captureMainAccountIdentityGeneration, isMainAccountIdentityGenerationLive } from "../main-account-cache";
12
13
  import { getMainAccountHardLockStatus } from "../main-account-hard-lock";
13
14
  import type { OcxConfig } from "../../types";
14
15
  import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
@@ -52,7 +53,8 @@ export async function runCodexCooldownRecoveryProbes(config: OcxConfig, now = Da
52
53
  return;
53
54
  }
54
55
  try {
55
- const result = await fetchPoolAccountQuota(claim.accountId, true, account.plan);
56
+ const result = await fetchPoolAccountQuota(claim.accountId, true, account.plan,
57
+ undefined, false, undefined, now);
56
58
  // Defence in depth: independent scopes are already excluded at the claim site.
57
59
  // Generic WHAM must never clear Reserve even if claim selection changes.
58
60
  const recovered = (claim.scope === undefined || claim.scope === "shared")
@@ -71,12 +73,26 @@ export async function runCodexCooldownRecoveryProbes(config: OcxConfig, now = Da
71
73
  }
72
74
 
73
75
  let mainHardLockRecoveryInFlight: Promise<void> | null = null;
76
+ let mainHardLockRecoveryAttempt: { identity: number; credential: number; after: number; delay: number } | undefined;
74
77
 
75
78
  /** Metadata-only recovery on the existing sweep; failures retain the observed policy block. */
76
79
  export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise<void> {
77
80
  if (mainHardLockRecoveryInFlight) return mainHardLockRecoveryInFlight;
78
- if (getMainAccountHardLockStatus(config).state !== "blocked"
79
- || isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
81
+ const status = getMainAccountHardLockStatus(config);
82
+ if (status.state !== "blocked") { mainHardLockRecoveryAttempt = undefined; return; }
83
+ if (isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
84
+ // A predicted reset is a scheduling hint, never proof that the hard lock can be lifted.
85
+ if (status.resetAt !== undefined && status.resetAt > Date.now()) return;
86
+ // Reconcile the owned physical credential before skipping work for an older one.
87
+ reconcileMainCodexAccountRuntimeState();
88
+ const physical = readCodexTokens();
89
+ if (physical) observeMainQuotaCredential(physical.access_token, physical.account_id);
90
+ const identity = captureMainAccountIdentityGeneration();
91
+ const credential = getMainQuotaCredentialGeneration();
92
+ const previous = mainHardLockRecoveryAttempt;
93
+ const queryAfter = nextCodexUsageQueryAt(`main:${captureConfigGeneration()}:${credential}`) ?? 0;
94
+ if ((previous?.identity === identity && previous.credential === credential
95
+ && previous.after > Date.now()) || queryAfter > Date.now()) return;
80
96
  const lease = tryAcquireNativeMainProfileClaim();
81
97
  if (!lease) return;
82
98
  mainHardLockRecoveryInFlight = (async () => {
@@ -88,7 +104,9 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
88
104
  try {
89
105
  // Refresh can require an exclusive credential claim: never hold WHAM's shared
90
106
  // claim while obtaining a valid token. The runtime lease spans both operations.
91
- if (!await getValidMainAccountToken({ preserveReauth: true })) return;
107
+ const prepared = await getValidMainAccountToken({ preserveReauth: true });
108
+ if (!prepared) return;
109
+ observeMainQuotaCredential(prepared.accessToken, prepared.chatgptAccountId);
92
110
  } catch (error) {
93
111
  if (error instanceof MainAccountTokenRefreshError && error.reason === "reauth"
94
112
  && isMainAccountIdentityGenerationLive(identityGeneration)) {
@@ -97,7 +115,41 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
97
115
  return;
98
116
  }
99
117
  if (isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
100
- await fetchMainAccountInfoAttempt(true, 1, lease, false, false);
118
+ const credential = getMainQuotaCredentialGeneration();
119
+ // Reconcile the physical bearer before consulting the previous credential's delay.
120
+ if (previous?.identity === identityGeneration && previous.credential === credential
121
+ && previous.after > Date.now()) return;
122
+ const currentQueryAfter = nextCodexUsageQueryAt(`main:${captureConfigGeneration()}:${credential}`) ?? 0;
123
+ if (currentQueryAfter > Date.now()) {
124
+ mainHardLockRecoveryAttempt = { identity: identityGeneration, credential,
125
+ delay: previous?.identity === identityGeneration && previous.credential === credential ? previous.delay : 0,
126
+ after: currentQueryAfter };
127
+ return;
128
+ }
129
+ const result = await fetchMainAccountInfoAttempt(true, 1, lease, false, false, false, config);
130
+ // Never charge a replacement credential for a late result from its predecessor.
131
+ if (isMainAccountIdentityGenerationLive(identityGeneration)
132
+ && credential === getMainQuotaCredentialGeneration()) {
133
+ const queryAfter = nextCodexUsageQueryAt(`main:${captureConfigGeneration()}:${credential}`) ?? 0;
134
+ const previousDelay = previous?.identity === identityGeneration && previous.credential === credential
135
+ ? previous.delay : 0;
136
+ if (result.quotaRefresh) {
137
+ const authStatus = result.quotaRefresh.status === "http_error"
138
+ ? result.quotaRefresh.httpStatus : undefined;
139
+ const transientAuth = (authStatus === 401 || authStatus === 403)
140
+ && !isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
141
+ if (transientAuth) mainHardLockRecoveryAttempt = undefined;
142
+ else {
143
+ const delay = nextQuotaQueryDelay(previousDelay || undefined);
144
+ mainHardLockRecoveryAttempt = getMainAccountHardLockStatus(config).state === "blocked"
145
+ ? { identity: identityGeneration, credential, delay, after: Math.max(Date.now() + delay, queryAfter) }
146
+ : undefined;
147
+ }
148
+ } else if (queryAfter > Date.now()) {
149
+ mainHardLockRecoveryAttempt = { identity: identityGeneration, credential,
150
+ delay: previousDelay, after: queryAfter };
151
+ }
152
+ }
101
153
  })().catch(() => {
102
154
  // Best-effort background metadata read; no cooldown/pause or policy clearing on failure.
103
155
  }).finally(() => {
@@ -169,6 +221,7 @@ export async function primeCodexPoolQuotas(
169
221
  // retry the restored credential is entitled to.
170
222
  const runtimeConfig = getRuntimeConfig(config);
171
223
  const configuredPoolIds = new Set((runtimeConfig.codexAccounts ?? []).map(account => account.id));
224
+ pruneRemovedCodexPoolUsageAccounts(configuredPoolIds);
172
225
  for (const accountId of poolQuotaPrimeAttemptedAt.keys()) {
173
226
  if (!configuredPoolIds.has(accountId)) poolQuotaPrimeAttemptedAt.delete(accountId);
174
227
  }
@@ -220,7 +273,8 @@ export async function primeCodexPoolQuotas(
220
273
  const bypassCachedQuota = !!observationStale;
221
274
  if (options.fetchMainInfo) await options.fetchMainInfo(bypassCachedQuota);
222
275
  // Cache bypass is passive observation, never an explicit reauthentication recovery.
223
- else await fetchMainAccountInfoAttempt(bypassCachedQuota, 1, mainLease, true, false);
276
+ else await fetchMainAccountInfoAttempt(bypassCachedQuota, 1, mainLease, true,
277
+ false, false, config);
224
278
  });
225
279
  } catch (error) {
226
280
  if (!isNativeMainClaimUnavailable(error)) throw error;