@bitkyc08/opencodex 2.55.0 → 2.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import { codexAccountLogLabel } from "../account-label";
|
|
2
|
+
import { getCodexAccountCredential, getValidCodexToken, isCodexAccountGenerationLive, readCodexAccountRecord } from "../account-store";
|
|
3
|
+
import { getAccountQuota, isCodexQuotaExhausted, setAccountQuotaFromParsed, withoutRetiredCodexQuota } from "../quota";
|
|
4
|
+
import type { StoredAccountQuota } from "../quota";
|
|
5
|
+
import { ConfigMutationLockError, mutatePersistedConfig } from "../../config";
|
|
6
|
+
import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
|
|
7
|
+
import { isCodexAccountPaused, setCodexAccountPaused } from "../account-pause";
|
|
8
|
+
import { getCodexAccountPriority } from "../account-priority";
|
|
9
|
+
import { clearThreadAccountMapForAccount, isCodexAccountPlanExcluded, reconcileCodexActiveAfterExclusion } from "../routing";
|
|
10
|
+
import { codexPlanValue, isThirtyDayOnlyCodexPlan } from "../plan";
|
|
11
|
+
import { isAccountNeedsReauth, markAccountNeedsReauth } from "../account-runtime-state";
|
|
12
|
+
import { getValidMainAccountToken, MainAccountTokenRefreshError, MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
13
|
+
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
|
|
14
|
+
import { captureMainAccountIdentityGeneration, getMainAccountCredentialPresence, isMainAccountIdentityGenerationLive } from "../main-account-cache";
|
|
15
|
+
import type { CodexQuotaRefreshOutcome } from "../quota-refresh-outcome";
|
|
16
|
+
import { getMainAccountHardLockStatus } from "../main-account-hard-lock";
|
|
17
|
+
import type { MainAccountHardLockStatus } from "../main-account-hard-lock";
|
|
18
|
+
import { emailMaskingEnabled, projectEmail } from "../../lib/privacy";
|
|
19
|
+
import type { CodexAccount, OcxConfig } from "../../types";
|
|
20
|
+
import { oauthAccountHealthFields, projectCodexAccountHealth } from "../../oauth/health";
|
|
21
|
+
import type { OAuthAccountHealth, OAuthHealthLabel } from "../../oauth/health";
|
|
22
|
+
import { isSelectableCodexPoolAccount } from "../account-id";
|
|
23
|
+
import type { AdmissionLease } from "../../lib/admission";
|
|
24
|
+
import { tryAcquireNativeMainProfileClaim } from "../native-main-admission";
|
|
25
|
+
import { withNativeMainCredentialClaim, isNativeMainClaimUnavailable } from "./http";
|
|
26
|
+
import { fetchMainAccountInfoAttempt, EMPTY_MAIN_ACCOUNT_INFO, mainResetCreditsForCurrentIdentity } from "./main-account-probe";
|
|
27
|
+
import { fetchPoolAccountQuota, PoolQuotaProbeBusyError, POOL_QUOTA_REFRESH_CONCURRENCY } from "./pool-quota-probe";
|
|
28
|
+
import type { PoolQuotaResult } from "./pool-quota-probe";
|
|
29
|
+
import { getRuntimeConfig, configuredPoolAccount, mapWithConcurrency } from "./runtime-config";
|
|
30
|
+
|
|
31
|
+
export function quotaForPlan<T extends Omit<StoredAccountQuota, "updatedAt"> | StoredAccountQuota | null>(
|
|
32
|
+
quota: T,
|
|
33
|
+
plan: unknown,
|
|
34
|
+
): T | null {
|
|
35
|
+
const visible = withoutRetiredCodexQuota(quota);
|
|
36
|
+
if (!visible || !isThirtyDayOnlyCodexPlan(plan)) return visible;
|
|
37
|
+
const quotaWindows = visible;
|
|
38
|
+
return {
|
|
39
|
+
...(quotaWindows.monthlyPercent !== undefined ? { monthlyPercent: quotaWindows.monthlyPercent } : {}),
|
|
40
|
+
...(quotaWindows.monthlyResetAt !== undefined ? { monthlyResetAt: quotaWindows.monthlyResetAt } : {}),
|
|
41
|
+
// A 30-day plan can still carry a burst window, and it blocks the account on its own.
|
|
42
|
+
// Dropping it here would show a healthy card for an account upstream is refusing (#1791).
|
|
43
|
+
...(quotaWindows.shortPercent !== undefined ? { shortPercent: quotaWindows.shortPercent } : {}),
|
|
44
|
+
...(quotaWindows.shortResetAt !== undefined ? { shortResetAt: quotaWindows.shortResetAt } : {}),
|
|
45
|
+
...(quotaWindows.shortWindowSeconds !== undefined ? { shortWindowSeconds: quotaWindows.shortWindowSeconds } : {}),
|
|
46
|
+
...(quotaWindows.customWindows !== undefined ? { customWindows: quotaWindows.customWindows } : {}),
|
|
47
|
+
...(quotaWindows.resetCredits !== undefined ? { resetCredits: quotaWindows.resetCredits } : {}),
|
|
48
|
+
...("updatedAt" in quotaWindows ? { updatedAt: quotaWindows.updatedAt } : {}),
|
|
49
|
+
} as T;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The main account is the only account whose DTO quota comes from the raw WHAM parse
|
|
54
|
+
* result instead of the merged store: `poolAccountDto` serializes what
|
|
55
|
+
* `commitPoolQuotaResponse` read back out of `getAccountQuota()`, while the main DTO
|
|
56
|
+
* spreads `mainInfo.quota` directly. `/wham/usage` carries `rate_limit_reset_credits`
|
|
57
|
+
* only intermittently, and the store exists to bridge that gap
|
|
58
|
+
* (`setAccountQuotaFromParsed` carries an existing `resetCredits` forward when the new
|
|
59
|
+
* snapshot omits it), so the main card lost its ticket badge on every response that
|
|
60
|
+
* happened to omit the summary while pool cards kept theirs.
|
|
61
|
+
*
|
|
62
|
+
* Only `resetCredits` is carried, deliberately, and only from an identity-tagged
|
|
63
|
+
* in-process observation rather than the alias-keyed store. The window fields have
|
|
64
|
+
* *clearing* semantics — a monthly-only snapshot must drop a stale weekly value (#382) —
|
|
65
|
+
* so reinstating the whole stored object would resurrect a window the parse meant to
|
|
66
|
+
* clear whenever the store write was refused by generation gating. A freshly parsed value
|
|
67
|
+
* always wins, including `0`: zero is defined, so it never takes the fill branch.
|
|
68
|
+
*/
|
|
69
|
+
export function mainQuotaWithCarriedResetCredits(
|
|
70
|
+
parsed: Omit<StoredAccountQuota, "updatedAt">,
|
|
71
|
+
): StoredAccountQuota {
|
|
72
|
+
const carried = parsed.resetCredits === undefined
|
|
73
|
+
? mainResetCreditsForCurrentIdentity()
|
|
74
|
+
: undefined;
|
|
75
|
+
return {
|
|
76
|
+
...parsed,
|
|
77
|
+
...(carried !== undefined ? { resetCredits: carried } : {}),
|
|
78
|
+
updatedAt: getAccountQuota(MAIN_CODEX_ACCOUNT_ID)?.updatedAt ?? Date.now(),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Why an account needs the operator. `missing_credential`, `refresh_failed`, and
|
|
84
|
+
* `quota_unauthorized` are the three causes this surface tells apart on its own. `unauthorized`
|
|
85
|
+
* and `forbidden` exist because the shared health projection may return them; today
|
|
86
|
+
* `projectCodexAccountHealth` only ever produces `refresh_failed`, so accepting the full union
|
|
87
|
+
* keeps this field correct if that projection widens rather than silently dropping a reason.
|
|
88
|
+
*/
|
|
89
|
+
export type CodexAccountReauthReason =
|
|
90
|
+
| "missing_credential"
|
|
91
|
+
| "refresh_failed"
|
|
92
|
+
| "quota_unauthorized"
|
|
93
|
+
| "unauthorized"
|
|
94
|
+
| "forbidden";
|
|
95
|
+
|
|
96
|
+
export function poolAccountDto(
|
|
97
|
+
config: OcxConfig,
|
|
98
|
+
account: CodexAccount,
|
|
99
|
+
quotaResult: PoolQuotaResult,
|
|
100
|
+
hasCredential: boolean,
|
|
101
|
+
paused: boolean,
|
|
102
|
+
priority: number,
|
|
103
|
+
maskEmails: boolean,
|
|
104
|
+
): CodexAuthAccountDto {
|
|
105
|
+
const plan = codexPlanValue(account.plan);
|
|
106
|
+
const quota = quotaForPlan(quotaResult.quota, plan);
|
|
107
|
+
const runtimeReauth = isAccountNeedsReauth(account.id);
|
|
108
|
+
const needsReauth = !hasCredential || quotaResult.needsReauth || runtimeReauth;
|
|
109
|
+
const health = projectCodexAccountHealth({ accountId: account.id, needsReauth });
|
|
110
|
+
// `needsReauth` is an OR of three independent causes plus a persisted verdict resolved inside the
|
|
111
|
+
// health projection. Emitting only the boolean is what left #4212's reporter guessing which
|
|
112
|
+
// account took their model away and why, so name the cause they actually have to act on.
|
|
113
|
+
const reauthReason: CodexAccountReauthReason | undefined = !hasCredential
|
|
114
|
+
? "missing_credential"
|
|
115
|
+
: runtimeReauth
|
|
116
|
+
? "refresh_failed"
|
|
117
|
+
: quotaResult.needsReauth
|
|
118
|
+
? "quota_unauthorized"
|
|
119
|
+
: health.status === "reauth_required" ? health.reason : undefined;
|
|
120
|
+
return {
|
|
121
|
+
id: account.id,
|
|
122
|
+
email: projectEmail(account.email, maskEmails) ?? account.email,
|
|
123
|
+
...(account.alias !== undefined ? { alias: account.alias } : {}),
|
|
124
|
+
...(plan !== undefined ? { plan } : {}),
|
|
125
|
+
logLabel: codexAccountLogLabel(account),
|
|
126
|
+
isMain: false,
|
|
127
|
+
paused,
|
|
128
|
+
priority,
|
|
129
|
+
quota: quota ? { ...quota } : null,
|
|
130
|
+
needsReauth: needsReauth || health.status === "reauth_required",
|
|
131
|
+
...(reauthReason !== undefined ? { reauthReason } : {}),
|
|
132
|
+
...(isCodexAccountPlanExcluded(config, account.id) ? {
|
|
133
|
+
selectionExcludedReason: "plan_excluded" as const,
|
|
134
|
+
selectionExcludedPlan: codexPlanValue(config.codexAccounts?.find(row => row.id === account.id)?.plan),
|
|
135
|
+
} : {}),
|
|
136
|
+
hasCredential,
|
|
137
|
+
...(quotaResult.quotaProbeSkipped ? { quotaProbeSkipped: true as const } : {}),
|
|
138
|
+
...oauthAccountHealthFields("codex", account.id, health),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface CodexAuthAccountDto {
|
|
143
|
+
id: string;
|
|
144
|
+
alias?: string;
|
|
145
|
+
email: string;
|
|
146
|
+
plan?: string | null;
|
|
147
|
+
logLabel?: string;
|
|
148
|
+
isMain: boolean;
|
|
149
|
+
paused: boolean;
|
|
150
|
+
/** Selection order; higher is used earlier. Always present, 0 when unset. */
|
|
151
|
+
priority: number;
|
|
152
|
+
quota: (StoredAccountQuota | (Omit<StoredAccountQuota, "updatedAt"> & { updatedAt: number })) | null;
|
|
153
|
+
needsReauth?: boolean;
|
|
154
|
+
/**
|
|
155
|
+
* Which of the independent causes behind `needsReauth` fired. Present only when the account
|
|
156
|
+
* needs the operator; `/api/oauth/accounts` already carries the same field name.
|
|
157
|
+
*/
|
|
158
|
+
reauthReason?: CodexAccountReauthReason;
|
|
159
|
+
/** Automatic selection policy only; explicit routes retain their usual auth checks. */
|
|
160
|
+
selectionExcludedReason?: "plan_excluded";
|
|
161
|
+
selectionExcludedPlan?: string;
|
|
162
|
+
hasCredential: boolean;
|
|
163
|
+
health: OAuthAccountHealth;
|
|
164
|
+
healthLabel: OAuthHealthLabel;
|
|
165
|
+
healthSummary: string;
|
|
166
|
+
healthAction?: string;
|
|
167
|
+
quotaProbeSkipped?: true;
|
|
168
|
+
quotaRefresh?: CodexQuotaRefreshOutcome;
|
|
169
|
+
mainAccountHardLock?: MainAccountHardLockStatus;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface FreshPoolPlanUpdate {
|
|
173
|
+
accountId: string;
|
|
174
|
+
plan: string;
|
|
175
|
+
credentialGeneration: number;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Persist only validated plan leaves against the latest disk snapshot. A quota GET must not save
|
|
180
|
+
* the long-lived runtime object wholesale: unrelated manual/provider writes may have landed while
|
|
181
|
+
* WHAM requests were in flight. Missing or malformed files fail closed: a read path must not
|
|
182
|
+
* recreate a deleted config from the server's older in-memory snapshot.
|
|
183
|
+
*/
|
|
184
|
+
export function reconcileFreshPoolAccountPlans(runtimeConfig: OcxConfig, updates: FreshPoolPlanUpdate[]): void {
|
|
185
|
+
if (updates.length === 0) return;
|
|
186
|
+
let outcome: ReturnType<typeof mutatePersistedConfig<FreshPoolPlanUpdate[]>>;
|
|
187
|
+
try {
|
|
188
|
+
outcome = mutatePersistedConfig(persistedConfig => {
|
|
189
|
+
const accepted: FreshPoolPlanUpdate[] = [];
|
|
190
|
+
let changed = false;
|
|
191
|
+
for (const update of updates) {
|
|
192
|
+
if (!isCodexAccountGenerationLive(update.accountId, update.credentialGeneration)) continue;
|
|
193
|
+
const liveAccount = configuredPoolAccount(runtimeConfig, update.accountId);
|
|
194
|
+
const persistedAccount = configuredPoolAccount(persistedConfig, update.accountId);
|
|
195
|
+
if (!liveAccount || !persistedAccount) continue;
|
|
196
|
+
accepted.push(update);
|
|
197
|
+
if (persistedAccount.plan !== update.plan) {
|
|
198
|
+
persistedAccount.plan = update.plan;
|
|
199
|
+
// WHAM is the authoritative plan source: stamp provenance so a later JWT
|
|
200
|
+
// reconcile cannot overwrite this observation within the same credential
|
|
201
|
+
// generation (src/codex/plan-from-token.ts jwtMayWritePlan). Stamped only
|
|
202
|
+
// alongside a real plan change: a steady-state refresh whose plan is
|
|
203
|
+
// unchanged must stay write-free (no-config-write contract), and an
|
|
204
|
+
// unchanged value needs no fence — a JWT rewrite to the same text is a
|
|
205
|
+
// no-op under the caller's own equality check.
|
|
206
|
+
persistedAccount.planSource = "wham";
|
|
207
|
+
persistedAccount.planCredentialGeneration = update.credentialGeneration;
|
|
208
|
+
changed = true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return { changed, value: accepted };
|
|
212
|
+
});
|
|
213
|
+
} catch (error) {
|
|
214
|
+
// Plan persistence is derived metadata on a read route. Contention must fail closed without
|
|
215
|
+
// turning account listing into a 500; a later refresh can retry against the latest files.
|
|
216
|
+
if (error instanceof ConfigMutationLockError) return;
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
if (outcome.status === "unavailable") return;
|
|
220
|
+
for (const update of outcome.value) {
|
|
221
|
+
// A replacement immediately after the durable commit is allowed to supersede the result, but
|
|
222
|
+
// the long-lived object must never be updated from that stale generation.
|
|
223
|
+
if (!isCodexAccountGenerationLive(update.accountId, update.credentialGeneration)) continue;
|
|
224
|
+
const liveAccount = configuredPoolAccount(runtimeConfig, update.accountId);
|
|
225
|
+
if (liveAccount) {
|
|
226
|
+
liveAccount.plan = update.plan;
|
|
227
|
+
liveAccount.planSource = "wham";
|
|
228
|
+
liveAccount.planCredentialGeneration = update.credentialGeneration;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface CodexAuthAccountsSnapshot {
|
|
234
|
+
accounts: CodexAuthAccountDto[];
|
|
235
|
+
mainIdentityGeneration: number;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function listCodexAuthAccountsSnapshot(
|
|
239
|
+
config: OcxConfig,
|
|
240
|
+
forceRefresh = false,
|
|
241
|
+
options: { validatePending?: boolean } = {},
|
|
242
|
+
): Promise<CodexAuthAccountsSnapshot> {
|
|
243
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
244
|
+
const poolAccounts = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
|
|
245
|
+
// One redaction decision for the whole snapshot, read once from the operator's config (#3859).
|
|
246
|
+
const maskEmails = emailMaskingEnabled(runtimeConfig);
|
|
247
|
+
const mainResult = await fetchMainAccountInfoAttempt(forceRefresh, 1);
|
|
248
|
+
const refreshedPool = await mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async account => {
|
|
249
|
+
const cred = getCodexAccountCredential(account.id);
|
|
250
|
+
let quotaResult: PoolQuotaResult;
|
|
251
|
+
if (!cred) {
|
|
252
|
+
quotaResult = { quota: null, needsReauth: true };
|
|
253
|
+
} else {
|
|
254
|
+
try {
|
|
255
|
+
quotaResult = await fetchPoolAccountQuota(account.id, forceRefresh, account.plan, getValidCodexToken, options.validatePending === true);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (!(error instanceof PoolQuotaProbeBusyError)) throw error;
|
|
258
|
+
quotaResult = {
|
|
259
|
+
quota: getAccountQuota(account.id),
|
|
260
|
+
needsReauth: false,
|
|
261
|
+
credentialGeneration: readCodexAccountRecord(account.id)?.generation,
|
|
262
|
+
quotaProbeSkipped: true,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return { accountId: account.id, quotaResult };
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// WHAM plan_type is authoritative only for the credential generation that fetched it. Collect
|
|
270
|
+
// changes after every parallel read settles, then apply one narrow disk patch for the batch.
|
|
271
|
+
const planUpdates = refreshedPool.flatMap(({ accountId, quotaResult }): FreshPoolPlanUpdate[] => {
|
|
272
|
+
const plan = quotaResult.freshPlan;
|
|
273
|
+
const credentialGeneration = quotaResult.freshCredentialGeneration;
|
|
274
|
+
return plan && credentialGeneration !== undefined
|
|
275
|
+
? [{ accountId, plan, credentialGeneration }]
|
|
276
|
+
: [];
|
|
277
|
+
});
|
|
278
|
+
reconcileFreshPoolAccountPlans(runtimeConfig, planUpdates);
|
|
279
|
+
|
|
280
|
+
const withQuota = refreshedPool.flatMap(({ accountId, quotaResult }) => {
|
|
281
|
+
const currentAccount = configuredPoolAccount(runtimeConfig, accountId);
|
|
282
|
+
if (!currentAccount) return [];
|
|
283
|
+
const currentCredential = getCodexAccountCredential(accountId);
|
|
284
|
+
if (!currentCredential) {
|
|
285
|
+
return [poolAccountDto(
|
|
286
|
+
runtimeConfig,
|
|
287
|
+
currentAccount,
|
|
288
|
+
{ quota: null, needsReauth: true },
|
|
289
|
+
false,
|
|
290
|
+
isCodexAccountPaused(runtimeConfig, accountId),
|
|
291
|
+
getCodexAccountPriority(runtimeConfig, accountId),
|
|
292
|
+
maskEmails,
|
|
293
|
+
)];
|
|
294
|
+
}
|
|
295
|
+
const resultGeneration = quotaResult.credentialGeneration ?? quotaResult.freshCredentialGeneration;
|
|
296
|
+
const generationLive = resultGeneration === undefined
|
|
297
|
+
|| isCodexAccountGenerationLive(accountId, resultGeneration);
|
|
298
|
+
const effectiveQuotaResult = !generationLive
|
|
299
|
+
? { quota: null, needsReauth: false }
|
|
300
|
+
: quotaResult;
|
|
301
|
+
// Response DTO can show the WHAM plan even when disk persistence fails closed (lock busy /
|
|
302
|
+
// missing config). Persistence still remains generation-gated via reconcileFreshPoolAccountPlans.
|
|
303
|
+
const dtoAccount = generationLive && quotaResult.freshPlan
|
|
304
|
+
? { ...currentAccount, plan: quotaResult.freshPlan }
|
|
305
|
+
: currentAccount;
|
|
306
|
+
return [poolAccountDto(
|
|
307
|
+
runtimeConfig,
|
|
308
|
+
dtoAccount,
|
|
309
|
+
effectiveQuotaResult,
|
|
310
|
+
true,
|
|
311
|
+
isCodexAccountPaused(runtimeConfig, accountId),
|
|
312
|
+
getCodexAccountPriority(runtimeConfig, accountId),
|
|
313
|
+
maskEmails,
|
|
314
|
+
)];
|
|
315
|
+
});
|
|
316
|
+
const fetchedMainGeneration = mainResult.identityGeneration ?? captureMainAccountIdentityGeneration();
|
|
317
|
+
const mainSnapshotLive = isMainAccountIdentityGenerationLive(fetchedMainGeneration);
|
|
318
|
+
const mainInfo = mainSnapshotLive ? mainResult.info : EMPTY_MAIN_ACCOUNT_INFO;
|
|
319
|
+
const hasMainCredential = mainSnapshotLive && mainResult.credentialChecked
|
|
320
|
+
? mainResult.hasCredential
|
|
321
|
+
: getMainAccountCredentialPresence() ?? false;
|
|
322
|
+
const mainMissingCredential = mainSnapshotLive && mainResult.credentialChecked && !hasMainCredential;
|
|
323
|
+
const mainNeedsReauth = mainMissingCredential || isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
324
|
+
const mainHealth = projectCodexAccountHealth({
|
|
325
|
+
accountId: MAIN_CODEX_ACCOUNT_ID,
|
|
326
|
+
needsReauth: mainNeedsReauth,
|
|
327
|
+
});
|
|
328
|
+
// The main row carries the same attribution as a pool row. Reaching this point without
|
|
329
|
+
// `mainMissingCredential` means the runtime reauth flag is what set `mainNeedsReauth`, so the
|
|
330
|
+
// cause is a refresh that did not complete.
|
|
331
|
+
const mainReauthReason: CodexAccountReauthReason | undefined = mainMissingCredential
|
|
332
|
+
? "missing_credential"
|
|
333
|
+
: mainNeedsReauth
|
|
334
|
+
? "refresh_failed"
|
|
335
|
+
: mainHealth.status === "reauth_required" ? mainHealth.reason : undefined;
|
|
336
|
+
const main: CodexAuthAccountDto = {
|
|
337
|
+
id: MAIN_CODEX_ACCOUNT_ID,
|
|
338
|
+
email: projectEmail(mainInfo.email, maskEmails) ?? "Codex App login",
|
|
339
|
+
plan: mainInfo.plan,
|
|
340
|
+
...(mainSnapshotLive && mainResult.quotaRefresh && mainResult.quotaRefreshGeneration !== undefined
|
|
341
|
+
&& isMainAccountIdentityGenerationLive(mainResult.quotaRefreshGeneration)
|
|
342
|
+
? { quotaRefresh: mainResult.quotaRefresh } : {}),
|
|
343
|
+
logLabel: "main",
|
|
344
|
+
isMain: true,
|
|
345
|
+
paused: isCodexAccountPaused(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
346
|
+
mainAccountHardLock: getMainAccountHardLockStatus(runtimeConfig),
|
|
347
|
+
priority: getCodexAccountPriority(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
348
|
+
hasCredential: hasMainCredential,
|
|
349
|
+
needsReauth: mainNeedsReauth,
|
|
350
|
+
...(mainReauthReason !== undefined ? { reauthReason: mainReauthReason } : {}),
|
|
351
|
+
quota: mainInfo.quota
|
|
352
|
+
? quotaForPlan(mainQuotaWithCarriedResetCredits(mainInfo.quota), mainInfo.plan)
|
|
353
|
+
: null,
|
|
354
|
+
...oauthAccountHealthFields("codex", MAIN_CODEX_ACCOUNT_ID, mainHealth),
|
|
355
|
+
};
|
|
356
|
+
return {
|
|
357
|
+
accounts: [main, ...withQuota],
|
|
358
|
+
mainIdentityGeneration: mainSnapshotLive
|
|
359
|
+
? fetchedMainGeneration
|
|
360
|
+
: captureMainAccountIdentityGeneration(),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** One opted-in account's metadata; reuse the bounded WHAM 401 recovery and generation fence. */
|
|
365
|
+
export async function refreshCodexQuotaForActivation(config: OcxConfig, accountId: string): Promise<void> {
|
|
366
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
367
|
+
const lease = tryAcquireNativeMainProfileClaim();
|
|
368
|
+
if (!lease) return;
|
|
369
|
+
try {
|
|
370
|
+
reconcileMainCodexAccountRuntimeState();
|
|
371
|
+
if (isAccountNeedsReauth(accountId)) return;
|
|
372
|
+
const identityGeneration = captureMainAccountIdentityGeneration();
|
|
373
|
+
const writerGeneration = captureConfigGeneration();
|
|
374
|
+
try {
|
|
375
|
+
// Refresh may need an exclusive claim; prepare before WHAM takes its shared claim.
|
|
376
|
+
if (!await getValidMainAccountToken({ preserveReauth: true })) return;
|
|
377
|
+
} catch (error) {
|
|
378
|
+
if (error instanceof MainAccountTokenRefreshError && error.reason === "reauth"
|
|
379
|
+
&& isMainAccountIdentityGenerationLive(identityGeneration)) {
|
|
380
|
+
markAccountNeedsReauth(accountId, writerGeneration);
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (isAccountNeedsReauth(accountId)) return;
|
|
385
|
+
await fetchMainAccountInfoAttempt(true, 1, lease, false, false);
|
|
386
|
+
} finally {
|
|
387
|
+
lease.release();
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const account = configuredPoolAccount(config, accountId);
|
|
392
|
+
if (!account) return;
|
|
393
|
+
const writerGeneration = captureConfigGeneration();
|
|
394
|
+
const result = await fetchPoolAccountQuota(accountId, true, account.plan);
|
|
395
|
+
if (result.needsReauth && result.credentialGeneration !== undefined) {
|
|
396
|
+
markAccountNeedsReauth(accountId, writerGeneration, result.credentialGeneration);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export async function listCodexAuthAccounts(
|
|
401
|
+
config: OcxConfig,
|
|
402
|
+
forceRefresh = false,
|
|
403
|
+
options: { validatePending?: boolean } = {},
|
|
404
|
+
): Promise<CodexAuthAccountDto[]> {
|
|
405
|
+
return (await listCodexAuthAccountsSnapshot(config, forceRefresh, options)).accounts;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export interface PauseExhaustedResult {
|
|
409
|
+
pausedAccountIds: string[];
|
|
410
|
+
checkedAccountCount: number;
|
|
411
|
+
failedAccountCount: number;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function selectFallbackAfterPause(config: OcxConfig, pausedActiveId: string): void {
|
|
415
|
+
reconcileCodexActiveAfterExclusion(config, pausedActiveId);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export async function pauseExhaustedCodexAccounts(
|
|
419
|
+
config: OcxConfig,
|
|
420
|
+
persistPausedAccounts: () => void,
|
|
421
|
+
): Promise<PauseExhaustedResult> {
|
|
422
|
+
const poolAccounts = (config.codexAccounts ?? []).filter(account => !account.isMain);
|
|
423
|
+
const nativeMainLease = tryAcquireNativeMainProfileClaim();
|
|
424
|
+
try {
|
|
425
|
+
const performPause = async (mainLease?: AdmissionLease): Promise<PauseExhaustedResult> => {
|
|
426
|
+
const mainWork = async (): Promise<{
|
|
427
|
+
shouldPause: boolean;
|
|
428
|
+
checkedAccountCount: number;
|
|
429
|
+
failedAccountCount: number;
|
|
430
|
+
}> => {
|
|
431
|
+
if (!mainLease) return { shouldPause: false, checkedAccountCount: 0, failedAccountCount: 1 };
|
|
432
|
+
const mainResult = await fetchMainAccountInfoAttempt(true, 1, mainLease, true);
|
|
433
|
+
if (!mainResult.credentialChecked || !mainResult.hasCredential) {
|
|
434
|
+
return { shouldPause: false, checkedAccountCount: 0, failedAccountCount: 0 };
|
|
435
|
+
}
|
|
436
|
+
if (!mainResult.freshQuota || !mainResult.info.plan) {
|
|
437
|
+
return { shouldPause: false, checkedAccountCount: 0, failedAccountCount: 1 };
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
shouldPause: !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
|
|
441
|
+
&& isCodexQuotaExhausted(mainResult.freshQuota, mainResult.info.plan),
|
|
442
|
+
checkedAccountCount: 1,
|
|
443
|
+
failedAccountCount: 0,
|
|
444
|
+
};
|
|
445
|
+
};
|
|
446
|
+
const [mainResult, poolResults] = await Promise.all([
|
|
447
|
+
mainWork(),
|
|
448
|
+
mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async account => {
|
|
449
|
+
if (!getCodexAccountCredential(account.id)) return { account, quotaResult: null };
|
|
450
|
+
try {
|
|
451
|
+
return {
|
|
452
|
+
account,
|
|
453
|
+
quotaResult: await fetchPoolAccountQuota(account.id, true, account.plan),
|
|
454
|
+
};
|
|
455
|
+
} catch {
|
|
456
|
+
// Settle each pool probe independently so a busy/failing account cannot
|
|
457
|
+
// abandon an already-confirmed main decision before atomic publication.
|
|
458
|
+
return { account, quotaResult: null };
|
|
459
|
+
}
|
|
460
|
+
}),
|
|
461
|
+
]);
|
|
462
|
+
|
|
463
|
+
let checkedAccountCount = mainResult.checkedAccountCount;
|
|
464
|
+
let failedAccountCount = mainResult.failedAccountCount;
|
|
465
|
+
const exhaustedIds: string[] = mainResult.shouldPause ? [MAIN_CODEX_ACCOUNT_ID] : [];
|
|
466
|
+
for (const { account, quotaResult } of poolResults) {
|
|
467
|
+
const currentAccount = (config.codexAccounts ?? []).find(candidate => candidate.id === account.id && !candidate.isMain);
|
|
468
|
+
if (!currentAccount) continue;
|
|
469
|
+
const generation = quotaResult?.freshCredentialGeneration;
|
|
470
|
+
const plan = quotaResult?.freshPlan ?? currentAccount.plan;
|
|
471
|
+
if (!quotaResult?.freshQuota || generation === undefined || !isCodexAccountGenerationLive(account.id, generation) || !plan) {
|
|
472
|
+
failedAccountCount += 1;
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
checkedAccountCount += 1;
|
|
476
|
+
if (!isCodexAccountPaused(config, account.id) && isCodexQuotaExhausted(quotaResult.freshQuota, plan)) {
|
|
477
|
+
exhaustedIds.push(account.id);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
for (const id of exhaustedIds) {
|
|
482
|
+
setCodexAccountPaused(config, id, true);
|
|
483
|
+
clearThreadAccountMapForAccount(id);
|
|
484
|
+
}
|
|
485
|
+
for (const id of exhaustedIds) selectFallbackAfterPause(config, id);
|
|
486
|
+
const result = {
|
|
487
|
+
pausedAccountIds: exhaustedIds,
|
|
488
|
+
checkedAccountCount,
|
|
489
|
+
failedAccountCount,
|
|
490
|
+
};
|
|
491
|
+
// Persist while both the in-process admission and cross-process shared
|
|
492
|
+
// claim still own the physical-main identity used for the decision.
|
|
493
|
+
if (result.pausedAccountIds.length > 0) persistPausedAccounts();
|
|
494
|
+
return result;
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
if (!nativeMainLease) return await performPause();
|
|
498
|
+
try {
|
|
499
|
+
return await withNativeMainCredentialClaim(() => performPause(nativeMainLease));
|
|
500
|
+
} catch (error) {
|
|
501
|
+
if (isNativeMainClaimUnavailable(error)) return await performPause();
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
} finally {
|
|
505
|
+
nativeMainLease?.release();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { withNativeMainSharedClaim } from "../native-main-claim";
|
|
2
|
+
import { resolveNativeProfileContext } from "../native-profile-store";
|
|
3
|
+
import { NativeProfileError } from "../native-profile-types";
|
|
4
|
+
|
|
5
|
+
export function isNativeMainClaimUnavailable(error: unknown): error is NativeProfileError {
|
|
6
|
+
return error instanceof NativeProfileError
|
|
7
|
+
&& (error.code === "NATIVE_MAIN_CLAIM_BUSY" || error.code === "NATIVE_MAIN_CLAIM_UNAVAILABLE");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function withNativeMainCredentialClaim<T>(operation: () => Promise<T>): Promise<T> {
|
|
11
|
+
return withNativeMainSharedClaim(resolveNativeProfileContext(), operation);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function jsonResponse(data: unknown, status = 200): Response {
|
|
15
|
+
return new Response(JSON.stringify(data), {
|
|
16
|
+
status,
|
|
17
|
+
headers: { "Content-Type": "application/json" },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function nativeMainProfileBusyResponse(): Response {
|
|
22
|
+
const response = jsonResponse({ error: "server_busy", code: "server_busy" }, 503);
|
|
23
|
+
response.headers.set("Retry-After", "1");
|
|
24
|
+
return response;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function manualImportDisabledResponse(): Response {
|
|
28
|
+
return jsonResponse({
|
|
29
|
+
error: "Manual Codex account import is disabled. Use OAuth login to add a pool account.",
|
|
30
|
+
code: "manual_import_disabled",
|
|
31
|
+
}, 403);
|
|
32
|
+
}
|