@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,5 @@
1
+ import { fetchCodexUsage } from "../quota-query-backoff";
2
+ import type { CodexUsageOwner } from "../quota-query-backoff";
1
3
  import { capturePoolQuotaWriter, getValidCodexToken, isCodexAccountGenerationLive, forceRefreshCodexPoolToken, markCodexAccountValidated, markCodexAccountValidationFailed, readCodexAccountRecord, isTerminalCodexPoolRefreshFailure, CodexCredentialGenerationConflictError, CodexCredentialRefreshLockTimeoutError, CodexCredentialRefreshBusyError, CodexCredentialRefreshStaleError, TokenRefreshError } from "../account-store";
2
4
  import type { PoolQuotaWriter } from "../quota-types";
3
5
  import { isValidWhamHistoryObservation, getAccountQuota, isCompleteCodexQuotaRecoverySnapshot, parseUsageQuota, setAccountQuotaFromParsed } from "../quota";
@@ -101,7 +103,9 @@ export function withQuotaProbeEvidence(
101
103
  result: PoolQuotaResult,
102
104
  evidence: PoolQuotaProbeEvidence,
103
105
  ): PoolQuotaResult {
104
- return evidence.attempted ? { ...result, quotaProbeAttempted: evidence.attempted } : result;
106
+ return evidence.attempted
107
+ && (result.quotaProbeAttempted?.dispatchSequence ?? 0) < evidence.attempted.dispatchSequence
108
+ ? { ...result, quotaProbeAttempted: evidence.attempted } : result;
105
109
  }
106
110
 
