@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.
Files changed (256) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  3. package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -3
  6. package/src/adapters/base.ts +21 -0
  7. package/src/adapters/codebuddy/adapter.ts +2 -1
  8. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  9. package/src/adapters/command-code.ts +1 -1
  10. package/src/adapters/cursor/envelope-echo.ts +8 -2
  11. package/src/adapters/cursor/transport-retry.ts +46 -1
  12. package/src/adapters/cursor.ts +4 -0
  13. package/src/adapters/google.ts +7 -7
  14. package/src/adapters/kiro/adapter.ts +42 -1
  15. package/src/adapters/kiro/payload.ts +17 -3
  16. package/src/adapters/kiro/reasoning.ts +70 -7
  17. package/src/adapters/kiro/stream.ts +8 -2
  18. package/src/adapters/kiro/wire.ts +2 -1
  19. package/src/adapters/kiro-events.ts +21 -13
  20. package/src/adapters/kiro-retry.ts +23 -4
  21. package/src/adapters/openai-chat/errors.ts +116 -0
  22. package/src/adapters/openai-chat/messages.ts +346 -0
  23. package/src/adapters/openai-chat/passthrough.ts +146 -0
  24. package/src/adapters/openai-chat/response-events.ts +117 -0
  25. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  26. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  27. package/src/adapters/openai-chat/tool-schema.ts +495 -0
  28. package/src/adapters/openai-chat/wire.ts +50 -0
  29. package/src/adapters/openai-chat.ts +40 -1452
  30. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  31. package/src/adapters/openai-responses/image-gen.ts +406 -0
  32. package/src/adapters/openai-responses/internal.ts +3 -0
  33. package/src/adapters/openai-responses/passthrough.ts +642 -0
  34. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  35. package/src/adapters/openai-responses/reasoning.ts +220 -0
  36. package/src/adapters/openai-responses/request-strips.ts +185 -0
  37. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  38. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  39. package/src/adapters/openai-responses/web-search.ts +156 -0
  40. package/src/adapters/openai-responses.ts +4 -2625
  41. package/src/bridge/errors.ts +58 -0
  42. package/src/bridge/internal.ts +174 -0
  43. package/src/bridge/response-json.ts +630 -0
  44. package/src/bridge/sse.ts +1462 -0
  45. package/src/bridge.ts +5 -2204
  46. package/src/chat/inbound.ts +12 -1
  47. package/src/claude/desktop-profile.ts +66 -9
  48. package/src/claude/outbound.ts +18 -0
  49. package/src/cli/account-main.ts +1 -1
  50. package/src/cli/capabilities.ts +2 -2
  51. package/src/cli/combo.ts +10 -1
  52. package/src/cli/index.ts +48 -5
  53. package/src/cli/registry.ts +2 -1
  54. package/src/cli/system-command.ts +4 -4
  55. package/src/clients/config-export.ts +7 -3
  56. package/src/codex/account-label.ts +14 -3
  57. package/src/codex/account-lifecycle.ts +3 -0
  58. package/src/codex/account-store.ts +184 -35
  59. package/src/codex/account-usability.ts +21 -0
  60. package/src/codex/auth-api/account-list.ts +507 -0
  61. package/src/codex/auth-api/http.ts +32 -0
  62. package/src/codex/auth-api/login-flow.ts +566 -0
  63. package/src/codex/auth-api/login-state.ts +64 -0
  64. package/src/codex/auth-api/main-account-probe.ts +331 -0
  65. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  66. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  67. package/src/codex/auth-api/reset-credit-service.ts +431 -0
  68. package/src/codex/auth-api/routes.ts +425 -0
  69. package/src/codex/auth-api/runtime-config.ts +48 -0
  70. package/src/codex/auth-api.ts +27 -3118
  71. package/src/codex/auth-context.ts +252 -35
  72. package/src/codex/catalog/aggregation.ts +80 -1
  73. package/src/codex/catalog/auto-review.ts +507 -0
  74. package/src/codex/catalog/build-entries.ts +981 -0
  75. package/src/codex/catalog/combo-member.ts +375 -0
  76. package/src/codex/catalog/derive-entry.ts +229 -0
  77. package/src/codex/catalog/effort.ts +0 -1
  78. package/src/codex/catalog/gated-native-warn.ts +63 -0
  79. package/src/codex/catalog/gather-capture.ts +533 -0
  80. package/src/codex/catalog/model-hints.ts +691 -0
  81. package/src/codex/catalog/model-visibility.ts +305 -0
  82. package/src/codex/catalog/provider-fetch.ts +52 -2942
  83. package/src/codex/catalog/provider-models.ts +685 -0
  84. package/src/codex/catalog/remote.ts +30 -0
  85. package/src/codex/catalog/restore.ts +132 -0
  86. package/src/codex/catalog/retained-sync.ts +714 -0
  87. package/src/codex/catalog/routed-gather.ts +895 -0
  88. package/src/codex/catalog/subagent-roster.ts +176 -0
  89. package/src/codex/catalog/sync.ts +52 -2698
  90. package/src/codex/cli-install-provenance.ts +7 -1
  91. package/src/codex/convergence.ts +7 -2
  92. package/src/codex/desktop-app/types.ts +11 -2
  93. package/src/codex/desktop-app/windows.ts +5 -5
  94. package/src/codex/inject/config-toml.ts +563 -0
  95. package/src/codex/inject/remove.ts +192 -0
  96. package/src/codex/inject/restore.ts +567 -0
  97. package/src/codex/inject/routing-classify.ts +109 -0
  98. package/src/codex/inject/routing-target.ts +125 -0
  99. package/src/codex/inject.ts +89 -1444
  100. package/src/codex/lineage.ts +458 -0
  101. package/src/codex/model-entitlements.ts +152 -15
  102. package/src/codex/pool-refresh-backoff.ts +161 -0
  103. package/src/codex/quota-rejection.ts +104 -15
  104. package/src/codex/routing/active-account.ts +194 -0
  105. package/src/codex/routing/cache-affinity.ts +70 -0
  106. package/src/codex/routing/cooldown-math.ts +285 -0
  107. package/src/codex/routing/health-store.ts +402 -0
  108. package/src/codex/routing/probe-lease.ts +358 -0
  109. package/src/codex/routing/selection.ts +780 -0
  110. package/src/codex/routing/thread-affinity.ts +586 -0
  111. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  112. package/src/codex/routing.ts +370 -2271
  113. package/src/codex/shim-fingerprint.ts +223 -0
  114. package/src/codex/shim-inspect.ts +175 -0
  115. package/src/codex/shim-probe.ts +367 -0
  116. package/src/codex/shim-restore-lock.ts +169 -0
  117. package/src/codex/shim-state-file.ts +151 -0
  118. package/src/codex/shim-templates.ts +265 -0
  119. package/src/codex/shim.ts +48 -1268
  120. package/src/codex/warmup.ts +1 -1
  121. package/src/combos/failover.ts +85 -0
  122. package/src/combos/request.ts +17 -10
  123. package/src/combos/types.ts +23 -2
  124. package/src/config/diagnostics.ts +705 -0
  125. package/src/config/feature-flags.ts +55 -0
  126. package/src/config/live-reconcile.ts +403 -0
  127. package/src/config/load-degrade.ts +880 -0
  128. package/src/config/mutation-lock.ts +244 -0
  129. package/src/config/openai-tier-backup.ts +268 -0
  130. package/src/config/pending-teardown.ts +31 -0
  131. package/src/config/persist-unlocked.ts +92 -0
  132. package/src/config/proxy-env.ts +188 -0
  133. package/src/config/salvage.ts +244 -0
  134. package/src/config/schema/config-schema.ts +640 -0
  135. package/src/config/schema/leaf-validators.ts +855 -0
  136. package/src/config/warn-memo.ts +28 -0
  137. package/src/config.ts +234 -4481
  138. package/src/generated/compatibility-version.json +649 -121
  139. package/src/images/loop.ts +1 -1
  140. package/src/lib/errors.ts +17 -0
  141. package/src/lib/request-execution-budget.ts +198 -23
  142. package/src/lib/spend-reservation-ledger.ts +958 -0
  143. package/src/lib/state-store-registrations.ts +6 -2
  144. package/src/lib/test-home-guard.ts +85 -1
  145. package/src/lib/upstream-retry.ts +132 -21
  146. package/src/lib/windows-elevation.ts +76 -14
  147. package/src/lib/workflow-budget.ts +553 -30
  148. package/src/oauth/index.ts +2 -2
  149. package/src/oauth/key-providers.ts +2 -2
  150. package/src/providers/kiro-models.ts +4 -3
  151. package/src/providers/label.ts +19 -1
  152. package/src/providers/model-discovery.ts +16 -0
  153. package/src/providers/quota/account-cache.ts +441 -0
  154. package/src/providers/quota/antigravity.ts +295 -0
  155. package/src/providers/quota/report-cache.ts +320 -0
  156. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  157. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  158. package/src/providers/quota.ts +324 -3079
  159. package/src/providers/registry/entries-core.ts +1228 -0
  160. package/src/providers/registry/entries-extended.ts +1213 -0
  161. package/src/providers/registry/model-seeds.ts +912 -0
  162. package/src/providers/registry/types.ts +352 -0
  163. package/src/providers/registry.ts +24 -3536
  164. package/src/responses/continuation-ownership.ts +29 -0
  165. package/src/responses/reasoning-envelope.ts +6 -3
  166. package/src/responses/state/replay-fingerprint.ts +80 -0
  167. package/src/responses/state/snapshot-codec.ts +104 -0
  168. package/src/responses/state/spill-failure.ts +118 -0
  169. package/src/responses/state/spill-queue.ts +665 -0
  170. package/src/responses/state/temp-recovery.ts +257 -0
  171. package/src/responses/state.ts +82 -1143
  172. package/src/routing/identity-domains.ts +456 -0
  173. package/src/routing/probe-lease.ts +613 -0
  174. package/src/server/chat-completions.ts +3 -1
  175. package/src/server/chat-native.ts +37 -9
  176. package/src/server/index/bounded-request.ts +88 -0
  177. package/src/server/index/live-sideband.ts +601 -0
  178. package/src/server/index/serve-options.ts +1766 -0
  179. package/src/server/index/startup-warnings.ts +213 -0
  180. package/src/server/index/websocket-handler.ts +339 -0
  181. package/src/server/index.ts +45 -2552
  182. package/src/server/inspection-tee.ts +107 -0
  183. package/src/server/live.ts +46 -1
  184. package/src/server/management/combo-routes.ts +10 -1
  185. package/src/server/management/route-registry.ts +26 -23
  186. package/src/server/management/shared.ts +8 -5
  187. package/src/server/management/workflow-budget-routes.ts +133 -0
  188. package/src/server/management-api.ts +12 -0
  189. package/src/server/relay-eager.ts +2 -0
  190. package/src/server/relay.ts +14 -19
  191. package/src/server/request-log-conversation.ts +9 -7
  192. package/src/server/request-log.ts +372 -4
  193. package/src/server/response-log-body.ts +153 -0
  194. package/src/server/responses/account-change-state.ts +307 -0
  195. package/src/server/responses/adapter-continuation.ts +540 -0
  196. package/src/server/responses/adapter-delivery.ts +208 -0
  197. package/src/server/responses/adapter-dispatch.ts +1042 -0
  198. package/src/server/responses/codex-ws-wire.ts +5 -0
  199. package/src/server/responses/collaboration.ts +74 -4
  200. package/src/server/responses/combo-session-recall.ts +68 -8
  201. package/src/server/responses/compact.ts +113 -17
  202. package/src/server/responses/completion-policy.ts +33 -0
  203. package/src/server/responses/core-auth.ts +529 -0
  204. package/src/server/responses/core-codex-account.ts +907 -0
  205. package/src/server/responses/core-combo-failure.ts +210 -0
  206. package/src/server/responses/core-combo.ts +787 -0
  207. package/src/server/responses/core-errors.ts +170 -0
  208. package/src/server/responses/core-lifetime.ts +95 -0
  209. package/src/server/responses/core-normalize.ts +350 -0
  210. package/src/server/responses/core-opaque-recovery.ts +380 -0
  211. package/src/server/responses/core-options.ts +159 -0
  212. package/src/server/responses/core-replay.ts +298 -0
  213. package/src/server/responses/core.ts +192 -8893
  214. package/src/server/responses/encrypted-payload.ts +0 -1
  215. package/src/server/responses/input-admission.ts +126 -6
  216. package/src/server/responses/passthrough-delivery.ts +869 -0
  217. package/src/server/responses/passthrough-dispatch.ts +1494 -0
  218. package/src/server/responses/passthrough-error.ts +38 -2
  219. package/src/server/responses/passthrough-execution.ts +54 -0
  220. package/src/server/responses/request-prepare.ts +1080 -0
  221. package/src/server/responses/request-send-budget.ts +259 -0
  222. package/src/server/responses/request-sidecar-auth.ts +149 -0
  223. package/src/server/responses/request-spend.ts +147 -0
  224. package/src/server/responses/request-transport.ts +803 -0
  225. package/src/server/responses/response-effects.ts +157 -0
  226. package/src/server/responses/run-turn-execution.ts +476 -0
  227. package/src/server/responses/sidecar-execution.ts +463 -0
  228. package/src/server/responses/terminal-guard.ts +65 -4
  229. package/src/server/responses-image-gen-repair.ts +1 -1
  230. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  231. package/src/server/workflow-refusal.ts +84 -0
  232. package/src/service/windows-ops.ts +210 -16
  233. package/src/service/windows-scheduler.ts +28 -21
  234. package/src/service.ts +1 -1
  235. package/src/types/config.ts +34 -1
  236. package/src/types/request.ts +8 -5
  237. package/src/types/tools.ts +24 -0
  238. package/src/types.ts +2 -0
  239. package/src/update/index.ts +10 -0
  240. package/src/update/stop-contract.d.mts +1 -0
  241. package/src/update/stop-contract.mjs +19 -0
  242. package/src/update/stop-decision.d.mts +1 -1
  243. package/src/update/stop-decision.mjs +12 -3
  244. package/src/usage/log.ts +147 -1
  245. package/src/usage/summary.ts +171 -21
  246. package/src/vision/anthropic-describe.ts +1 -1
  247. package/src/vision/describe.ts +5 -5
  248. package/src/web-search/anthropic-executor.ts +1 -1
  249. package/src/web-search/exa-executor.ts +1 -1
  250. package/src/web-search/executor.ts +1 -1
  251. package/src/web-search/gemini-executor.ts +1 -1
  252. package/src/web-search/loop.ts +1 -1
  253. package/src/web-search/ollama-executor.ts +1 -1
  254. package/src/web-search/parse.ts +67 -14
  255. package/src/web-search/passthrough-bridge.ts +64 -31
  256. package/src/web-search/xai-executor.ts +1 -1
