@bitkyc08/opencodex 2.55.0-preview.20260914 → 2.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/gui/dist/assets/{index-DH2PUHqr.js → index-D4zuyIxQ.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +2 -1
  4. package/src/adapters/base.ts +21 -0
  5. package/src/adapters/cursor/transport-retry.ts +46 -1
  6. package/src/adapters/cursor.ts +4 -0
  7. package/src/adapters/kiro/adapter.ts +42 -1
  8. package/src/adapters/kiro-retry.ts +23 -4
  9. package/src/adapters/openai-chat/errors.ts +116 -0
  10. package/src/adapters/openai-chat/messages.ts +346 -0
  11. package/src/adapters/openai-chat/passthrough.ts +146 -0
  12. package/src/adapters/openai-chat/response-events.ts +117 -0
  13. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  14. package/src/adapters/openai-chat/tool-schema.ts +477 -0
  15. package/src/adapters/openai-chat/wire.ts +50 -0
  16. package/src/adapters/openai-chat.ts +33 -1445
  17. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  18. package/src/adapters/openai-responses/image-gen.ts +406 -0
  19. package/src/adapters/openai-responses/internal.ts +3 -0
  20. package/src/adapters/openai-responses/passthrough.ts +611 -0
  21. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  22. package/src/adapters/openai-responses/reasoning.ts +220 -0
  23. package/src/adapters/openai-responses/request-strips.ts +185 -0
  24. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  25. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  26. package/src/adapters/openai-responses/web-search.ts +156 -0
  27. package/src/adapters/openai-responses.ts +4 -2625
  28. package/src/bridge/errors.ts +34 -0
  29. package/src/bridge/internal.ts +174 -0
  30. package/src/bridge/response-json.ts +624 -0
  31. package/src/bridge/sse.ts +1444 -0
  32. package/src/bridge.ts +5 -2204
  33. package/src/chat/inbound.ts +12 -1
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-store.ts +71 -9
  36. package/src/codex/auth-api/account-list.ts +507 -0
  37. package/src/codex/auth-api/http.ts +32 -0
  38. package/src/codex/auth-api/login-flow.ts +554 -0
  39. package/src/codex/auth-api/login-state.ts +64 -0
  40. package/src/codex/auth-api/main-account-probe.ts +331 -0
  41. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  42. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  43. package/src/codex/auth-api/reset-credit-service.ts +422 -0
  44. package/src/codex/auth-api/routes.ts +425 -0
  45. package/src/codex/auth-api/runtime-config.ts +48 -0
  46. package/src/codex/auth-api.ts +27 -3118
  47. package/src/codex/auth-context.ts +95 -28
  48. package/src/codex/catalog/auto-review.ts +507 -0
  49. package/src/codex/catalog/build-entries.ts +981 -0
  50. package/src/codex/catalog/combo-member.ts +375 -0
  51. package/src/codex/catalog/derive-entry.ts +229 -0
  52. package/src/codex/catalog/effort.ts +0 -1
  53. package/src/codex/catalog/gated-native-warn.ts +63 -0
  54. package/src/codex/catalog/gather-capture.ts +533 -0
  55. package/src/codex/catalog/model-hints.ts +691 -0
  56. package/src/codex/catalog/model-visibility.ts +304 -0
  57. package/src/codex/catalog/provider-fetch.ts +52 -2942
  58. package/src/codex/catalog/provider-models.ts +685 -0
  59. package/src/codex/catalog/restore.ts +132 -0
  60. package/src/codex/catalog/retained-sync.ts +706 -0
  61. package/src/codex/catalog/routed-gather.ts +858 -0
  62. package/src/codex/catalog/subagent-roster.ts +176 -0
  63. package/src/codex/catalog/sync.ts +52 -2698
  64. package/src/codex/inject/config-toml.ts +563 -0
  65. package/src/codex/inject/remove.ts +192 -0
  66. package/src/codex/inject/restore.ts +540 -0
  67. package/src/codex/inject/routing-classify.ts +109 -0
  68. package/src/codex/inject/routing-target.ts +125 -0
  69. package/src/codex/inject.ts +81 -1436
  70. package/src/codex/lineage.ts +458 -0
  71. package/src/codex/pool-refresh-backoff.ts +152 -0
  72. package/src/codex/routing/active-account.ts +194 -0
  73. package/src/codex/routing/cooldown-math.ts +275 -0
  74. package/src/codex/routing/health-store.ts +402 -0
  75. package/src/codex/routing/probe-lease.ts +358 -0
  76. package/src/codex/routing/selection.ts +703 -0
  77. package/src/codex/routing/thread-affinity.ts +538 -0
  78. package/src/codex/routing.ts +353 -2234
  79. package/src/codex/shim-fingerprint.ts +223 -0
  80. package/src/codex/shim-inspect.ts +175 -0
  81. package/src/codex/shim-probe.ts +367 -0
  82. package/src/codex/shim-restore-lock.ts +169 -0
  83. package/src/codex/shim-state-file.ts +151 -0
  84. package/src/codex/shim-templates.ts +265 -0
  85. package/src/codex/shim.ts +48 -1268
  86. package/src/config/diagnostics.ts +705 -0
  87. package/src/config/feature-flags.ts +55 -0
  88. package/src/config/live-reconcile.ts +403 -0
  89. package/src/config/load-degrade.ts +880 -0
  90. package/src/config/mutation-lock.ts +244 -0
  91. package/src/config/openai-tier-backup.ts +268 -0
  92. package/src/config/persist-unlocked.ts +92 -0
  93. package/src/config/proxy-env.ts +188 -0
  94. package/src/config/salvage.ts +244 -0
  95. package/src/config/schema/config-schema.ts +640 -0
  96. package/src/config/schema/leaf-validators.ts +855 -0
  97. package/src/config/warn-memo.ts +28 -0
  98. package/src/config.ts +234 -4481
  99. package/src/generated/compatibility-version.json +539 -39
  100. package/src/lib/request-execution-budget.ts +69 -20
  101. package/src/lib/spend-reservation-ledger.ts +940 -0
  102. package/src/lib/upstream-retry.ts +55 -11
  103. package/src/lib/workflow-budget.ts +553 -30
  104. package/src/providers/quota/account-cache.ts +441 -0
  105. package/src/providers/quota/antigravity.ts +295 -0
  106. package/src/providers/quota/report-cache.ts +320 -0
  107. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  108. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  109. package/src/providers/quota.ts +324 -3079
  110. package/src/providers/registry/entries-core.ts +1221 -0
  111. package/src/providers/registry/entries-extended.ts +1204 -0
  112. package/src/providers/registry/model-seeds.ts +908 -0
  113. package/src/providers/registry/types.ts +352 -0
  114. package/src/providers/registry.ts +24 -3536
  115. package/src/responses/continuation-ownership.ts +29 -0
  116. package/src/responses/state/replay-fingerprint.ts +80 -0
  117. package/src/responses/state/snapshot-codec.ts +104 -0
  118. package/src/responses/state/spill-failure.ts +118 -0
  119. package/src/responses/state/spill-queue.ts +665 -0
  120. package/src/responses/state/temp-recovery.ts +257 -0
  121. package/src/responses/state.ts +82 -1143
  122. package/src/routing/identity-domains.ts +449 -0
  123. package/src/routing/probe-lease.ts +511 -0
  124. package/src/server/index/bounded-request.ts +88 -0
  125. package/src/server/index/live-sideband.ts +565 -0
  126. package/src/server/index/serve-options.ts +1766 -0
  127. package/src/server/index/startup-warnings.ts +213 -0
  128. package/src/server/index/websocket-handler.ts +335 -0
  129. package/src/server/index.ts +40 -2547
  130. package/src/server/management/route-registry.ts +26 -23
  131. package/src/server/management/shared.ts +8 -5
  132. package/src/server/management/workflow-budget-routes.ts +133 -0
  133. package/src/server/management-api.ts +12 -0
  134. package/src/server/request-log-conversation.ts +9 -7
  135. package/src/server/request-log.ts +245 -1
  136. package/src/server/responses/account-change-state.ts +233 -0
  137. package/src/server/responses/adapter-continuation.ts +514 -0
  138. package/src/server/responses/adapter-delivery.ts +214 -0
  139. package/src/server/responses/adapter-dispatch.ts +971 -0
  140. package/src/server/responses/compact.ts +59 -4
  141. package/src/server/responses/completion-policy.ts +33 -0
  142. package/src/server/responses/core-auth.ts +527 -0
  143. package/src/server/responses/core-codex-account.ts +859 -0
  144. package/src/server/responses/core-combo-failure.ts +210 -0
  145. package/src/server/responses/core-combo.ts +707 -0
  146. package/src/server/responses/core-errors.ts +152 -0
  147. package/src/server/responses/core-lifetime.ts +95 -0
  148. package/src/server/responses/core-normalize.ts +350 -0
  149. package/src/server/responses/core-opaque-recovery.ts +380 -0
  150. package/src/server/responses/core-options.ts +159 -0
  151. package/src/server/responses/core-replay.ts +225 -0
  152. package/src/server/responses/core.ts +192 -8893
  153. package/src/server/responses/passthrough-delivery.ts +856 -0
  154. package/src/server/responses/passthrough-dispatch.ts +1476 -0
  155. package/src/server/responses/passthrough-execution.ts +54 -0
  156. package/src/server/responses/request-prepare.ts +970 -0
  157. package/src/server/responses/request-send-budget.ts +164 -0
  158. package/src/server/responses/request-sidecar-auth.ts +149 -0
  159. package/src/server/responses/request-transport.ts +744 -0
  160. package/src/server/responses/response-effects.ts +157 -0
  161. package/src/server/responses/run-turn-execution.ts +448 -0
  162. package/src/server/responses/sidecar-execution.ts +469 -0
  163. package/src/server/responses-image-gen-repair.ts +1 -1
  164. package/src/server/workflow-refusal.ts +84 -0
  165. package/src/types/config.ts +30 -0
  166. package/src/usage/log.ts +146 -0
  167. package/src/usage/summary.ts +171 -21
@@ -0,0 +1,422 @@
1
+ import { getValidCodexToken, isCodexAccountGenerationLive, readCodexAccountRecord, CodexCredentialGenerationConflictError } from "../account-store";
2
+ import { isCompleteCodexQuotaRecoverySnapshot } from "../quota";
3
+ import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
4
+ import { claimManualResetCooldowns, settleManualResetCooldown } from "../routing";
5
+ import type { ManualResetCooldownClaim } from "../routing";
6
+ import { readCodexTokens } from "../auth-collision";
7
+ import { extractAccountId } from "../../oauth/chatgpt";
8
+ import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
9
+ import { getMainQuotaCredentialGeneration, isMainQuotaWriterLive, matchesMainQuotaCredential, observeMainQuotaCredential } from "../main-account-cache";
10
+ import type { OcxConfig } from "../../types";
11
+ import { BOUNDED_BODY_MAX_BYTES, readBoundedResponseBody } from "../../lib/bounded-body";
12
+ import { cancelBodyOnAbort, signalWithTimeout } from "../../lib/abort";
13
+ import { hasLegacyMainCodexPoolAccount, isValidCodexAccountId } from "../account-id";
14
+ import { markManualResetCreditOperationAmbiguous, openManualResetCreditOperation, settleManualResetCreditOperation } from "../reset-credit-operation-ledger";
15
+ import type { AdmissionLease } from "../../lib/admission";
16
+ import { tryAcquireNativeMainProfileClaim } from "../native-main-admission";
17
+ import { jsonResponse, nativeMainProfileBusyResponse, withNativeMainCredentialClaim, isNativeMainClaimUnavailable } from "./http";
18
+ import { fetchMainAccountInfoAttempt } from "./main-account-probe";
19
+ import type { MainResetQuotaProof } from "./main-account-probe";
20
+ import { currentQuotaDispatchSequence, fetchPoolAccountQuota } from "./pool-quota-probe";
21
+ import { getRuntimeConfig, configuredPoolAccount } from "./runtime-config";
22
+
23
+ interface ResetCreditAuth {
24
+ isMain: boolean;
25
+ accessToken: string;
26
+ chatgptAccountId: string;
27
+ nativeMainLease?: AdmissionLease;
28
+ nativeMainSharedClaimHeld?: true;
29
+ poolGeneration?: number;
30
+ mainProof?: MainResetQuotaProof;
31
+ }
32
+
33
+ async function withResetCreditAuth<T>(
34
+ runtimeConfig: OcxConfig,
35
+ accountId: string,
36
+ operation: (auth: ResetCreditAuth) => Promise<T>,
37
+ ): Promise<{ ok: true; value: T } | { ok: false; response: Response }> {
38
+ if (accountId === MAIN_CODEX_ACCOUNT_ID) {
39
+ if (hasLegacyMainCodexPoolAccount(runtimeConfig.codexAccounts)) {
40
+ return { ok: false, response: jsonResponse({ error: "Remove the legacy __main__ pool row before using the Desktop account" }, 409) };
41
+ }
42
+ const nativeMainLease = tryAcquireNativeMainProfileClaim();
43
+ if (!nativeMainLease) return { ok: false, response: nativeMainProfileBusyResponse() };
44
+ try {
45
+ try {
46
+ return await withNativeMainCredentialClaim(async () => {
47
+ const tokens = readCodexTokens();
48
+ if (!tokens) {
49
+ return { ok: false, response: jsonResponse({ error: "Main Codex account not logged in" }, 401) };
50
+ }
51
+ reconcileMainCodexAccountRuntimeState();
52
+ const physicalId = extractAccountId(tokens.id_token, tokens.access_token) ?? tokens.account_id;
53
+ const writer = physicalId === tokens.account_id
54
+ ? observeMainQuotaCredential(tokens.access_token, tokens.account_id) : undefined;
55
+ return {
56
+ ok: true,
57
+ value: await operation({
58
+ isMain: true,
59
+ ...(writer ? { mainProof: { writer, credentialGeneration: getMainQuotaCredentialGeneration() } } : {}),
60
+ accessToken: tokens.access_token,
61
+ chatgptAccountId: tokens.account_id,
62
+ nativeMainLease,
63
+ nativeMainSharedClaimHeld: true,
64
+ }),
65
+ };
66
+ });
67
+ } catch (error) {
68
+ if (isNativeMainClaimUnavailable(error)) {
69
+ return { ok: false, response: nativeMainProfileBusyResponse() };
70
+ }
71
+ throw error;
72
+ }
73
+ } finally {
74
+ nativeMainLease.release();
75
+ }
76
+ }
77
+ if (!isValidCodexAccountId(accountId)) {
78
+ return { ok: false, response: jsonResponse({ error: "Invalid account id format" }, 400) };
79
+ }
80
+ if (!configuredPoolAccount(runtimeConfig, accountId)) {
81
+ return { ok: false, response: jsonResponse({ error: "Unknown Codex account" }, 404) };
82
+ }
83
+ const cred = await getValidCodexToken(accountId);
84
+ return {
85
+ ok: true,
86
+ value: await operation({
87
+ isMain: false,
88
+ poolGeneration: cred.generation,
89
+ accessToken: cred.accessToken,
90
+ chatgptAccountId: cred.chatgptAccountId,
91
+ }),
92
+ };
93
+ }
94
+
95
+ function safeResetCreditsDto(input: unknown): { credits: { granted_at: string; expires_at: string }[]; available_count?: number } {
96
+ const obj = typeof input === "object" && input !== null ? input as Record<string, unknown> : {};
97
+ const rawCredits = Array.isArray(obj.credits) ? obj.credits : [];
98
+ const credits = rawCredits.flatMap((raw): { granted_at: string; expires_at: string }[] => {
99
+ if (typeof raw !== "object" || raw === null) return [];
100
+ const credit = raw as Record<string, unknown>;
101
+ return typeof credit.granted_at === "string" && typeof credit.expires_at === "string"
102
+ ? [{ granted_at: credit.granted_at, expires_at: credit.expires_at }]
103
+ : [];
104
+ });
105
+ const rawAvailable = (obj.rate_limit_reset_credits as { available_count?: unknown } | null | undefined)?.available_count
106
+ ?? obj.available_count;
107
+ return {
108
+ credits,
109
+ ...(typeof rawAvailable === "number" && Number.isFinite(rawAvailable) ? { available_count: rawAvailable } : {}),
110
+ };
111
+ }
112
+
113
+ function safeResetCreditConsumeDto(input: unknown): { code: string } {
114
+ const obj = typeof input === "object" && input !== null ? input as Record<string, unknown> : {};
115
+ return { code: typeof obj.code === "string" ? obj.code : "unknown" };
116
+ }
117
+
118
+ /**
119
+ * Background reset-credit access for the auto-redeemer (#822). Goes through the same
120
+ * account/lease wrapper as the management routes, but takes a caller-owned
121
+ * `redeem_request_id` so a journaled id can be replayed idempotently after a crash.
122
+ * Throws on any auth or upstream failure; the caller treats a throw on consume as ambiguous.
123
+ */
124
+ export function createResetCreditWhamClient(config: OcxConfig, accountId: string): {
125
+ inspect: () => Promise<{ credits: { granted_at: string; expires_at: string }[] }>;
126
+ consume: (redeemRequestId: string) => Promise<{ code: string }>;
127
+ } {
128
+ const run = async <T>(operation: (auth: ResetCreditAuth) => Promise<T>): Promise<T> => {
129
+ const result = await withResetCreditAuth(getRuntimeConfig(config), accountId, operation);
130
+ if (result.ok) return result.value;
131
+ throw new Error(`reset-credit auth unavailable (${result.response.status})`);
132
+ };
133
+ return {
134
+ inspect: () => run(async auth => {
135
+ const resp = await fetch("https://chatgpt.com/backend-api/wham/rate-limit-reset-credits", {
136
+ headers: { Authorization: `Bearer ${auth.accessToken}`, "ChatGPT-Account-Id": auth.chatgptAccountId },
137
+ signal: AbortSignal.timeout(8000),
138
+ });
139
+ if (!resp.ok) { await resp.body?.cancel().catch(() => {}); throw new Error(`upstream ${resp.status}`); }
140
+ const parsed = await readResetCreditJson(resp, AbortSignal.timeout(8000));
141
+ if (!parsed.ok) throw new Error("invalid upstream reset-credit response");
142
+ return { credits: safeResetCreditsDto(parsed.value).credits };
143
+ }),
144
+ consume: redeemRequestId => run(async auth => {
145
+ const resp = await fetch("https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume", {
146
+ method: "POST",
147
+ headers: {
148
+ Authorization: `Bearer ${auth.accessToken}`,
149
+ "ChatGPT-Account-Id": auth.chatgptAccountId,
150
+ "Content-Type": "application/json",
151
+ },
152
+ body: JSON.stringify({ redeem_request_id: redeemRequestId }),
153
+ signal: AbortSignal.timeout(10_000),
154
+ });
155
+ if (!resp.ok) { await resp.body?.cancel().catch(() => {}); throw new Error(`upstream ${resp.status}`); }
156
+ return safeResetCreditConsumeDto(await resp.json());
157
+ }),
158
+ };
159
+ }
160
+
161
+ type ResetCreditJsonRead =
162
+ | { ok: true; value: unknown }
163
+ | { ok: false };
164
+
165
+ function cancelResponseBodyWithoutWaiting(body: ReadableStream<Uint8Array> | null): void {
166
+ if (!body) return;
167
+ try {
168
+ void body.cancel().catch(() => undefined);
169
+ } catch {
170
+ // Some stream implementations throw synchronously from cancel().
171
+ }
172
+ }
173
+
174
+ async function readResetCreditJson(
175
+ response: Response,
176
+ signal: AbortSignal,
177
+ ): Promise<ResetCreditJsonRead> {
178
+ const declaredLength = Number(response.headers.get("content-length"));
179
+ if (Number.isSafeInteger(declaredLength)
180
+ && declaredLength >= 0
181
+ && declaredLength > BOUNDED_BODY_MAX_BYTES) {
182
+ cancelResponseBodyWithoutWaiting(response.body);
183
+ return { ok: false };
184
+ }
185
+ try {
186
+ const body = await readBoundedResponseBody(response, {
187
+ signal,
188
+ maxBytes: BOUNDED_BODY_MAX_BYTES,
189
+ fatalUtf8: true,
190
+ });
191
+ if (!body.displaySafe || body.truncated || !body.text.trim()) return { ok: false };
192
+ return { ok: true, value: JSON.parse(body.text) as unknown };
193
+ } catch {
194
+ return { ok: false };
195
+ }
196
+ }
197
+
198
+ function manualResetAuthStillLive(accountId: string, auth: ResetCreditAuth): boolean {
199
+ if (!auth.isMain) {
200
+ const record = readCodexAccountRecord(accountId);
201
+ return auth.poolGeneration !== undefined
202
+ && isCodexAccountGenerationLive(accountId, auth.poolGeneration)
203
+ && record?.credential?.chatgptAccountId === auth.chatgptAccountId;
204
+ }
205
+ const tokens = readCodexTokens();
206
+ return !!auth.mainProof && !!tokens
207
+ && tokens.access_token === auth.accessToken && tokens.account_id === auth.chatgptAccountId
208
+ && isMainQuotaWriterLive(auth.mainProof.writer)
209
+ && auth.mainProof.credentialGeneration === getMainQuotaCredentialGeneration()
210
+ && matchesMainQuotaCredential(auth.accessToken, auth.chatgptAccountId);
211
+ }
212
+
213
+ /** A confirmed spend remains successful even when its optional usage observation fails. */
214
+ async function refreshAfterManualReset(
215
+ config: OcxConfig,
216
+ accountId: string,
217
+ auth: ResetCreditAuth,
218
+ claims: ManualResetCooldownClaim[],
219
+ didReset: boolean,
220
+ ): Promise<number | undefined> {
221
+ const afterDispatchSequence = currentQuotaDispatchSequence();
222
+ try {
223
+ if (!manualResetAuthStillLive(accountId, auth)) return undefined;
224
+ if (auth.isMain) {
225
+ const result = await fetchMainAccountInfoAttempt(true, 1, auth.nativeMainLease,
226
+ auth.nativeMainSharedClaimHeld === true, false);
227
+ const proof = result.resetRecoveryProof;
228
+ const recovered = didReset && manualResetAuthStillLive(accountId, auth)
229
+ && !!proof && !!auth.mainProof
230
+ && proof.dispatchSequence > afterDispatchSequence
231
+ && proof.credentialGeneration === auth.mainProof.credentialGeneration
232
+ && proof.writer.identityKey === auth.mainProof.writer.identityKey
233
+ && proof.writer.identityGeneration === auth.mainProof.writer.identityGeneration
234
+ && isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.info.plan);
235
+ for (const claim of claims) settleManualResetCooldown(getRuntimeConfig(config), claim, recovered);
236
+ return manualResetAuthStillLive(accountId, auth) ? result.freshResetCredits : undefined;
237
+ }
238
+ const account = configuredPoolAccount(getRuntimeConfig(config), accountId);
239
+ if (!account) return undefined;
240
+ // Reuse the just-authenticated consume credential for the first usage request.
241
+ // getValidCodexToken can silently advance a generation without exposing refresh
242
+ // provenance. A 401 here instead uses the existing classified refresh/replay path.
243
+ const resetToken: typeof getValidCodexToken = async () => {
244
+ if (auth.poolGeneration === undefined || !manualResetAuthStillLive(accountId, auth)) {
245
+ throw new CodexCredentialGenerationConflictError();
246
+ }
247
+ return { accessToken: auth.accessToken, chatgptAccountId: auth.chatgptAccountId, generation: auth.poolGeneration };
248
+ };
249
+ // `validatePending` is false here: a manual reset settles cooldown, and finishing deferred
250
+ // registration stays reserved for an explicit dashboard account-list refresh.
251
+ const result = await fetchPoolAccountQuota(accountId, true, account.plan, didReset ? resetToken : getValidCodexToken,
252
+ false, didReset ? afterDispatchSequence : undefined);
253
+ const record = readCodexAccountRecord(accountId);
254
+ const recovered = didReset && record?.credential?.chatgptAccountId === auth.chatgptAccountId
255
+ && (result.quotaProbeAttempted?.dispatchSequence ?? 0) > afterDispatchSequence
256
+ && isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan);
257
+ for (const claim of claims) settleManualResetCooldown(getRuntimeConfig(config), claim, recovered, {
258
+ credentialGeneration: result.freshCredentialGeneration,
259
+ refreshLineage: result.resetRefreshLineage,
260
+ });
261
+ return record?.credential?.chatgptAccountId === auth.chatgptAccountId ? result.freshResetCredits : undefined;
262
+ } catch {
263
+ // The upstream reset already happened. A failed refresh must not invite another spend.
264
+ return undefined;
265
+ }
266
+ }
267
+
268
+ export async function inspectResetCredits(config: OcxConfig, accountId: string, signal: AbortSignal): Promise<Response> {
269
+ const result = await withResetCreditAuth(getRuntimeConfig(config), accountId, async auth => {
270
+ const linkedSignal = signalWithTimeout(8000, signal);
271
+ let detachBodyAbort = () => {};
272
+ try {
273
+ let resp: Response;
274
+ try {
275
+ resp = await fetch(
276
+ "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
277
+ {
278
+ headers: {
279
+ Authorization: `Bearer ${auth.accessToken}`,
280
+ "ChatGPT-Account-Id": auth.chatgptAccountId,
281
+ },
282
+ signal: linkedSignal.signal,
283
+ },
284
+ );
285
+ } catch (error) {
286
+ if (linkedSignal.signal.aborted) {
287
+ return jsonResponse({ error: "Invalid upstream reset-credit response" }, 502);
288
+ }
289
+ throw error;
290
+ }
291
+ // Own the response body before the bounded reader attaches. If the client
292
+ // disconnects in that narrow window, Bun otherwise tears down the native
293
+ // body off the awaited path and can report an unhandled rejection.
294
+ detachBodyAbort = cancelBodyOnAbort(resp.body, linkedSignal.signal);
295
+ if (!resp.ok) {
296
+ await resp.body?.cancel().catch(() => {});
297
+ return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
298
+ }
299
+ const parsed = await readResetCreditJson(resp, linkedSignal.signal);
300
+ if (!parsed.ok) {
301
+ return jsonResponse({ error: "Invalid upstream reset-credit response" }, 502);
302
+ }
303
+ return jsonResponse(safeResetCreditsDto(parsed.value));
304
+ } finally {
305
+ detachBodyAbort();
306
+ linkedSignal.cleanup();
307
+ }
308
+ });
309
+ return result.ok ? result.value : result.response;
310
+ }
311
+
312
+ export async function consumeResetCredits(config: OcxConfig, accountId: string, requestedOperationId: string | undefined): Promise<Response> {
313
+ const operation = await withResetCreditAuth(getRuntimeConfig(config), accountId, async auth => {
314
+ // The ledger keys manual operations by the *physical* ChatGPT account, which is
315
+ // only known after the auth wrapper resolves credentials. Open here, not earlier.
316
+ let identity = requestedOperationId === undefined
317
+ ? undefined
318
+ : {
319
+ accountId,
320
+ chatgptAccountId: auth.chatgptAccountId,
321
+ operationId: requestedOperationId,
322
+ } as const;
323
+ let idempotencyKey: string;
324
+ if (identity) {
325
+ const opened = openManualResetCreditOperation(identity);
326
+ if (opened.kind === "terminal") {
327
+ // Durably settled already: replay the recorded outcome instead of
328
+ // trusting upstream idempotency for an irreversible spend. No
329
+ // `remaining` — that field is only reported from a freshly parsed
330
+ // available_count, and a replay has none.
331
+ return jsonResponse({ code: opened.code, replayed: true });
332
+ }
333
+ if (opened.kind === "identity-mismatch") {
334
+ return jsonResponse({
335
+ error: "operation_id_owned_by_another_account",
336
+ code: "identity_mismatch",
337
+ }, 409);
338
+ }
339
+ if (opened.kind !== "execute") {
340
+ // capacity | unavailable -> fail closed. Falling back to a random id
341
+ // would silently reintroduce the double-spend this identity prevents.
342
+ const response = jsonResponse({
343
+ error: opened.kind === "capacity"
344
+ ? "reset_credit_ledger_capacity"
345
+ : "reset_credit_ledger_unavailable",
346
+ code: opened.kind,
347
+ }, 503);
348
+ response.headers.set("Retry-After", "1");
349
+ return response;
350
+ }
351
+ // Canonical id, which an alias join may map to an earlier caller id.
352
+ identity = { ...identity, operationId: opened.operationId };
353
+ idempotencyKey = opened.operationId;
354
+ } else {
355
+ idempotencyKey = crypto.randomUUID();
356
+ }
357
+ const claims = manualResetAuthStillLive(accountId, auth)
358
+ ? claimManualResetCooldowns(getRuntimeConfig(config), accountId, Date.now(), auth.poolGeneration) : [];
359
+ try {
360
+ let resp: Response;
361
+ try {
362
+ resp = await fetch(
363
+ "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
364
+ {
365
+ method: "POST",
366
+ headers: {
367
+ Authorization: `Bearer ${auth.accessToken}`,
368
+ "ChatGPT-Account-Id": auth.chatgptAccountId,
369
+ "Content-Type": "application/json",
370
+ },
371
+ body: JSON.stringify({ redeem_request_id: idempotencyKey }),
372
+ signal: AbortSignal.timeout(10_000),
373
+ },
374
+ );
375
+ } catch (error) {
376
+ // Dispatch outcome unknown: the credit may or may not have been spent.
377
+ // Mark ambiguous so a replay of this same id is never treated as new.
378
+ if (identity) markManualResetCreditOperationAmbiguous(identity);
379
+ throw error;
380
+ }
381
+ if (!resp.ok) {
382
+ await resp.body?.cancel().catch(() => {});
383
+ if (identity) markManualResetCreditOperationAmbiguous(identity);
384
+ return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
385
+ }
386
+ const result = safeResetCreditConsumeDto(await resp.json());
387
+ if (identity) {
388
+ // Narrow explicitly rather than casting: `safeResetCreditConsumeDto`
389
+ // normalizes anything unrecognized to "unknown", and settling that
390
+ // would come back as a mismatch and leave the row pending anyway.
391
+ // Settlement failure never downgrades the user-visible outcome: the
392
+ // spend already happened upstream, and reporting failure would invite
393
+ // a manual retry -- the exact double-spend this unit removes.
394
+ if (result.code === "reset" || result.code === "already_redeemed"
395
+ || result.code === "nothing_to_reset" || result.code === "no_credit") {
396
+ settleManualResetCreditOperation(identity, result.code);
397
+ } else {
398
+ markManualResetCreditOperationAmbiguous(identity);
399
+ }
400
+ }
401
+ // After a successful redeem (or an idempotent already_redeemed), refresh WHAM usage
402
+ // and return remaining only when that refresh freshly parsed available_count.
403
+ // Do not fall back to a preserved cached resetCredits (failed/omitted refresh).
404
+ if (result.code === "reset" || result.code === "already_redeemed") {
405
+ const freshResetCredits = await refreshAfterManualReset(
406
+ config, accountId, auth, claims, result.code === "reset",
407
+ );
408
+ return jsonResponse({
409
+ code: result.code,
410
+ ...(typeof freshResetCredits === "number" && Number.isFinite(freshResetCredits)
411
+ ? { remaining: freshResetCredits }
412
+ : {}),
413
+ });
414
+ }
415
+ return jsonResponse(result);
416
+ } finally {
417
+ // Release only this invocation's leases, including every ambiguous/error outcome.
418
+ for (const claim of claims) settleManualResetCooldown(getRuntimeConfig(config), claim, false);
419
+ }
420
+ });
421
+ return operation.ok ? operation.value : operation.response;
422
+ }