Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 31 additions & 134 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,21 @@ import {
lookupCodexCliTokensByEmail,
isCodexCliSyncEnabled,
} from "./lib/accounts.js";
import {
resolveActiveIndex,
formatRateLimitEntry,
formatActiveIndexByFamilyLabels,
formatRateLimitStatusByFamily,
} from "./lib/accounts/account-view.js";
import {
cloneAccountStorage,
createEmptyAccountStorage,
} from "./lib/accounts/storage-view.js";
import {
setActiveIndexForAllFamilies,
normalizeActiveIndexByFamily,
removeAccountAndReconcileActiveIndexes,
} from "./lib/accounts/active-index.js";
import {
getStoragePath,
loadAccounts,
Expand Down Expand Up @@ -676,21 +691,6 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
}
};

const resolveActiveIndex = (
storage: {
activeIndex: number;
activeIndexByFamily?: Partial<Record<ModelFamily, number>>;
accounts: unknown[];
},
family: ModelFamily = "codex",
): number => {
const total = storage.accounts.length;
if (total === 0) return 0;
const rawCandidate = storage.activeIndexByFamily?.[family] ?? storage.activeIndex;
const raw = Number.isFinite(rawCandidate) ? rawCandidate : 0;
return Math.max(0, Math.min(raw, total - 1));
};

const hydrateEmails = async (
storage: AccountStorageV3 | null,
): Promise<AccountStorageV3 | null> => {
Expand Down Expand Up @@ -755,40 +755,6 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
return storage;
};

const getRateLimitResetTimeForFamily = (
account: { rateLimitResetTimes?: Record<string, number | undefined> },
now: number,
family: ModelFamily,
): number | null => {
const times = account.rateLimitResetTimes;
if (!times) return null;

let minReset: number | null = null;
const prefix = `${family}:`;
for (const [key, value] of Object.entries(times)) {
if (typeof value !== "number") continue;
if (value <= now) continue;
if (key !== family && !key.startsWith(prefix)) continue;
if (minReset === null || value < minReset) {
minReset = value;
}
}

return minReset;
};

const formatRateLimitEntry = (
account: { rateLimitResetTimes?: Record<string, number | undefined> },
now: number,
family: ModelFamily = "codex",
): string | null => {
const resetAt = getRateLimitResetTimeForFamily(account, now, family);
if (typeof resetAt !== "number") return null;
const remaining = resetAt - now;
if (remaining <= 0) return null;
return `resets in ${formatWaitTime(remaining)}`;
};