@@ -0,0 +1,358 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { isCodexAccountGenerationLive, readCodexAccountRecord, type CodexRefreshProvenance } from "../account-store";
3
+ import { isCodexAccountPaused } from "../account-pause";
4
+ import { isSelectableCodexPoolAccount } from "../account-id";
5
+ import { isAccountNeedsReauth } from "../account-runtime-state";
6
+ import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
7
+ import type { OcxConfig } from "../../types";
8
+ import { CODEX_QUOTA_PROBE_INTERVAL_MS, type CodexUpstreamOutcomeMeta } from "./cooldown-math";
9
+ import {
10
+ deleteScopedHealth,
11
+ getAccountHealth,
12
+ listScopedHealthEntries,
13
+ scopedHealthFor,
14
+ setAccountHealth,
15
+ setScopedHealth,
16
+ type CodexQuotaScope,
17
+ type CodexUpstreamHealth,
18
+ } from "./health-store";
19
+
20
+ export type CodexQuotaRecoveryProbeClaim = {
21
+ accountId: string;
22
+ scope?: CodexQuotaScope;
23
+ leaseId: string;
24
+ cooldownGeneration: number;
25
+ credentialGeneration: number;
26
+ /** Claim-time `replacedAt`; unchanged after a probe-owned refresh, stamped on external replacement. */
27
+ credentialReplacedAt?: number;
28
+ };
29
+
30
+ export type CodexQuotaRecoveryProbeProof = {
31
+ credentialGeneration?: number;
32
+ };
33
+
34
+ /**
35
+ * Grant at most one probe lease per interval for a cooled-down account.
36
+ *
37
+ * A cooled-down account is short-circuited locally, so it never sends traffic and
38
+ * no organic 2xx can prove that upstream quota recovered — the cooldown can only
39
+ * end by expiry or a proxy restart (#433). Releasing a single probe breaks that
40
+ * deadlock. Explicit Retry-After cooldowns are excluded: those are literal retry
41
+ * directives, not window announcements.
42
+ *
43
+ * Returns the lease id, or null when no probe may go out right now.
44
+ */
45
+ export function tryAcquireCodexQuotaProbeLease(accountId: string, now = Date.now()): string | null {
46
+ if (!canAcquireCodexQuotaProbeLease(accountId, now)) return null;
47
+ const health = getAccountHealth(accountId)!;
48
+ const probeLeaseId = randomUUID();
49
+ setAccountHealth(accountId, {
50
+ ...health,
51
+ probeLeaseId,
52
+ probeLeaseGeneration: health.cooldownGeneration ?? 0,
53
+ lastProbeAt: now,
54
+ });
55
+ return probeLeaseId;
56
+ }
57
+
58
+ /** Side-effect-free check mirroring {@link tryAcquireCodexQuotaProbeLease} eligibility. */
59
+ export function canAcquireCodexQuotaProbeLease(accountId: string, now = Date.now()): boolean {
60
+ return canAcquireQuotaProbeLease(getAccountHealth(accountId), now);
61
+ }
62
+
63
+ function canAcquireQuotaProbeLease(health: CodexUpstreamHealth | undefined, now: number): boolean {
64
+ if (!health) return false;
65
+ const cooldownUntil = health.cooldownUntil;
66
+ if (typeof cooldownUntil !== "number" || !Number.isFinite(cooldownUntil) || cooldownUntil <= now) return false;
67
+ if (health.cooldownSource === "retry-after") return false;
68
+ if (health.probeLeaseId !== undefined) return false;
69
+ const origin = health.lastProbeAt ?? health.cooldownSince ?? cooldownUntil;
70
+ return now - origin >= CODEX_QUOTA_PROBE_INTERVAL_MS;
71
+ }
72
+
73
+ /**
74
+ * Claim due reset-derived cooldown probes without consulting account selection.
75
+ * Added Pool credentials only; owned main usage recovery is handled separately.
76
+ */
77
+ export function claimDueCodexQuotaRecoveryProbes(
78
+ config: OcxConfig,
79
+ limit: number,
80
+ now = Date.now(),
81
+ ): CodexQuotaRecoveryProbeClaim[] {
82
+ const boundedLimit = Math.max(0, Math.floor(limit));
83
+ if (boundedLimit === 0) return [];
84
+ const candidates: Array<{
85
+ accountId: string;
86
+ scope?: CodexQuotaScope;
87
+ health: CodexUpstreamHealth;
88
+ credentialGeneration: number;
89
+ credentialReplacedAt?: number;
90
+ order: number;
91
+ }> = [];
92
+ for (const [order, account] of (config.codexAccounts ?? []).entries()) {
93
+ if (!isSelectableCodexPoolAccount(account)
94
+ || isCodexAccountPaused(config, account.id)
95
+ || isAccountNeedsReauth(account.id)) continue;
96
+ const record = readCodexAccountRecord(account.id);
97
+ if (!record?.credential || record.deletedAt != null) continue;
98
+ const due = [
99
+ { scope: undefined, health: getAccountHealth(account.id) },
100
+ ...[...(listScopedHealthEntries(account.id))].map(([scope, health]) => ({ scope, health })),
101
+ ].filter((entry): entry is { scope?: CodexQuotaScope; health: CodexUpstreamHealth } =>
102
+ // Generic WHAM evidence can recover only ordinary quota, never Reserve.
103
+ // Do not spend this account's one claim per pass on an independent scope and
104
+ // delay the shared scope that the response can actually recover.
105
+ (entry.scope === undefined || entry.scope === "shared")
106
+ && entry.health?.cooldownSource === "reset-derived"
107
+ && canAcquireQuotaProbeLease(entry.health, now))
108
+ .sort((a, b) =>
109
+ (a.health.lastProbeAt ?? a.health.cooldownSince ?? 0)
110
+ - (b.health.lastProbeAt ?? b.health.cooldownSince ?? 0));
111
+ const candidate = due[0];
112
+ if (candidate) candidates.push({
113
+ accountId: account.id,
114
+ ...(candidate.scope ? { scope: candidate.scope } : {}),
115
+ health: candidate.health,
116
+ credentialGeneration: record.generation,
117
+ ...(record.replacedAt !== undefined ? { credentialReplacedAt: record.replacedAt } : {}),
118
+ order,
119
+ });
120
+ }
121
+ candidates.sort((a, b) => {
122
+ const age = (a.health.lastProbeAt ?? a.health.cooldownSince ?? 0)
123
+ - (b.health.lastProbeAt ?? b.health.cooldownSince ?? 0);
124
+ return age || a.order - b.order;
125
+ });
126
+ return candidates.slice(0, boundedLimit).map(candidate => {
127
+ const leaseId = randomUUID();
128
+ const next = {
129
+ ...candidate.health,
130
+ probeLeaseId: leaseId,
131
+ probeLeaseGeneration: candidate.health.cooldownGeneration ?? 0,
132
+ lastProbeAt: now,
133
+ };
134
+ if (candidate.scope) setScopedHealth(candidate.accountId, candidate.scope, next);
135
+ else setAccountHealth(candidate.accountId, next);
136
+ return {
137
+ accountId: candidate.accountId,
138
+ ...(candidate.scope ? { scope: candidate.scope } : {}),
139
+ leaseId,
140
+ cooldownGeneration: candidate.health.cooldownGeneration ?? 0,
141
+ credentialGeneration: candidate.credentialGeneration,
142
+ ...(candidate.credentialReplacedAt !== undefined
143
+ ? { credentialReplacedAt: candidate.credentialReplacedAt }
144
+ : {}),
145
+ };
146
+ });
147
+ }
148
+
149
+ type CooldownRecoveryLease = Pick<CodexQuotaRecoveryProbeClaim,
150
+ "accountId" | "scope" | "leaseId" | "cooldownGeneration">;
151
+
152
+ export type ManualResetCooldownClaim =
153
+ | { kind: "pool"; probe: CodexQuotaRecoveryProbeClaim }
154
+ | { kind: "main"; probe: CooldownRecoveryLease };
155
+
156
+ function manualResetAccountEligible(config: OcxConfig, accountId: string): boolean {
157
+ return !isCodexAccountPaused(config, accountId) && !isAccountNeedsReauth(accountId)
158
+ && (accountId === MAIN_CODEX_ACCOUNT_ID
159
+ || (config.codexAccounts ?? []).some(account => account.id === accountId && isSelectableCodexPoolAccount(account)));
160
+ }
161
+
162
+ /** Explicit reset bypasses probe pacing, never another owner's lease or quota scope. */
163
+ export function claimManualResetCooldowns(
164
+ config: OcxConfig,
165
+ accountId: string,
166
+ now = Date.now(),
167
+ expectedPoolGeneration?: number,
168
+ ): ManualResetCooldownClaim[] {
169
+ if (!manualResetAccountEligible(config, accountId)) return [];
170
+ const record = accountId === MAIN_CODEX_ACCOUNT_ID ? undefined : readCodexAccountRecord(accountId);
171
+ if (accountId !== MAIN_CODEX_ACCOUNT_ID && (!record?.credential || record.deletedAt != null)) return [];
172
+ if (record && expectedPoolGeneration !== undefined && record.generation !== expectedPoolGeneration) return [];
173
+ const claims: ManualResetCooldownClaim[] = [];
174
+ for (const scope of [undefined, "shared"] as const) {
175
+ const health = scope ? scopedHealthFor(accountId, scope) : getAccountHealth(accountId);
176
+ if (!health || health.cooldownSource !== "reset-derived" || health.probeLeaseId !== undefined
177
+ || !Number.isFinite(health.cooldownUntil) || !(health.cooldownUntil! > now)) continue;
178
+ const leaseId = randomUUID();
179
+ const cooldownGeneration = health.cooldownGeneration ?? 0;
180
+ const next = { ...health, probeLeaseId: leaseId, probeLeaseGeneration: cooldownGeneration, lastProbeAt: now };
181
+ if (scope) setScopedHealth(accountId, scope, next);
182
+ else setAccountHealth(accountId, next);
183
+ const probe = { accountId, scope, leaseId, cooldownGeneration };
184
+ claims.push(record ? { kind: "pool", probe: {
185
+ ...probe, credentialGeneration: record.generation, credentialReplacedAt: record.replacedAt,
186
+ } } : { kind: "main", probe });
187
+ }
188
+ return claims;
189
+ }
190
+
191
+ export type ManualResetRefreshLineage = Readonly<{
192
+ fromGeneration: number;
193
+ toGeneration: number;
194
+ provenance: CodexRefreshProvenance;
195
+ }>;
196
+
197
+ type ManualResetQuotaProof = CodexQuotaRecoveryProbeProof & {
198
+ refreshLineage?: ManualResetRefreshLineage;
199
+ };
200
+
201
+ /** Main proof is checked by the already-owned auth operation, never by a Pool record. */
202
+ export function settleManualResetCooldown(
203
+ config: OcxConfig,
204
+ claim: ManualResetCooldownClaim,
205
+ recovered: boolean,
206
+ proof: ManualResetQuotaProof = {},
207
+ now = Date.now(),
208
+ ): boolean {
209
+ if (!recovered) return settleCooldownRecoveryLease(claim.probe, false, now);
210
+ const eligible = manualResetAccountEligible(config, claim.probe.accountId);
211
+ if (claim.kind === "main") return settleCooldownRecoveryLease(claim.probe, eligible, now);
212
+ const lineage = proof.refreshLineage;
213
+ // Equal wall-clock replacement stamps do not establish ancestry. Manual +1
214
+ // recovery additionally needs the actual forced-refresh result for this edge.
215
+ const ownedGeneration = proof.credentialGeneration === claim.probe.credentialGeneration
216
+ || (proof.credentialGeneration === claim.probe.credentialGeneration + 1
217
+ && lineage?.fromGeneration === claim.probe.credentialGeneration
218
+ && lineage.toGeneration === proof.credentialGeneration
219
+ && (lineage.provenance === "self-refresh" || lineage.provenance === "joined-lineage"));
220
+ return settleCodexQuotaRecoveryProbe(claim.probe, eligible && ownedGeneration, proof, now);
221
+ }
222
+
223
+ /** Settle one background recovery claim without mutating account-wide outcome state. */
224
+ export function settleCodexQuotaRecoveryProbe(
225
+ claim: CodexQuotaRecoveryProbeClaim,
226
+ recovered: boolean,
227
+ proof: CodexQuotaRecoveryProbeProof,
228
+ now = Date.now(),
229
+ ): boolean {
230
+ const health = claim.scope
231
+ ? scopedHealthFor(claim.accountId, claim.scope)
232
+ : getAccountHealth(claim.accountId);
233
+ if (!health || health.probeLeaseId !== claim.leaseId) return false;
234
+ const currentRecord = readCodexAccountRecord(claim.accountId);
235
+ const proofGeneration = proof.credentialGeneration;
236
+ // A probe-owned token refresh (getValidCodexToken) advances the credential generation by
237
+ // exactly one while preserving `replacedAt`; an external credential replacement bumps the
238
+ // generation too but stamps a fresh `replacedAt`. Accept the +1 transition only when the
239
+ // claim-time lineage is intact AND the generation the fresh quota was proven under is live.
240
+ const generationFenced = proofGeneration !== undefined
241
+ && (proofGeneration === claim.credentialGeneration
242
+ ? isCodexAccountGenerationLive(claim.accountId, proofGeneration)
243
+ : proofGeneration === claim.credentialGeneration + 1
244
+ && currentRecord?.replacedAt === claim.credentialReplacedAt
245
+ && isCodexAccountGenerationLive(claim.accountId, proofGeneration));
246
+ return settleCooldownRecoveryLease(claim, recovered && generationFenced, now);
247
+ }
248
+
249
+ function settleCooldownRecoveryLease(claim: CooldownRecoveryLease, recovered: boolean, now: number): boolean {
250
+ const health = claim.scope ? scopedHealthFor(claim.accountId, claim.scope) : getAccountHealth(claim.accountId);
251
+ if (!health || health.probeLeaseId !== claim.leaseId) return false;
252
+ const fenced = (claim.scope === undefined || claim.scope === "shared")
253
+ && health.cooldownSource === "reset-derived"
254
+ && (health.cooldownGeneration ?? 0) === claim.cooldownGeneration
255
+ && (health.probeLeaseGeneration ?? 0) === claim.cooldownGeneration;
256
+ if (!recovered || !fenced) {
257
+ const released = withProbeLeaseReleased(health, now);
258
+ if (claim.scope) setScopedHealth(claim.accountId, claim.scope, released);
259
+ else setAccountHealth(claim.accountId, released);
260
+ return false;
261
+ }
262
+ if (claim.scope) {
263
+ deleteScopedHealth(claim.accountId, claim.scope);
264
+ } else {
265
+ const {
266
+ cooldownUntil: _until,
267
+ cooldownSince: _since,
268
+ cooldownSource: _source,
269
+ probeLeaseId: _leaseId,
270
+ probeLeaseGeneration: _leaseGeneration,
271
+ // "The quota window moved" is a statement about the whole refusal, so the avoidance it
272
+ // announced goes with the block it produced. Leaving it would make this escape hatch stop
273
+ // escaping: the account would still be passed over by every selection it is meant to win.
274
+ quotaAvoidUntil: _avoid,
275
+ ...rest
276
+ } = health;
277
+ setAccountHealth(claim.accountId, {
278
+ ...rest,
279
+ cooldownGeneration: claim.cooldownGeneration + 1,
280
+ lastProbeAt: now,
281
+ });
282
+ }
283
+ return true;
284
+ }
285
+
286
+ /** Acquire the recovery probe for one confirmed model-specific quota group. */
287
+ export function tryAcquireCodexQuotaScopeProbeLease(
288
+ accountId: string,
289
+ scope: CodexQuotaScope,
290
+ now = Date.now(),
291
+ ): string | null {
292
+ const health = scopedHealthFor(accountId, scope);
293
+ if (!canAcquireQuotaProbeLease(health, now)) return null;
294
+ const probeLeaseId = randomUUID();
295
+ setScopedHealth(accountId, scope, {
296
+ ...health!,
297
+ probeLeaseId,
298
+ probeLeaseGeneration: health!.cooldownGeneration ?? 0,
299
+ lastProbeAt: now,
300
+ });
301
+ return probeLeaseId;
302
+ }
303
+
304
+ /** Side-effect-free check for a confirmed model-specific quota probe. */
305
+ export function canAcquireCodexQuotaScopeProbeLease(
306
+ accountId: string,
307
+ scope: CodexQuotaScope,
308
+ now = Date.now(),
309
+ ): boolean {
310
+ return canAcquireQuotaProbeLease(scopedHealthFor(accountId, scope), now);
311
+ }
312
+
313
+ /**
314
+ * Hand a probe lease back without recording an upstream outcome. Used by paths
315
+ * that take a lease and then fail before any request reaches upstream.
316
+ */
317
+ export function releaseCodexQuotaProbeLease(accountId: string, leaseId: string, now = Date.now()): void {
318
+ const health = getAccountHealth(accountId);
319
+ if (!health || health.probeLeaseId !== leaseId) return;
320
+ setAccountHealth(accountId, withProbeLeaseReleased(health, now));
321
+ }
322
+
323
+ /** Release a model-specific quota probe when the request never reaches upstream. */
324
+ export function releaseCodexQuotaScopeProbeLease(
325
+ accountId: string,
326
+ scope: CodexQuotaScope,
327
+ leaseId: string,
328
+ now = Date.now(),
329
+ ): void {
330
+ const health = scopedHealthFor(accountId, scope);
331
+ if (!health || health.probeLeaseId !== leaseId) return;
332
+ setScopedHealth(accountId, scope, withProbeLeaseReleased(health, now));
333
+ }
334
+
335
+ /**
336
+ * True when this outcome belongs to the account's in-flight probe. The
337
+ * undefined-id guard matters: without it an outcome carrying no lease would match
338
+ * an account holding no lease and be mistaken for the probe owner.
339
+ */
340
+ export function ownsProbeLease(health: CodexUpstreamHealth | undefined, meta: CodexUpstreamOutcomeMeta): boolean {
341
+ return meta.probeLeaseId !== undefined && meta.probeLeaseId === health?.probeLeaseId;
342
+ }
343
+
344
+ /**
345
+ * True when the owning probe may still clear the cooldown. A later 429 bumps the
346
+ * generation, so a probe that started under an older cooldown must not erase the
347
+ * newer restriction (which may carry an explicit Retry-After).
348
+ */
349
+ export function probeMayClearCooldown(health: CodexUpstreamHealth | undefined, meta: CodexUpstreamOutcomeMeta): boolean {
350
+ return ownsProbeLease(health, meta)
351
+ && (health!.probeLeaseGeneration ?? 0) === (health!.cooldownGeneration ?? 0);
352
+ }
353
+
354
+ /** Strip the in-flight lease while preserving every hard-cooldown field. */
355
+ export function withProbeLeaseReleased(health: CodexUpstreamHealth, now: number): CodexUpstreamHealth {
356
+ const { probeLeaseId: _id, probeLeaseGeneration: _gen, ...rest } = health;
357
+ return { ...rest, lastProbeAt: now };
358
+ }