@bitkyc08/opencodex 2.37.0 → 2.39.0-preview.20260901
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +127 -16
- package/gui/dist/assets/index-6HZDkIpX.js +112 -0
- package/gui/dist/assets/index-uvENYLin.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/aside.svg +3 -0
- package/gui/dist/provider-icons/baseten.svg +13 -0
- package/gui/dist/provider-icons/bizrouter.svg +41 -0
- package/gui/dist/provider-icons/cerebras.svg +26 -0
- package/gui/dist/provider-icons/deepinfra.svg +75 -0
- package/gui/dist/provider-icons/deepseek-harness.svg +3 -0
- package/gui/dist/provider-icons/digitalocean.svg +10 -0
- package/gui/dist/provider-icons/featherless.svg +4 -0
- package/gui/dist/provider-icons/gajae-code.svg +410 -0
- package/gui/dist/provider-icons/hermes-agent.svg +207 -0
- package/gui/dist/provider-icons/hyperbolic.svg +18 -0
- package/gui/dist/provider-icons/kilo.svg +13 -0
- package/gui/dist/provider-icons/litellm.svg +1 -0
- package/gui/dist/provider-icons/minimax.svg +1 -0
- package/gui/dist/provider-icons/nanogpt.svg +74 -0
- package/gui/dist/provider-icons/nebius.svg +1 -0
- package/gui/dist/provider-icons/neuralwatt.svg +27 -0
- package/gui/dist/provider-icons/nous.svg +149 -0
- package/gui/dist/provider-icons/novita.svg +32 -0
- package/gui/dist/provider-icons/oh-my-pi.svg +11 -0
- package/gui/dist/provider-icons/openclaw.svg +54 -0
- package/gui/dist/provider-icons/orcarouter.svg +175 -0
- package/gui/dist/provider-icons/parallel.svg +13 -0
- package/gui/dist/provider-icons/prime-agent.svg +21 -0
- package/gui/dist/provider-icons/sambanova.svg +276 -0
- package/gui/dist/provider-icons/scaleway.svg +11 -0
- package/gui/dist/provider-icons/siliconflow.svg +18 -0
- package/gui/dist/provider-icons/synthetic.svg +12 -0
- package/gui/dist/provider-icons/together.svg +18 -0
- package/gui/dist/provider-icons/umans.svg +30 -0
- package/gui/dist/provider-icons/venice.svg +165 -0
- package/gui/dist/provider-icons/vultr.svg +15 -0
- package/gui/dist/provider-icons/zai.svg +218 -0
- package/gui/dist/provider-icons/zcode.svg +219 -0
- package/gui/dist/provider-icons/zenmux.svg +1 -0
- package/package.json +1 -1
- package/src/adapters/cursor/live-models.ts +1 -0
- package/src/adapters/cursor/protobuf-request.ts +4 -1
- package/src/adapters/cursor/tool-definitions.ts +36 -4
- package/src/adapters/openai-responses.ts +72 -10
- package/src/bridge.ts +15 -15
- package/src/cli/capabilities.ts +14 -0
- package/src/cli/codex-cli-update.ts +96 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +54 -18
- package/src/cli/export-command.ts +18 -17
- package/src/cli/help.ts +2 -2
- package/src/cli/index.ts +423 -32
- package/src/cli/integrations.ts +24 -2
- package/src/cli/launcher-context.ts +53 -2
- package/src/cli/opencode.ts +126 -33
- package/src/cli/registry.ts +16 -10
- package/src/cli/system-command.ts +6 -1
- package/src/cli/uninstall-plan.ts +86 -0
- package/src/clients/config-export.ts +293 -28
- package/src/codex/account-store.ts +131 -12
- package/src/codex/auth-api.ts +202 -33
- package/src/codex/autostart-health.ts +3 -3
- package/src/codex/catalog/provider-fetch.ts +96 -10
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/cli-install-provenance.ts +795 -0
- package/src/codex/convergence.ts +4 -3
- package/src/codex/credential-mutation-epoch.ts +11 -0
- package/src/codex/history-job.ts +10 -0
- package/src/codex/history-manifest.ts +35 -2
- package/src/codex/history-provider.ts +196 -19
- package/src/codex/history-worker.ts +3 -0
- package/src/codex/main-account.ts +2 -0
- package/src/codex/model-entitlements.ts +430 -27
- package/src/codex/native-profile-manager.ts +4 -0
- package/src/codex/quota-401-recovery.ts +190 -0
- package/src/codex/quota-recovery-timing.ts +28 -0
- package/src/codex/quota.ts +6 -0
- package/src/codex/reset-credit-operation-ledger.ts +1411 -0
- package/src/codex/reset-credit-recovery.ts +20 -2
- package/src/codex/routing.ts +68 -16
- package/src/codex/shim.ts +204 -18
- package/src/codex/subagent-model-fallback.ts +4 -1
- package/src/codex/user-identity.ts +2 -1
- package/src/config/paths.ts +18 -3
- package/src/config/pending-teardown-names.d.mts +8 -0
- package/src/config/pending-teardown-names.mjs +69 -0
- package/src/config/pending-teardown.ts +286 -0
- package/src/config.ts +23 -0
- package/src/generated/compatibility-version.json +155 -67
- package/src/integrations/journal.ts +12 -1
- package/src/integrations/registry.ts +112 -0
- package/src/integrations/state.ts +67 -5
- package/src/integrations/writer.ts +107 -16
- package/src/lib/bounded-subprocess.ts +36 -0
- package/src/lib/process-control.ts +30 -5
- package/src/lib/state-store-registrations.ts +8 -0
- package/src/lib/strict-semver.ts +47 -0
- package/src/lib/windows-elevation.ts +32 -1
- package/src/lib/windows-secret-acl.ts +47 -25
- package/src/lib/windows-service-mutation-lock.ts +133 -0
- package/src/lib/windows-user-principal.ts +15 -17
- package/src/oauth/index.ts +220 -13
- package/src/oauth/store.ts +220 -18
- package/src/responses/spill-store.ts +354 -29
- package/src/responses/state.ts +775 -8
- package/src/server/index.ts +4 -3
- package/src/server/lifecycle.ts +5 -1
- package/src/server/management/integration-routes.ts +36 -5
- package/src/server/management/model-rows.ts +11 -2
- package/src/server/management/provider-routes.ts +4 -0
- package/src/server/management/system-restart.ts +5 -5
- package/src/server/management-api.ts +72 -13
- package/src/server/responses/encrypted-payload.ts +18 -1
- package/src/server/startup-action-control.ts +3 -2
- package/src/server/stop-teardown.ts +84 -0
- package/src/service.ts +749 -53
- package/src/sidecar/candidates.ts +1 -1
- package/src/update/codex-cli-update-launch-policy.d.mts +18 -0
- package/src/update/codex-cli-update-launch-policy.mjs +30 -0
- package/src/update/index.ts +39 -6
- package/src/update/job.ts +10 -11
- package/src/update/proxy-liveness-probe.d.mts +6 -0
- package/src/update/proxy-liveness-probe.mjs +84 -0
- package/src/update/stop-contract.d.mts +2 -0
- package/src/update/stop-contract.mjs +15 -0
- package/src/update/stop-decision.d.mts +10 -0
- package/src/update/stop-decision.mjs +34 -0
- package/src/vision/eligibility.ts +19 -1
- package/src/vision/index.ts +4 -18
- package/gui/dist/assets/index-CowztZdo.js +0 -112
- package/gui/dist/assets/index-jqE_VOKI.css +0 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { isCodexAccountGenerationLive, type CodexRefreshProvenance } from "./account-store";
|
|
3
|
+
import { QUOTA_RECOVERY_LEASE_MS } from "./quota-recovery-timing";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One refresh-and-replay per credential lineage, for a WHAM 401 (#3019).
|
|
7
|
+
*
|
|
8
|
+
* A bare 401 from `backend-api/wham/usage` is what a stale-but-refreshable bearer produces
|
|
9
|
+
* after a plan change, so quarantining on it tells the operator to re-authenticate an
|
|
10
|
+
* account that was fine. The fix is to refresh and replay once — and `once` is the whole
|
|
11
|
+
* problem, because an unbounded retry against an upstream 401 is a self-inflicted
|
|
12
|
+
* credential-stuffing loop.
|
|
13
|
+
*
|
|
14
|
+
* ## Why a claim id rather than the lineage
|
|
15
|
+
*
|
|
16
|
+
* Keying the budget on the lineage cannot separate an old claimant from a later retry on
|
|
17
|
+
* the same lineage: claim L, a transient failure releases L, another poll claims L, and
|
|
18
|
+
* the first caller's late settlement spends the second caller's claim. The claim id makes
|
|
19
|
+
* every mutation a compare-and-set on `(accountId, lineage, claimId)`, so a stale
|
|
20
|
+
* completion is a no-op instead of somebody else's budget.
|
|
21
|
+
*
|
|
22
|
+
* ## Why `spent` never expires and `claimed` does
|
|
23
|
+
*
|
|
24
|
+
* Expiring a spent record would hand the same lineage another refresh, which is exactly
|
|
25
|
+
* the property this module exists to hold. But a caller can be cancelled or die between
|
|
26
|
+
* claim and settle, so `claimed` carries a bounded lease: an expired lease is reclaimable,
|
|
27
|
+
* and is never promoted to `spent` because the refresh may not have happened.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export type RecoveryRecord =
|
|
31
|
+
| { state: "claimed"; lineage: number; claimId: string; expiresAt: number }
|
|
32
|
+
/**
|
|
33
|
+
* `terminal` marks a lineage whose refresh proved the grant is dead. It is spent AND the
|
|
34
|
+
* account must keep reporting needs-reauth: without it, the next bare 401 finds the
|
|
35
|
+
* budget used, reports transient, and a dead credential looks healthy.
|
|
36
|
+
*/
|
|
37
|
+
| { state: "spent"; lineage: number; terminal?: true }
|
|
38
|
+
| { state: "backoff"; lineage: number; nextAttemptAt: number };
|
|
39
|
+
|
|
40
|
+
export type ClaimResult =
|
|
41
|
+
| { granted: true; claimId: string }
|
|
42
|
+
| { granted: false; reason: "spent" | "in-flight" | "backoff" };
|
|
43
|
+
|
|
44
|
+
const records = new Map<string, RecoveryRecord>();
|
|
45
|
+
|
|
46
|
+
function leaseExpired(record: RecoveryRecord, now: number): boolean {
|
|
47
|
+
return record.state === "claimed" && record.expiresAt <= now;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Claim the one refresh this lineage is entitled to.
|
|
52
|
+
*
|
|
53
|
+
* A live claim blocks EVERY lineage for the account, not just its own. The refresh it
|
|
54
|
+
* fences commits `G -> G+1` before the claimant can settle, so during that window a
|
|
55
|
+
* `G+1` claim would otherwise be granted and the late settlement would land on nothing.
|
|
56
|
+
*/
|
|
57
|
+
export function claimQuotaRecovery(
|
|
58
|
+
accountId: string,
|
|
59
|
+
lineage: number,
|
|
60
|
+
now: number = Date.now(),
|
|
61
|
+
): ClaimResult {
|
|
62
|
+
const existing = records.get(accountId);
|
|
63
|
+
if (existing && !leaseExpired(existing, now)) {
|
|
64
|
+
if (existing.state === "claimed") return { granted: false, reason: "in-flight" };
|
|
65
|
+
if (existing.state === "spent") {
|
|
66
|
+
// Only this lineage is fenced. A different one is a new grant with its own budget.
|
|
67
|
+
if (existing.lineage === lineage) return { granted: false, reason: "spent" };
|
|
68
|
+
} else if (existing.lineage === lineage && existing.nextAttemptAt > now) {
|
|
69
|
+
return { granted: false, reason: "backoff" };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const claimId = randomUUID();
|
|
73
|
+
records.set(accountId, { state: "claimed", lineage, claimId, expiresAt: now + QUOTA_RECOVERY_LEASE_MS });
|
|
74
|
+
return { granted: true, claimId };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function heldClaim(accountId: string, claimId: string): Extract<RecoveryRecord, { state: "claimed" }> | null {
|
|
78
|
+
const existing = records.get(accountId);
|
|
79
|
+
if (!existing || existing.state !== "claimed" || existing.claimId !== claimId) return null;
|
|
80
|
+
return existing;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Record the outcome of a claimed refresh.
|
|
85
|
+
*
|
|
86
|
+
* Which lineage gets fenced depends on the outcome, and getting this backwards is how a
|
|
87
|
+
* fresh credential loses the recovery it is entitled to:
|
|
88
|
+
*
|
|
89
|
+
* - `self-refresh` and `joined-lineage` are this lineage's own attempt, so the RETURNED
|
|
90
|
+
* generation is spent. The returned one, never the rejected one: a successful token
|
|
91
|
+
* response can rotate only the refresh grant, leaving the access token byte-identical,
|
|
92
|
+
* and the credential has already moved by then.
|
|
93
|
+
* - `external-replacement` means somebody else's credential is now stored. The claimed
|
|
94
|
+
* OLD lineage is spent — this caller did use its attempt — and the returned generation
|
|
95
|
+
* is left untouched so it can claim on its own next 401.
|
|
96
|
+
*/
|
|
97
|
+
export function settleQuotaRecovery(
|
|
98
|
+
accountId: string,
|
|
99
|
+
claimId: string,
|
|
100
|
+
outcome: { provenance: CodexRefreshProvenance; generation: number },
|
|
101
|
+
): void {
|
|
102
|
+
const held = heldClaim(accountId, claimId);
|
|
103
|
+
if (!held) return; // superseded or already settled: never disturb a newer claimant
|
|
104
|
+
const fenced = outcome.provenance === "external-replacement" ? held.lineage : outcome.generation;
|
|
105
|
+
records.set(accountId, { state: "spent", lineage: fenced });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Record a refresh that failed with proof the grant itself is dead.
|
|
110
|
+
*
|
|
111
|
+
* Distinct from {@link releaseQuotaRecovery}: a revoked or expired grant will not become
|
|
112
|
+
* valid on the next poll, so backing off would let a later 401 report the account healthy
|
|
113
|
+
* while it is not. The lineage is fenced durably and the caller quarantines.
|
|
114
|
+
*/
|
|
115
|
+
export function settleQuotaRecoveryTerminal(accountId: string, claimId: string): void {
|
|
116
|
+
const held = heldClaim(accountId, claimId);
|
|
117
|
+
if (!held) return;
|
|
118
|
+
records.set(accountId, { state: "spent", lineage: held.lineage, terminal: true });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Did this lineage's one refresh prove the grant dead? */
|
|
122
|
+
export function quotaRecoveryTerminalFor(accountId: string, lineage: number): boolean {
|
|
123
|
+
const record = records.get(accountId);
|
|
124
|
+
return record?.state === "spent" && record.lineage === lineage && record.terminal === true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Release a claim whose refresh failed without proving anything about the credential.
|
|
129
|
+
*
|
|
130
|
+
* Into BACKOFF, not into eligibility: a failed quota request does not refresh the quota
|
|
131
|
+
* timestamp, so successive dashboard and background polls would each issue another token
|
|
132
|
+
* refresh — the loop, reopened from the other side.
|
|
133
|
+
*/
|
|
134
|
+
export function releaseQuotaRecovery(
|
|
135
|
+
accountId: string,
|
|
136
|
+
claimId: string,
|
|
137
|
+
backoffMs: number,
|
|
138
|
+
now: number = Date.now(),
|
|
139
|
+
): void {
|
|
140
|
+
const held = heldClaim(accountId, claimId);
|
|
141
|
+
if (!held) return;
|
|
142
|
+
records.set(accountId, { state: "backoff", lineage: held.lineage, nextAttemptAt: now + backoffMs });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Drop records for accounts that no longer exist, and fences whose credential has moved.
|
|
147
|
+
*
|
|
148
|
+
* A live claim is exempt. Its refresh commits the generation forward, so "the fenced
|
|
149
|
+
* generation is no longer stored" is the EXPECTED state mid-flight, and removing it there
|
|
150
|
+
* would let a second claim in before the first settles.
|
|
151
|
+
*/
|
|
152
|
+
export function reconcileQuotaRecovery(liveAccountIds: ReadonlySet<string>, now: number = Date.now()): number {
|
|
153
|
+
let removed = 0;
|
|
154
|
+
for (const [accountId, record] of [...records]) {
|
|
155
|
+
if (!liveAccountIds.has(accountId)) {
|
|
156
|
+
records.delete(accountId);
|
|
157
|
+
removed += 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (record.state === "claimed" && !leaseExpired(record, now)) continue;
|
|
161
|
+
if (!isCodexAccountGenerationLive(accountId, record.lineage)) {
|
|
162
|
+
records.delete(accountId);
|
|
163
|
+
removed += 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return removed;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Drop expired backoff windows and abandoned leases. */
|
|
170
|
+
export function sweepExpiredQuotaRecovery(now: number = Date.now()): number {
|
|
171
|
+
let removed = 0;
|
|
172
|
+
for (const [accountId, record] of [...records]) {
|
|
173
|
+
// A spent record is durable: expiring it would grant the same lineage another refresh.
|
|
174
|
+
if (record.state === "spent") continue;
|
|
175
|
+
const stale = record.state === "backoff" ? record.nextAttemptAt <= now : leaseExpired(record, now);
|
|
176
|
+
if (stale) {
|
|
177
|
+
records.delete(accountId);
|
|
178
|
+
removed += 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return removed;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function quotaRecoveryRecordForTests(accountId: string): RecoveryRecord | undefined {
|
|
185
|
+
return records.get(accountId);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function resetQuotaRecoveryForTests(): void {
|
|
189
|
+
records.clear();
|
|
190
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timing contract shared by the credential refresh, the WHAM quota request, and the
|
|
3
|
+
* 401-recovery budget (#3019).
|
|
4
|
+
*
|
|
5
|
+
* A leaf on purpose: `auth-api.ts` imports the recovery store to claim and settle, so the
|
|
6
|
+
* recovery store cannot import `auth-api.ts` back for the WHAM timeout. Both of them, and
|
|
7
|
+
* `account-store.ts`, import this instead.
|
|
8
|
+
*
|
|
9
|
+
* These were three inline literals in three files. The lease has to outlast the operations
|
|
10
|
+
* it fences — a lease that expires mid-refresh admits a second claim for a lineage that is
|
|
11
|
+
* already being refreshed — so it is derived here rather than restated as a round number
|
|
12
|
+
* that drifts away from what it covers.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The refresh flight's own `AbortSignal.timeout` ceiling. */
|
|
16
|
+
export const CODEX_REFRESH_FLIGHT_CEILING_MS = 30_000;
|
|
17
|
+
|
|
18
|
+
/** Deadline for one `backend-api/wham/usage` request. */
|
|
19
|
+
export const WHAM_REQUEST_TIMEOUT_MS = 8_000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* How long a recovery claim stays valid without settlement.
|
|
23
|
+
*
|
|
24
|
+
* The sequence a claim covers is: WHAM request → refresh → WHAM replay. Both quota legs sit
|
|
25
|
+
* inside the lease, which is why the request timeout is counted twice.
|
|
26
|
+
*/
|
|
27
|
+
export const QUOTA_RECOVERY_LEASE_MS =
|
|
28
|
+
CODEX_REFRESH_FLIGHT_CEILING_MS + WHAM_REQUEST_TIMEOUT_MS * 2;
|
package/src/codex/quota.ts
CHANGED
|
@@ -110,6 +110,12 @@ function mayCommitAccountQuota(accountId: string, writerGeneration: number): boo
|
|
|
110
110
|
// Valid upstream percentages are normalized to 0..100. Keep "unknown" outside that domain so an
|
|
111
111
|
// actually exhausted account is still eligible for threshold rotation.
|
|
112
112
|
export const CODEX_UNKNOWN_USAGE_SCORE = 101;
|
|
113
|
+
/**
|
|
114
|
+
* A window reading at or above this is a measured refusal, not a position on a scale.
|
|
115
|
+
*
|
|
116
|
+
* Separate from `CODEX_UNKNOWN_USAGE_SCORE` because they mean opposite things: unknown is
|
|
117
|
+
* "we have not observed this account", 100 is "we observed it and it is full".
|
|
118
|
+
*/
|
|
113
119
|
export const CODEX_EXHAUSTED_USAGE_PERCENT = 100;
|
|
114
120
|
|
|
115
121
|
export function isCodexQuotaExhausted(
|