const applyUiRuntimeFromConfig = (
pluginConfig: ReturnType<typeof loadPluginConfig>,
): UiRuntimeOptions => {
Expand Down Expand Up @@ -999,11 +965,7 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => {
account.lastUsed = now;
account.lastSwitchReason = "rotation";
}
storage.activeIndex = index;
storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
for (const family of MODEL_FAMILIES) {
storage.activeIndexByFamily[family] = index;
}
setActiveIndexForAllFamilies(storage, index);

await saveAccounts(storage);
if (cachedAccountManager) {
Expand Down Expand Up @@ -2432,20 +2394,9 @@ while (attempted.size < Math.max(1, accountCount)) {
let refreshAccountIndex: number | undefined;

const clampActiveIndices = (storage: AccountStorageV3): void => {
const count = storage.accounts.length;
if (count === 0) {
storage.activeIndex = 0;
storage.activeIndexByFamily = {};
return;
}
storage.activeIndex = Math.max(0, Math.min(storage.activeIndex, count - 1));
storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
for (const family of MODEL_FAMILIES) {
const raw = storage.activeIndexByFamily[family];
const candidate =
typeof raw === "number" && Number.isFinite(raw) ? raw : storage.activeIndex;
storage.activeIndexByFamily[family] = Math.max(0, Math.min(candidate, count - 1));
}
normalizeActiveIndexByFamily(storage, storage.accounts.length, {
clearFamilyMapWhenEmpty: true,
});
};

const isFlaggableFailure = (failure: Extract<TokenResult, { type: "failed" }>): boolean => {
Expand Down Expand Up @@ -2706,14 +2657,8 @@ while (attempted.size < Math.max(1, accountCount)) {
const runAccountCheck = async (deepProbe: boolean): Promise<void> => {
const loadedStorage = await hydrateEmails(await loadAccounts());
const workingStorage = loadedStorage
? {
...loadedStorage,
accounts: loadedStorage.accounts.map((account) => ({ ...account })),
activeIndexByFamily: loadedStorage.activeIndexByFamily
? { ...loadedStorage.activeIndexByFamily }
: {},
}
: { version: 3 as const, accounts: [], activeIndex: 0, activeIndexByFamily: {} };
? cloneAccountStorage(loadedStorage)
: createEmptyAccountStorage();

if (workingStorage.accounts.length === 0) {
console.log("\nNo accounts to check.\n");
Expand Down Expand Up @@ -3059,14 +3004,8 @@ while (attempted.size < Math.max(1, accountCount)) {
while (true) {
const loadedStorage = await hydrateEmails(await loadAccounts());
const workingStorage = loadedStorage
? {
...loadedStorage,
accounts: loadedStorage.accounts.map((account) => ({ ...account })),
activeIndexByFamily: loadedStorage.activeIndexByFamily
? { ...loadedStorage.activeIndexByFamily }
: {},
}
: { version: 3 as const, accounts: [], activeIndex: 0, activeIndexByFamily: {} };
? cloneAccountStorage(loadedStorage)
: createEmptyAccountStorage();
const flaggedStorage = await loadFlaggedAccounts();

if (workingStorage.accounts.length === 0 && flaggedStorage.accounts.length === 0) {
Expand Down Expand Up @@ -3566,11 +3505,7 @@ while (attempted.size < Math.max(1, accountCount)) {
account.lastSwitchReason = "rotation";
}

storage.activeIndex = targetIndex;
storage.activeIndexByFamily = storage.activeIndexByFamily ?? {};
for (const family of MODEL_FAMILIES) {
storage.activeIndexByFamily[family] = targetIndex;
}
setActiveIndexForAllFamilies(storage, targetIndex);
try {
await saveAccounts(storage);
} catch (saveError) {
Expand Down Expand Up @@ -3647,21 +3582,14 @@ while (attempted.size < Math.max(1, accountCount)) {

lines.push("");
lines.push(...formatUiSection(ui, "Active index by model family"));
for (const family of MODEL_FAMILIES) {
const idx = storage.activeIndexByFamily?.[family];
const familyIndexLabel =
typeof idx === "number" && Number.isFinite(idx) ? String(idx + 1) : "-";
lines.push(formatUiItem(ui, `${family}: ${familyIndexLabel}`));
for (const line of formatActiveIndexByFamilyLabels(storage.activeIndexByFamily)) {
lines.push(formatUiItem(ui, line));
}

lines.push("");
lines.push(...formatUiSection(ui, "Rate limits by model family (per account)"));
storage.accounts.forEach((account, index) => {
const statuses = MODEL_FAMILIES.map((family) => {
const resetAt = getRateLimitResetTimeForFamily(account, now, family);
if (typeof resetAt !== "number") return `${family}=ok`;
return `${family}=${formatWaitTime(resetAt - now)}`;
});
const statuses = formatRateLimitStatusByFamily(account, now);
lines.push(formatUiItem(ui, `Account ${index + 1}: ${statuses.join(" | ")}`));
});

Expand Down Expand Up @@ -3700,21 +3628,14 @@ while (attempted.size < Math.max(1, accountCount)) {

lines.push("");
lines.push("Active index by model family:");
for (const family of MODEL_FAMILIES) {
const idx = storage.activeIndexByFamily?.[family];
const familyIndexLabel =
typeof idx === "number" && Number.isFinite(idx) ? String(idx + 1) : "-";
lines.push(` ${family}: ${familyIndexLabel}`);
for (const line of formatActiveIndexByFamilyLabels(storage.activeIndexByFamily)) {
lines.push(` ${line}`);
}

lines.push("");
lines.push("Rate limits by model family (per account):");
storage.accounts.forEach((account, index) => {
const statuses = MODEL_FAMILIES.map((family) => {
const resetAt = getRateLimitResetTimeForFamily(account, now, family);
if (typeof resetAt !== "number") return `${family}=ok`;
return `${family}=${formatWaitTime(resetAt - now)}`;
});
const statuses = formatRateLimitStatusByFamily(account, now);
lines.push(` Account ${index + 1}: ${statuses.join(" | ")}`);
});

Expand Down Expand Up @@ -3933,31 +3854,7 @@ while (attempted.size < Math.max(1, accountCount)) {

const label = formatAccountLabel(account, targetIndex);

storage.accounts.splice(targetIndex, 1);

if (storage.accounts.length === 0) {
storage.activeIndex = 0;
storage.activeIndexByFamily = {};
} else {
if (storage.activeIndex >= storage.accounts.length) {
storage.activeIndex = 0;
} else if (storage.activeIndex > targetIndex) {
storage.activeIndex -= 1;
}

if (storage.activeIndexByFamily) {
for (const family of MODEL_FAMILIES) {
const idx = storage.activeIndexByFamily[family];
if (typeof idx === "number") {
if (idx >= storage.accounts.length) {
storage.activeIndexByFamily[family] = 0;
} else if (idx > targetIndex) {
storage.activeIndexByFamily[family] = idx - 1;
}
}
}
}
}
removeAccountAndReconcileActiveIndexes(storage, targetIndex);

try {
await saveAccounts(storage);
Expand Down
82 changes: 82 additions & 0 deletions lib/accounts/account-view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { MODEL_FAMILIES, type ModelFamily } from "../prompts/codex.js";
import { formatWaitTime } from "./rate-limits.js";

export interface ActiveAccountStorageView {
activeIndex: number;
activeIndexByFamily?: Partial<Record<ModelFamily, number>>;
accounts: unknown[];
}

export interface AccountRateLimitView {
rateLimitResetTimes?: Record<string, number | undefined>;
}

export type ActiveIndexByFamilyView = Partial<Record<ModelFamily, number>> | undefined;

export function resolveActiveIndex(
storage: ActiveAccountStorageView,
family: ModelFamily = "codex",
): number {
const total = storage.accounts.length;
if (total === 0) return 0;
const rawCandidate = storage.activeIndexByFamily?.[family] ?? storage.activeIndex;
const raw = Number.isFinite(rawCandidate) ? rawCandidate : 0;
return Math.max(0, Math.min(raw, total - 1));
}
Comment on lines +22 to +25
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

normalize active index to an integer before clamping.

line 22 in lib/accounts/account-view.ts can return a fractional index (for example 1.7). callers treat this as an array index in lib/codex-manager.ts:3340-3344 and lib/codex-manager.ts:3937-3944, which can produce undefined active-account reads.

proposed fix
 export function resolveActiveIndex(
 	storage: ActiveAccountStorageView,
 	family: ModelFamily = "codex",
 ): number {
 	const total = storage.accounts.length;
 	if (total === 0) return 0;
 	const rawCandidate = storage.activeIndexByFamily?.[family] ?? storage.activeIndex;
-	const raw = Number.isFinite(rawCandidate) ? rawCandidate : 0;
-	return Math.max(0, Math.min(raw, total - 1));
+	const raw = Number.isFinite(rawCandidate) ? Math.floor(rawCandidate) : 0;
+	return Math.max(0, Math.min(raw, total - 1));
 }

please also add a regression in test/account-view.test.ts for fractional activeIndex and activeIndexByFamily values.

As per coding guidelines, "lib/**: focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const rawCandidate = storage.activeIndexByFamily?.[family] ?? storage.activeIndex;
const raw = Number.isFinite(rawCandidate) ? rawCandidate : 0;
return Math.max(0, Math.min(raw, total - 1));
}
const rawCandidate = storage.activeIndexByFamily?.[family] ?? storage.activeIndex;
const raw = Number.isFinite(rawCandidate) ? Math.floor(rawCandidate) : 0;
return Math.max(0, Math.min(raw, total - 1));
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/accounts/account-view.ts` around lines 20 - 23, The active-index clamp
currently can return fractional values; coerce the chosen rawCandidate to an
integer before clamping (use Math.trunc or equivalent) so
storage.activeIndexByFamily and storage.activeIndex never produce fractional
indices; update the normalization in lib/accounts/account-view.ts where
storage.activeIndexByFamily?.[family] ?? storage.activeIndex is processed to
first coerce to an integer then apply Math.max(0, Math.min(..., total - 1)). Add
a regression vitest in test/account-view.test.ts that sets fractional values for
activeIndex and activeIndexByFamily and asserts the returned active index is an
integer and within bounds; ensure the change does not log any sensitive
tokens/emails and mention the new test in the test suite metadata.


export function getRateLimitResetTimeForFamily(
account: AccountRateLimitView,
now: number,
family: ModelFamily,
): number | null {
const times = account.rateLimitResetTimes;
if (!times) return null;

let minReset: number | null = null;
const prefix = `${family}:`;
for (const [key, value] of Object.entries(times)) {
if (typeof value !== "number") continue;
if (value <= now) continue;
if (key !== family && !key.startsWith(prefix)) continue;
if (minReset === null || value < minReset) {
minReset = value;
}
Comment on lines +37 to +43
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

filter out non-finite reset timestamps before selecting minimum.

line 36 currently accepts NaN/Infinity because they are still number. that can flow into formatRateLimitEntry (lib/accounts/account-view.ts:52-56) and generate malformed output.

proposed fix
 	for (const [key, value] of Object.entries(times)) {
-		if (typeof value !== "number") continue;
+		if (typeof value !== "number" || !Number.isFinite(value)) continue;
 		if (value <= now) continue;
 		if (key !== family && !key.startsWith(prefix)) continue;
 		if (minReset === null || value < minReset) {
 			minReset = value;
 		}
 	}

please add regression coverage in test/account-view.test.ts for NaN, Infinity, and -Infinity values inside rateLimitResetTimes.

As per coding guidelines, "lib/**: focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails."

Also applies to: 52-56

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/accounts/account-view.ts` around lines 35 - 41, Update the loop that
scans Object.entries(times) to skip non-finite numbers by using
Number.isFinite(value) (so NaN/Infinity/-Infinity are ignored) before comparing
to now and updating minReset; this change affects the code path that later calls
formatRateLimitEntry, so ensure formatRateLimitEntry still receives only finite
timestamps. Add regression tests in test/account-view.test.ts that set
rateLimitResetTimes to NaN, Infinity, and -Infinity and assert the rendered
output is well-formed (i.e., those entries are ignored and do not produce
malformed output), referencing the same account-view logic that computes
minReset and formats entries.

}

return minReset;
}

export function formatRateLimitEntry(
account: AccountRateLimitView,
now: number,
family: ModelFamily = "codex",
): string | null {
const resetAt = getRateLimitResetTimeForFamily(account, now, family);
if (typeof resetAt !== "number") return null;
const remaining = resetAt - now;
if (remaining <= 0) return null;
return `resets in ${formatWaitTime(remaining)}`;
}

export function formatRateLimitStatusByFamily(
account: AccountRateLimitView,
now: number,
families: readonly ModelFamily[] = MODEL_FAMILIES,
): string[] {
return families.map((family) => {
const resetAt = getRateLimitResetTimeForFamily(account, now, family);
if (typeof resetAt !== "number") return `${family}=ok`;
return `${family}=${formatWaitTime(resetAt - now)}`;
});
}

export function formatActiveIndexByFamilyLabels(
activeIndexByFamily: ActiveIndexByFamilyView,
families: readonly ModelFamily[] = MODEL_FAMILIES,
): string[] {
return families.map((family) => {
const idx = activeIndexByFamily?.[family];
const label = typeof idx === "number" && Number.isFinite(idx) ? String(idx + 1) : "-";
return `${family}: ${label}`;
});
}
Loading