@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005
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.
- package/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-CppMz-6w.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
- package/gui/dist/assets/index-DQt9srNi.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +62 -19
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +33 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +37 -9
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +29 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1190
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +35 -9
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +109 -0
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +55 -82
- package/src/cli/doctor.ts +22 -4
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +101 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +69 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +9 -1
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +116 -64
- package/src/cli/registry.ts +90 -24
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +42 -9
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +57 -80
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/tray-proxy.ts +3 -2
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +53 -67
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +60 -11
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +57 -16
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/main-account.ts +144 -11
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +36 -1
- package/src/codex/quota.ts +67 -16
- package/src/codex/refresh.ts +20 -9
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +145 -0
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +54 -107
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +101 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/jev.ts +13 -3
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +23 -1
- package/src/config/load-degrade.ts +7 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +43 -14
- package/src/config/schema/leaf-validators.ts +8 -3
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +645 -237
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +50 -5
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +106 -19
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +107 -35
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +20 -15
- package/src/oauth/anthropic.ts +34 -3
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +42 -35
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +29 -15
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +13 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +12 -134
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +69 -11
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +29 -27
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +152 -28
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +67 -30
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +64 -7
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +6 -0
- package/src/types/provider.ts +28 -10
- package/src/update/index.ts +2 -1
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
|
@@ -4,6 +4,7 @@ import { isThirtyDayOnlyCodexPlan } from "./plan";
|
|
|
4
4
|
import {
|
|
5
5
|
CODEX_EXHAUSTED_USAGE_PERCENT,
|
|
6
6
|
isTerminalShortWindow,
|
|
7
|
+
hasSpendableCodexCredits,
|
|
7
8
|
resetAtToMs,
|
|
8
9
|
type StoredAccountQuota,
|
|
9
10
|
} from "./quota-types";
|
|
@@ -14,7 +15,7 @@ import {
|
|
|
14
15
|
* Upstream does not refuse an account that holds ChatGPT credits at 100%: it serves the request
|
|
15
16
|
* and draws the balance. Selection only takes an account off on quota after a refusal, so an
|
|
16
17
|
* account with credits was never moved off. Spending is opt-in: only the ids in
|
|
17
|
-
* `creditCodexAccountIds` may keep serving from credits, and every other account leaves rotation
|
|
18
|
+
* `creditCodexAccountIds` may keep serving from fresh spendable credits, and every other account leaves rotation
|
|
18
19
|
* at 100% and comes back after its reset. Pool accounts and the `__main__` login both carry the
|
|
19
20
|
* switch; the main login is also checked where the main-account hard lock is.
|
|
20
21
|
*/
|
|
@@ -95,5 +96,6 @@ export function isCodexAccountHeldForCredits(
|
|
|
95
96
|
plan: unknown,
|
|
96
97
|
now: number,
|
|
97
98
|
): boolean {
|
|
98
|
-
return !codexAccountUsesCreditsAfterLimit(config, accountId) &&
|
|
99
|
+
return !(codexAccountUsesCreditsAfterLimit(config, accountId) && hasSpendableCodexCredits(quota, now))
|
|
100
|
+
&& isCodexUsageLimitReached(quota, plan, now);
|
|
99
101
|
}
|
|
@@ -18,6 +18,27 @@ const reauthAccounts = new Map<string, number | undefined>();
|
|
|
18
18
|
let lastReconciledGeneration = 0;
|
|
19
19
|
let liveAccountIds = new Set<string>();
|
|
20
20
|
|
|
21
|
+
// Refusal evidence is independent of the ordinary quarantine flag. Profile switches make a key
|
|
22
|
+
// inapplicable, not forgotten; the oldest of at most 64 records is evicted to bound process state.
|
|
23
|
+
const refusedMainRefreshGrants = new Set<string>();
|
|
24
|
+
const MAX_REFUSED_MAIN_REFRESH_GRANTS = 64;
|
|
25
|
+
|
|
26
|
+
export function markMainRefreshGrantRejected(key: string): void {
|
|
27
|
+
refusedMainRefreshGrants.add(key);
|
|
28
|
+
if (refusedMainRefreshGrants.size > MAX_REFUSED_MAIN_REFRESH_GRANTS) {
|
|
29
|
+
refusedMainRefreshGrants.delete(refusedMainRefreshGrants.values().next().value!);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isMainRefreshGrantRejected(key: string): boolean {
|
|
34
|
+
return refusedMainRefreshGrants.has(key);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function clearMainRefreshGrantRejection(key: string): void {
|
|
38
|
+
refusedMainRefreshGrants.delete(key);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
21
42
|
export function markAccountNeedsReauth(
|
|
22
43
|
id: string,
|
|
23
44
|
writerGeneration = captureConfigGeneration(),
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { classifyChatgptRefreshFailure, noteChatgptRefreshFailure, type ChatgptRefreshDiagnosticCode } from "./chatgpt-refresh-failure";
|
|
2
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
3
|
+
export { classifyChatgptRefreshFailure } from "./chatgpt-refresh-failure";
|
|
1
4
|
import { readOrcaAuthSource } from "./orca-auth-source";
|
|
2
5
|
import { createHash } from "node:crypto";
|
|
3
6
|
import { closeSync, existsSync, fstatSync, readFileSync, mkdirSync, openSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -526,7 +529,8 @@ const CHATGPT_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
|
526
529
|
|
|
527
530
|
export class TokenRefreshError extends Error {
|
|
528
531
|
reason: "expired" | "revoked" | "unknown";
|
|
529
|
-
constructor(reason: "expired" | "revoked" | "unknown", message: string
|
|
532
|
+
constructor(reason: "expired" | "revoked" | "unknown", message: string,
|
|
533
|
+
readonly status?: number, readonly code?: ChatgptRefreshDiagnosticCode) {
|
|
530
534
|
super(message);
|
|
531
535
|
this.name = "TokenRefreshError";
|
|
532
536
|
this.reason = reason;
|
|
@@ -1322,52 +1326,12 @@ async function resolveCodexToken(
|
|
|
1322
1326
|
signal,
|
|
1323
1327
|
});
|
|
1324
1328
|
if (!res.ok) {
|
|
1325
|
-
const
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
error_description?: string;
|
|
1332
|
-
};
|
|
1333
|
-
if (typeof parsed.error === "string") {
|
|
1334
|
-
errCodeExact = parsed.error.trim();
|
|
1335
|
-
errDesc = [parsed.error, parsed.error_description].filter(Boolean).join(": ");
|
|
1336
|
-
} else if (parsed.error && typeof parsed.error === "object") {
|
|
1337
|
-
errCodeExact = typeof parsed.error.code === "string" ? parsed.error.code.trim() : undefined;
|
|
1338
|
-
errDesc = [parsed.error.code, parsed.error.message, parsed.error_description].filter(Boolean).join(": ");
|
|
1339
|
-
} else {
|
|
1340
|
-
errDesc = parsed.error_description || `HTTP ${res.status}`;
|
|
1341
|
-
}
|
|
1342
|
-
if (!errDesc) errDesc = `HTTP ${res.status}`;
|
|
1343
|
-
} catch { errDesc = `HTTP ${res.status}`; }
|
|
1344
|
-
// `invalid_grant` is the standard OAuth code for a refresh token that is no longer
|
|
1345
|
-
// usable, and upstream sends it bare with no description. Without it here the dead
|
|
1346
|
-
// grant is classified "unknown", which callers treat as transient — so the account
|
|
1347
|
-
// is never retired and every request repeats the same doomed refresh (#2887).
|
|
1348
|
-
//
|
|
1349
|
-
// Matched on the exact `error` CODE, not anywhere in the combined text: a transient
|
|
1350
|
-
// `server_error` whose description happens to mention invalid_grant would otherwise
|
|
1351
|
-
// retire a healthy account, which is the failure this whole change exists to remove.
|
|
1352
|
-
//
|
|
1353
|
-
// That rule binds the DESCRIPTION words too. "invalidated", "revoked" and "expired" read
|
|
1354
|
-
// as terminal prose, but upstream puts arbitrary text there: a `server_error` whose
|
|
1355
|
-
// description says "token was revoked" or "session expired" is still a 5xx blip, and
|
|
1356
|
-
// retiring the account on it is exactly the false quarantine #2887 exists to prevent.
|
|
1357
|
-
// So a body that carries a structured code is classified by that code ALONE. The
|
|
1358
|
-
// substring fallback survives only where there is no structured code to read at all --
|
|
1359
|
-
// a description-only body, or one this parser could not decode -- because there the
|
|
1360
|
-
// prose is the only signal upstream gave us.
|
|
1361
|
-
const structuredCode = errCodeExact ? errCodeExact : undefined;
|
|
1362
|
-
const proseIsOnlySignal = structuredCode === undefined;
|
|
1363
|
-
const reason = structuredCode === "invalid_grant"
|
|
1364
|
-
|| structuredCode === "refresh_token_invalidated"
|
|
1365
|
-
|| (proseIsOnlySignal
|
|
1366
|
-
&& (errDesc.includes("invalidated") || errDesc.includes("revoked"))) ? "revoked" as const
|
|
1367
|
-
: structuredCode === "refresh_token_expired"
|
|
1368
|
-
|| (proseIsOnlySignal && errDesc.includes("expired")) ? "expired" as const
|
|
1369
|
-
: "unknown" as const;
|
|
1370
|
-
throw new TokenRefreshError(reason, `Codex token refresh failed (${reason}); reauthenticate the account.`);
|
|
1329
|
+
const body = await readBoundedResponseBody(res, { signal, maxBytes: 16_384, fatalUtf8: true })
|
|
1330
|
+
.catch(() => undefined);
|
|
1331
|
+
const failure = classifyChatgptRefreshFailure(res.status, body?.displaySafe ? body.text : "");
|
|
1332
|
+
const { reason } = failure;
|
|
1333
|
+
noteChatgptRefreshFailure("pool", res.status, failure);
|
|
1334
|
+
throw new TokenRefreshError(reason, `Codex token refresh failed (${reason}); reauthenticate the account.`, res.status, failure.code);
|
|
1371
1335
|
}
|
|
1372
1336
|
const data = (await res.json()) as { access_token: string; refresh_token?: string; expires_in: number };
|
|
1373
1337
|
// Guard against a missing/non-finite/negative expires_in (malformed upstream
|
|
@@ -2,8 +2,7 @@ import { readCodexAccountRecord } from "./account-store";
|
|
|
2
2
|
import { isAccountNeedsReauth } from "./account-runtime-state";
|
|
3
3
|
import {
|
|
4
4
|
MAIN_CODEX_ACCOUNT_ID,
|
|
5
|
-
|
|
6
|
-
isMainAccountCredentialUsable,
|
|
5
|
+
getMainAccountCredentialStatus,
|
|
7
6
|
isMainAccountTokenLive,
|
|
8
7
|
} from "./main-account";
|
|
9
8
|
import { hasLegacyMainCodexPoolAccount, isSelectableCodexPoolAccount } from "./account-id";
|
|
@@ -14,7 +13,7 @@ import { isMainAccountHardLocked } from "./main-account-hard-lock";
|
|
|
14
13
|
export interface CodexAccountUsabilityOptions {
|
|
15
14
|
/** Route using cached runtime state only; the caller must reject selected main before auth. */
|
|
16
15
|
nativeMainSelectionOnly?: boolean;
|
|
17
|
-
/**
|
|
16
|
+
/** Liveness override; ownership/read fences independently control physical credential reads. */
|
|
18
17
|
isMainAccountTokenLive?: typeof isMainAccountTokenLive;
|
|
19
18
|
/** Confirmed account ids for an account-gated model; omitted for ordinary native models. */
|
|
20
19
|
modelEligibleAccountIds?: ReadonlySet<string>;
|
|
@@ -88,15 +87,19 @@ export function codexAccountUnusableReason(
|
|
|
88
87
|
// A legacy pool row with the sentinel makes an active `__main__` ambiguous.
|
|
89
88
|
// Fail closed until the authenticated compatibility-delete path removes it.
|
|
90
89
|
if (hasLegacyMainCodexPoolAccount(config.codexAccounts)) return "legacy_pool_sentinel";
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
// before reservation or token materialization. Treat cached main as a routing
|
|
94
|
-
// candidate without touching the credential file so affinity is not rebound.
|
|
90
|
+
// These scopes cannot consume physical stored-main state, even indirectly through an
|
|
91
|
+
// ordinary quarantine plus hasMainAccountRefreshGrant(). Caller liveness is request-owned.
|
|
95
92
|
if (options.nativeMainSelectionOnly) return undefined;
|
|
93
|
+
if (options.requestOwnedMainCredential) {
|
|
94
|
+
return options.isMainAccountTokenLive?.() === false ? "main_credential_unavailable" : undefined;
|
|
95
|
+
}
|
|
96
|
+
const credential = getMainAccountCredentialStatus();
|
|
97
|
+
if (credential.refreshGrantRejected) return "needs_reauth";
|
|
98
|
+
if (isAccountNeedsReauth(accountId) && !credential.hasRefreshGrant) return "needs_reauth";
|
|
96
99
|
// Main account: a refresh grant is enough to route; materialization refreshes before I/O.
|
|
97
100
|
const mainLive = options.isMainAccountTokenLive
|
|
98
101
|
? options.isMainAccountTokenLive()
|
|
99
|
-
:
|
|
102
|
+
: credential.usable;
|
|
100
103
|
return mainLive ? undefined : "main_credential_unavailable";
|
|
101
104
|
}
|
|
102
105
|
const exists = (config.codexAccounts ?? [])
|
|
@@ -15,7 +15,7 @@ import { getCodexAccountAutoSwitchThresholdOverride } from "../account-auto-swit
|
|
|
15
15
|
import { clearThreadAccountMapForAccount, isCodexAccountPlanExcluded, reconcileCodexActiveAfterExclusion } from "../routing";
|
|
16
16
|
import { codexPlanValue, isThirtyDayOnlyCodexPlan } from "../plan";
|
|
17
17
|
import { isAccountNeedsReauth, markAccountNeedsReauth } from "../account-runtime-state";
|
|
18
|
-
import { getValidMainAccountToken,
|
|
18
|
+
import { getValidMainAccountToken, isMainAccountRefreshGrantRejected, MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
19
19
|
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
|
|
20
20
|
import { captureMainAccountIdentityGeneration, getMainAccountCredentialPresence, getMainAccountInfoCache, getObservedMainQuotaIdentityKey, isMainAccountIdentityGenerationLive } from "../main-account-cache";
|
|
21
21
|
import type { CodexQuotaRefreshOutcome } from "../quota-refresh-outcome";
|
|
@@ -342,60 +342,76 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
342
342
|
maskEmails,
|
|
343
343
|
)];
|
|
344
344
|
});
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
accountId: MAIN_CODEX_ACCOUNT_ID,
|
|
359
|
-
needsReauth: mainNeedsReauth,
|
|
360
|
-
});
|
|
361
|
-
// The main row carries the same attribution as a pool row. Reaching this point without
|
|
362
|
-
// `mainMissingCredential` means the runtime reauth flag is what set `mainNeedsReauth`, so the
|
|
363
|
-
// cause is a refresh that did not complete.
|
|
364
|
-
const mainReauthReason: CodexAccountReauthReason | undefined = mainMissingCredential
|
|
365
|
-
? "missing_credential"
|
|
366
|
-
: mainNeedsReauth
|
|
367
|
-
? "refresh_failed"
|
|
368
|
-
: mainHealth.status === "reauth_required" ? mainHealth.reason : undefined;
|
|
369
|
-
const main: CodexAuthAccountDto = {
|
|
370
|
-
id: MAIN_CODEX_ACCOUNT_ID,
|
|
371
|
-
email: projectEmail(mainInfo.email, maskEmails) ?? "Codex App login",
|
|
372
|
-
plan: mainInfo.plan,
|
|
373
|
-
...(mainSnapshotLive && mainResult.quotaRefresh && mainResult.quotaRefreshGeneration !== undefined
|
|
345
|
+
const projectSnapshot = (mainRefreshRefused = false): CodexAuthAccountsSnapshot => {
|
|
346
|
+
const fetchedMainGeneration = mainResult.identityGeneration ?? captureMainAccountIdentityGeneration();
|
|
347
|
+
const mainSnapshotLive = isMainAccountIdentityGenerationLive(fetchedMainGeneration);
|
|
348
|
+
// An ordinary same-account return can be parsed after its credential was replaced.
|
|
349
|
+
// The card and hard-lock status must describe the same published quota snapshot.
|
|
350
|
+
const mainInfo = mainSnapshotLive
|
|
351
|
+
? mainResult.infoUnpublished ? getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO : mainResult.info
|
|
352
|
+
: EMPTY_MAIN_ACCOUNT_INFO;
|
|
353
|
+
const hasMainCredential = mainSnapshotLive && mainResult.credentialChecked
|
|
354
|
+
? mainResult.hasCredential
|
|
355
|
+
: getMainAccountCredentialPresence() ?? false;
|
|
356
|
+
const mainMissingCredential = mainSnapshotLive && mainResult.credentialChecked && !hasMainCredential;
|
|
357
|
+
const liveQuotaRefresh = mainSnapshotLive && mainResult.quotaRefreshGeneration !== undefined
|
|
374
358
|
&& isMainAccountIdentityGenerationLive(mainResult.quotaRefreshGeneration)
|
|
375
|
-
?
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
:
|
|
359
|
+
? mainResult.quotaRefresh : undefined;
|
|
360
|
+
const mainNeedsReauth = mainMissingCredential || mainRefreshRefused || isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
361
|
+
const mainHealth = projectCodexAccountHealth({
|
|
362
|
+
accountId: MAIN_CODEX_ACCOUNT_ID,
|
|
363
|
+
needsReauth: mainNeedsReauth,
|
|
364
|
+
});
|
|
365
|
+
// The main row carries the same attribution as a pool row. Reaching this point without
|
|
366
|
+
// Only a current terminal usage probe can attribute the reauth cause. A transient status
|
|
367
|
+
// never replaces a refresh failure, including a separately recorded refused grant.
|
|
368
|
+
const mainReauthReason: CodexAccountReauthReason | undefined = mainMissingCredential
|
|
369
|
+
? "missing_credential"
|
|
370
|
+
: mainNeedsReauth
|
|
371
|
+
? mainResult.terminalAuthFailure === true && liveQuotaRefresh?.status === "http_error"
|
|
372
|
+
? "unauthorized" : "refresh_failed"
|
|
373
|
+
: mainHealth.status === "reauth_required" ? mainHealth.reason : undefined;
|
|
374
|
+
const main: CodexAuthAccountDto = {
|
|
375
|
+
id: MAIN_CODEX_ACCOUNT_ID,
|
|
376
|
+
email: projectEmail(mainInfo.email, maskEmails) ?? "Codex App login",
|
|
377
|
+
plan: mainInfo.plan,
|
|
378
|
+
...(liveQuotaRefresh ? { quotaRefresh: liveQuotaRefresh } : {}),
|
|
379
|
+
logLabel: "main",
|
|
380
|
+
isMain: true,
|
|
381
|
+
paused: isCodexAccountPaused(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
382
|
+
mainAccountHardLock: { ...getMainAccountHardLockStatus(runtimeConfig),
|
|
383
|
+
externalUsage: getMainAccountExternalUsageWarning(getObservedMainQuotaIdentityKey()) },
|
|
384
|
+
priority: getCodexAccountPriority(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
385
|
+
autoSwitchThresholdOverride: getCodexAccountAutoSwitchThresholdOverride(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
386
|
+
creditsAfterLimit: codexAccountUsesCreditsAfterLimit(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
387
|
+
hasCredential: hasMainCredential,
|
|
388
|
+
needsReauth: mainNeedsReauth,
|
|
389
|
+
...(mainReauthReason !== undefined ? { reauthReason: mainReauthReason } : {}),
|
|
390
|
+
quota: mainInfo.quota
|
|
391
|
+
? quotaForPlan(mainQuotaWithCarriedResetCredits(mainInfo.quota), mainInfo.plan)
|
|
392
|
+
: null,
|
|
393
|
+
...codexCreditsDtoField(runtimeConfig, MAIN_CODEX_ACCOUNT_ID, getMainChatgptAccountId()),
|
|
394
|
+
...oauthAccountHealthFields("codex", MAIN_CODEX_ACCOUNT_ID, mainHealth),
|
|
395
|
+
};
|
|
396
|
+
return {
|
|
397
|
+
accounts: [main, ...withQuota],
|
|
398
|
+
mainIdentityGeneration: mainSnapshotLive
|
|
399
|
+
? fetchedMainGeneration
|
|
400
|
+
: captureMainAccountIdentityGeneration(),
|
|
401
|
+
};
|
|
398
402
|
};
|
|
403
|
+
// Project the refusal and DTO in the same owned synchronous observation. A same-account grant
|
|
404
|
+
// replacement during earlier pool probes must not inherit the previous grant's refusal.
|
|
405
|
+
const projectionLease = tryAcquireNativeMainProfileClaim();
|
|
406
|
+
if (!projectionLease) return projectSnapshot();
|
|
407
|
+
try {
|
|
408
|
+
return await withNativeMainCredentialClaim(async () => projectSnapshot(isMainAccountRefreshGrantRejected()));
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (isNativeMainClaimUnavailable(error)) return projectSnapshot();
|
|
411
|
+
throw error;
|
|
412
|
+
} finally {
|
|
413
|
+
projectionLease.release();
|
|
414
|
+
}
|
|
399
415
|
}
|
|
400
416
|
|
|
401
417
|
/** One opted-in account's metadata; reuse the bounded WHAM 401 recovery and generation fence. */
|
|
@@ -406,16 +422,10 @@ export async function refreshCodexQuotaForActivation(config: OcxConfig, accountI
|
|
|
406
422
|
try {
|
|
407
423
|
reconcileMainCodexAccountRuntimeState();
|
|
408
424
|
if (isAccountNeedsReauth(accountId)) return;
|
|
409
|
-
const identityGeneration = captureMainAccountIdentityGeneration();
|
|
410
|
-
const writerGeneration = captureConfigGeneration();
|
|
411
425
|
try {
|
|
412
|
-
// Refresh
|
|
426
|
+
// Refresh owns credential-scoped refusal evidence; do not recreate a global quarantine.
|
|
413
427
|
if (!await getValidMainAccountToken({ preserveReauth: true })) return;
|
|
414
|
-
} catch
|
|
415
|
-
if (error instanceof MainAccountTokenRefreshError && error.reason === "reauth"
|
|
416
|
-
&& isMainAccountIdentityGenerationLive(identityGeneration)) {
|
|
417
|
-
markAccountNeedsReauth(accountId, writerGeneration);
|
|
418
|
-
}
|
|
428
|
+
} catch {
|
|
419
429
|
return;
|
|
420
430
|
}
|
|
421
431
|
if (isAccountNeedsReauth(accountId)) return;
|
|
@@ -476,7 +486,7 @@ export async function pauseExhaustedCodexAccounts(
|
|
|
476
486
|
}
|
|
477
487
|
return {
|
|
478
488
|
shouldPause: !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
|
|
479
|
-
&& isCodexQuotaExhausted(mainResult.freshQuota, mainResult.info.plan),
|
|
489
|
+
&& isCodexQuotaExhausted(mainResult.freshQuota, mainResult.info.plan, codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID)),
|
|
480
490
|
checkedAccountCount: 1,
|
|
481
491
|
failedAccountCount: 0,
|
|
482
492
|
};
|
|
@@ -511,7 +521,7 @@ export async function pauseExhaustedCodexAccounts(
|
|
|
511
521
|
continue;
|
|
512
522
|
}
|
|
513
523
|
checkedAccountCount += 1;
|
|
514
|
-
if (!isCodexAccountPaused(config, account.id) && isCodexQuotaExhausted(quotaResult.freshQuota, plan)) {
|
|
524
|
+
if (!isCodexAccountPaused(config, account.id) && isCodexQuotaExhausted(quotaResult.freshQuota, plan, codexAccountUsesCreditsAfterLimit(config, account.id))) {
|
|
515
525
|
exhaustedIds.push(account.id);
|
|
516
526
|
}
|
|
517
527
|
}
|
|
@@ -16,14 +16,15 @@ import { getMainAccountPlan, isMainAccountTokenVerifiablyLive, MAIN_CODEX_ACCOUN
|
|
|
16
16
|
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
|
|
17
17
|
import { captureMainAccountIdentityGeneration, clearMainAccountInfoCache, getMainAccountInfoCache, getMainQuotaCredentialGeneration, isMainAccountIdentityGenerationLive, isMainQuotaWriterLive, matchesMainQuotaCredential, observeMainQuotaCredential, setMainAccountCredentialPresence, setMainAccountInfoCache } from "../main-account-cache";
|
|
18
18
|
import type { MainQuotaWriter, MainAccountInfo } from "../main-account-cache";
|
|
19
|
-
import
|
|
19
|
+
import { isCodexTerminalAuthCode } from "../quota-refresh-outcome";
|
|
20
|
+
import type { CodexQuotaRefreshOutcome, CodexTerminalAuthCode } from "../quota-refresh-outcome";
|
|
20
21
|
import { observeMainReserveRevocation } from "../reserve-availability";
|
|
21
22
|
import type { AdmissionLease } from "../../lib/admission";
|
|
22
23
|
import { nonEmptyPlan } from "./runtime-config";
|
|
23
24
|
import { tryAcquireNativeMainProfileClaim } from "../native-main-admission";
|
|
24
25
|
import { WHAM_REQUEST_TIMEOUT_MS } from "../quota-recovery-timing";
|
|
25
26
|
import { withNativeMainCredentialClaim, isNativeMainClaimUnavailable } from "./http";
|
|
26
|
-
import {
|
|
27
|
+
import { readMainAuthErrorCode, currentQuotaDispatchSequence, nextQuotaDispatchSequence, isQuotaDispatchCurrent, publishQuotaDispatch } from "./pool-quota-probe";
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Last reset-credit count this process parsed for the main account, tagged with the
|
|
@@ -79,14 +80,18 @@ function sharedMainQuotaPacingEnabled(config: OcxConfig): boolean {
|
|
|
79
80
|
* itself would make a real 401 permanently transient.
|
|
80
81
|
*/
|
|
81
82
|
export async function isTerminalMainAuthResponse(resp: Response, accessTokenLive: boolean): Promise<boolean> {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
return (await classifyMainAuthResponse(resp, accessTokenLive)).terminal;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** As `isTerminalMainAuthResponse`, also naming the provider code that made it terminal. */
|
|
87
|
+
async function classifyMainAuthResponse(
|
|
88
|
+
resp: Response,
|
|
89
|
+
accessTokenLive: boolean,
|
|
90
|
+
): Promise<{ terminal: boolean; code?: CodexTerminalAuthCode }> {
|
|
91
|
+
if (resp.status !== 401 && resp.status !== 403) return { terminal: false };
|
|
88
92
|
const code = await readMainAuthErrorCode(resp);
|
|
89
|
-
|
|
93
|
+
if (isCodexTerminalAuthCode(code)) return { terminal: true, code };
|
|
94
|
+
return { terminal: resp.status === 401 && !accessTokenLive };
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
export interface MainResetQuotaProof {
|
|
@@ -103,6 +108,8 @@ export interface MainAccountInfoFetchResult {
|
|
|
103
108
|
quotaRefresh?: CodexQuotaRefreshOutcome;
|
|
104
109
|
/** Internal dispatch fence for diagnostics only; never copied into a public DTO or cache. */
|
|
105
110
|
quotaRefreshGeneration?: number;
|
|
111
|
+
/** This current-credential usage response supplied terminal auth evidence; never public. */
|
|
112
|
+
terminalAuthFailure?: true;
|
|
106
113
|
/** Whether this attempt safely inspected the physical native-main credential. */
|
|
107
114
|
credentialChecked: boolean;
|
|
108
115
|
/** Meaningful only when credentialChecked is true. */
|
|
@@ -288,7 +295,8 @@ export async function fetchMainAccountInfoWhileOwned(
|
|
|
288
295
|
const resp = admission.response;
|
|
289
296
|
quotaPhase = "publish";
|
|
290
297
|
if (!resp.ok) {
|
|
291
|
-
const
|
|
298
|
+
const authFailure = await classifyMainAuthResponse(resp, isMainAccountTokenVerifiablyLive());
|
|
299
|
+
const terminalAuthFailure = authFailure.terminal;
|
|
292
300
|
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
|
|
293
301
|
if (retried) return retried;
|
|
294
302
|
if (!isQuotaDispatchCurrent(dispatchSequence) || !credentialIsCurrent()) {
|
|
@@ -302,10 +310,15 @@ export async function fetchMainAccountInfoWhileOwned(
|
|
|
302
310
|
if (diagnosticStillLive) quotaRefreshGeneration = captureMainAccountIdentityGeneration();
|
|
303
311
|
markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
|
|
304
312
|
}
|
|
313
|
+
// Keep the last-known plan visible: after a plan change revokes the session, the plan the
|
|
314
|
+
// account HAD is what tells the operator why it stopped working.
|
|
305
315
|
return {
|
|
306
|
-
info: EMPTY_MAIN_ACCOUNT_INFO,
|
|
307
|
-
|
|
316
|
+
info: { ...EMPTY_MAIN_ACCOUNT_INFO, plan: nonEmptyPlan(cached?.plan) ?? nonEmptyPlan(getMainAccountPlan()) },
|
|
317
|
+
credentialChecked: true, hasCredential: true,
|
|
318
|
+
quotaRefresh: { status: "http_error", httpStatus: resp.status,
|
|
319
|
+
...(authFailure.code ? { code: authFailure.code } : {}) },
|
|
308
320
|
quotaRefreshGeneration,
|
|
321
|
+
...(terminalAuthFailure ? { terminalAuthFailure: true as const } : {}),
|
|
309
322
|
};
|
|
310
323
|
}
|
|
311
324
|
quotaPhase = "body";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { codexAccountUsesCreditsAfterLimit } from "../account-credit-use";
|
|
1
2
|
import { nextCodexUsageQueryAt, nextQuotaQueryDelay, pruneRemovedCodexPoolUsageAccounts } from "../quota-query-backoff";
|
|
2
3
|
import { CODEX_PRIORITY_FAILBACK_REFRESH_MS } from "../account-priority";
|
|
3
4
|
import { codexQuotaHasFreshUsage } from "../quota-observation-freshness";
|
|
@@ -6,8 +7,8 @@ import { getAccountQuota, isCompleteCodexQuotaRecoverySnapshot } from "../quota"
|
|
|
6
7
|
import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
|
|
7
8
|
import { claimDueCodexQuotaRecoveryProbes, settleCodexQuotaRecoveryProbe } from "../routing";
|
|
8
9
|
import { readCodexTokens } from "../auth-collision";
|
|
9
|
-
import { isAccountNeedsReauth
|
|
10
|
-
import { getValidMainAccountToken,
|
|
10
|
+
import { isAccountNeedsReauth } from "../account-runtime-state";
|
|
11
|
+
import { getValidMainAccountToken, MAIN_CODEX_ACCOUNT_ID, getMainAccountPlan } from "../main-account";
|
|
11
12
|
import { captureConfigGeneration, registerStateSweepAfterTick } from "../../lib/state-store-sweeper";
|
|
12
13
|
import { observeMainQuotaCredential, getMainQuotaCredentialGeneration, captureMainAccountIdentityGeneration, isMainAccountIdentityGenerationLive } from "../main-account-cache";
|
|
13
14
|
import { getMainAccountHardLockStatus } from "../main-account-hard-lock";
|
|
@@ -58,7 +59,7 @@ export async function runCodexCooldownRecoveryProbes(config: OcxConfig, now = Da
|
|
|
58
59
|
// Defence in depth: independent scopes are already excluded at the claim site.
|
|
59
60
|
// Generic WHAM must never clear Reserve even if claim selection changes.
|
|
60
61
|
const recovered = (claim.scope === undefined || claim.scope === "shared")
|
|
61
|
-
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan);
|
|
62
|
+
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan, codexAccountUsesCreditsAfterLimit(config, claim.accountId));
|
|
62
63
|
settleCodexQuotaRecoveryProbe(claim, recovered, {
|
|
63
64
|
credentialGeneration: result.freshCredentialGeneration,
|
|
64
65
|
}, now);
|
|
@@ -100,18 +101,14 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
|
|
|
100
101
|
if (getMainAccountHardLockStatus(config).state !== "blocked"
|
|
101
102
|
|| isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
|
|
102
103
|
const identityGeneration = captureMainAccountIdentityGeneration();
|
|
103
|
-
const writerGeneration = captureConfigGeneration();
|
|
104
104
|
try {
|
|
105
105
|
// Refresh can require an exclusive credential claim: never hold WHAM's shared
|
|
106
106
|
// claim while obtaining a valid token. The runtime lease spans both operations.
|
|
107
107
|
const prepared = await getValidMainAccountToken({ preserveReauth: true });
|
|
108
108
|
if (!prepared) return;
|
|
109
109
|
observeMainQuotaCredential(prepared.accessToken, prepared.chatgptAccountId);
|
|
110
|
-
} catch
|
|
111
|
-
|
|
112
|
-
&& isMainAccountIdentityGenerationLive(identityGeneration)) {
|
|
113
|
-
markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
|
|
114
|
-
}
|
|
110
|
+
} catch {
|
|
111
|
+
// Native refresh retains its own grant-scoped refusal; global quarantine would outlive it.
|
|
115
112
|
return;
|
|
116
113
|
}
|
|
117
114
|
if (isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
|
|
@@ -15,14 +15,12 @@ import { WHAM_REQUEST_TIMEOUT_MS } from "../quota-recovery-timing";
|
|
|
15
15
|
import { claimQuotaRecovery, fencePropagatedQuotaRecovery, quotaRecoveryTerminalFor, releaseQuotaRecovery, settleQuotaRecovery, settleQuotaRecoveryTerminal } from "../quota-401-recovery";
|
|
16
16
|
import { seedLoginRowsForTests } from "./login-state";
|
|
17
17
|
import { nonEmptyPlan } from "./runtime-config";
|
|
18
|
+
import { CODEX_TERMINAL_AUTH_CODES } from "../quota-refresh-outcome";
|
|
18
19
|
|
|
19
20
|
export const POOL_CACHE_TTL = 5 * 60_000;
|
|
20
21
|
export const POOL_QUOTA_REFRESH_CONCURRENCY = 4;
|
|
21
22
|
|
|
22
|
-
export const MAIN_TERMINAL_AUTH_CODES = new Set(
|
|
23
|
-
"invalid_workspace_selected",
|
|
24
|
-
"invalid_refresh_token",
|
|
25
|
-
]);
|
|
23
|
+
export const MAIN_TERMINAL_AUTH_CODES: ReadonlySet<string> = new Set(CODEX_TERMINAL_AUTH_CODES);
|
|
26
24
|
|
|
27
25
|
export async function readMainAuthErrorCode(resp: Response): Promise<unknown> {
|
|
28
26
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { codexAccountUsesCreditsAfterLimit } from "../account-credit-use";
|
|
1
2
|
import { getValidCodexToken, isCodexAccountGenerationLive, readCodexAccountRecord, CodexCredentialGenerationConflictError } from "../account-store";
|
|
2
3
|
import { isCompleteCodexQuotaRecoverySnapshot } from "../quota";
|
|
3
4
|
import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
|
|
@@ -233,7 +234,7 @@ async function refreshAfterManualReset(
|
|
|
233
234
|
&& proof.credentialGeneration === auth.mainProof.credentialGeneration
|
|
234
235
|
&& proof.writer.identityKey === auth.mainProof.writer.identityKey
|
|
235
236
|
&& proof.writer.identityGeneration === auth.mainProof.writer.identityGeneration
|
|
236
|
-
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.info.plan);
|
|
237
|
+
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.info.plan, codexAccountUsesCreditsAfterLimit(getRuntimeConfig(config), accountId));
|
|
237
238
|
for (const claim of claims) settleManualResetCooldown(getRuntimeConfig(config), claim, recovered);
|
|
238
239
|
return manualResetAuthStillLive(accountId, auth) ? result.freshResetCredits : undefined;
|
|
239
240
|
}
|
|
@@ -255,7 +256,7 @@ async function refreshAfterManualReset(
|
|
|
255
256
|
const record = readCodexAccountRecord(accountId);
|
|
256
257
|
const recovered = didReset && record?.credential?.chatgptAccountId === auth.chatgptAccountId
|
|
257
258
|
&& (result.quotaProbeAttempted?.dispatchSequence ?? 0) > afterDispatchSequence
|
|
258
|
-
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan);
|
|
259
|
+
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan, codexAccountUsesCreditsAfterLimit(getRuntimeConfig(config), accountId));
|
|
259
260
|
for (const claim of claims) settleManualResetCooldown(getRuntimeConfig(config), claim, recovered, {
|
|
260
261
|
credentialGeneration: result.freshCredentialGeneration,
|
|
261
262
|
refreshLineage: result.resetRefreshLineage,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasSpendableCodexCredits } from "./quota-types";
|
|
1
2
|
import { noteMainAccountActivity } from "./main-account-external-usage";
|
|
2
3
|
import { codexAccountPriorityFailbackEnabled } from "./account-priority";
|
|
3
4
|
import type { PoolQuotaWriter } from "./quota-types";
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
import { isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
|
|
17
18
|
import { ConfigMutationLockError } from "../config";
|
|
18
19
|
import { NativeProfileError } from "./native-profile-types";
|
|
19
|
-
import { isCodexAccountUsable } from "./account-usability";
|
|
20
|
+
import { codexAccountUnusableReason, isCodexAccountUsable } from "./account-usability";
|
|
20
21
|
import { reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
|
|
21
22
|
import {
|
|
22
23
|
MAIN_CODEX_ACCOUNT_ID,
|
|
@@ -102,7 +103,7 @@ import { getEffectiveCodexAutoSwitchThreshold } from "./account-auto-switch";
|
|
|
102
103
|
function requestOwnedMainPinHasQuotaHeadroom(config: OcxConfig): boolean {
|
|
103
104
|
const threshold = getEffectiveCodexAutoSwitchThreshold(config, MAIN_CODEX_ACCOUNT_ID);
|
|
104
105
|
if (threshold <= 0) return true;
|
|
105
|
-
const usage = computeCodexUsageScore(getAccountQuota(MAIN_CODEX_ACCOUNT_ID));
|
|
106
|
+
const usage = computeCodexUsageScore(getAccountQuota(MAIN_CODEX_ACCOUNT_ID), undefined, Date.now(), codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID));
|
|
106
107
|
return usage >= CODEX_UNKNOWN_USAGE_SCORE || usage < threshold;
|
|
107
108
|
}
|
|
108
109
|
|
|
@@ -378,9 +379,11 @@ export class CodexAuthContextError extends Error {
|
|
|
378
379
|
}
|
|
379
380
|
|
|
380
381
|
export class CodexPoolAuthenticationError extends Error {
|
|
381
|
-
|
|
382
|
+
readonly quarantinedMain: boolean;
|
|
383
|
+
constructor(message = "OpenAI account pool has no usable account credential", options?: { quarantinedMain?: boolean }) {
|
|
382
384
|
super(message);
|
|
383
385
|
this.name = "CodexPoolAuthenticationError";
|
|
386
|
+
this.quarantinedMain = options?.quarantinedMain === true;
|
|
384
387
|
}
|
|
385
388
|
}
|
|
386
389
|
|
|
@@ -530,7 +533,7 @@ export class CodexMainAccountCreditsOffError extends CodexAccountCooldownError {
|
|
|
530
533
|
super(MAIN_CODEX_ACCOUNT_ID, resetAt ?? 0);
|
|
531
534
|
this.name = "CodexMainAccountCreditsOffError";
|
|
532
535
|
this.resetAt = resetAt;
|
|
533
|
-
this.message = "Codex main account reached its usage limit, and spending ChatGPT credits is off
|
|
536
|
+
this.message = "Codex main account reached its usage limit, and spending ChatGPT credits is off or no fresh spendable balance is available."
|
|
534
537
|
+ " Choose another account, wait for the limit to reset, or allow the main account under \"Use credits\" in Codex Auth.";
|
|
535
538
|
}
|
|
536
539
|
}
|
|
@@ -723,7 +726,8 @@ export function unwrapUpstreamRetryEvidenceError(error: unknown): unknown {
|
|
|
723
726
|
* file, which several callers are forbidden to open, so every long window counts instead.
|
|
724
727
|
*/
|
|
725
728
|
function mainCreditsHoldResetAt(config: Pick<OcxConfig, "creditCodexAccountIds">): number | undefined {
|
|
726
|
-
if (codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID)
|
|
729
|
+
if (codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID)
|
|
730
|
+
&& hasSpendableCodexCredits(getMainPolicyQuota())) return undefined;
|
|
727
731
|
return codexUsageLimitResetAt(getMainPolicyQuota(), undefined, Date.now());
|
|
728
732
|
}
|
|
729
733
|
|
|
@@ -920,7 +924,7 @@ export function shouldMarkAccountNeedsReauthForCodexAuthFailure(cause: unknown):
|
|
|
920
924
|
&& !(cause instanceof CodexCredentialRefreshBusyError)
|
|
921
925
|
&& !(cause instanceof CodexCredentialRefreshStaleError)
|
|
922
926
|
&& !(cause instanceof MainAuthJsonChangedDuringRefreshError)
|
|
923
|
-
&& !(cause instanceof MainAccountTokenRefreshError
|
|
927
|
+
&& !(cause instanceof MainAccountTokenRefreshError)
|
|
924
928
|
&& !(cause instanceof NativeProfileError && cause.retryable)
|
|
925
929
|
&& !(cause instanceof DOMException && cause.name === "AbortError")
|
|
926
930
|
&& !(cause instanceof ConfigMutationLockError);
|
|
@@ -1314,7 +1318,12 @@ export async function resolveCodexAuthContext(
|
|
|
1314
1318
|
"Selected Codex account does not support this model",
|
|
1315
1319
|
);
|
|
1316
1320
|
}
|
|
1317
|
-
|
|
1321
|
+
const mainReason = fixedAccountId === MAIN_CODEX_ACCOUNT_ID && !nativeMainReadsForbidden
|
|
1322
|
+
&& !policy.pausedCodexAccountIds?.includes(fixedAccountId)
|
|
1323
|
+
? codexAccountUnusableReason(config, fixedAccountId, selectionOptions) : undefined;
|
|
1324
|
+
throw new CodexPoolAuthenticationError("Selected Codex account is unavailable", {
|
|
1325
|
+
quarantinedMain: mainReason === "needs_reauth",
|
|
1326
|
+
});
|
|
1318
1327
|
}
|
|
1319
1328
|
// Recovery or a turn drain deliberately makes physical main unobservable.
|
|
1320
1329
|
// If no healthy pool route is available, report the temporary fence rather
|
|
@@ -1339,7 +1348,12 @@ export async function resolveCodexAuthContext(
|
|
|
1339
1348
|
: "Codex accounts that support this model are currently unavailable",
|
|
1340
1349
|
);
|
|
1341
1350
|
}
|
|
1342
|
-
throw new CodexPoolAuthenticationError(
|
|
1351
|
+
throw new CodexPoolAuthenticationError(undefined, {
|
|
1352
|
+
quarantinedMain: !nativeMainReadsForbidden
|
|
1353
|
+
&& options.excludeAccountId !== MAIN_CODEX_ACCOUNT_ID
|
|
1354
|
+
&& !policy.pausedCodexAccountIds?.includes(MAIN_CODEX_ACCOUNT_ID)
|
|
1355
|
+
&& codexAccountUnusableReason(config, MAIN_CODEX_ACCOUNT_ID, selectionOptions) === "needs_reauth",
|
|
1356
|
+
});
|
|
1343
1357
|
}
|
|
1344
1358
|
accountId = selected;
|
|
1345
1359
|
if (accountId === MAIN_CODEX_ACCOUNT_ID) assertMainAccountPolicy(policy);
|
|
@@ -1370,10 +1384,15 @@ export async function resolveCodexAuthContext(
|
|
|
1370
1384
|
throw new CodexPoolAuthenticationError("Selected Codex account is unavailable");
|
|
1371
1385
|
}
|
|
1372
1386
|
if (isAccountNeedsReauth(accountId)) {
|
|
1373
|
-
throw new CodexPoolAuthenticationError("Selected Codex account needs reauthentication"
|
|
1387
|
+
throw new CodexPoolAuthenticationError("Selected Codex account needs reauthentication", {
|
|
1388
|
+
quarantinedMain: accountId === MAIN_CODEX_ACCOUNT_ID && !nativeMainReadsForbidden,
|
|
1389
|
+
});
|
|
1374
1390
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1391
|
+
const unusableReason = codexAccountUnusableReason(config, accountId, selectionOptions);
|
|
1392
|
+
if (unusableReason !== undefined) {
|
|
1393
|
+
throw new CodexPoolAuthenticationError("Selected Codex account is unavailable", {
|
|
1394
|
+
quarantinedMain: accountId === MAIN_CODEX_ACCOUNT_ID && unusableReason === "needs_reauth",
|
|
1395
|
+
});
|
|
1377
1396
|
}
|
|
1378
1397
|
}
|
|
1379
1398
|
} catch (cause) {
|