From e2dd12a2353edb8620d3a5968566cfeace143ffc Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Mon, 27 Apr 2026 22:47:40 +0900 Subject: [PATCH] =?UTF-8?q?Codex=20switch:=20translate=20settings-index=20?= =?UTF-8?q?=E2=86=94=20rotation-index=20by=20auth=20path=20(#51)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/codex-token-rotation.ts | 10 +++++++ src/settings-store.ts | 54 +++++++++++++++++++++++++++++++++++++ src/web-dashboard-server.ts | 12 ++++----- 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/codex-token-rotation.ts b/src/codex-token-rotation.ts index b93bbe9..aad2c6a 100644 --- a/src/codex-token-rotation.ts +++ b/src/codex-token-rotation.ts @@ -261,6 +261,16 @@ export function getCurrentCodexAccountIndex(): number { 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. */ export function setCurrentCodexAccountIndex(targetIndex: number): void { if (accounts.length === 0) { diff --git a/src/settings-store.ts b/src/settings-store.ts index 57aa11e..0b2b9c0 100644 --- a/src/settings-store.ts +++ b/src/settings-store.ts @@ -15,6 +15,12 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; +import { + findCodexAccountIndexByAuthPath, + getActiveCodexAuthPath, + setCurrentCodexAccountIndex as setRotationIndex, +} from './codex-token-rotation.js'; + export interface ClaudeAccountSummary { index: number; expiresAt: number | null; @@ -441,6 +447,54 @@ export async function refreshCodexAccount( 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<{ refreshed: number[]; failed: Array<{ index: number; error: string }>; diff --git a/src/web-dashboard-server.ts b/src/web-dashboard-server.ts index 506d680..d92dd6f 100644 --- a/src/web-dashboard-server.ts +++ b/src/web-dashboard-server.ts @@ -48,6 +48,7 @@ import { } from './web-dashboard-data.js'; import { addClaudeAccountFromToken, + getActiveCodexSettingsIndex, getFastMode, getModelConfig, listClaudeAccounts, @@ -55,13 +56,10 @@ import { refreshAllCodexAccounts, refreshCodexAccount, removeAccountDirectory, + setActiveCodexSettingsIndex, updateFastMode, updateModelConfig, } from './settings-store.js'; -import { - getCurrentCodexAccountIndex, - setCurrentCodexAccountIndex, -} from './codex-token-rotation.js'; const DEFAULT_STATUS_MAX_AGE_MS = 10 * 60 * 1000; const ROOM_MESSAGE_ID_CACHE_LIMIT = 500; @@ -1393,10 +1391,10 @@ export function createWebDashboardHandler( ); } try { - setCurrentCodexAccountIndex(idx); + setActiveCodexSettingsIndex(idx); return jsonResponse({ ok: true, - codexCurrentIndex: getCurrentCodexAccountIndex(), + codexCurrentIndex: getActiveCodexSettingsIndex(), }); } catch (err) { const message = err instanceof Error ? err.message : String(err); @@ -1602,7 +1600,7 @@ export function createWebDashboardHandler( return jsonResponse({ claude: listClaudeAccounts(), codex: listCodexAccounts(), - codexCurrentIndex: getCurrentCodexAccountIndex(), + codexCurrentIndex: getActiveCodexSettingsIndex(), }); }