Codex switch: translate settings-index ↔ rotation-index by auth path (#51)
settings-store lists codex accounts as
index 0 = ~/.codex/auth.json (default)
index N = ~/.codex-accounts/{N}/auth.json
but codex-token-rotation only loads ~/.codex-accounts/{N} when those
dirs exist (it ignores ~/.codex/auth.json in that mode), so the
rotation array's indices are off-by-one vs settings indices.
Old buggy behavior:
- "사용중" badge pointed at the wrong account.
- Clicking 전환 on UI #N called setCurrentCodexAccountIndex(N) which
selected rotation array element N — typically a different
~/.codex-accounts/{N+1} entry. Effectively the UI told us a
different account was now active than what the next codex spawn
actually used.
Fix
- New findCodexAccountIndexByAuthPath(path) on the rotation module
exposes a path-based lookup.
- New getActiveCodexSettingsIndex() / setActiveCodexSettingsIndex(idx)
in settings-store translate via codexAuthPath() ↔
findCodexAccountIndexByAuthPath().
- /api/settings/accounts.codexCurrentIndex and PUT
/api/settings/accounts/codex/current both go through the
settings-side translation, so UI indices are now authoritative.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -261,6 +261,16 @@ export function getCurrentCodexAccountIndex(): number {
|
|||||||
return accounts.length === 0 ? 0 : currentIndex;
|
return accounts.length === 0 ? 0 : currentIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Find the rotation-array index that owns the given auth.json path. */
|
||||||
|
export function findCodexAccountIndexByAuthPath(
|
||||||
|
authPath: string,
|
||||||
|
): number | null {
|
||||||
|
for (let i = 0; i < accounts.length; i += 1) {
|
||||||
|
if (accounts[i]?.authPath === authPath) return i;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/** Manually switch the active codex account. Clears its rate-limit cooldown. */
|
/** Manually switch the active codex account. Clears its rate-limit cooldown. */
|
||||||
export function setCurrentCodexAccountIndex(targetIndex: number): void {
|
export function setCurrentCodexAccountIndex(targetIndex: number): void {
|
||||||
if (accounts.length === 0) {
|
if (accounts.length === 0) {
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ import fs from 'node:fs';
|
|||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import {
|
||||||
|
findCodexAccountIndexByAuthPath,
|
||||||
|
getActiveCodexAuthPath,
|
||||||
|
setCurrentCodexAccountIndex as setRotationIndex,
|
||||||
|
} from './codex-token-rotation.js';
|
||||||
|
|
||||||
export interface ClaudeAccountSummary {
|
export interface ClaudeAccountSummary {
|
||||||
index: number;
|
index: number;
|
||||||
expiresAt: number | null;
|
expiresAt: number | null;
|
||||||
@@ -441,6 +447,54 @@ export async function refreshCodexAccount(
|
|||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* settings-store lists codex accounts in an order that includes the default
|
||||||
|
* `~/.codex/auth.json` as index 0 plus `~/.codex-accounts/{N}` as index N.
|
||||||
|
* The rotation array (codex-token-rotation) only loads `~/.codex-accounts/{N}`
|
||||||
|
* when those dirs exist (it ignores `~/.codex/auth.json` in that mode), so its
|
||||||
|
* array indices are off-by-one vs. the settings indices. Translate via path.
|
||||||
|
*/
|
||||||
|
export function getActiveCodexSettingsIndex(): number | null {
|
||||||
|
const activePath = getActiveCodexAuthPath();
|
||||||
|
if (!activePath) return null;
|
||||||
|
// Walk the same listing order as listCodexAccounts() to find the matching
|
||||||
|
// settings-store index.
|
||||||
|
if (codexAuthPath(0) === activePath && fs.existsSync(activePath)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const dir = path.join(os.homedir(), '.codex-accounts');
|
||||||
|
if (fs.existsSync(dir)) {
|
||||||
|
const indices = fs
|
||||||
|
.readdirSync(dir)
|
||||||
|
.filter((e) => /^\d+$/.test(e))
|
||||||
|
.map((e) => Number.parseInt(e, 10))
|
||||||
|
.filter((n) => Number.isFinite(n) && n >= 1)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
for (const i of indices) {
|
||||||
|
if (codexAuthPath(i) === activePath) return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setActiveCodexSettingsIndex(settingsIndex: number): void {
|
||||||
|
const file = codexAuthPath(settingsIndex);
|
||||||
|
if (!fs.existsSync(file)) {
|
||||||
|
throw new Error(
|
||||||
|
`codex auth.json not found for settings index ${settingsIndex}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const rotationIndex = findCodexAccountIndexByAuthPath(file);
|
||||||
|
if (rotationIndex === null) {
|
||||||
|
throw new Error(
|
||||||
|
`codex switch: settings #${settingsIndex} (${file}) is not part of the rotation pool. ` +
|
||||||
|
`Rotation only manages accounts under ~/.codex-accounts/. ` +
|
||||||
|
`If you want to use ~/.codex/auth.json directly, remove the ~/.codex-accounts dir first.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setRotationIndex(rotationIndex);
|
||||||
|
}
|
||||||
|
|
||||||
export async function refreshAllCodexAccounts(): Promise<{
|
export async function refreshAllCodexAccounts(): Promise<{
|
||||||
refreshed: number[];
|
refreshed: number[];
|
||||||
failed: Array<{ index: number; error: string }>;
|
failed: Array<{ index: number; error: string }>;
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import {
|
|||||||
} from './web-dashboard-data.js';
|
} from './web-dashboard-data.js';
|
||||||
import {
|
import {
|
||||||
addClaudeAccountFromToken,
|
addClaudeAccountFromToken,
|
||||||
|
getActiveCodexSettingsIndex,
|
||||||
getFastMode,
|
getFastMode,
|
||||||
getModelConfig,
|
getModelConfig,
|
||||||
listClaudeAccounts,
|
listClaudeAccounts,
|
||||||
@@ -55,13 +56,10 @@ import {
|
|||||||
refreshAllCodexAccounts,
|
refreshAllCodexAccounts,
|
||||||
refreshCodexAccount,
|
refreshCodexAccount,
|
||||||
removeAccountDirectory,
|
removeAccountDirectory,
|
||||||
|
setActiveCodexSettingsIndex,
|
||||||
updateFastMode,
|
updateFastMode,
|
||||||
updateModelConfig,
|
updateModelConfig,
|
||||||
} from './settings-store.js';
|
} from './settings-store.js';
|
||||||
import {
|
|
||||||
getCurrentCodexAccountIndex,
|
|
||||||
setCurrentCodexAccountIndex,
|
|
||||||
} from './codex-token-rotation.js';
|
|
||||||
|
|
||||||
const DEFAULT_STATUS_MAX_AGE_MS = 10 * 60 * 1000;
|
const DEFAULT_STATUS_MAX_AGE_MS = 10 * 60 * 1000;
|
||||||
const ROOM_MESSAGE_ID_CACHE_LIMIT = 500;
|
const ROOM_MESSAGE_ID_CACHE_LIMIT = 500;
|
||||||
@@ -1393,10 +1391,10 @@ export function createWebDashboardHandler(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
setCurrentCodexAccountIndex(idx);
|
setActiveCodexSettingsIndex(idx);
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
ok: true,
|
ok: true,
|
||||||
codexCurrentIndex: getCurrentCodexAccountIndex(),
|
codexCurrentIndex: getActiveCodexSettingsIndex(),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
@@ -1602,7 +1600,7 @@ export function createWebDashboardHandler(
|
|||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
claude: listClaudeAccounts(),
|
claude: listClaudeAccounts(),
|
||||||
codex: listCodexAccounts(),
|
codex: listCodexAccounts(),
|
||||||
codexCurrentIndex: getCurrentCodexAccountIndex(),
|
codexCurrentIndex: getActiveCodexSettingsIndex(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user