@bitkyc08/opencodex 2.10.0 → 2.10.1
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/AGENTS_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
package/src/codex/auth-api.ts
CHANGED
|
@@ -16,11 +16,13 @@ import {
|
|
|
16
16
|
import { deleteCodexAccount, reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
|
|
17
17
|
import { isCodexAccountPaused, setCodexAccountPaused } from "./account-pause";
|
|
18
18
|
import {
|
|
19
|
+
claimDueCodexQuotaRecoveryProbes,
|
|
19
20
|
clearCodexAccountCooldown,
|
|
20
21
|
clearThreadAccountMapForAccount,
|
|
21
22
|
getEffectiveActiveCodexAccountId,
|
|
22
23
|
reconcileCodexActiveAfterExclusion,
|
|
23
24
|
resetCodexRoutingForManualSelection,
|
|
25
|
+
settleCodexQuotaRecoveryProbe,
|
|
24
26
|
} from "./routing";
|
|
25
27
|
import {
|
|
26
28
|
normalizeAccountPoolStickyLimit,
|
|
@@ -35,6 +37,7 @@ import { clearAccountNeedsReauth, isAccountNeedsReauth, markAccountNeedsReauth }
|
|
|
35
37
|
import {
|
|
36
38
|
clearAccountQuota,
|
|
37
39
|
getAccountQuota,
|
|
40
|
+
isCompleteCodexQuotaRecoverySnapshot,
|
|
38
41
|
isCodexQuotaExhausted,
|
|
39
42
|
listAccountQuotas,
|
|
40
43
|
parseUsageQuota,
|
|
@@ -53,11 +56,15 @@ export {
|
|
|
53
56
|
} from "./quota";
|
|
54
57
|
import { extractAccountId, decodeJwtPayload } from "../oauth/chatgpt";
|
|
55
58
|
import { getMainAccountPlan, MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
|
|
56
|
-
import { captureConfigGeneration } from "../lib/state-store-sweeper";
|
|
59
|
+
import { captureConfigGeneration, registerStateSweepAfterTick } from "../lib/state-store-sweeper";
|
|
57
60
|
import { reconcileLiveStateStores } from "../lib/state-store-registrations";
|
|
58
61
|
import {
|
|
62
|
+
captureMainAccountIdentityGeneration,
|
|
59
63
|
clearMainAccountInfoCache,
|
|
64
|
+
getMainAccountCredentialPresence,
|
|
60
65
|
getMainAccountInfoCache,
|
|
66
|
+
isMainAccountIdentityGenerationLive,
|
|
67
|
+
setMainAccountCredentialPresence,
|
|
61
68
|
setMainAccountInfoCache,
|
|
62
69
|
type MainAccountInfo,
|
|
63
70
|
} from "./main-account-cache";
|
|
@@ -82,7 +89,20 @@ import {
|
|
|
82
89
|
isValidCodexAccountId,
|
|
83
90
|
} from "./account-id";
|
|
84
91
|
import { codexAccountIdNamespaceCollisionError } from "./account-namespace-match";
|
|
85
|
-
import { ResourceAdmissionError } from "../lib/admission";
|
|
92
|
+
import { ResourceAdmissionError, type AdmissionLease } from "../lib/admission";
|
|
93
|
+
import { tryAcquireNativeMainProfileClaim } from "./native-main-admission";
|
|
94
|
+
import { withNativeMainSharedClaim } from "./native-main-claim";
|
|
95
|
+
import { resolveNativeProfileContext } from "./native-profile-store";
|
|
96
|
+
import { NativeProfileError } from "./native-profile-types";
|
|
97
|
+
|
|
98
|
+
function isNativeMainClaimUnavailable(error: unknown): error is NativeProfileError {
|
|
99
|
+
return error instanceof NativeProfileError
|
|
100
|
+
&& (error.code === "NATIVE_MAIN_CLAIM_BUSY" || error.code === "NATIVE_MAIN_CLAIM_UNAVAILABLE");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function withNativeMainCredentialClaim<T>(operation: () => Promise<T>): Promise<T> {
|
|
104
|
+
return withNativeMainSharedClaim(resolveNativeProfileContext(), operation);
|
|
105
|
+
}
|
|
86
106
|
|
|
87
107
|
function jsonResponse(data: unknown, status = 200): Response {
|
|
88
108
|
return new Response(JSON.stringify(data), {
|
|
@@ -91,6 +111,12 @@ function jsonResponse(data: unknown, status = 200): Response {
|
|
|
91
111
|
});
|
|
92
112
|
}
|
|
93
113
|
|
|
114
|
+
function nativeMainProfileBusyResponse(): Response {
|
|
115
|
+
const response = jsonResponse({ error: "server_busy", code: "server_busy" }, 503);
|
|
116
|
+
response.headers.set("Retry-After", "1");
|
|
117
|
+
return response;
|
|
118
|
+
}
|
|
119
|
+
|
|
94
120
|
const MANUAL_IMPORT_ENV = "OPENCODEX_ENABLE_UNVERIFIED_CODEX_IMPORT";
|
|
95
121
|
|
|
96
122
|
const MAX_CODEX_LOGIN_STATE_ROWS = 32;
|
|
@@ -185,20 +211,52 @@ function poolAccountDto(
|
|
|
185
211
|
};
|
|
186
212
|
}
|
|
187
213
|
|
|
188
|
-
|
|
214
|
+
interface ResetCreditAuth {
|
|
215
|
+
isMain: boolean;
|
|
216
|
+
accessToken: string;
|
|
217
|
+
chatgptAccountId: string;
|
|
218
|
+
nativeMainLease?: AdmissionLease;
|
|
219
|
+
nativeMainSharedClaimHeld?: true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function withResetCreditAuth<T>(
|
|
189
223
|
runtimeConfig: OcxConfig,
|
|
190
224
|
accountId: string,
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
| { ok: false; response: Response }
|
|
194
|
-
> {
|
|
225
|
+
operation: (auth: ResetCreditAuth) => Promise<T>,
|
|
226
|
+
): Promise<{ ok: true; value: T } | { ok: false; response: Response }> {
|
|
195
227
|
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
196
228
|
if (hasLegacyMainCodexPoolAccount(runtimeConfig.codexAccounts)) {
|
|
197
229
|
return { ok: false, response: jsonResponse({ error: "Remove the legacy __main__ pool row before using the Desktop account" }, 409) };
|
|
198
230
|
}
|
|
199
|
-
const
|
|
200
|
-
if (!
|
|
201
|
-
|
|
231
|
+
const nativeMainLease = tryAcquireNativeMainProfileClaim();
|
|
232
|
+
if (!nativeMainLease) return { ok: false, response: nativeMainProfileBusyResponse() };
|
|
233
|
+
try {
|
|
234
|
+
try {
|
|
235
|
+
return await withNativeMainCredentialClaim(async () => {
|
|
236
|
+
const tokens = readCodexTokens();
|
|
237
|
+
if (!tokens) {
|
|
238
|
+
return { ok: false, response: jsonResponse({ error: "Main Codex account not logged in" }, 401) };
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
ok: true,
|
|
242
|
+
value: await operation({
|
|
243
|
+
isMain: true,
|
|
244
|
+
accessToken: tokens.access_token,
|
|
245
|
+
chatgptAccountId: tokens.account_id,
|
|
246
|
+
nativeMainLease,
|
|
247
|
+
nativeMainSharedClaimHeld: true,
|
|
248
|
+
}),
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (isNativeMainClaimUnavailable(error)) {
|
|
253
|
+
return { ok: false, response: nativeMainProfileBusyResponse() };
|
|
254
|
+
}
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
} finally {
|
|
258
|
+
nativeMainLease.release();
|
|
259
|
+
}
|
|
202
260
|
}
|
|
203
261
|
if (!isValidCodexAccountId(accountId)) {
|
|
204
262
|
return { ok: false, response: jsonResponse({ error: "Invalid account id format" }, 400) };
|
|
@@ -207,7 +265,14 @@ async function resolveResetCreditAuth(
|
|
|
207
265
|
return { ok: false, response: jsonResponse({ error: "Unknown Codex account" }, 404) };
|
|
208
266
|
}
|
|
209
267
|
const cred = await getValidCodexToken(accountId);
|
|
210
|
-
return {
|
|
268
|
+
return {
|
|
269
|
+
ok: true,
|
|
270
|
+
value: await operation({
|
|
271
|
+
isMain: false,
|
|
272
|
+
accessToken: cred.accessToken,
|
|
273
|
+
chatgptAccountId: cred.chatgptAccountId,
|
|
274
|
+
}),
|
|
275
|
+
};
|
|
211
276
|
}
|
|
212
277
|
|
|
213
278
|
function safeResetCreditsDto(input: unknown): { credits: { granted_at: string; expires_at: string }[]; available_count?: number } {
|
|
@@ -357,15 +422,33 @@ async function isTerminalMainAuthResponse(resp: Response): Promise<boolean> {
|
|
|
357
422
|
|
|
358
423
|
interface MainAccountInfoFetchResult {
|
|
359
424
|
info: MainAccountInfo;
|
|
425
|
+
/** Whether this attempt safely inspected the physical native-main credential. */
|
|
426
|
+
credentialChecked: boolean;
|
|
427
|
+
/** Meaningful only when credentialChecked is true. */
|
|
428
|
+
hasCredential: boolean;
|
|
429
|
+
/** Main identity generation captured while the native-main claim was held. */
|
|
430
|
+
identityGeneration?: number;
|
|
360
431
|
/** Present only when this call freshly parsed a WHAM usage response. */
|
|
361
432
|
freshQuota?: Omit<StoredAccountQuota, "updatedAt">;
|
|
362
433
|
/** Present only when this call's WHAM response included `rate_limit_reset_credits.available_count`. */
|
|
363
434
|
freshResetCredits?: number;
|
|
364
435
|
}
|
|
365
436
|
|
|
437
|
+
export interface MainAccountInfoSnapshot {
|
|
438
|
+
info: MainAccountInfo;
|
|
439
|
+
mainIdentityGeneration: number;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export async function fetchMainAccountInfoSnapshot(forceRefresh = false): Promise<MainAccountInfoSnapshot> {
|
|
443
|
+
const result = await fetchMainAccountInfoAttempt(forceRefresh, 1);
|
|
444
|
+
return {
|
|
445
|
+
info: result.info,
|
|
446
|
+
mainIdentityGeneration: result.identityGeneration ?? captureMainAccountIdentityGeneration(),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
366
450
|
export async function fetchMainAccountInfo(forceRefresh = false): Promise<MainAccountInfo> {
|
|
367
|
-
|
|
368
|
-
return info;
|
|
451
|
+
return (await fetchMainAccountInfoSnapshot(forceRefresh)).info;
|
|
369
452
|
}
|
|
370
453
|
|
|
371
454
|
const EMPTY_MAIN_ACCOUNT_INFO: MainAccountInfo = { email: null, plan: null, quota: null };
|
|
@@ -373,19 +456,64 @@ const EMPTY_MAIN_ACCOUNT_INFO: MainAccountInfo = { email: null, plan: null, quot
|
|
|
373
456
|
async function retryMainAccountInfoIfIdentityChanged(
|
|
374
457
|
requestAccountId: string | null,
|
|
375
458
|
retriesRemaining: number,
|
|
459
|
+
nativeMainLease: AdmissionLease,
|
|
376
460
|
): Promise<MainAccountInfoFetchResult | null> {
|
|
377
461
|
const currentAccountId = getMainChatgptAccountId();
|
|
378
462
|
if (currentAccountId === null || currentAccountId === requestAccountId) return null;
|
|
379
463
|
reconcileMainCodexAccountRuntimeState();
|
|
380
464
|
return retriesRemaining > 0
|
|
381
|
-
?
|
|
382
|
-
: { info: EMPTY_MAIN_ACCOUNT_INFO };
|
|
465
|
+
? fetchMainAccountInfoWhileOwned(true, retriesRemaining - 1, nativeMainLease)
|
|
466
|
+
: { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function fetchMainAccountInfoAttempt(
|
|
470
|
+
forceRefresh: boolean,
|
|
471
|
+
retriesRemaining: number,
|
|
472
|
+
existingNativeMainLease?: AdmissionLease,
|
|
473
|
+
nativeMainSharedClaimHeld = false,
|
|
474
|
+
): Promise<MainAccountInfoFetchResult> {
|
|
475
|
+
const nativeMainLease = existingNativeMainLease ?? tryAcquireNativeMainProfileClaim();
|
|
476
|
+
if (!nativeMainLease) {
|
|
477
|
+
return {
|
|
478
|
+
info: EMPTY_MAIN_ACCOUNT_INFO,
|
|
479
|
+
credentialChecked: false,
|
|
480
|
+
hasCredential: false,
|
|
481
|
+
identityGeneration: captureMainAccountIdentityGeneration(),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
try {
|
|
485
|
+
const operation = async () => ({
|
|
486
|
+
...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease),
|
|
487
|
+
identityGeneration: captureMainAccountIdentityGeneration(),
|
|
488
|
+
});
|
|
489
|
+
if (nativeMainSharedClaimHeld) return await operation();
|
|
490
|
+
try {
|
|
491
|
+
return await withNativeMainCredentialClaim(operation);
|
|
492
|
+
} catch (error) {
|
|
493
|
+
if (isNativeMainClaimUnavailable(error)) {
|
|
494
|
+
return {
|
|
495
|
+
info: EMPTY_MAIN_ACCOUNT_INFO,
|
|
496
|
+
credentialChecked: false,
|
|
497
|
+
hasCredential: false,
|
|
498
|
+
identityGeneration: captureMainAccountIdentityGeneration(),
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
} finally {
|
|
504
|
+
if (!existingNativeMainLease) nativeMainLease.release();
|
|
505
|
+
}
|
|
383
506
|
}
|
|
384
507
|
|
|
385
|
-
async function
|
|
508
|
+
async function fetchMainAccountInfoWhileOwned(
|
|
509
|
+
forceRefresh: boolean,
|
|
510
|
+
retriesRemaining: number,
|
|
511
|
+
nativeMainLease: AdmissionLease,
|
|
512
|
+
): Promise<MainAccountInfoFetchResult> {
|
|
386
513
|
const writerGeneration = captureConfigGeneration();
|
|
387
514
|
reconcileMainCodexAccountRuntimeState();
|
|
388
515
|
const tokenRead = readCodexTokensResult();
|
|
516
|
+
setMainAccountCredentialPresence(tokenRead.status === "ok");
|
|
389
517
|
if (tokenRead.status !== "ok") {
|
|
390
518
|
// A local read failure is NOT proof of sign-out: a missing file can be a non-atomic rewrite
|
|
391
519
|
// gap, and malformed JSON can be a half-written file. Clearing the cache and marking the
|
|
@@ -394,13 +522,13 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
|
|
|
394
522
|
// routing stays fail-closed because getMainAccountToken() re-reads the file itself, and the
|
|
395
523
|
// account DTO still reports hasCredential=false while the file is unreadable.
|
|
396
524
|
const preserved = getMainAccountInfoCache();
|
|
397
|
-
return { info: preserved ?? EMPTY_MAIN_ACCOUNT_INFO };
|
|
525
|
+
return { info: preserved ?? EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: false };
|
|
398
526
|
}
|
|
399
527
|
const tokens = tokenRead.tokens;
|
|
400
528
|
const requestAccountId = extractAccountId(tokens.id_token, tokens.access_token) ?? (tokens.account_id || null);
|
|
401
529
|
const cached = getMainAccountInfoCache();
|
|
402
530
|
if (!forceRefresh && cached && Date.now() - cached.ts < MAIN_CACHE_TTL) {
|
|
403
|
-
return { info: cached };
|
|
531
|
+
return { info: cached, credentialChecked: true, hasCredential: true };
|
|
404
532
|
}
|
|
405
533
|
try {
|
|
406
534
|
const resp = await fetch("https://chatgpt.com/backend-api/wham/usage", {
|
|
@@ -409,16 +537,16 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
|
|
|
409
537
|
});
|
|
410
538
|
if (!resp.ok) {
|
|
411
539
|
const terminalAuthFailure = await isTerminalMainAuthResponse(resp);
|
|
412
|
-
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining);
|
|
540
|
+
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease);
|
|
413
541
|
if (retried) return retried;
|
|
414
542
|
if (terminalAuthFailure) {
|
|
415
543
|
clearMainAccountInfoCache();
|
|
416
544
|
markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
|
|
417
545
|
}
|
|
418
|
-
return { info: EMPTY_MAIN_ACCOUNT_INFO };
|
|
546
|
+
return { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
|
|
419
547
|
}
|
|
420
548
|
const data = (await resp.json()) as WhamUsageResponse;
|
|
421
|
-
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining);
|
|
549
|
+
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease);
|
|
422
550
|
if (retried) return retried;
|
|
423
551
|
const plan = nonEmptyPlan(data.plan_type) ?? nonEmptyPlan(cached?.plan) ?? nonEmptyPlan(getMainAccountPlan());
|
|
424
552
|
const quota = parseUsageQuota({ ...data, ...(plan ? { plan_type: plan } : {}) });
|
|
@@ -439,12 +567,14 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
|
|
|
439
567
|
}
|
|
440
568
|
return {
|
|
441
569
|
info: result,
|
|
570
|
+
credentialChecked: true,
|
|
571
|
+
hasCredential: true,
|
|
442
572
|
...(quota ? { freshQuota: quota } : {}),
|
|
443
573
|
...(freshResetCredits !== undefined ? { freshResetCredits } : {}),
|
|
444
574
|
};
|
|
445
575
|
} catch {
|
|
446
|
-
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining);
|
|
447
|
-
return retried ?? { info: EMPTY_MAIN_ACCOUNT_INFO };
|
|
576
|
+
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease);
|
|
577
|
+
return retried ?? { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
|
|
448
578
|
}
|
|
449
579
|
}
|
|
450
580
|
|
|
@@ -689,6 +819,60 @@ async function fetchPoolAccountQuota(accountId: string, forceRefresh = false, co
|
|
|
689
819
|
}
|
|
690
820
|
|
|
691
821
|
let primeInFlight: Promise<void> | null = null;
|
|
822
|
+
let cooldownRecoveryInFlight: Promise<void> | null = null;
|
|
823
|
+
|
|
824
|
+
export async function runCodexCooldownRecoveryProbes(config: OcxConfig, now = Date.now()): Promise<void> {
|
|
825
|
+
const openai = config.providers[OPENAI_CODEX_PROVIDER_ID];
|
|
826
|
+
if (!openai
|
|
827
|
+
|| openai.disabled === true
|
|
828
|
+
|| !isCanonicalOpenAiForwardProvider(openai)
|
|
829
|
+
|| providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, openai) !== "pool") return;
|
|
830
|
+
if (cooldownRecoveryInFlight) return cooldownRecoveryInFlight;
|
|
831
|
+
cooldownRecoveryInFlight = (async () => {
|
|
832
|
+
const claims = claimDueCodexQuotaRecoveryProbes(config, POOL_QUOTA_REFRESH_CONCURRENCY, now);
|
|
833
|
+
await mapWithConcurrency(claims, POOL_QUOTA_REFRESH_CONCURRENCY, async claim => {
|
|
834
|
+
const account = configuredPoolAccount(config, claim.accountId);
|
|
835
|
+
if (!account) {
|
|
836
|
+
settleCodexQuotaRecoveryProbe(claim, false, {}, now);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
try {
|
|
840
|
+
const result = await fetchPoolAccountQuota(claim.accountId, true, account.plan);
|
|
841
|
+
// Defence in depth: `spark` is already excluded at the claim site, since generic WHAM
|
|
842
|
+
// cannot prove a spark recovery. Keep the settle-side guard so a future claim change
|
|
843
|
+
// cannot silently start clearing spark on generic evidence.
|
|
844
|
+
const recovered = claim.scope !== "spark"
|
|
845
|
+
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan);
|
|
846
|
+
settleCodexQuotaRecoveryProbe(claim, recovered, {
|
|
847
|
+
credentialGeneration: result.freshCredentialGeneration,
|
|
848
|
+
}, now);
|
|
849
|
+
} catch {
|
|
850
|
+
settleCodexQuotaRecoveryProbe(claim, false, {}, now);
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
})().catch(() => {
|
|
854
|
+
// Background recovery is best-effort; routing keeps the cooldown on failure.
|
|
855
|
+
}).finally(() => { cooldownRecoveryInFlight = null; });
|
|
856
|
+
return cooldownRecoveryInFlight;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
export function registerCodexCooldownRecoveryProbeWorker(config: OcxConfig): void {
|
|
860
|
+
registerStateSweepAfterTick({
|
|
861
|
+
name: "codex-cooldown-recovery",
|
|
862
|
+
afterTick: () => { void runCodexCooldownRecoveryProbes(config); },
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
export interface PrimeCodexPoolQuotasOptions {
|
|
867
|
+
/** Test seams for proving fenced/recovery priming performs no native-main work. */
|
|
868
|
+
reconcileMainAccount?: typeof reconcileMainCodexAccountRuntimeState;
|
|
869
|
+
readMainTokens?: typeof readCodexTokens;
|
|
870
|
+
fetchMainInfo?: typeof fetchMainAccountInfo;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function tryAcquireNativeMainPrimeLease(): AdmissionLease | null {
|
|
874
|
+
return tryAcquireNativeMainProfileClaim();
|
|
875
|
+
}
|
|
692
876
|
|
|
693
877
|
/**
|
|
694
878
|
* Best-effort prime of pool-account (and main) quota so the rotation engine has
|
|
@@ -705,7 +889,11 @@ let primeInFlight: Promise<void> | null = null;
|
|
|
705
889
|
* cost, so the worst case is one WHAM call per account per TTL window. Failures
|
|
706
890
|
* are swallowed: a blocked WSL network must never crash startup or a request.
|
|
707
891
|
*/
|
|
708
|
-
export async function primeCodexPoolQuotas(
|
|
892
|
+
export async function primeCodexPoolQuotas(
|
|
893
|
+
config: OcxConfig,
|
|
894
|
+
reason: string,
|
|
895
|
+
options: PrimeCodexPoolQuotasOptions = {},
|
|
896
|
+
): Promise<void> {
|
|
709
897
|
const openai = config.providers[OPENAI_CODEX_PROVIDER_ID];
|
|
710
898
|
if (
|
|
711
899
|
!openai
|
|
@@ -714,10 +902,6 @@ export async function primeCodexPoolQuotas(config: OcxConfig, reason: string): P
|
|
|
714
902
|
|| providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, openai) !== "pool"
|
|
715
903
|
) return;
|
|
716
904
|
if (primeInFlight) return primeInFlight;
|
|
717
|
-
// Seed the observed physical main identity before startup/lazy priming can populate quota or
|
|
718
|
-
// plan state. Otherwise the first post-startup account switch sees no previous identity and
|
|
719
|
-
// skips the purge that protects the stable __main__ alias.
|
|
720
|
-
reconcileMainCodexAccountRuntimeState();
|
|
721
905
|
primeInFlight = (async () => {
|
|
722
906
|
const runtimeConfig = getRuntimeConfig(config);
|
|
723
907
|
const pool = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
|
|
@@ -725,10 +909,30 @@ export async function primeCodexPoolQuotas(config: OcxConfig, reason: string): P
|
|
|
725
909
|
const q = getAccountQuota(a.id);
|
|
726
910
|
return !q || Date.now() - q.updatedAt >= POOL_CACHE_TTL;
|
|
727
911
|
});
|
|
728
|
-
const primeMain =
|
|
912
|
+
const primeMain = async () => {
|
|
913
|
+
const mainLease = tryAcquireNativeMainPrimeLease();
|
|
914
|
+
if (!mainLease) return;
|
|
915
|
+
try {
|
|
916
|
+
try {
|
|
917
|
+
await withNativeMainCredentialClaim(async () => {
|
|
918
|
+
// Keep one local owner and one cross-process reader from physical
|
|
919
|
+
// identity reconciliation through WHAM and all quota publication.
|
|
920
|
+
(options.reconcileMainAccount ?? reconcileMainCodexAccountRuntimeState)();
|
|
921
|
+
if (getAccountQuota(MAIN_CODEX_ACCOUNT_ID)) return;
|
|
922
|
+
if (!(options.readMainTokens ?? readCodexTokens)()) return;
|
|
923
|
+
if (options.fetchMainInfo) await options.fetchMainInfo(false);
|
|
924
|
+
else await fetchMainAccountInfoAttempt(false, 1, mainLease, true);
|
|
925
|
+
});
|
|
926
|
+
} catch (error) {
|
|
927
|
+
if (!isNativeMainClaimUnavailable(error)) throw error;
|
|
928
|
+
}
|
|
929
|
+
} finally {
|
|
930
|
+
mainLease.release();
|
|
931
|
+
}
|
|
932
|
+
};
|
|
729
933
|
try {
|
|
730
934
|
await Promise.allSettled([
|
|
731
|
-
primeMain
|
|
935
|
+
primeMain(),
|
|
732
936
|
mapWithConcurrency(stale, POOL_QUOTA_REFRESH_CONCURRENCY, async a => {
|
|
733
937
|
if (!getCodexAccountCredential(a.id)) return;
|
|
734
938
|
await fetchPoolAccountQuota(a.id, false, a.plan);
|
|
@@ -750,10 +954,27 @@ export function clearCodexQuotaPrimeState(): void {
|
|
|
750
954
|
primeInFlight = null;
|
|
751
955
|
}
|
|
752
956
|
|
|
753
|
-
|
|
957
|
+
/** Test-only reset for the worker-level single-flight. */
|
|
958
|
+
export function clearCodexCooldownRecoveryProbeState(): void {
|
|
959
|
+
cooldownRecoveryInFlight = null;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
export function effectiveCodexAuthAccountId(config: OcxConfig): string {
|
|
963
|
+
return getEffectiveActiveCodexAccountId(config) ?? MAIN_CODEX_ACCOUNT_ID;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
export interface CodexAuthAccountsSnapshot {
|
|
967
|
+
accounts: CodexAuthAccountDto[];
|
|
968
|
+
mainIdentityGeneration: number;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
export async function listCodexAuthAccountsSnapshot(
|
|
972
|
+
config: OcxConfig,
|
|
973
|
+
forceRefresh = false,
|
|
974
|
+
): Promise<CodexAuthAccountsSnapshot> {
|
|
754
975
|
const runtimeConfig = getRuntimeConfig(config);
|
|
755
976
|
const poolAccounts = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
|
|
756
|
-
const
|
|
977
|
+
const mainResult = await fetchMainAccountInfoAttempt(forceRefresh, 1);
|
|
757
978
|
const refreshedPool = await mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async account => {
|
|
758
979
|
const cred = getCodexAccountCredential(account.id);
|
|
759
980
|
let quotaResult: PoolQuotaResult;
|
|
@@ -816,8 +1037,14 @@ export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = fa
|
|
|
816
1037
|
isCodexAccountPaused(runtimeConfig, accountId),
|
|
817
1038
|
)];
|
|
818
1039
|
});
|
|
819
|
-
const
|
|
820
|
-
const
|
|
1040
|
+
const fetchedMainGeneration = mainResult.identityGeneration ?? captureMainAccountIdentityGeneration();
|
|
1041
|
+
const mainSnapshotLive = isMainAccountIdentityGenerationLive(fetchedMainGeneration);
|
|
1042
|
+
const mainInfo = mainSnapshotLive ? mainResult.info : EMPTY_MAIN_ACCOUNT_INFO;
|
|
1043
|
+
const hasMainCredential = mainSnapshotLive && mainResult.credentialChecked
|
|
1044
|
+
? mainResult.hasCredential
|
|
1045
|
+
: getMainAccountCredentialPresence() ?? false;
|
|
1046
|
+
const mainNeedsReauth = (mainSnapshotLive && mainResult.credentialChecked && !hasMainCredential)
|
|
1047
|
+
|| isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
821
1048
|
const mainHealth = projectCodexAccountHealth({
|
|
822
1049
|
accountId: MAIN_CODEX_ACCOUNT_ID,
|
|
823
1050
|
needsReauth: mainNeedsReauth,
|
|
@@ -830,10 +1057,24 @@ export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = fa
|
|
|
830
1057
|
paused: isCodexAccountPaused(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
831
1058
|
hasCredential: hasMainCredential,
|
|
832
1059
|
needsReauth: mainNeedsReauth,
|
|
833
|
-
quota: mainInfo.quota ? {
|
|
1060
|
+
quota: mainInfo.quota ? {
|
|
1061
|
+
...quotaForPlan({
|
|
1062
|
+
...mainInfo.quota,
|
|
1063
|
+
updatedAt: getAccountQuota(MAIN_CODEX_ACCOUNT_ID)?.updatedAt ?? Date.now(),
|
|
1064
|
+
}, mainInfo.plan),
|
|
1065
|
+
} : null,
|
|
834
1066
|
...oauthAccountHealthFields("codex", MAIN_CODEX_ACCOUNT_ID, mainHealth),
|
|
835
1067
|
};
|
|
836
|
-
return
|
|
1068
|
+
return {
|
|
1069
|
+
accounts: [main, ...withQuota],
|
|
1070
|
+
mainIdentityGeneration: mainSnapshotLive
|
|
1071
|
+
? fetchedMainGeneration
|
|
1072
|
+
: captureMainAccountIdentityGeneration(),
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = false): Promise<CodexAuthAccountDto[]> {
|
|
1077
|
+
return (await listCodexAuthAccountsSnapshot(config, forceRefresh)).accounts;
|
|
837
1078
|
}
|
|
838
1079
|
|
|
839
1080
|
interface PauseExhaustedResult {
|
|
@@ -846,57 +1087,95 @@ function selectFallbackAfterPause(config: OcxConfig, pausedActiveId: string): vo
|
|
|
846
1087
|
reconcileCodexActiveAfterExclusion(config, pausedActiveId);
|
|
847
1088
|
}
|
|
848
1089
|
|
|
849
|
-
async function pauseExhaustedCodexAccounts(
|
|
1090
|
+
async function pauseExhaustedCodexAccounts(
|
|
1091
|
+
config: OcxConfig,
|
|
1092
|
+
persistPausedAccounts: () => void,
|
|
1093
|
+
): Promise<PauseExhaustedResult> {
|
|
850
1094
|
const poolAccounts = (config.codexAccounts ?? []).filter(account => !account.isMain);
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1095
|
+
const nativeMainLease = tryAcquireNativeMainProfileClaim();
|
|
1096
|
+
try {
|
|
1097
|
+
const performPause = async (mainLease?: AdmissionLease): Promise<PauseExhaustedResult> => {
|
|
1098
|
+
const mainWork = async (): Promise<{
|
|
1099
|
+
shouldPause: boolean;
|
|
1100
|
+
checkedAccountCount: number;
|
|
1101
|
+
failedAccountCount: number;
|
|
1102
|
+
}> => {
|
|
1103
|
+
if (!mainLease) return { shouldPause: false, checkedAccountCount: 0, failedAccountCount: 1 };
|
|
1104
|
+
const mainResult = await fetchMainAccountInfoAttempt(true, 1, mainLease, true);
|
|
1105
|
+
if (!mainResult.credentialChecked || !mainResult.hasCredential) {
|
|
1106
|
+
return { shouldPause: false, checkedAccountCount: 0, failedAccountCount: 0 };
|
|
1107
|
+
}
|
|
1108
|
+
if (!mainResult.freshQuota || !mainResult.info.plan) {
|
|
1109
|
+
return { shouldPause: false, checkedAccountCount: 0, failedAccountCount: 1 };
|
|
1110
|
+
}
|
|
1111
|
+
return {
|
|
1112
|
+
shouldPause: !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
|
|
1113
|
+
&& isCodexQuotaExhausted(mainResult.freshQuota, mainResult.info.plan),
|
|
1114
|
+
checkedAccountCount: 1,
|
|
1115
|
+
failedAccountCount: 0,
|
|
1116
|
+
};
|
|
859
1117
|
};
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1118
|
+
const [mainResult, poolResults] = await Promise.all([
|
|
1119
|
+
mainWork(),
|
|
1120
|
+
mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async account => {
|
|
1121
|
+
if (!getCodexAccountCredential(account.id)) return { account, quotaResult: null };
|
|
1122
|
+
try {
|
|
1123
|
+
return {
|
|
1124
|
+
account,
|
|
1125
|
+
quotaResult: await fetchPoolAccountQuota(account.id, true, account.plan),
|
|
1126
|
+
};
|
|
1127
|
+
} catch {
|
|
1128
|
+
// Settle each pool probe independently so a busy/failing account cannot
|
|
1129
|
+
// abandon an already-confirmed main decision before atomic publication.
|
|
1130
|
+
return { account, quotaResult: null };
|
|
1131
|
+
}
|
|
1132
|
+
}),
|
|
1133
|
+
]);
|
|
1134
|
+
|
|
1135
|
+
let checkedAccountCount = mainResult.checkedAccountCount;
|
|
1136
|
+
let failedAccountCount = mainResult.failedAccountCount;
|
|
1137
|
+
const exhaustedIds: string[] = mainResult.shouldPause ? [MAIN_CODEX_ACCOUNT_ID] : [];
|
|
1138
|
+
for (const { account, quotaResult } of poolResults) {
|
|
1139
|
+
const currentAccount = (config.codexAccounts ?? []).find(candidate => candidate.id === account.id && !candidate.isMain);
|
|
1140
|
+
if (!currentAccount) continue;
|
|
1141
|
+
const generation = quotaResult?.freshCredentialGeneration;
|
|
1142
|
+
const plan = quotaResult?.freshPlan ?? currentAccount.plan;
|
|
1143
|
+
if (!quotaResult?.freshQuota || generation === undefined || !isCodexAccountGenerationLive(account.id, generation) || !plan) {
|
|
1144
|
+
failedAccountCount += 1;
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
checkedAccountCount += 1;
|
|
1148
|
+
if (!isCodexAccountPaused(config, account.id) && isCodexQuotaExhausted(quotaResult.freshQuota, plan)) {
|
|
1149
|
+
exhaustedIds.push(account.id);
|
|
1150
|
+
}
|
|
874
1151
|
}
|
|
875
|
-
} else {
|
|
876
|
-
failedAccountCount += 1;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
for (const { account, quotaResult } of poolResults) {
|
|
880
|
-
const currentAccount = (config.codexAccounts ?? []).find(candidate => candidate.id === account.id && !candidate.isMain);
|
|
881
|
-
if (!currentAccount) continue;
|
|
882
|
-
const generation = quotaResult?.freshCredentialGeneration;
|
|
883
|
-
const plan = quotaResult?.freshPlan ?? currentAccount.plan;
|
|
884
|
-
if (!quotaResult?.freshQuota || generation === undefined || !isCodexAccountGenerationLive(account.id, generation) || !plan) {
|
|
885
|
-
failedAccountCount += 1;
|
|
886
|
-
continue;
|
|
887
|
-
}
|
|
888
|
-
checkedAccountCount += 1;
|
|
889
|
-
if (!isCodexAccountPaused(config, account.id) && isCodexQuotaExhausted(quotaResult.freshQuota, plan)) {
|
|
890
|
-
exhaustedIds.push(account.id);
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
1152
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1153
|
+
for (const id of exhaustedIds) {
|
|
1154
|
+
setCodexAccountPaused(config, id, true);
|
|
1155
|
+
clearThreadAccountMapForAccount(id);
|
|
1156
|
+
}
|
|
1157
|
+
for (const id of exhaustedIds) selectFallbackAfterPause(config, id);
|
|
1158
|
+
const result = {
|
|
1159
|
+
pausedAccountIds: exhaustedIds,
|
|
1160
|
+
checkedAccountCount,
|
|
1161
|
+
failedAccountCount,
|
|
1162
|
+
};
|
|
1163
|
+
// Persist while both the in-process admission and cross-process shared
|
|
1164
|
+
// claim still own the physical-main identity used for the decision.
|
|
1165
|
+
if (result.pausedAccountIds.length > 0) persistPausedAccounts();
|
|
1166
|
+
return result;
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
if (!nativeMainLease) return await performPause();
|
|
1170
|
+
try {
|
|
1171
|
+
return await withNativeMainCredentialClaim(() => performPause(nativeMainLease));
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
if (isNativeMainClaimUnavailable(error)) return await performPause();
|
|
1174
|
+
throw error;
|
|
1175
|
+
}
|
|
1176
|
+
} finally {
|
|
1177
|
+
nativeMainLease?.release();
|
|
897
1178
|
}
|
|
898
|
-
for (const id of exhaustedIds) selectFallbackAfterPause(config, id);
|
|
899
|
-
return { pausedAccountIds: exhaustedIds, checkedAccountCount, failedAccountCount };
|
|
900
1179
|
}
|
|
901
1180
|
|
|
902
1181
|
export async function handleCodexAuthAPI(
|
|
@@ -1020,7 +1299,10 @@ export async function handleCodexAuthAPI(
|
|
|
1020
1299
|
|
|
1021
1300
|
if (url.pathname === "/api/codex-auth/accounts/pause-exhausted" && req.method === "PUT") {
|
|
1022
1301
|
const runtimeConfig = getRuntimeConfig(config);
|
|
1023
|
-
const result = await pauseExhaustedCodexAccounts(
|
|
1302
|
+
const result = await pauseExhaustedCodexAccounts(
|
|
1303
|
+
runtimeConfig,
|
|
1304
|
+
() => saveRuntimeConfig(config, runtimeConfig),
|
|
1305
|
+
);
|
|
1024
1306
|
const { pausedAccountIds, checkedAccountCount, failedAccountCount } = result;
|
|
1025
1307
|
if (checkedAccountCount === 0 && failedAccountCount > 0) {
|
|
1026
1308
|
return jsonResponse({
|
|
@@ -1030,7 +1312,6 @@ export async function handleCodexAuthAPI(
|
|
|
1030
1312
|
failedAccountCount,
|
|
1031
1313
|
}, 502);
|
|
1032
1314
|
}
|
|
1033
|
-
if (pausedAccountIds.length > 0) saveRuntimeConfig(config, runtimeConfig);
|
|
1034
1315
|
return jsonResponse({
|
|
1035
1316
|
ok: true,
|
|
1036
1317
|
pausedAccountIds,
|
|
@@ -1169,24 +1450,24 @@ export async function handleCodexAuthAPI(
|
|
|
1169
1450
|
if (!accountId) return jsonResponse({ error: "accountId required" }, 400);
|
|
1170
1451
|
|
|
1171
1452
|
try {
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1453
|
+
const result = await withResetCreditAuth(getRuntimeConfig(config), accountId, async auth => {
|
|
1454
|
+
const resp = await fetch(
|
|
1455
|
+
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
|
|
1456
|
+
{
|
|
1457
|
+
headers: {
|
|
1458
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
1459
|
+
"ChatGPT-Account-Id": auth.chatgptAccountId,
|
|
1460
|
+
},
|
|
1461
|
+
signal: AbortSignal.timeout(8000),
|
|
1181
1462
|
},
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
return jsonResponse(
|
|
1188
|
-
}
|
|
1189
|
-
return
|
|
1463
|
+
);
|
|
1464
|
+
if (!resp.ok) {
|
|
1465
|
+
await resp.body?.cancel().catch(() => {});
|
|
1466
|
+
return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
|
|
1467
|
+
}
|
|
1468
|
+
return jsonResponse(safeResetCreditsDto(await resp.json()));
|
|
1469
|
+
});
|
|
1470
|
+
return result.ok ? result.value : result.response;
|
|
1190
1471
|
} catch (e) {
|
|
1191
1472
|
return jsonResponse({ error: e instanceof Error ? e.message : "Reset credit lookup failed" }, 500);
|
|
1192
1473
|
}
|
|
@@ -1195,49 +1476,55 @@ export async function handleCodexAuthAPI(
|
|
|
1195
1476
|
if (url.pathname === "/api/codex-auth/reset-credits/consume" && req.method === "POST") {
|
|
1196
1477
|
const body = (await req.json().catch(() => ({}))) as { accountId?: string };
|
|
1197
1478
|
if (!body.accountId) return jsonResponse({ error: "accountId required" }, 400);
|
|
1479
|
+
const accountId = body.accountId;
|
|
1198
1480
|
|
|
1199
1481
|
try {
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1482
|
+
const operation = await withResetCreditAuth(getRuntimeConfig(config), accountId, async auth => {
|
|
1483
|
+
const idempotencyKey = crypto.randomUUID();
|
|
1484
|
+
const resp = await fetch(
|
|
1485
|
+
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
|
|
1486
|
+
{
|
|
1487
|
+
method: "POST",
|
|
1488
|
+
headers: {
|
|
1489
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
1490
|
+
"ChatGPT-Account-Id": auth.chatgptAccountId,
|
|
1491
|
+
"Content-Type": "application/json",
|
|
1492
|
+
},
|
|
1493
|
+
body: JSON.stringify({ redeem_request_id: idempotencyKey }),
|
|
1494
|
+
signal: AbortSignal.timeout(10_000),
|
|
1212
1495
|
},
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
if (!resp.ok) {
|
|
1218
|
-
await resp.body?.cancel().catch(() => {});
|
|
1219
|
-
return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
|
|
1220
|
-
}
|
|
1221
|
-
const result = safeResetCreditConsumeDto(await resp.json());
|
|
1222
|
-
// After a successful redeem (or an idempotent already_redeemed), refresh WHAM usage
|
|
1223
|
-
// and return remaining only when that refresh freshly parsed available_count.
|
|
1224
|
-
// Do not fall back to a preserved cached resetCredits (failed/omitted refresh).
|
|
1225
|
-
if (result.code === "reset" || result.code === "already_redeemed") {
|
|
1226
|
-
let freshResetCredits: number | undefined;
|
|
1227
|
-
if (auth.isMain) {
|
|
1228
|
-
({ freshResetCredits } = await fetchMainAccountInfoAttempt(true, 1));
|
|
1229
|
-
} else {
|
|
1230
|
-
const account = configuredPoolAccount(getRuntimeConfig(config), body.accountId);
|
|
1231
|
-
({ freshResetCredits } = await fetchPoolAccountQuota(body.accountId, true, account?.plan));
|
|
1496
|
+
);
|
|
1497
|
+
if (!resp.ok) {
|
|
1498
|
+
await resp.body?.cancel().catch(() => {});
|
|
1499
|
+
return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
|
|
1232
1500
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1501
|
+
const result = safeResetCreditConsumeDto(await resp.json());
|
|
1502
|
+
// After a successful redeem (or an idempotent already_redeemed), refresh WHAM usage
|
|
1503
|
+
// and return remaining only when that refresh freshly parsed available_count.
|
|
1504
|
+
// Do not fall back to a preserved cached resetCredits (failed/omitted refresh).
|
|
1505
|
+
if (result.code === "reset" || result.code === "already_redeemed") {
|
|
1506
|
+
let freshResetCredits: number | undefined;
|
|
1507
|
+
if (auth.isMain) {
|
|
1508
|
+
({ freshResetCredits } = await fetchMainAccountInfoAttempt(
|
|
1509
|
+
true,
|
|
1510
|
+
1,
|
|
1511
|
+
auth.nativeMainLease,
|
|
1512
|
+
auth.nativeMainSharedClaimHeld === true,
|
|
1513
|
+
));
|
|
1514
|
+
} else {
|
|
1515
|
+
const account = configuredPoolAccount(getRuntimeConfig(config), accountId);
|
|
1516
|
+
({ freshResetCredits } = await fetchPoolAccountQuota(accountId, true, account?.plan));
|
|
1517
|
+
}
|
|
1518
|
+
return jsonResponse({
|
|
1519
|
+
code: result.code,
|
|
1520
|
+
...(typeof freshResetCredits === "number" && Number.isFinite(freshResetCredits)
|
|
1521
|
+
? { remaining: freshResetCredits }
|
|
1522
|
+
: {}),
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
return jsonResponse(result);
|
|
1526
|
+
});
|
|
1527
|
+
return operation.ok ? operation.value : operation.response;
|
|
1241
1528
|
} catch (e) {
|
|
1242
1529
|
if (e instanceof PoolQuotaProbeBusyError) {
|
|
1243
1530
|
const response = jsonResponse({ error: "server_busy", code: "server_busy" }, 503);
|