@bitkyc08/opencodex 2.42.0 → 2.43.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/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import {
|
|
3
3
|
atomicWriteFile,
|
|
4
|
+
deleteConfigTopLevelKey,
|
|
4
5
|
getConfigPath,
|
|
5
6
|
saveConfigPreservingClaudeCode,
|
|
6
7
|
withConfigMutationLockSync,
|
|
@@ -12,9 +13,10 @@ import { MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
|
|
|
12
13
|
import { clearAccountQuota } from "./quota";
|
|
13
14
|
import { clearCodexUpstreamHealthForAccount, clearThreadAccountMapForAccount } from "./routing";
|
|
14
15
|
import { invalidateCodexWebSocketsForAccount } from "./websocket-registry";
|
|
15
|
-
import { clearMainAccountCredentialPresence, clearMainAccountInfoCache } from "./main-account-cache";
|
|
16
|
+
import { clearMainAccountCredentialPresence, clearMainAccountInfoCache, observeMainQuotaIdentity } from "./main-account-cache";
|
|
16
17
|
import { forgetCodexAccountPause } from "./account-pause";
|
|
17
18
|
import { clearCodexAccountPin, forgetCodexAccountPriority } from "./account-priority";
|
|
19
|
+
import { forgetCodexQuotaAutoRefreshAccount } from "./quota-auto-refresh-state";
|
|
18
20
|
import { codexAccountNamespaceEntries, codexAccountPickerEnabled } from "./account-namespaces";
|
|
19
21
|
import type { OcxConfig } from "../types";
|
|
20
22
|
|
|
@@ -64,9 +66,13 @@ export function reconcileMainCodexAccountRuntimeState(): boolean {
|
|
|
64
66
|
if (currentAccountId === null) return false;
|
|
65
67
|
const previousAccountId = observedMainChatgptAccountId;
|
|
66
68
|
observedMainChatgptAccountId = currentAccountId;
|
|
67
|
-
if (previousAccountId === undefined || previousAccountId === currentAccountId)
|
|
69
|
+
if (previousAccountId === undefined || previousAccountId === currentAccountId) {
|
|
70
|
+
observeMainQuotaIdentity(currentAccountId);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
68
73
|
|
|
69
74
|
purgeMainCodexAccountRuntimeState();
|
|
75
|
+
observeMainQuotaIdentity(currentAccountId);
|
|
70
76
|
return true;
|
|
71
77
|
}
|
|
72
78
|
|
|
@@ -79,11 +85,15 @@ export function applyConfirmedMainCodexAccountTransition(
|
|
|
79
85
|
toAccountId: string,
|
|
80
86
|
): boolean {
|
|
81
87
|
if (!fromAccountId || !toAccountId || fromAccountId === toAccountId) {
|
|
82
|
-
if (toAccountId)
|
|
88
|
+
if (toAccountId) {
|
|
89
|
+
observedMainChatgptAccountId = toAccountId;
|
|
90
|
+
observeMainQuotaIdentity(toAccountId);
|
|
91
|
+
}
|
|
83
92
|
return false;
|
|
84
93
|
}
|
|
85
94
|
observedMainChatgptAccountId = toAccountId;
|
|
86
95
|
purgeMainCodexAccountRuntimeState();
|
|
96
|
+
observeMainQuotaIdentity(toAccountId);
|
|
87
97
|
return true;
|
|
88
98
|
}
|
|
89
99
|
|
|
@@ -135,6 +145,12 @@ export function deleteCodexAccount(runtimeConfig: OcxConfig, accountId: string):
|
|
|
135
145
|
.filter(account => account.isMain || account.id !== accountId);
|
|
136
146
|
forgetCodexAccountPause(runtimeConfig, accountId);
|
|
137
147
|
forgetCodexAccountPriority(runtimeConfig, accountId);
|
|
148
|
+
if (runtimeConfig.codexQuotaAutoRefresh?.[accountId]) {
|
|
149
|
+
const retained = { ...runtimeConfig.codexQuotaAutoRefresh };
|
|
150
|
+
delete retained[accountId];
|
|
151
|
+
if (Object.keys(retained).length > 0) runtimeConfig.codexQuotaAutoRefresh = retained;
|
|
152
|
+
else deleteConfigTopLevelKey(runtimeConfig, "codexQuotaAutoRefresh");
|
|
153
|
+
}
|
|
138
154
|
clearCodexAccountPin(runtimeConfig, accountId);
|
|
139
155
|
if (runtimeConfig.activeCodexAccountId === accountId) runtimeConfig.activeCodexAccountId = undefined;
|
|
140
156
|
|
|
@@ -167,6 +183,7 @@ export function deleteCodexAccount(runtimeConfig: OcxConfig, accountId: string):
|
|
|
167
183
|
return hadVisiblePickerBinding;
|
|
168
184
|
});
|
|
169
185
|
|
|
186
|
+
forgetCodexQuotaAutoRefreshAccount(accountId);
|
|
170
187
|
if (cleanupFailed) throw new CodexAccountDeleteCleanupError();
|
|
171
188
|
return pickerVisibilityChanged;
|
|
172
189
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { hasLegacyMainCodexPoolAccount, isSelectableCodexPoolAccount } from "./account-id";
|
|
10
10
|
import type { OcxConfig } from "../types";
|
|
11
11
|
import { isNativeMainTrafficBlocked } from "./native-profile-startup";
|
|
12
|
+
import { isMainAccountHardLocked } from "./main-account-hard-lock";
|
|
12
13
|
|
|
13
14
|
export interface CodexAccountUsabilityOptions {
|
|
14
15
|
/** Route using cached runtime state only; the caller must reject selected main before auth. */
|
|
@@ -26,6 +27,7 @@ export function isCodexAccountUsable(
|
|
|
26
27
|
): boolean {
|
|
27
28
|
if (options.modelEligibleAccountIds && !options.modelEligibleAccountIds.has(accountId)) return false;
|
|
28
29
|
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
30
|
+
if (isMainAccountHardLocked(config)) return false;
|
|
29
31
|
// Startup recovery owns the physical auth/vault boundary. Never parse or select
|
|
30
32
|
// native __main__ while an encrypted switch journal is pending or inconclusive.
|
|
31
33
|
if (!options.nativeMainSelectionOnly && isNativeMainTrafficBlocked()) return false;
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
isCompleteCodexQuotaRecoverySnapshot,
|
|
70
70
|
isCodexQuotaExhausted,
|
|
71
71
|
listAccountQuotas,
|
|
72
|
+
parseMainPolicyUsageQuota,
|
|
72
73
|
parseUsageQuota,
|
|
73
74
|
setAccountQuotaFromParsed,
|
|
74
75
|
updateAccountQuota,
|
|
@@ -84,7 +85,10 @@ export {
|
|
|
84
85
|
updateAccountQuota,
|
|
85
86
|
} from "./quota";
|
|
86
87
|
import { extractAccountId } from "../oauth/chatgpt";
|
|
87
|
-
import {
|
|
88
|
+
import {
|
|
89
|
+
getMainAccountPlan, getValidMainAccountToken, isMainAccountTokenVerifiablyLive,
|
|
90
|
+
MainAccountTokenRefreshError, MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan,
|
|
91
|
+
} from "./main-account";
|
|
88
92
|
import { captureConfigGeneration, registerStateSweepAfterTick } from "../lib/state-store-sweeper";
|
|
89
93
|
import { reconcileLiveStateStores } from "../lib/state-store-registrations";
|
|
90
94
|
import {
|
|
@@ -92,12 +96,17 @@ import {
|
|
|
92
96
|
clearMainAccountInfoCache,
|
|
93
97
|
getMainAccountCredentialPresence,
|
|
94
98
|
getMainAccountInfoCache,
|
|
99
|
+
getMainQuotaCredentialGeneration,
|
|
95
100
|
isMainAccountIdentityGenerationLive,
|
|
101
|
+
matchesMainQuotaCredential,
|
|
102
|
+
observeMainQuotaCredential,
|
|
96
103
|
setMainAccountCredentialPresence,
|
|
97
104
|
setMainAccountInfoCache,
|
|
98
105
|
type MainAccountInfo,
|
|
99
106
|
} from "./main-account-cache";
|
|
100
107
|
export { clearMainAccountInfoCache } from "./main-account-cache";
|
|
108
|
+
import { getMainAccountHardLockStatus, type MainAccountHardLockStatus } from "./main-account-hard-lock";
|
|
109
|
+
import { observeMainReserveRevocation } from "./reserve-availability";
|
|
101
110
|
import { maskEmail } from "../lib/privacy";
|
|
102
111
|
import { codexWarmupFailureReason, warmCodexAccount } from "./warmup";
|
|
103
112
|
export { maskEmail } from "../lib/privacy";
|
|
@@ -120,6 +129,12 @@ import {
|
|
|
120
129
|
isValidCodexAccountId,
|
|
121
130
|
} from "./account-id";
|
|
122
131
|
import { codexAccountIdNamespaceCollisionError } from "./account-namespace-match";
|
|
132
|
+
import {
|
|
133
|
+
markManualResetCreditOperationAmbiguous,
|
|
134
|
+
openManualResetCreditOperation,
|
|
135
|
+
settleManualResetCreditOperation,
|
|
136
|
+
} from "./reset-credit-operation-ledger";
|
|
137
|
+
import { isCodexResetCreditOperationId } from "./reset-credit-recovery";
|
|
123
138
|
import { ResourceAdmissionError, type AdmissionLease } from "../lib/admission";
|
|
124
139
|
import { tryAcquireNativeMainProfileClaim } from "./native-main-admission";
|
|
125
140
|
import { withNativeMainSharedClaim } from "./native-main-claim";
|
|
@@ -812,6 +827,7 @@ async function fetchMainAccountInfoAttempt(
|
|
|
812
827
|
retriesRemaining: number,
|
|
813
828
|
existingNativeMainLease?: AdmissionLease,
|
|
814
829
|
nativeMainSharedClaimHeld = false,
|
|
830
|
+
explicitRefresh: boolean = forceRefresh,
|
|
815
831
|
): Promise<MainAccountInfoFetchResult> {
|
|
816
832
|
const nativeMainLease = existingNativeMainLease ?? tryAcquireNativeMainProfileClaim();
|
|
817
833
|
if (!nativeMainLease) {
|
|
@@ -824,7 +840,7 @@ async function fetchMainAccountInfoAttempt(
|
|
|
824
840
|
}
|
|
825
841
|
try {
|
|
826
842
|
const operation = async () => ({
|
|
827
|
-
...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease),
|
|
843
|
+
...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease, explicitRefresh),
|
|
828
844
|
identityGeneration: captureMainAccountIdentityGeneration(),
|
|
829
845
|
});
|
|
830
846
|
if (nativeMainSharedClaimHeld) return await operation();
|
|
@@ -878,6 +894,12 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
878
894
|
if (!forceRefresh && cached && Date.now() - cached.ts < MAIN_CACHE_TTL) {
|
|
879
895
|
return { info: cached, credentialChecked: true, hasCredential: true };
|
|
880
896
|
}
|
|
897
|
+
// Bind quota to the owned credential and the account actually selected by WHAM's header.
|
|
898
|
+
// A conflicting legacy token/account tuple is not evidence for the new policy.
|
|
899
|
+
const mainQuotaWriter = requestAccountId === tokens.account_id
|
|
900
|
+
? observeMainQuotaCredential(tokens.access_token, tokens.account_id)
|
|
901
|
+
: undefined;
|
|
902
|
+
const mainQuotaCredentialGeneration = getMainQuotaCredentialGeneration();
|
|
881
903
|
try {
|
|
882
904
|
const resp = await fetch("https://chatgpt.com/backend-api/wham/usage", {
|
|
883
905
|
headers: { Authorization: `Bearer ${tokens.access_token}`, "ChatGPT-Account-Id": tokens.account_id },
|
|
@@ -896,8 +918,16 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
896
918
|
const data = (await resp.json()) as WhamUsageResponse;
|
|
897
919
|
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
|
|
898
920
|
if (retried) return retried;
|
|
921
|
+
// A delayed response from a replaced bearer cannot revoke a newer Reserve grant,
|
|
922
|
+
// even in the same workspace or after an A→B→A credential transition.
|
|
923
|
+
if (mainQuotaCredentialGeneration === getMainQuotaCredentialGeneration()
|
|
924
|
+
&& matchesMainQuotaCredential(tokens.access_token, tokens.account_id)) {
|
|
925
|
+
observeMainReserveRevocation(data, mainQuotaWriter);
|
|
926
|
+
}
|
|
899
927
|
const plan = nonEmptyPlan(data.plan_type) ?? nonEmptyPlan(cached?.plan) ?? nonEmptyPlan(getMainAccountPlan());
|
|
900
|
-
const
|
|
928
|
+
const usage = { ...data, ...(plan ? { plan_type: plan } : {}) };
|
|
929
|
+
const quota = parseUsageQuota(usage);
|
|
930
|
+
const policyQuota = parseMainPolicyUsageQuota(usage);
|
|
901
931
|
const freshResetCredits = quota?.resetCredits;
|
|
902
932
|
// Tag the count with the identity it was read from, so a later response that omits the
|
|
903
933
|
// summary can restore the badge without ever crossing an account boundary.
|
|
@@ -923,7 +953,7 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
923
953
|
// score and auto-switch the main account exactly like a pool account (Option A).
|
|
924
954
|
setMainAccountPlan(result.plan);
|
|
925
955
|
if (result.quota) {
|
|
926
|
-
setAccountQuotaFromParsed(MAIN_CODEX_ACCOUNT_ID, result.quota, writerGeneration);
|
|
956
|
+
setAccountQuotaFromParsed(MAIN_CODEX_ACCOUNT_ID, result.quota, writerGeneration, mainQuotaWriter, policyQuota);
|
|
927
957
|
}
|
|
928
958
|
return {
|
|
929
959
|
info: result,
|
|
@@ -1031,6 +1061,7 @@ export interface CodexAuthAccountDto {
|
|
|
1031
1061
|
healthSummary: string;
|
|
1032
1062
|
healthAction?: string;
|
|
1033
1063
|
quotaProbeSkipped?: true;
|
|
1064
|
+
mainAccountHardLock?: MainAccountHardLockStatus;
|
|
1034
1065
|
}
|
|
1035
1066
|
|
|
1036
1067
|
interface FreshPoolPlanUpdate {
|
|
@@ -1419,10 +1450,9 @@ export async function runCodexCooldownRecoveryProbes(config: OcxConfig, now = Da
|
|
|
1419
1450
|
}
|
|
1420
1451
|
try {
|
|
1421
1452
|
const result = await fetchPoolAccountQuota(claim.accountId, true, account.plan);
|
|
1422
|
-
// Defence in depth:
|
|
1423
|
-
//
|
|
1424
|
-
|
|
1425
|
-
const recovered = claim.scope !== "spark"
|
|
1453
|
+
// Defence in depth: independent scopes are already excluded at the claim site.
|
|
1454
|
+
// Generic WHAM must never clear Spark or Reserve even if claim selection changes.
|
|
1455
|
+
const recovered = (claim.scope === undefined || claim.scope === "shared")
|
|
1426
1456
|
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan);
|
|
1427
1457
|
settleCodexQuotaRecoveryProbe(claim, recovered, {
|
|
1428
1458
|
credentialGeneration: result.freshCredentialGeneration,
|
|
@@ -1437,10 +1467,50 @@ export async function runCodexCooldownRecoveryProbes(config: OcxConfig, now = Da
|
|
|
1437
1467
|
return cooldownRecoveryInFlight;
|
|
1438
1468
|
}
|
|
1439
1469
|
|
|
1470
|
+
let mainHardLockRecoveryInFlight: Promise<void> | null = null;
|
|
1471
|
+
|
|
1472
|
+
/** Metadata-only recovery on the existing sweep; failures retain the observed policy block. */
|
|
1473
|
+
export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise<void> {
|
|
1474
|
+
if (mainHardLockRecoveryInFlight) return mainHardLockRecoveryInFlight;
|
|
1475
|
+
if (getMainAccountHardLockStatus(config).state !== "blocked"
|
|
1476
|
+
|| isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
|
|
1477
|
+
const lease = tryAcquireNativeMainProfileClaim();
|
|
1478
|
+
if (!lease) return;
|
|
1479
|
+
mainHardLockRecoveryInFlight = (async () => {
|
|
1480
|
+
reconcileMainCodexAccountRuntimeState();
|
|
1481
|
+
if (getMainAccountHardLockStatus(config).state !== "blocked"
|
|
1482
|
+
|| isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
|
|
1483
|
+
const identityGeneration = captureMainAccountIdentityGeneration();
|
|
1484
|
+
const writerGeneration = captureConfigGeneration();
|
|
1485
|
+
try {
|
|
1486
|
+
// Refresh can require an exclusive credential claim: never hold WHAM's shared
|
|
1487
|
+
// claim while obtaining a valid token. The runtime lease spans both operations.
|
|
1488
|
+
if (!await getValidMainAccountToken({ preserveReauth: true })) return;
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
if (error instanceof MainAccountTokenRefreshError && error.reason === "reauth"
|
|
1491
|
+
&& isMainAccountIdentityGenerationLive(identityGeneration)) {
|
|
1492
|
+
markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
|
|
1493
|
+
}
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
if (isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
|
|
1497
|
+
await fetchMainAccountInfoAttempt(true, 1, lease, false, false);
|
|
1498
|
+
})().catch(() => {
|
|
1499
|
+
// Best-effort background metadata read; no cooldown/pause or policy clearing on failure.
|
|
1500
|
+
}).finally(() => {
|
|
1501
|
+
lease.release();
|
|
1502
|
+
mainHardLockRecoveryInFlight = null;
|
|
1503
|
+
});
|
|
1504
|
+
return mainHardLockRecoveryInFlight;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1440
1507
|
export function registerCodexCooldownRecoveryProbeWorker(config: OcxConfig): void {
|
|
1441
1508
|
registerStateSweepAfterTick({
|
|
1442
1509
|
name: "codex-cooldown-recovery",
|
|
1443
|
-
afterTick: () => {
|
|
1510
|
+
afterTick: () => {
|
|
1511
|
+
void runCodexCooldownRecoveryProbes(config);
|
|
1512
|
+
void runMainAccountHardLockRecovery(config);
|
|
1513
|
+
},
|
|
1444
1514
|
});
|
|
1445
1515
|
}
|
|
1446
1516
|
|
|
@@ -1702,6 +1772,7 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
1702
1772
|
logLabel: "main",
|
|
1703
1773
|
isMain: true,
|
|
1704
1774
|
paused: isCodexAccountPaused(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
1775
|
+
mainAccountHardLock: getMainAccountHardLockStatus(runtimeConfig),
|
|
1705
1776
|
priority: getCodexAccountPriority(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
1706
1777
|
hasCredential: hasMainCredential,
|
|
1707
1778
|
needsReauth: mainNeedsReauth,
|
|
@@ -2168,31 +2239,107 @@ export async function handleCodexAuthAPI(
|
|
|
2168
2239
|
}
|
|
2169
2240
|
|
|
2170
2241
|
if (url.pathname === "/api/codex-auth/reset-credits/consume" && req.method === "POST") {
|
|
2171
|
-
const body = (await req.json().catch(() => ({}))) as {
|
|
2242
|
+
const body = (await req.json().catch(() => ({}))) as {
|
|
2243
|
+
accountId?: string;
|
|
2244
|
+
operationId?: unknown;
|
|
2245
|
+
};
|
|
2172
2246
|
if (!body.accountId) return jsonResponse({ error: "accountId required" }, 400);
|
|
2173
2247
|
const accountId = body.accountId;
|
|
2248
|
+
// Optional caller-owned idempotency identity (#3375 axis D). Absent => legacy
|
|
2249
|
+
// behavior: a fresh random redeem_request_id and no durable ledger row.
|
|
2250
|
+
// The ledger throws TypeError on a malformed id, so the format check has to
|
|
2251
|
+
// happen here rather than at the call site, or it surfaces as a 500.
|
|
2252
|
+
const hasOperationId = body.operationId !== undefined;
|
|
2253
|
+
if (hasOperationId && !isCodexResetCreditOperationId(body.operationId)) {
|
|
2254
|
+
return jsonResponse({ error: "Invalid operationId format" }, 400);
|
|
2255
|
+
}
|
|
2256
|
+
const requestedOperationId = hasOperationId ? body.operationId as string : undefined;
|
|
2174
2257
|
|
|
2175
2258
|
try {
|
|
2176
2259
|
const operation = await withResetCreditAuth(getRuntimeConfig(config), accountId, async auth => {
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2260
|
+
// The ledger keys manual operations by the *physical* ChatGPT account, which is
|
|
2261
|
+
// only known after the auth wrapper resolves credentials. Open here, not earlier.
|
|
2262
|
+
const identity = requestedOperationId === undefined
|
|
2263
|
+
? undefined
|
|
2264
|
+
: {
|
|
2265
|
+
accountId,
|
|
2266
|
+
chatgptAccountId: auth.chatgptAccountId,
|
|
2267
|
+
operationId: requestedOperationId,
|
|
2268
|
+
} as const;
|
|
2269
|
+
let idempotencyKey: string;
|
|
2270
|
+
if (identity) {
|
|
2271
|
+
const opened = openManualResetCreditOperation(identity);
|
|
2272
|
+
if (opened.kind === "terminal") {
|
|
2273
|
+
// Durably settled already: replay the recorded outcome instead of
|
|
2274
|
+
// trusting upstream idempotency for an irreversible spend. No
|
|
2275
|
+
// `remaining` — that field is only reported from a freshly parsed
|
|
2276
|
+
// available_count, and a replay has none.
|
|
2277
|
+
return jsonResponse({ code: opened.code, replayed: true });
|
|
2278
|
+
}
|
|
2279
|
+
if (opened.kind === "identity-mismatch") {
|
|
2280
|
+
return jsonResponse({
|
|
2281
|
+
error: "operation_id_owned_by_another_account",
|
|
2282
|
+
code: "identity_mismatch",
|
|
2283
|
+
}, 409);
|
|
2284
|
+
}
|
|
2285
|
+
if (opened.kind !== "execute") {
|
|
2286
|
+
// capacity | unavailable -> fail closed. Falling back to a random id
|
|
2287
|
+
// would silently reintroduce the double-spend this identity prevents.
|
|
2288
|
+
const response = jsonResponse({
|
|
2289
|
+
error: opened.kind === "capacity"
|
|
2290
|
+
? "reset_credit_ledger_capacity"
|
|
2291
|
+
: "reset_credit_ledger_unavailable",
|
|
2292
|
+
code: opened.kind,
|
|
2293
|
+
}, 503);
|
|
2294
|
+
response.headers.set("Retry-After", "1");
|
|
2295
|
+
return response;
|
|
2296
|
+
}
|
|
2297
|
+
// Canonical id, which an alias join may map to an earlier caller id.
|
|
2298
|
+
idempotencyKey = opened.operationId;
|
|
2299
|
+
} else {
|
|
2300
|
+
idempotencyKey = crypto.randomUUID();
|
|
2301
|
+
}
|
|
2302
|
+
let resp: Response;
|
|
2303
|
+
try {
|
|
2304
|
+
resp = await fetch(
|
|
2305
|
+
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
|
|
2306
|
+
{
|
|
2307
|
+
method: "POST",
|
|
2308
|
+
headers: {
|
|
2309
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
2310
|
+
"ChatGPT-Account-Id": auth.chatgptAccountId,
|
|
2311
|
+
"Content-Type": "application/json",
|
|
2312
|
+
},
|
|
2313
|
+
body: JSON.stringify({ redeem_request_id: idempotencyKey }),
|
|
2314
|
+
signal: AbortSignal.timeout(10_000),
|
|
2186
2315
|
},
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2316
|
+
);
|
|
2317
|
+
} catch (error) {
|
|
2318
|
+
// Dispatch outcome unknown: the credit may or may not have been spent.
|
|
2319
|
+
// Mark ambiguous so a replay of this same id is never treated as new.
|
|
2320
|
+
if (identity) markManualResetCreditOperationAmbiguous(identity);
|
|
2321
|
+
throw error;
|
|
2322
|
+
}
|
|
2191
2323
|
if (!resp.ok) {
|
|
2192
2324
|
await resp.body?.cancel().catch(() => {});
|
|
2325
|
+
if (identity) markManualResetCreditOperationAmbiguous(identity);
|
|
2193
2326
|
return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
|
|
2194
2327
|
}
|
|
2195
2328
|
const result = safeResetCreditConsumeDto(await resp.json());
|
|
2329
|
+
if (identity) {
|
|
2330
|
+
// Narrow explicitly rather than casting: `safeResetCreditConsumeDto`
|
|
2331
|
+
// normalizes anything unrecognized to "unknown", and settling that
|
|
2332
|
+
// would come back as a mismatch and leave the row pending anyway.
|
|
2333
|
+
// Settlement failure never downgrades the user-visible outcome: the
|
|
2334
|
+
// spend already happened upstream, and reporting failure would invite
|
|
2335
|
+
// a manual retry -- the exact double-spend this unit removes.
|
|
2336
|
+
if (result.code === "reset" || result.code === "already_redeemed"
|
|
2337
|
+
|| result.code === "nothing_to_reset" || result.code === "no_credit") {
|
|
2338
|
+
settleManualResetCreditOperation(identity, result.code);
|
|
2339
|
+
} else {
|
|
2340
|
+
markManualResetCreditOperationAmbiguous(identity);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2196
2343
|
// After a successful redeem (or an idempotent already_redeemed), refresh WHAM usage
|
|
2197
2344
|
// and return remaining only when that refresh freshly parsed available_count.
|
|
2198
2345
|
// Do not fall back to a preserved cached resetCredits (failed/omitted refresh).
|