@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.
Files changed (131) hide show
  1. package/bin/ocx.mjs +127 -16
  2. package/gui/dist/assets/index-6HZDkIpX.js +112 -0
  3. package/gui/dist/assets/index-uvENYLin.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/aside.svg +3 -0
  6. package/gui/dist/provider-icons/baseten.svg +13 -0
  7. package/gui/dist/provider-icons/bizrouter.svg +41 -0
  8. package/gui/dist/provider-icons/cerebras.svg +26 -0
  9. package/gui/dist/provider-icons/deepinfra.svg +75 -0
  10. package/gui/dist/provider-icons/deepseek-harness.svg +3 -0
  11. package/gui/dist/provider-icons/digitalocean.svg +10 -0
  12. package/gui/dist/provider-icons/featherless.svg +4 -0
  13. package/gui/dist/provider-icons/gajae-code.svg +410 -0
  14. package/gui/dist/provider-icons/hermes-agent.svg +207 -0
  15. package/gui/dist/provider-icons/hyperbolic.svg +18 -0
  16. package/gui/dist/provider-icons/kilo.svg +13 -0
  17. package/gui/dist/provider-icons/litellm.svg +1 -0
  18. package/gui/dist/provider-icons/minimax.svg +1 -0
  19. package/gui/dist/provider-icons/nanogpt.svg +74 -0
  20. package/gui/dist/provider-icons/nebius.svg +1 -0
  21. package/gui/dist/provider-icons/neuralwatt.svg +27 -0
  22. package/gui/dist/provider-icons/nous.svg +149 -0
  23. package/gui/dist/provider-icons/novita.svg +32 -0
  24. package/gui/dist/provider-icons/oh-my-pi.svg +11 -0
  25. package/gui/dist/provider-icons/openclaw.svg +54 -0
  26. package/gui/dist/provider-icons/orcarouter.svg +175 -0
  27. package/gui/dist/provider-icons/parallel.svg +13 -0
  28. package/gui/dist/provider-icons/prime-agent.svg +21 -0
  29. package/gui/dist/provider-icons/sambanova.svg +276 -0
  30. package/gui/dist/provider-icons/scaleway.svg +11 -0
  31. package/gui/dist/provider-icons/siliconflow.svg +18 -0
  32. package/gui/dist/provider-icons/synthetic.svg +12 -0
  33. package/gui/dist/provider-icons/together.svg +18 -0
  34. package/gui/dist/provider-icons/umans.svg +30 -0
  35. package/gui/dist/provider-icons/venice.svg +165 -0
  36. package/gui/dist/provider-icons/vultr.svg +15 -0
  37. package/gui/dist/provider-icons/zai.svg +218 -0
  38. package/gui/dist/provider-icons/zcode.svg +219 -0
  39. package/gui/dist/provider-icons/zenmux.svg +1 -0
  40. package/package.json +1 -1
  41. package/src/adapters/cursor/live-models.ts +1 -0
  42. package/src/adapters/cursor/protobuf-request.ts +4 -1
  43. package/src/adapters/cursor/tool-definitions.ts +36 -4
  44. package/src/adapters/openai-responses.ts +72 -10
  45. package/src/bridge.ts +15 -15
  46. package/src/cli/capabilities.ts +14 -0
  47. package/src/cli/codex-cli-update.ts +96 -0
  48. package/src/cli/codex-shim-autorestore.ts +3 -0
  49. package/src/cli/dispatch.ts +54 -18
  50. package/src/cli/export-command.ts +18 -17
  51. package/src/cli/help.ts +2 -2
  52. package/src/cli/index.ts +423 -32
  53. package/src/cli/integrations.ts +24 -2
  54. package/src/cli/launcher-context.ts +53 -2
  55. package/src/cli/opencode.ts +126 -33
  56. package/src/cli/registry.ts +16 -10
  57. package/src/cli/system-command.ts +6 -1
  58. package/src/cli/uninstall-plan.ts +86 -0
  59. package/src/clients/config-export.ts +293 -28
  60. package/src/codex/account-store.ts +131 -12
  61. package/src/codex/auth-api.ts +202 -33
  62. package/src/codex/autostart-health.ts +3 -3
  63. package/src/codex/catalog/provider-fetch.ts +96 -10
  64. package/src/codex/catalog/sync.ts +4 -3
  65. package/src/codex/cli-install-provenance.ts +795 -0
  66. package/src/codex/convergence.ts +4 -3
  67. package/src/codex/credential-mutation-epoch.ts +11 -0
  68. package/src/codex/history-job.ts +10 -0
  69. package/src/codex/history-manifest.ts +35 -2
  70. package/src/codex/history-provider.ts +196 -19
  71. package/src/codex/history-worker.ts +3 -0
  72. package/src/codex/main-account.ts +2 -0
  73. package/src/codex/model-entitlements.ts +430 -27
  74. package/src/codex/native-profile-manager.ts +4 -0
  75. package/src/codex/quota-401-recovery.ts +190 -0
  76. package/src/codex/quota-recovery-timing.ts +28 -0
  77. package/src/codex/quota.ts +6 -0
  78. package/src/codex/reset-credit-operation-ledger.ts +1411 -0
  79. package/src/codex/reset-credit-recovery.ts +20 -2
  80. package/src/codex/routing.ts +68 -16
  81. package/src/codex/shim.ts +204 -18
  82. package/src/codex/subagent-model-fallback.ts +4 -1
  83. package/src/codex/user-identity.ts +2 -1
  84. package/src/config/paths.ts +18 -3
  85. package/src/config/pending-teardown-names.d.mts +8 -0
  86. package/src/config/pending-teardown-names.mjs +69 -0
  87. package/src/config/pending-teardown.ts +286 -0
  88. package/src/config.ts +23 -0
  89. package/src/generated/compatibility-version.json +155 -67
  90. package/src/integrations/journal.ts +12 -1
  91. package/src/integrations/registry.ts +112 -0
  92. package/src/integrations/state.ts +67 -5
  93. package/src/integrations/writer.ts +107 -16
  94. package/src/lib/bounded-subprocess.ts +36 -0
  95. package/src/lib/process-control.ts +30 -5
  96. package/src/lib/state-store-registrations.ts +8 -0
  97. package/src/lib/strict-semver.ts +47 -0
  98. package/src/lib/windows-elevation.ts +32 -1
  99. package/src/lib/windows-secret-acl.ts +47 -25
  100. package/src/lib/windows-service-mutation-lock.ts +133 -0
  101. package/src/lib/windows-user-principal.ts +15 -17
  102. package/src/oauth/index.ts +220 -13
  103. package/src/oauth/store.ts +220 -18
  104. package/src/responses/spill-store.ts +354 -29
  105. package/src/responses/state.ts +775 -8
  106. package/src/server/index.ts +4 -3
  107. package/src/server/lifecycle.ts +5 -1
  108. package/src/server/management/integration-routes.ts +36 -5
  109. package/src/server/management/model-rows.ts +11 -2
  110. package/src/server/management/provider-routes.ts +4 -0
  111. package/src/server/management/system-restart.ts +5 -5
  112. package/src/server/management-api.ts +72 -13
  113. package/src/server/responses/encrypted-payload.ts +18 -1
  114. package/src/server/startup-action-control.ts +3 -2
  115. package/src/server/stop-teardown.ts +84 -0
  116. package/src/service.ts +749 -53
  117. package/src/sidecar/candidates.ts +1 -1
  118. package/src/update/codex-cli-update-launch-policy.d.mts +18 -0
  119. package/src/update/codex-cli-update-launch-policy.mjs +30 -0
  120. package/src/update/index.ts +39 -6
  121. package/src/update/job.ts +10 -11
  122. package/src/update/proxy-liveness-probe.d.mts +6 -0
  123. package/src/update/proxy-liveness-probe.mjs +84 -0
  124. package/src/update/stop-contract.d.mts +2 -0
  125. package/src/update/stop-contract.mjs +15 -0
  126. package/src/update/stop-decision.d.mts +10 -0
  127. package/src/update/stop-decision.mjs +34 -0
  128. package/src/vision/eligibility.ts +19 -1
  129. package/src/vision/index.ts +4 -18
  130. package/gui/dist/assets/index-CowztZdo.js +0 -112
  131. 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;
@@ -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(