@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.
- package/gui/dist/assets/{index-DH2PUHqr.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +611 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +554 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +422 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +95 -28
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +335 -0
- package/src/server/index.ts +40 -2547
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- package/src/usage/summary.ts +171 -21
|
@@ -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
|
+
}
|