107
111
  export interface PoolQuotaRefreshFlight {
@@ -167,6 +171,7 @@ export async function recoverPoolQuotaFrom401(ctx: {
167
171
  resp: Response;
168
172
  quotaProbeEvidence: PoolQuotaProbeEvidence;
169
173
  onCredentialGeneration?: (generation: number) => void;
174
+ recoveryProbeNow?: number;
170
175
  }): Promise<PoolQuotaResult> {
171
176
  const { accountId, existing, configuredPlan, rejectedAccessToken, rejectedGeneration, resp } = ctx;
172
177
 
@@ -252,37 +257,61 @@ export async function recoverPoolQuotaFrom401(ctx: {
252
257
  ctx.onCredentialGeneration?.(refreshed.generation);
253
258
 
254
259
  const writerGeneration = captureConfigGeneration();
255
- markQuotaProbeAttempted(ctx.quotaProbeEvidence, refreshed.generation);
256
260
  const poolWriter = capturePoolQuotaWriter(accountId, refreshed);
257
- const replay = await fetch("https://chatgpt.com/backend-api/wham/usage", {
261
+ const replayRead = await fetchCodexUsage<PoolQuotaResult>(`pool:${accountId}:${writerGeneration}:${refreshed.generation}`, {
258
262
  headers: {
259
263
  Authorization: `Bearer ${refreshed.accessToken}`,
260
264
  "ChatGPT-Account-Id": refreshed.chatgptAccountId,
261
265
  },
262
266
  signal: AbortSignal.timeout(WHAM_REQUEST_TIMEOUT_MS),
263
- });
264
- if (!replay.ok) {
265
- if (replay.status === 401 && await isTerminalPoolAuthResponse(replay)) {
266
- // The refresh already settled this claim non-terminally, so the record alone would
267
- // let the next poll call a dead credential healthy. The evidence is about the
268
- // REFRESHED credential, which is what the replay used.
269
- markAccountNeedsReauth(accountId, writerGeneration, refreshed.generation);
270
- return { quota: existing ?? null, needsReauth: true, reauthReason: "quota_unauthorized", credentialGeneration: refreshed.generation };
267
+ }, () => markQuotaProbeAttempted(ctx.quotaProbeEvidence, refreshed.generation),
268
+ ctx.recoveryProbeNow === undefined ? { poolAccountId: accountId }
269
+ : { poolAccountId: accountId, recoveryProbe: true, now: () => ctx.recoveryProbeNow! });
270
+ if (!replayRead) return { quota: existing ?? null, needsReauth: false, credentialGeneration: refreshed.generation, quotaProbeSkipped: true };
271
+ if (replayRead.kind === "joined") {
272
+ if (!isCodexAccountGenerationLive(accountId, refreshed.generation)
273
+ || ctx.quotaProbeEvidence.mayPublish?.() === false) {
274
+ return { quota: getAccountQuota(accountId), needsReauth: false, credentialGeneration: refreshed.generation };
271
275
  }
272
- return { quota: existing ?? null, needsReauth: false, credentialGeneration: refreshed.generation };
276
+ const joined = replayRead.result;
277
+ return joined.freshCredentialGeneration === refreshed.generation ? {
278
+ ...joined,
279
+ resetRefreshLineage: {
280
+ fromGeneration: rejectedGeneration, toGeneration: refreshed.generation,
281
+ provenance: refreshed.provenance,
282
+ },
283
+ } : joined;
273
284
  }
274
- const result = await commitPoolQuotaResponse(replay, {
275
- accountId, existing, configuredPlan, generation: refreshed.generation, writerGeneration, poolWriter,
276
- mayPublish: ctx.quotaProbeEvidence.mayPublish,
277
- });
278
- return result.freshCredentialGeneration === refreshed.generation ? {
279
- ...result,
280
- resetRefreshLineage: {
281
- fromGeneration: rejectedGeneration,
282
- toGeneration: refreshed.generation,
283
- provenance: refreshed.provenance,
284
- },
285
- } : result;
285
+ let usableReplay = false;
286
+ const finishReplay = async (): Promise<PoolQuotaResult> => {
287
+ const replay = replayRead.response;
288
+ if (!replay.ok) {
289
+ if (replay.status === 401 && await isTerminalPoolAuthResponse(replay)) {
290
+ // The refresh already settled this claim non-terminally, so the record alone would
291
+ // let the next poll call a dead credential healthy. The evidence is about the
292
+ // REFRESHED credential, which is what the replay used.
293
+ markAccountNeedsReauth(accountId, writerGeneration, refreshed.generation);
294
+ return { quota: existing ?? null, needsReauth: true, reauthReason: "quota_unauthorized", credentialGeneration: refreshed.generation };
295
+ }
296
+ return { quota: existing ?? null, needsReauth: false, credentialGeneration: refreshed.generation };
297
+ }
298
+ const result = await commitPoolQuotaResponse(replay, {
299
+ accountId, existing, configuredPlan, generation: refreshed.generation, writerGeneration, poolWriter,
300
+ mayPublish: ctx.quotaProbeEvidence.mayPublish,
301
+ });
302
+ usableReplay = result.freshQuota !== undefined;
303
+ return result.freshCredentialGeneration === refreshed.generation ? {
304
+ ...result,
305
+ resetRefreshLineage: {
306
+ fromGeneration: rejectedGeneration,
307
+ toGeneration: refreshed.generation,
308
+ provenance: refreshed.provenance,
309
+ },
310
+ } : result;
311
+ };
312
+ let replayOutcome: PoolQuotaResult | undefined;
313
+ try { replayOutcome = await finishReplay(); return replayOutcome; }
314
+ finally { replayRead.settle(usableReplay, replayOutcome); }
286
315
  }
287
316
 
288
317
  /** Backoff after a refresh failure that proved nothing about the credential. */
@@ -338,8 +367,9 @@ export async function commitPoolQuotaResponse(
338
367
  if (!isCodexAccountGenerationLive(accountId, generation)) {
339
368
  return { quota: null, needsReauth: false, credentialGeneration: generation };
340
369
  }
341
- setAccountQuotaFromParsed(accountId, quota, writerGeneration, undefined, quota,
342
- ctx.poolWriter && isValidWhamHistoryObservation(data) ? { writer: ctx.poolWriter, observedAt, source: "wham", raw: quota } : undefined);
370
+ const validPolicyObservation = isValidWhamHistoryObservation(data);
371
+ setAccountQuotaFromParsed(accountId, quota, writerGeneration, undefined, validPolicyObservation ? quota : null,
372
+ ctx.poolWriter && validPolicyObservation ? { writer: ctx.poolWriter, observedAt, source: "wham", raw: quota } : undefined);
343
373
  return {
344
374
  quota: getAccountQuota(accountId),
345
375
  needsReauth: false,
@@ -358,81 +388,100 @@ export async function fetchFreshPoolAccountQuota(
358
388
  onCredentialGeneration?: (generation: number) => void,
359
389
  getValidToken: typeof getValidCodexToken = getValidCodexToken,
360
390
  quotaProbeEvidence: PoolQuotaProbeEvidence = {},
391
+ afterDispatchSequence?: number,
392
+ recoveryProbeNow?: number,
361
393
  ): Promise<PoolQuotaResult> {
362
394
  const writerGeneration = captureConfigGeneration();
363
395
  let requestCredentialGeneration = readCodexAccountRecord(accountId)?.generation;
364
- try {
365
- const { accessToken, chatgptAccountId, generation } = await getValidToken(accountId);
366
- const poolWriter = capturePoolQuotaWriter(accountId, { accessToken, chatgptAccountId, generation });
367
- requestCredentialGeneration = generation;
368
- onCredentialGeneration?.(generation);
369
- markQuotaProbeAttempted(quotaProbeEvidence, generation);
370
- const resp = await fetch("https://chatgpt.com/backend-api/wham/usage", {
371
- headers: { Authorization: `Bearer ${accessToken}`, "ChatGPT-Account-Id": chatgptAccountId },
372
- signal: AbortSignal.timeout(8000),
373
- });
374
- if (!resp.ok) {
375
- if (resp.status !== 401) {
376
- return withQuotaProbeEvidence(
377
- { quota: existing ?? null, needsReauth: false, credentialGeneration: generation },
396
+ const readState: { owner?: CodexUsageOwner<PoolQuotaResult>; usable: boolean } = { usable: false };
397
+ const read = async (): Promise<PoolQuotaResult> => {
398
+ try {
399
+ const { accessToken, chatgptAccountId, generation } = await getValidToken(accountId);
400
+ const poolWriter = capturePoolQuotaWriter(accountId, { accessToken, chatgptAccountId, generation });
401
+ requestCredentialGeneration = generation;
402
+ onCredentialGeneration?.(generation);
403
+ const admission = await fetchCodexUsage<PoolQuotaResult>(
404
+ `pool:${accountId}:${writerGeneration}:${generation}${afterDispatchSequence === undefined ? "" : `:post-reset:${afterDispatchSequence}`}`, {
405
+ headers: { Authorization: `Bearer ${accessToken}`, "ChatGPT-Account-Id": chatgptAccountId },
406
+ signal: AbortSignal.timeout(8000),
407
+ }, () => markQuotaProbeAttempted(quotaProbeEvidence, generation),
408
+ recoveryProbeNow === undefined ? { poolAccountId: accountId }
409
+ : { poolAccountId: accountId, recoveryProbe: true, now: () => recoveryProbeNow });
410
+ if (!admission) return { quota: existing ?? null, needsReauth: false, credentialGeneration: generation, quotaProbeSkipped: true };
411
+ if (admission.kind === "joined") return isCodexAccountGenerationLive(accountId, generation)
412
+ && quotaProbeEvidence.mayPublish?.() !== false
413
+ ? admission.result
414
+ : { quota: getAccountQuota(accountId), needsReauth: false, credentialGeneration: generation };
415
+ readState.owner = admission;
416
+ const resp = admission.response;
417
+ if (!resp.ok) {
418
+ if (resp.status !== 401) {
419
+ return withQuotaProbeEvidence(
420
+ { quota: existing ?? null, needsReauth: false, credentialGeneration: generation },
421
+ quotaProbeEvidence,
422
+ );
423
+ }
424
+ // A bare 401 is what a stale-but-refreshable bearer produces after a plan change, so
425
+ // quarantining on it tells the operator to re-authenticate an account that was fine
426
+ // (#3019). Refresh once, replay once, and only then decide.
427
+ const recovered = await recoverPoolQuotaFrom401({
428
+ accountId,
429
+ existing,
430
+ configuredPlan,
431
+ rejectedAccessToken: accessToken,
432
+ rejectedGeneration: generation,
433
+ resp,
378
434
  quotaProbeEvidence,
379
- );
435
+ onCredentialGeneration,
436
+ recoveryProbeNow,
437
+ });
438
+ return withQuotaProbeEvidence(recovered, quotaProbeEvidence);
380
439
  }
381
- // A bare 401 is what a stale-but-refreshable bearer produces after a plan change, so
382
- // quarantining on it tells the operator to re-authenticate an account that was fine
383
- // (#3019). Refresh once, replay once, and only then decide.
384
- const recovered = await recoverPoolQuotaFrom401({
385
- accountId,
386
- existing,
387
- configuredPlan,
388
- rejectedAccessToken: accessToken,
389
- rejectedGeneration: generation,
390
- resp,
391
- quotaProbeEvidence,
392
- onCredentialGeneration,
440
+ const committed = await commitPoolQuotaResponse(resp, {
441
+ accountId, existing, configuredPlan, generation, writerGeneration, poolWriter,
442
+ mayPublish: quotaProbeEvidence.mayPublish,
393
443
  });
394
- return withQuotaProbeEvidence(recovered, quotaProbeEvidence);
395
- }
396
- const committed = await commitPoolQuotaResponse(resp, {
397
- accountId, existing, configuredPlan, generation, writerGeneration, poolWriter,
398
- mayPublish: quotaProbeEvidence.mayPublish,
399
- });
400
- return withQuotaProbeEvidence(committed, quotaProbeEvidence);
401
- } catch (e) {
402
- if (e instanceof CodexCredentialGenerationConflictError || e instanceof CodexCredentialRefreshLockTimeoutError
403
- || e instanceof CodexCredentialRefreshBusyError || e instanceof CodexCredentialRefreshStaleError) {
404
- return withQuotaProbeEvidence({
405
- quota: existing ?? null,
406
- needsReauth: false,
407
- credentialGeneration: requestCredentialGeneration,
408
- quotaProbeSkipped: true,
409
- }, quotaProbeEvidence);
410
- }
411
- // Terminal means the grant itself is dead or missing; an `unknown` refresh failure (a
412
- // token-endpoint 5xx, a transport blip) may clear, so it reports transient like the
413
- // 401-recovery path instead of quarantining a healthy account (#2887). A revoked or
414
- // expired grant is also written to the record as a terminal validation failure, the same
415
- // verdict the token guardian persists: the in-memory mark dies with this process, and
416
- // the stored verdict is what keeps a cached listing from calling the dead grant healthy
417
- // after a restart.
418
- if (isTerminalCodexPoolRefreshFailure(e)) {
419
- if (e instanceof TokenRefreshError && isTerminalRefreshError(e)) {
420
- markCodexAccountValidationFailed(accountId, `refresh_${e.reason}`, {
421
- expectedGeneration: requestCredentialGeneration,
422
- terminal: true,
423
- });
444
+ readState.usable = committed.freshQuota !== undefined;
445
+ return withQuotaProbeEvidence(committed, quotaProbeEvidence);
446
+ } catch (e) {
447
+ if (e instanceof CodexCredentialGenerationConflictError || e instanceof CodexCredentialRefreshLockTimeoutError
448
+ || e instanceof CodexCredentialRefreshBusyError || e instanceof CodexCredentialRefreshStaleError) {
449
+ return withQuotaProbeEvidence({
450
+ quota: existing ?? null,
451
+ needsReauth: false,
452
+ credentialGeneration: requestCredentialGeneration,
453
+ quotaProbeSkipped: true,
454
+ }, quotaProbeEvidence);
455
+ }
456
+ // Terminal means the grant itself is dead or missing; an `unknown` refresh failure (a
457
+ // token-endpoint 5xx, a transport blip) may clear, so it reports transient like the
458
+ // 401-recovery path instead of quarantining a healthy account (#2887). A revoked or
459
+ // expired grant is also written to the record as a terminal validation failure, the same
460
+ // verdict the token guardian persists: the in-memory mark dies with this process, and
461
+ // the stored verdict is what keeps a cached listing from calling the dead grant healthy
462
+ // after a restart.
463
+ if (isTerminalCodexPoolRefreshFailure(e)) {
464
+ if (e instanceof TokenRefreshError && isTerminalRefreshError(e)) {
465
+ markCodexAccountValidationFailed(accountId, `refresh_${e.reason}`, {
466
+ expectedGeneration: requestCredentialGeneration,
467
+ terminal: true,
468
+ });
469
+ }
470
+ markAccountNeedsReauth(accountId, captureConfigGeneration(), requestCredentialGeneration);
471
+ return withQuotaProbeEvidence(
472
+ { quota: existing ?? null, needsReauth: true, reauthReason: "refresh_failed", credentialGeneration: requestCredentialGeneration },
473
+ quotaProbeEvidence,
474
+ );
424
475
  }
425
- markAccountNeedsReauth(accountId, captureConfigGeneration(), requestCredentialGeneration);
426
476
  return withQuotaProbeEvidence(
427
- { quota: existing ?? null, needsReauth: true, reauthReason: "refresh_failed", credentialGeneration: requestCredentialGeneration },
477
+ { quota: existing ?? null, needsReauth: false, credentialGeneration: requestCredentialGeneration },
428
478
  quotaProbeEvidence,
429
479
  );
430
480
  }
431
- return withQuotaProbeEvidence(
432
- { quota: existing ?? null, needsReauth: false, credentialGeneration: requestCredentialGeneration },
433
- quotaProbeEvidence,
434
- );
435
- }
481
+ };
482
+ let outcome: PoolQuotaResult | undefined;
483
+ try { outcome = await read(); return outcome; }
484
+ finally { readState.owner?.settle(readState.usable, outcome); }
436
485
  }
437
486
 
438
487
  export async function fetchPoolAccountQuota(
@@ -442,6 +491,7 @@ export async function fetchPoolAccountQuota(
442
491
  getValidToken: typeof getValidCodexToken = getValidCodexToken,
443
492
  validatePending = false,
444
493
  afterDispatchSequence?: number,
494
+ recoveryProbeNow?: number,
445
495
  ): Promise<PoolQuotaResult> {
446
496
  const existing = getAccountQuota(accountId);
447
497
  if (afterDispatchSequence === undefined && !forceRefresh && existing && Date.now() - existing.updatedAt < POOL_CACHE_TTL) {
@@ -489,6 +539,8 @@ export async function fetchPoolAccountQuota(
489
539
  onDispatch: sequence => { state.dispatchSequence = sequence; },
490
540
  mayPublish: () => state.superseded !== true,
491
541
  },
542
+ afterDispatchSequence,
543
+ recoveryProbeNow,
492
544
  ).then(async result => {
493
545
  // A passive flight has consumed its validation decision. Remove it before
494
546
  // promise settlement queues other continuations, so a late explicit caller
@@ -225,7 +225,7 @@ async function refreshAfterManualReset(
225
225
  if (!manualResetAuthStillLive(accountId, auth)) return undefined;
226
226
  if (auth.isMain) {
227
227
  const result = await fetchMainAccountInfoAttempt(true, 1, auth.nativeMainLease,
228
- auth.nativeMainSharedClaimHeld === true, false);
228
+ auth.nativeMainSharedClaimHeld === true, false, didReset, config);
229
229
  const proof = result.resetRecoveryProof;
230
230
  const recovered = didReset && manualResetAuthStillLive(accountId, auth)
231
231
  && !!proof && !!auth.mainProof
@@ -531,6 +531,7 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco
531
531
  defaultModel: prov.defaultModel ?? null,
532
532
  ctx: prov.contextWindow ?? null,
533
533
  ctxW: prov.modelContextWindows ?? null,
534
+ ctxTier: prov.modelContextTiers ?? null,
534
535
  maxIn: prov.modelMaxInputTokens ?? null,
535
536
  maxOut: prov.modelMaxOutputTokens ?? null,
536
537
  autoCompact: prov.modelAutoCompactTokenLimits ?? null,
@@ -45,6 +45,7 @@ import type { FastPolicyAuthority } from "../../providers/fastwire";
45
45
  import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../../providers/registry";
46
46
  import { parseAntigravityAvailableModels, registerAntigravityDiscoveredWireModels } from "../../providers/antigravity-models";
47
47
  import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap";
48
+ import { githubCopilotCatalogContextWindow } from "../../providers/github-copilot-context";
48
49
  import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
49
50
  import { effectiveModelAliases } from "../../providers/default-aliases";
50
51
  import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec";
@@ -310,9 +311,8 @@ export function applyProviderConfigHints(
310
311
  ...modelWithoutServiceTier
311
312
  } = model;
312
313
  // 已发现窗口只允许被配置值压低;缺窗口时,已开的 Context cap 就是实际窗口。
313
- const discoveredWindow = typeof model.contextWindow === "number" && model.contextWindow > 0
314
- ? model.contextWindow
315
- : undefined;
314
+ const discoveredWindow = githubCopilotCatalogContextWindow(name, prov, model.id,
315
+ typeof model.contextWindow === "number" && model.contextWindow > 0 ? model.contextWindow : undefined);
316
316
  const projectedLimits = clampObservedModelLimits(staticPolicy.model, {
317
317
  ...(discoveredWindow !== undefined ? { contextWindow: discoveredWindow } : {}),
318
318
  ...(typeof model.maxInputTokens === "number" && model.maxInputTokens > 0 ? { maxInputTokens: model.maxInputTokens } : {}),
@@ -424,6 +424,8 @@ export async function fetchProviderModelsWithAuth(
424
424
  // away, and every client that keys an effort control off this field —
425
425
  // the Pi-shaped exports — renders no control at all.
426
426
  ...(liveResult.efforts[id]?.length ? { reasoningEfforts: liveResult.efforts[id] } : {}),
427
+ // The family's effective enabled default, matching an unset effort at request time.
428
+ ...(liveResult.defaultEfforts[id] ? { defaultReasoningEffort: liveResult.defaultEfforts[id] } : {}),
427
429
  // The account catalog's per-base supportsImages vote collapses to one
428
430
  // modalities value. It spreads before the hints so exact
429
431
  // modelCapabilities declarations, the legacy modelInputModalities
@@ -0,0 +1,28 @@
1
+ /** Bounded event projection owned by one live low-quota registration. */
2
+ export type LowQuotaEvent = {
3
+ accountId: string;
4
+ window: "short" | "weekly";
5
+ percentUsed: number;
6
+ resetAt: number | null;
7
+ timestamp: number;
8
+ status: "pending" | "logged" | "delivered" | "succeeded" | "failed" | "cancelled";
9
+ delivery: "notice" | "pause-save";
10
+ };
11
+
12
+ const CAPACITY = 100;
13
+
14
+ export function createLowQuotaEventLedger(): {
15
+ publish(event: LowQuotaEvent): void;
16
+ list(limit?: number): LowQuotaEvent[];
17
+ } {
18
+ const events: LowQuotaEvent[] = [];
19
+ return {
20
+ publish(event) {
21
+ events.unshift({ ...event });
22
+ if (events.length > CAPACITY) events.length = CAPACITY;
23
+ },
24
+ list(limit = 20) {
25
+ return events.slice(0, Math.max(0, Math.min(CAPACITY, limit))).map(event => ({ ...event }));
26
+ },
27
+ };
28
+ }
@@ -0,0 +1,23 @@
1
+ import type { StoredAccountQuota } from "./quota-types";
2
+
3
+ type Observer = (accountId: string, quota: Omit<StoredAccountQuota, "updatedAt">) => void;
4
+ const observers = new Map<symbol, Observer>();
5
+
6
+ /** The composition root owns the live config; quota writers know only this synchronous slot. */
7
+ export function registerLowQuotaObserver(observer: Observer): () => void {
8
+ const owner = Symbol();
9
+ observers.set(owner, observer);
10
+ return () => { observers.delete(owner); };
11
+ }
12
+
13
+ /** Only newly accepted evidence belongs here, never carried or disk-hydrated windows. */
14
+ export function observeCodexLowQuota(accountId: string, quota: Omit<StoredAccountQuota, "updatedAt">): void {
15
+ for (const observer of observers.values()) {
16
+ try {
17
+ observer(accountId, quota);
18
+ } catch {
19
+ // One server's optional policy cannot suppress another server's observation.
20
+ console.warn("[codex-low-quota] protection action failed");
21
+ }
22
+ }
23
+ }