@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
|
@@ -27,6 +27,20 @@ export type CodexResetCreditConsumeCode =
|
|
|
27
27
|
| "nothing_to_reset"
|
|
28
28
|
| "no_credit";
|
|
29
29
|
|
|
30
|
+
declare const CODEX_RESERVED_OPERATION_ID_BRAND: unique symbol;
|
|
31
|
+
|
|
32
|
+
/** An operation id whose durable reservation was validated by the operation ledger. */
|
|
33
|
+
export type CodexReservedOperationId = string & {
|
|
34
|
+
readonly [CODEX_RESERVED_OPERATION_ID_BRAND]: true;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const CODEX_RESET_CREDIT_OPERATION_ID_PATTERN =
|
|
38
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
39
|
+
|
|
40
|
+
export function isCodexResetCreditOperationId(value: unknown): value is string {
|
|
41
|
+
return typeof value === "string" && CODEX_RESET_CREDIT_OPERATION_ID_PATTERN.test(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
export type CodexResetCreditRecoveryAuthorization = Readonly<{
|
|
31
45
|
enabled: boolean;
|
|
32
46
|
/**
|
|
@@ -189,7 +203,9 @@ const RESET_ELIGIBLE_CODES = {
|
|
|
189
203
|
insufficient_quota: true,
|
|
190
204
|
} as const satisfies Record<CodexResetEligibleExhaustionCode, true>;
|
|
191
205
|
|
|
192
|
-
function
|
|
206
|
+
export function snapshotCodexResetCreditRecoveryGeneration(
|
|
207
|
+
input: unknown,
|
|
208
|
+
): CodexResetCreditRecoveryGeneration {
|
|
193
209
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
194
210
|
throw new TypeError("generation must be an object");
|
|
195
211
|
}
|
|
@@ -251,6 +267,8 @@ function compareGenerationOrder(
|
|
|
251
267
|
return 0;
|
|
252
268
|
}
|
|
253
269
|
|
|
270
|
+
export const compareCodexResetCreditRecoveryGenerationOrder = compareGenerationOrder;
|
|
271
|
+
|
|
254
272
|
function authorizedResetRejection(authorization: CodexResetCreditRecoveryAuthorization): boolean {
|
|
255
273
|
const hasOwn = Object.prototype.hasOwnProperty;
|
|
256
274
|
if (!hasOwn.call(authorization, "enabled")
|
|
@@ -507,7 +525,7 @@ export class CodexResetCreditRecoveryCoordinator {
|
|
|
507
525
|
let requestSignal: AbortSignal | undefined;
|
|
508
526
|
try {
|
|
509
527
|
requestSignal = snapshotRequestSignal(options);
|
|
510
|
-
generationSnapshot =
|
|
528
|
+
generationSnapshot = snapshotCodexResetCreditRecoveryGeneration(generation);
|
|
511
529
|
} catch (error) {
|
|
512
530
|
rejectAttempt(error);
|
|
513
531
|
return attempt;
|
package/src/codex/routing.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
seedPoolRotationAccount,
|
|
18
18
|
selectPriorityTier,
|
|
19
19
|
} from "./pool-rotation";
|
|
20
|
-
import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota } from "./quota";
|
|
20
|
+
import { CODEX_EXHAUSTED_USAGE_PERCENT, CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota } from "./quota";
|
|
21
21
|
import { isThirtyDayOnlyCodexPlan } from "./plan";
|
|
22
22
|
import {
|
|
23
23
|
MAIN_CODEX_ACCOUNT_ID,
|
|
@@ -364,7 +364,8 @@ export function computeCodexUsageScore(quota: {
|
|
|
364
364
|
weeklyPercent?: number;
|
|
365
365
|
monthlyPercent?: number;
|
|
366
366
|
shortPercent?: number;
|
|
367
|
-
|
|
367
|
+
shortResetAt?: number;
|
|
368
|
+
} | null, plan?: unknown, now: number = Date.now()): number {
|
|
368
369
|
if (!quota) return CODEX_UNKNOWN_USAGE_SCORE;
|
|
369
370
|
const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
|
|
370
371
|
const longWindows = isThirtyDayOnlyCodexPlan(plan)
|
|
@@ -376,11 +377,50 @@ export function computeCodexUsageScore(quota: {
|
|
|
376
377
|
// account whose weekly/monthly usage is entirely unverified look like the emptiest in the
|
|
377
378
|
// pool, so `pickLowestUsageAmong` would send every request to it. Unknown has to stay
|
|
378
379
|
// unknown until a governing window is actually observed.
|
|
379
|
-
|
|
380
|
+
//
|
|
381
|
+
// A FULL burst window is the exception (#3029). It is not an optimistic guess about an
|
|
382
|
+
// unobserved window — it is a direct observation that the account cannot serve a request
|
|
383
|
+
// right now, whatever its monthly position turns out to be. Unknown-means-selectable is
|
|
384
|
+
// correct for uncertainty and wrong for a measured refusal: the account stays selected,
|
|
385
|
+
// `applyQuotaAutoSwitch` never fires, and the pool wedges on an exhausted credential.
|
|
386
|
+
if (knownLong.length === 0) {
|
|
387
|
+
return isTerminalShortWindow(quota, now) ? CODEX_EXHAUSTED_USAGE_PERCENT : CODEX_UNKNOWN_USAGE_SCORE;
|
|
388
|
+
}
|
|
380
389
|
const values = finite(quota.shortPercent) ? [...knownLong, quota.shortPercent] : knownLong;
|
|
381
390
|
return Math.max(...values);
|
|
382
391
|
}
|
|
383
392
|
|
|
393
|
+
/**
|
|
394
|
+
* A short-only reading that proves the account is blocked NOW.
|
|
395
|
+
*
|
|
396
|
+
* Freshness is not optional. `getAccountQuota` performs no expiry check, partial updates
|
|
397
|
+
* carry the old short tuple forward, and disk hydration accepts a persisted reading for
|
|
398
|
+
* hours — so scoring 100 from `shortPercent` alone would keep excluding an account whose
|
|
399
|
+
* five-hour window has since reset. That is #3029 pointed the other way: the issue is that
|
|
400
|
+
* an exhausted account stays selected, and "a recovered account stays excluded" trades one
|
|
401
|
+
* unusable pool for another.
|
|
402
|
+
*
|
|
403
|
+
* A reading with no `shortResetAt` cannot be aged, so it stays unknown. The conservative
|
|
404
|
+
* direction here is the one that keeps an account selectable: a wrongly-selected account
|
|
405
|
+
* fails one request, while a wrongly-excluded one is invisible until someone reads the pool
|
|
406
|
+
* by hand.
|
|
407
|
+
*/
|
|
408
|
+
function isTerminalShortWindow(
|
|
409
|
+
quota: { shortPercent?: number; shortResetAt?: number },
|
|
410
|
+
now: number,
|
|
411
|
+
): boolean {
|
|
412
|
+
if (typeof quota.shortPercent !== "number" || !Number.isFinite(quota.shortPercent)) return false;
|
|
413
|
+
if (quota.shortPercent < CODEX_EXHAUSTED_USAGE_PERCENT) return false;
|
|
414
|
+
const resetAt = quota.shortResetAt;
|
|
415
|
+
if (typeof resetAt !== "number" || !Number.isFinite(resetAt) || resetAt <= 0) return false;
|
|
416
|
+
// Both units reach storage: `normalizeResetAt` does not scale, and the GUI disambiguates
|
|
417
|
+
// by magnitude at read time. A comparison written against one assumption is off by 1000x
|
|
418
|
+
// against the other, and in the seconds-read-as-milliseconds direction every terminal
|
|
419
|
+
// reading looks like it reset in 1970 — a fix that passes its own test and does nothing.
|
|
420
|
+
const resetAtMs = resetAt < 10_000_000_000 ? resetAt * 1000 : resetAt;
|
|
421
|
+
return resetAtMs > now;
|
|
422
|
+
}
|
|
423
|
+
|
|
384
424
|
export function classifyCodexUpstreamOutcome(
|
|
385
425
|
outcome: CodexUpstreamOutcome,
|
|
386
426
|
denial?: "workspace" | "entitlement",
|
|
@@ -1131,7 +1171,7 @@ function getEligiblePoolAccounts(
|
|
|
1131
1171
|
return selectPriorityTier(
|
|
1132
1172
|
ids,
|
|
1133
1173
|
codexAccountPriorityLookup(config),
|
|
1134
|
-
id => hasCodexQuotaHeadroom(config, id, selectionOptions),
|
|
1174
|
+
id => hasCodexQuotaHeadroom(config, id, selectionOptions, now),
|
|
1135
1175
|
pinnedCodexAccountId(config),
|
|
1136
1176
|
);
|
|
1137
1177
|
}
|
|
@@ -1163,12 +1203,14 @@ function hasCodexQuotaHeadroom(
|
|
|
1163
1203
|
config: OcxConfig,
|
|
1164
1204
|
accountId: string,
|
|
1165
1205
|
selectionOptions?: CodexAccountUsabilityOptions,
|
|
1206
|
+
now: number = Date.now(),
|
|
1166
1207
|
): boolean {
|
|
1167
1208
|
const threshold = config.autoSwitchThreshold ?? 80;
|
|
1168
1209
|
if (threshold <= 0) return true;
|
|
1169
1210
|
const usage = computeCodexUsageScore(
|
|
1170
1211
|
getAccountQuota(accountId),
|
|
1171
1212
|
getPoolAccountPlanForSelection(config, accountId, selectionOptions),
|
|
1213
|
+
now,
|
|
1172
1214
|
);
|
|
1173
1215
|
if (isUnknownUsage(usage)) return true;
|
|
1174
1216
|
return usage < threshold;
|
|
@@ -1188,7 +1230,7 @@ function pickFillFirstCodexAccount(
|
|
|
1188
1230
|
if (eligible.length === 0) return null;
|
|
1189
1231
|
|
|
1190
1232
|
const active = getEffectiveActiveCodexAccountId(config);
|
|
1191
|
-
if (active && eligible.includes(active) && hasCodexQuotaHeadroom(config, active, selectionOptions)) {
|
|
1233
|
+
if (active && eligible.includes(active) && hasCodexQuotaHeadroom(config, active, selectionOptions, now)) {
|
|
1192
1234
|
return active;
|
|
1193
1235
|
}
|
|
1194
1236
|
|
|
@@ -1200,7 +1242,7 @@ function pickNextFillFirstCodexAccount(
|
|
|
1200
1242
|
config: OcxConfig,
|
|
1201
1243
|
afterId: string | null,
|
|
1202
1244
|
eligible: readonly string[] = listEligibleCodexAccountIds(config, Date.now()),
|
|
1203
|
-
|
|
1245
|
+
now = Date.now(),
|
|
1204
1246
|
selectionOptions?: CodexAccountUsabilityOptions,
|
|
1205
1247
|
): string | null {
|
|
1206
1248
|
if (eligible.length === 0) return null;
|
|
@@ -1208,7 +1250,7 @@ function pickNextFillFirstCodexAccount(
|
|
|
1208
1250
|
if (!afterId) {
|
|
1209
1251
|
// Prefer an under-threshold account when starting with no active cursor.
|
|
1210
1252
|
for (const id of ordered) {
|
|
1211
|
-
if (hasCodexQuotaHeadroom(config, id, selectionOptions)) return id;
|
|
1253
|
+
if (hasCodexQuotaHeadroom(config, id, selectionOptions, now)) return id;
|
|
1212
1254
|
}
|
|
1213
1255
|
return ordered[0] ?? null;
|
|
1214
1256
|
}
|
|
@@ -1223,7 +1265,7 @@ function pickNextFillFirstCodexAccount(
|
|
|
1223
1265
|
const startIdx = stableAll.indexOf(afterId);
|
|
1224
1266
|
if (startIdx < 0) {
|
|
1225
1267
|
for (const id of ordered) {
|
|
1226
|
-
if (hasCodexQuotaHeadroom(config, id, selectionOptions)) return id;
|
|
1268
|
+
if (hasCodexQuotaHeadroom(config, id, selectionOptions, now)) return id;
|
|
1227
1269
|
}
|
|
1228
1270
|
return ordered[0] ?? null;
|
|
1229
1271
|
}
|
|
@@ -1234,7 +1276,7 @@ function pickNextFillFirstCodexAccount(
|
|
|
1234
1276
|
const candidate = stableAll[(startIdx + step) % stableAll.length]!;
|
|
1235
1277
|
if (!eligible.includes(candidate)) continue;
|
|
1236
1278
|
if (!fallback) fallback = candidate;
|
|
1237
|
-
if (hasCodexQuotaHeadroom(config, candidate, selectionOptions)) return candidate;
|
|
1279
|
+
if (hasCodexQuotaHeadroom(config, candidate, selectionOptions, now)) return candidate;
|
|
1238
1280
|
}
|
|
1239
1281
|
return fallback ?? ordered[0] ?? null;
|
|
1240
1282
|
}
|
|
@@ -1356,6 +1398,7 @@ function pickLowerUsageAccount(
|
|
|
1356
1398
|
const usage = computeCodexUsageScore(
|
|
1357
1399
|
getAccountQuota(id),
|
|
1358
1400
|
getPoolAccountPlanForSelection(config, id, selectionOptions),
|
|
1401
|
+
now,
|
|
1359
1402
|
);
|
|
1360
1403
|
if (usage < bestUsage) {
|
|
1361
1404
|
best = id;
|
|
@@ -1370,6 +1413,7 @@ function pickLowestUsageAmong(
|
|
|
1370
1413
|
config: OcxConfig,
|
|
1371
1414
|
ids: readonly string[],
|
|
1372
1415
|
selectionOptions?: CodexAccountUsabilityOptions,
|
|
1416
|
+
now: number = Date.now(),
|
|
1373
1417
|
): string | null {
|
|
1374
1418
|
let best: string | null = null;
|
|
1375
1419
|
let bestUsage = Number.POSITIVE_INFINITY;
|
|
@@ -1377,6 +1421,7 @@ function pickLowestUsageAmong(
|
|
|
1377
1421
|
const usage = computeCodexUsageScore(
|
|
1378
1422
|
getAccountQuota(id),
|
|
1379
1423
|
getPoolAccountPlanForSelection(config, id, selectionOptions),
|
|
1424
|
+
now,
|
|
1380
1425
|
);
|
|
1381
1426
|
if (usage < bestUsage) {
|
|
1382
1427
|
best = id;
|
|
@@ -1397,6 +1442,7 @@ export function pickLowestUsageCodexAccount(
|
|
|
1397
1442
|
config,
|
|
1398
1443
|
getEligiblePoolAccounts(config, excludeId, now, quotaScope, selectionOptions),
|
|
1399
1444
|
selectionOptions,
|
|
1445
|
+
now,
|
|
1400
1446
|
);
|
|
1401
1447
|
}
|
|
1402
1448
|
|
|
@@ -1540,7 +1586,7 @@ function pickPriorityPreemption(
|
|
|
1540
1586
|
if (
|
|
1541
1587
|
pinned !== undefined
|
|
1542
1588
|
&& eligible.includes(pinned)
|
|
1543
|
-
&& hasCodexQuotaHeadroom(config, pinned, selectionOptions)
|
|
1589
|
+
&& hasCodexQuotaHeadroom(config, pinned, selectionOptions, now)
|
|
1544
1590
|
) return null;
|
|
1545
1591
|
const priorityOf = codexAccountPriorityLookup(config);
|
|
1546
1592
|
if (priorityOf(eligible[0]!) <= priorityOf(active)) return null;
|
|
@@ -1548,8 +1594,9 @@ function pickPriorityPreemption(
|
|
|
1548
1594
|
// picking one would hand the request straight back to a drained account.
|
|
1549
1595
|
return pickLowestUsageAmong(
|
|
1550
1596
|
config,
|
|
1551
|
-
eligible.filter(id => hasCodexQuotaHeadroom(config, id, selectionOptions)),
|
|
1597
|
+
eligible.filter(id => hasCodexQuotaHeadroom(config, id, selectionOptions, now)),
|
|
1552
1598
|
selectionOptions,
|
|
1599
|
+
now,
|
|
1553
1600
|
);
|
|
1554
1601
|
}
|
|
1555
1602
|
|
|
@@ -1566,6 +1613,7 @@ function releaseDrainedCodexAccountPin(
|
|
|
1566
1613
|
CodexAccountUsabilityOptions,
|
|
1567
1614
|
"nativeMainSelectionOnly" | "isMainAccountTokenLive"
|
|
1568
1615
|
>,
|
|
1616
|
+
now: number = Date.now(),
|
|
1569
1617
|
): void {
|
|
1570
1618
|
const pinned = pinnedCodexAccountId(config);
|
|
1571
1619
|
if (pinned === undefined) return;
|
|
@@ -1580,7 +1628,7 @@ function releaseDrainedCodexAccountPin(
|
|
|
1580
1628
|
// is readable. Cached reauth and configured pause state were handled above.
|
|
1581
1629
|
if (pinned === MAIN_CODEX_ACCOUNT_ID && selectionOptions?.nativeMainSelectionOnly === true) return;
|
|
1582
1630
|
const drained = !isCodexAccountUsable(config, pinned, selectionOptions)
|
|
1583
|
-
|| !hasCodexQuotaHeadroom(config, pinned, selectionOptions);
|
|
1631
|
+
|| !hasCodexQuotaHeadroom(config, pinned, selectionOptions, now);
|
|
1584
1632
|
if (!drained) return;
|
|
1585
1633
|
clearCodexAccountPin(config);
|
|
1586
1634
|
saveConfigPreservingClaudeCode(config);
|
|
@@ -1600,6 +1648,7 @@ function applyQuotaAutoSwitch(
|
|
|
1600
1648
|
const activeUsage = computeCodexUsageScore(
|
|
1601
1649
|
quota,
|
|
1602
1650
|
getPoolAccountPlanForSelection(config, active, selectionOptions),
|
|
1651
|
+
now,
|
|
1603
1652
|
);
|
|
1604
1653
|
// Unknown usage is not evidence that a user's explicit selection crossed the
|
|
1605
1654
|
// threshold. Wait for quota priming instead of rotating among guesses.
|
|
@@ -1633,7 +1682,7 @@ function isHealthySharedCodexSelection(
|
|
|
1633
1682
|
selectionOptions: CodexAccountUsabilityOptions | undefined,
|
|
1634
1683
|
): boolean {
|
|
1635
1684
|
return isCodexAccountSelectable(config, accountId, now, quotaScope, selectionOptions)
|
|
1636
|
-
&& hasCodexQuotaHeadroom(config, accountId, selectionOptions)
|
|
1685
|
+
&& hasCodexQuotaHeadroom(config, accountId, selectionOptions, now)
|
|
1637
1686
|
&& !shouldFailover(config, accountId, now);
|
|
1638
1687
|
}
|
|
1639
1688
|
|
|
@@ -1720,6 +1769,7 @@ function previewReusableAffinityAccount(
|
|
|
1720
1769
|
const usage = computeCodexUsageScore(
|
|
1721
1770
|
getAccountQuota(entry.accountId),
|
|
1722
1771
|
getPoolAccountPlanForSelection(config, entry.accountId, selectionOptions),
|
|
1772
|
+
now,
|
|
1723
1773
|
);
|
|
1724
1774
|
if (!isUnknownUsage(usage) && usage >= threshold) {
|
|
1725
1775
|
const best = pickLowerUsageAccount(
|
|
@@ -1755,6 +1805,7 @@ function reevaluateAffinityQuota(
|
|
|
1755
1805
|
? computeCodexUsageScore(
|
|
1756
1806
|
getAccountQuota(entry.accountId),
|
|
1757
1807
|
getPoolAccountPlanForSelection(config, entry.accountId, selectionOptions),
|
|
1808
|
+
now,
|
|
1758
1809
|
)
|
|
1759
1810
|
: 0;
|
|
1760
1811
|
const overThreshold = threshold > 0 && !isUnknownUsage(usage) && usage >= threshold;
|
|
@@ -1848,6 +1899,7 @@ export function previewCodexAccountForRequest(
|
|
|
1848
1899
|
const usage = computeCodexUsageScore(
|
|
1849
1900
|
getAccountQuota(active),
|
|
1850
1901
|
getPoolAccountPlanForSelection(config, active, selectionOptions),
|
|
1902
|
+
now,
|
|
1851
1903
|
);
|
|
1852
1904
|
if (!isUnknownUsage(usage) && usage >= threshold) {
|
|
1853
1905
|
active = pickLowerUsageAccount(config, active, usage, now, quotaScope, selectionOptions);
|
|
@@ -1887,7 +1939,7 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
1887
1939
|
// revive after quota resets. Independent model scopes must never persist a
|
|
1888
1940
|
// change to shared routing state.
|
|
1889
1941
|
if (!isIndependentCodexQuotaScope(quotaScope)) {
|
|
1890
|
-
releaseDrainedCodexAccountPin(config, sharedStateSelectionOptions(selectionOptions));
|
|
1942
|
+
releaseDrainedCodexAccountPin(config, sharedStateSelectionOptions(selectionOptions), now);
|
|
1891
1943
|
}
|
|
1892
1944
|
const sharedActiveBeforeSelection = getEffectiveActiveCodexAccountId(config);
|
|
1893
1945
|
const preserveSharedSelectionForModelDetour = modelScopedSelection && (
|
|
@@ -1945,7 +1997,7 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
1945
1997
|
&& isCodexAccountSelectable(config, entry.accountId, now, quotaScope, selectionOptions);
|
|
1946
1998
|
const failoverReady = shouldFailover(config, entry.accountId, now);
|
|
1947
1999
|
const healthyForSharedAffinity = selectableForSharedState
|
|
1948
|
-
&& hasCodexQuotaHeadroom(config, entry.accountId, sharedSelectionOptions)
|
|
2000
|
+
&& hasCodexQuotaHeadroom(config, entry.accountId, sharedSelectionOptions, now)
|
|
1949
2001
|
&& !failoverReady;
|
|
1950
2002
|
if (
|
|
1951
2003
|
selectableForRequest
|
|
@@ -2041,7 +2093,7 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
2041
2093
|
sharedSelectionOptions,
|
|
2042
2094
|
);
|
|
2043
2095
|
const activeHealthyForSharedSelection = activeSelectableForSharedState
|
|
2044
|
-
&& hasCodexQuotaHeadroom(config, active, sharedSelectionOptions)
|
|
2096
|
+
&& hasCodexQuotaHeadroom(config, active, sharedSelectionOptions, now)
|
|
2045
2097
|
&& !shouldFailover(config, active, now);
|
|
2046
2098
|
if (!isCodexAccountSelectable(config, active, now, quotaScope, selectionOptions)) {
|
|
2047
2099
|
const fallback = pickLowestUsageCodexAccount(config, active, now, quotaScope, selectionOptions);
|
package/src/codex/shim.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { basename, delimiter, dirname, extname, join, posix } from "node:path";
|
|
4
|
+
import { basename, delimiter, dirname, extname, join, posix, win32 } from "node:path";
|
|
5
5
|
import {
|
|
6
6
|
chmodSync,
|
|
7
7
|
closeSync,
|
|
@@ -264,6 +264,28 @@ interface ShimFileState {
|
|
|
264
264
|
preserveOnly?: boolean;
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
export type CodexShimBackingForCommand =
|
|
268
|
+
| Readonly<{ status: "not-tracked" }>
|
|
269
|
+
| Readonly<{
|
|
270
|
+
status: "matched";
|
|
271
|
+
selectedRole: "wrapper" | "backing";
|
|
272
|
+
backingPath: string;
|
|
273
|
+
backingKind: "backup" | "real";
|
|
274
|
+
}>
|
|
275
|
+
| Readonly<{
|
|
276
|
+
status: "unknown";
|
|
277
|
+
reason:
|
|
278
|
+
| "state_invalid"
|
|
279
|
+
| "platform_mismatch"
|
|
280
|
+
| "ambiguous_match"
|
|
281
|
+
| "preserve_only"
|
|
282
|
+
| "backing_missing"
|
|
283
|
+
| "backing_mismatch"
|
|
284
|
+
| "binding_unavailable"
|
|
285
|
+
| "wrapper_unhealthy"
|
|
286
|
+
| "version_manager_refused";
|
|
287
|
+
}>;
|
|
288
|
+
|
|
267
289
|
interface ShimPathFingerprint {
|
|
268
290
|
dev: number;
|
|
269
291
|
ino: number;
|
|
@@ -616,8 +638,13 @@ function backupPathFor(path: string): string {
|
|
|
616
638
|
* deliberately excluded: a false positive here refuses a restore that would
|
|
617
639
|
* otherwise be correct.
|
|
618
640
|
*/
|
|
619
|
-
export function isVersionManagerOwnedCodexPath(
|
|
620
|
-
|
|
641
|
+
export function isVersionManagerOwnedCodexPath(
|
|
642
|
+
path: string,
|
|
643
|
+
platform: NodeJS.Platform = process.platform,
|
|
644
|
+
): boolean {
|
|
645
|
+
const normalized = (platform === "win32"
|
|
646
|
+
? win32.normalize(path).replace(/\\/g, "/")
|
|
647
|
+
: posix.normalize(path)).toLowerCase();
|
|
621
648
|
return normalized.includes("/mise/installs/")
|
|
622
649
|
|| normalized.includes("/mise/shims/")
|
|
623
650
|
|| normalized.includes("/.asdf/installs/")
|
|
@@ -1078,6 +1105,7 @@ exit $LASTEXITCODE
|
|
|
1078
1105
|
|
|
1079
1106
|
interface ShimStateReadResult {
|
|
1080
1107
|
state: ShimState | null;
|
|
1108
|
+
present: boolean;
|
|
1081
1109
|
warning?: string;
|
|
1082
1110
|
}
|
|
1083
1111
|
|
|
@@ -1087,7 +1115,17 @@ function fileErrorCode(error: unknown): string | undefined {
|
|
|
1087
1115
|
: undefined;
|
|
1088
1116
|
}
|
|
1089
1117
|
|
|
1090
|
-
function readBoundedRegularFile(path: string, maxBytes: number): { content: string } | { warning: string } | null {
|
|
1118
|
+
function readBoundedRegularFile(path: string, maxBytes: number): { bytes: Buffer; content: string } | { warning: string } | null {
|
|
1119
|
+
let lexicalBefore: Stats;
|
|
1120
|
+
try {
|
|
1121
|
+
lexicalBefore = lstatSync(path);
|
|
1122
|
+
if (lexicalBefore.isSymbolicLink() || !lexicalBefore.isFile()) {
|
|
1123
|
+
return { warning: `Codex shim state is not a direct regular file at ${path}; auto-restore skipped.` };
|
|
1124
|
+
}
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
if (fileErrorCode(error) === "ENOENT") return null;
|
|
1127
|
+
return { warning: `Codex shim state could not be inspected at ${path}.` };
|
|
1128
|
+
}
|
|
1091
1129
|
let fd: number;
|
|
1092
1130
|
try {
|
|
1093
1131
|
fd = openSync(path, "r");
|
|
@@ -1113,25 +1151,33 @@ function readBoundedRegularFile(path: string, maxBytes: number): { content: stri
|
|
|
1113
1151
|
return { warning: `Codex shim state exceeds the 1 MiB startup limit at ${path}; auto-restore skipped.` };
|
|
1114
1152
|
}
|
|
1115
1153
|
const after = fstatSync(fd);
|
|
1154
|
+
let lexicalAfter: Stats;
|
|
1155
|
+
try {
|
|
1156
|
+
lexicalAfter = lstatSync(path);
|
|
1157
|
+
} catch {
|
|
1158
|
+
return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
|
|
1159
|
+
}
|
|
1116
1160
|
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size
|
|
1117
|
-
|| before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs
|
|
1161
|
+
|| before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs
|
|
1162
|
+
|| lexicalBefore.dev !== before.dev || lexicalBefore.ino !== before.ino
|
|
1163
|
+
|| lexicalAfter.isSymbolicLink() || lexicalAfter.dev !== after.dev || lexicalAfter.ino !== after.ino) {
|
|
1118
1164
|
return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
|
|
1119
1165
|
}
|
|
1120
|
-
return { content: buffer.toString("utf8") };
|
|
1166
|
+
return { bytes: buffer, content: buffer.toString("utf8") };
|
|
1121
1167
|
} finally {
|
|
1122
1168
|
closeSync(fd);
|
|
1123
1169
|
}
|
|
1124
1170
|
}
|
|
1125
1171
|
|
|
1126
|
-
function readStateResult(): ShimStateReadResult {
|
|
1127
|
-
const bounded = readBoundedRegularFile(
|
|
1128
|
-
if (!bounded) return { state: null };
|
|
1129
|
-
if ("warning" in bounded) return { state: null, warning: bounded.warning };
|
|
1172
|
+
function readStateResult(path = statePath()): ShimStateReadResult {
|
|
1173
|
+
const bounded = readBoundedRegularFile(path, CODEX_SHIM_STATE_MAX_BYTES);
|
|
1174
|
+
if (!bounded) return { state: null, present: false };
|
|
1175
|
+
if ("warning" in bounded) return { state: null, present: true, warning: bounded.warning };
|
|
1130
1176
|
try {
|
|
1131
1177
|
const value = JSON.parse(bounded.content) as unknown;
|
|
1132
|
-
if (!value || typeof value !== "object") return { state: null };
|
|
1178
|
+
if (!value || typeof value !== "object") return { state: null, present: true };
|
|
1133
1179
|
const state = value as Record<string, unknown>;
|
|
1134
|
-
if (typeof state.platform !== "string") return { state: null };
|
|
1180
|
+
if (typeof state.platform !== "string") return { state: null, present: true };
|
|
1135
1181
|
const validFile = (item: unknown): item is ShimFileState => {
|
|
1136
1182
|
if (!item || typeof item !== "object") return false;
|
|
1137
1183
|
const file = item as Record<string, unknown>;
|
|
@@ -1142,13 +1188,13 @@ function readStateResult(): ShimStateReadResult {
|
|
|
1142
1188
|
&& (file.preserveOnly === undefined || typeof file.preserveOnly === "boolean");
|
|
1143
1189
|
};
|
|
1144
1190
|
if (state.wrappers !== undefined) {
|
|
1145
|
-
if (!Array.isArray(state.wrappers) || state.wrappers.length === 0 || !state.wrappers.every(validFile)) return { state: null };
|
|
1191
|
+
if (!Array.isArray(state.wrappers) || state.wrappers.length === 0 || !state.wrappers.every(validFile)) return { state: null, present: true };
|
|
1146
1192
|
} else if (!validFile(state)) {
|
|
1147
|
-
return { state: null };
|
|
1193
|
+
return { state: null, present: true };
|
|
1148
1194
|
}
|
|
1149
|
-
return { state: state as unknown as ShimState };
|
|
1195
|
+
return { state: state as unknown as ShimState, present: true };
|
|
1150
1196
|
} catch {
|
|
1151
|
-
return { state: null };
|
|
1197
|
+
return { state: null, present: true };
|
|
1152
1198
|
}
|
|
1153
1199
|
}
|
|
1154
1200
|
|
|
@@ -1156,6 +1202,146 @@ function readState(): ShimState | null {
|
|
|
1156
1202
|
return readStateResult().state;
|
|
1157
1203
|
}
|
|
1158
1204
|
|
|
1205
|
+
export function isLocalAbsoluteInspectionPath(path: string, platform: NodeJS.Platform): boolean {
|
|
1206
|
+
if (platform !== "win32") return posix.isAbsolute(path);
|
|
1207
|
+
const normalized = path.replace(/\//g, "\\");
|
|
1208
|
+
// UNC and device namespaces can initiate remote I/O while a nominally local
|
|
1209
|
+
// inspection is resolving user-controlled paths. Root-relative paths are
|
|
1210
|
+
// drive-context dependent, so require an explicit local drive as well.
|
|
1211
|
+
return win32.isAbsolute(path)
|
|
1212
|
+
&& /^[a-z]:\\/i.test(normalized)
|
|
1213
|
+
&& !normalized.startsWith("\\\\");
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function windowsShimInspectionIsDeferred(platform: NodeJS.Platform): boolean {
|
|
1217
|
+
return platform === "win32";
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/** Resolve one selected command through already-recorded shim state, without repair. */
|
|
1221
|
+
export function inspectCodexShimBackingForCommand(
|
|
1222
|
+
selectedCommand: string,
|
|
1223
|
+
platform: NodeJS.Platform = process.platform,
|
|
1224
|
+
configDir: string = getConfigDir(),
|
|
1225
|
+
): CodexShimBackingForCommand {
|
|
1226
|
+
// Pathname prechecks cannot prevent a writable Windows ancestor from being
|
|
1227
|
+
// replaced with a remote reparse point before the later state/fingerprint
|
|
1228
|
+
// reads. Keep the exported read-only helper fail-closed until those reads are
|
|
1229
|
+
// performed through a handle-bound Windows provenance layer.
|
|
1230
|
+
if (windowsShimInspectionIsDeferred(platform)) {
|
|
1231
|
+
return Object.freeze({ status: "unknown" as const, reason: "binding_unavailable" as const });
|
|
1232
|
+
}
|
|
1233
|
+
if (!isLocalAbsoluteInspectionPath(configDir, platform)) {
|
|
1234
|
+
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
1235
|
+
}
|
|
1236
|
+
const stateFile = join(configDir, "codex-shim.json");
|
|
1237
|
+
try {
|
|
1238
|
+
const stateEntry = lstatSync(stateFile);
|
|
1239
|
+
if (stateEntry.isSymbolicLink()) {
|
|
1240
|
+
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
1241
|
+
}
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
if (fileErrorCode(error) !== "ENOENT") {
|
|
1244
|
+
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
const result = readStateResult(stateFile);
|
|
1248
|
+
if (!result.state) {
|
|
1249
|
+
return result.present
|
|
1250
|
+
? Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const })
|
|
1251
|
+
: Object.freeze({ status: "not-tracked" as const });
|
|
1252
|
+
}
|
|
1253
|
+
const pathApi = platform === "win32" ? win32 : posix;
|
|
1254
|
+
const samePath = (left: string, right: string): boolean => {
|
|
1255
|
+
const normalizedLeft = pathApi.resolve(left);
|
|
1256
|
+
const normalizedRight = pathApi.resolve(right);
|
|
1257
|
+
return platform === "win32"
|
|
1258
|
+
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
|
|
1259
|
+
: normalizedLeft === normalizedRight;
|
|
1260
|
+
};
|
|
1261
|
+
const files = stateFiles(result.state);
|
|
1262
|
+
if (files.some(file => !file.wrapperPath || !file.originalPath || !file.backupPath
|
|
1263
|
+
|| ![file.wrapperPath, file.originalPath, file.backupPath, file.realPath]
|
|
1264
|
+
.filter((path): path is string => typeof path === "string")
|
|
1265
|
+
.every(path => isLocalAbsoluteInspectionPath(path, platform)))) {
|
|
1266
|
+
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
1267
|
+
}
|
|
1268
|
+
const wrapperKeys = files.map(file => platform === "win32"
|
|
1269
|
+
? pathApi.resolve(file.wrapperPath).toLowerCase()
|
|
1270
|
+
: pathApi.resolve(file.wrapperPath));
|
|
1271
|
+
if (new Set(wrapperKeys).size !== wrapperKeys.length) {
|
|
1272
|
+
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
1273
|
+
}
|
|
1274
|
+
const selectedFingerprint = shimPathFingerprint(selectedCommand);
|
|
1275
|
+
if (!selectedFingerprint) {
|
|
1276
|
+
return Object.freeze({ status: "unknown" as const, reason: "binding_unavailable" as const });
|
|
1277
|
+
}
|
|
1278
|
+
const selectedIdentity = selectedFingerprint.target ?? selectedFingerprint;
|
|
1279
|
+
const sameEffectiveIdentity = (fingerprint: ShimPathFingerprint | null): boolean => {
|
|
1280
|
+
if (!fingerprint) return false;
|
|
1281
|
+
const identity = fingerprint.target ?? fingerprint;
|
|
1282
|
+
return identity.dev === selectedIdentity.dev && identity.ino === selectedIdentity.ino;
|
|
1283
|
+
};
|
|
1284
|
+
const matches = files.flatMap(file => {
|
|
1285
|
+
const backingPath = file.realPath ?? file.backupPath;
|
|
1286
|
+
const roles: Array<"wrapper" | "backing"> = [];
|
|
1287
|
+
if (samePath(file.wrapperPath, selectedCommand)
|
|
1288
|
+
|| sameEffectiveIdentity(shimPathFingerprint(file.wrapperPath))) {
|
|
1289
|
+
roles.push("wrapper");
|
|
1290
|
+
}
|
|
1291
|
+
if (samePath(backingPath, selectedCommand)
|
|
1292
|
+
|| sameEffectiveIdentity(shimPathFingerprint(backingPath))) {
|
|
1293
|
+
roles.push("backing");
|
|
1294
|
+
}
|
|
1295
|
+
return roles.map(selectedRole => ({ file, backingPath, selectedRole }));
|
|
1296
|
+
});
|
|
1297
|
+
if (matches.length === 0) return Object.freeze({ status: "not-tracked" as const });
|
|
1298
|
+
if (result.state.platform !== platform) {
|
|
1299
|
+
return Object.freeze({ status: "unknown" as const, reason: "platform_mismatch" as const });
|
|
1300
|
+
}
|
|
1301
|
+
if (matches.length !== 1) {
|
|
1302
|
+
return Object.freeze({ status: "unknown" as const, reason: "ambiguous_match" as const });
|
|
1303
|
+
}
|
|
1304
|
+
const { file, backingPath, selectedRole } = matches[0]!;
|
|
1305
|
+
if (file.preserveOnly === true) {
|
|
1306
|
+
return Object.freeze({ status: "unknown" as const, reason: "preserve_only" as const });
|
|
1307
|
+
}
|
|
1308
|
+
const backing = statFingerprint(backingPath, true);
|
|
1309
|
+
if (!backing || backing.size <= 0 || samePath(backingPath, file.wrapperPath)) {
|
|
1310
|
+
return Object.freeze({ status: "unknown" as const, reason: "backing_missing" as const });
|
|
1311
|
+
}
|
|
1312
|
+
const wrapperProbe = stableShimPathProbe(file.wrapperPath);
|
|
1313
|
+
if (!wrapperProbe || !isHealthyShimProbe(wrapperProbe, result.state.platform)) {
|
|
1314
|
+
return Object.freeze({
|
|
1315
|
+
status: "unknown" as const,
|
|
1316
|
+
reason: isVersionManagerOwnedCodexPath(file.wrapperPath)
|
|
1317
|
+
? "version_manager_refused" as const
|
|
1318
|
+
: "wrapper_unhealthy" as const,
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
const wrapperIdentity = wrapperProbe.fingerprint.target ?? wrapperProbe.fingerprint;
|
|
1322
|
+
if (backing.dev === wrapperIdentity.dev && backing.ino === wrapperIdentity.ino) {
|
|
1323
|
+
return Object.freeze({ status: "unknown" as const, reason: "backing_mismatch" as const });
|
|
1324
|
+
}
|
|
1325
|
+
const wrapperExt = extname(file.wrapperPath).toLowerCase();
|
|
1326
|
+
const invokesBacking = platform !== "win32"
|
|
1327
|
+
? wrapperProbe.prefix.includes(`exec ${shQuote(backingPath)} "$@"`)
|
|
1328
|
+
: wrapperExt === ".cmd" || wrapperExt === ".bat"
|
|
1329
|
+
? wrapperProbe.prefix.includes(windowsBatchSet("OCX_REAL_CODEX", backingPath))
|
|
1330
|
+
&& wrapperProbe.prefix.includes('"%OCX_REAL_CODEX%" %*')
|
|
1331
|
+
: wrapperExt === ".ps1"
|
|
1332
|
+
? wrapperProbe.prefix.includes(`& ${psString(backingPath)} @args`)
|
|
1333
|
+
: wrapperProbe.prefix.includes(`exec ${shQuote(gitBashPath(backingPath))} "$@"`);
|
|
1334
|
+
if (!invokesBacking) {
|
|
1335
|
+
return Object.freeze({ status: "unknown" as const, reason: "backing_mismatch" as const });
|
|
1336
|
+
}
|
|
1337
|
+
return Object.freeze({
|
|
1338
|
+
status: "matched" as const,
|
|
1339
|
+
selectedRole,
|
|
1340
|
+
backingPath,
|
|
1341
|
+
backingKind: file.realPath !== undefined ? "real" as const : "backup" as const,
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1159
1345
|
function statePath(): string {
|
|
1160
1346
|
return join(getConfigDir(), "codex-shim.json");
|
|
1161
1347
|
}
|
|
@@ -1283,7 +1469,7 @@ function stateFiles(state: ShimState): ShimFileState[] {
|
|
|
1283
1469
|
}
|
|
1284
1470
|
|
|
1285
1471
|
function primaryState(files: ShimFileState[]): ShimState {
|
|
1286
|
-
const first = files[0]
|
|
1472
|
+
const first = files[0]!;
|
|
1287
1473
|
return { platform: process.platform, ...first, wrappers: files };
|
|
1288
1474
|
}
|
|
1289
1475
|
|
|
@@ -2067,7 +2253,7 @@ export function autoRestoreCodexShim(options: {
|
|
|
2067
2253
|
const state = stateRead.state;
|
|
2068
2254
|
if (!state) {
|
|
2069
2255
|
if (stateRead.warning) return { status: "ineligible", message: stateRead.warning };
|
|
2070
|
-
return { status:
|
|
2256
|
+
return { status: stateRead.present ? "ineligible" : "not-installed" };
|
|
2071
2257
|
}
|
|
2072
2258
|
if (state.platform !== process.platform) return { status: "ineligible" };
|
|
2073
2259
|
|
|
@@ -226,7 +226,10 @@ export function isNativeModelQuotaExhausted(
|
|
|
226
226
|
const resolvedAccountId = resolveRouteFallbackAccountId(route, config, accountId);
|
|
227
227
|
if (!resolvedAccountId) return false;
|
|
228
228
|
const quota = getAccountQuota(resolvedAccountId);
|
|
229
|
-
|
|
229
|
+
// Subagent fallback reads the same score, so a stale terminal reading would push
|
|
230
|
+
// subagents off a native model whose window has already reset. Thread the caller's clock
|
|
231
|
+
// rather than letting the scorer read wall time - the two would silently diverge.
|
|
232
|
+
const usage = computeCodexUsageScore(quota, getPoolAccountPlan(config, resolvedAccountId), now);
|
|
230
233
|
if (usage >= CODEX_UNKNOWN_USAGE_SCORE) return false;
|
|
231
234
|
return usage >= quotaThreshold(config);
|
|
232
235
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import { isAbsolute, join, resolve } from "node:path";
|
|
19
19
|
|
|
20
20
|
import { resolveTrustedWindowsPowerShellExe } from "../lib/windows-elevation";
|
|
21
|
+
import { WINDOWS_PRINCIPAL_LOOKUP_TIMEOUT_MS } from "../lib/windows-user-principal";
|
|
21
22
|
|
|
22
23
|
import type {
|
|
23
24
|
ResolveCodexCoordinatorDatabasePath,
|
|
@@ -55,7 +56,7 @@ const SID_PATTERN = /^S-1-(?:\d+-)+\d+$/i;
|
|
|
55
56
|
* fails the lookup and the caller still refuses rather than writing. Only the
|
|
56
57
|
* ceiling moved, and it moved for the case where the lookup would have succeeded.
|
|
57
58
|
*/
|
|
58
|
-
const WINDOWS_POWERSHELL_LOOKUP_TIMEOUT_MS =
|
|
59
|
+
const WINDOWS_POWERSHELL_LOOKUP_TIMEOUT_MS = WINDOWS_PRINCIPAL_LOOKUP_TIMEOUT_MS;
|
|
59
60
|
|
|
60
61
|
function windowsIdentityLookupTimeoutMs(): number {
|
|
61
62
|
return WINDOWS_POWERSHELL_LOOKUP_TIMEOUT_MS;
|