import { homedir } from 'node:os'; import { join } from 'node:path'; import { CODEX_HOME_DIRS_ENV, PROFILE_COOLDOWN_MS } from '../config/defaults.js'; export interface CodexProfile { id: string; codexHome: string; cooldownUntil?: number | undefined; failureCount: number; lastFailureReason?: string | undefined; } interface ProfileManagerOptions { codexHomes?: string[] | undefined; cooldownMs?: number | undefined; now?: (() => number) | undefined; } function defaultCodexHome(): string { return join(homedir(), '.codex'); } function dedupeHomes(codexHomes: string[]): string[] { const seen = new Set(); const result: string[] = []; for (const home of codexHomes) { const normalized = home.trim(); if (!normalized || seen.has(normalized)) { continue; } seen.add(normalized); result.push(normalized); } return result.length > 0 ? result : [defaultCodexHome()]; } export class ProfileManager { private readonly cooldownMs: number; private readonly now: () => number; private readonly profiles: CodexProfile[]; private currentIndex = 0; constructor(options: ProfileManagerOptions = {}) { this.cooldownMs = options.cooldownMs ?? PROFILE_COOLDOWN_MS; this.now = options.now ?? Date.now; this.profiles = dedupeHomes(options.codexHomes ?? [defaultCodexHome()]).map((codexHome, index) => ({ id: `profile-${index + 1}`, codexHome, failureCount: 0, })); this.currentIndex = this.findFirstAvailableIndex(); } static fromEnvironment(now?: () => number): ProfileManager { const raw = process.env[CODEX_HOME_DIRS_ENV]; const homes = raw ? raw.split(':').map((entry) => entry.trim()).filter(Boolean) : [defaultCodexHome()]; return new ProfileManager({ codexHomes: homes, now }); } getCurrentProfile(): CodexProfile { this.resetExpiredCooldowns(); return { ...this.profiles[this.currentIndex]! }; } getProfiles(): CodexProfile[] { this.resetExpiredCooldowns(); return this.profiles.map((profile) => ({ ...profile })); } markFailure(reason: string): { rotated: boolean; profile?: CodexProfile } { const current = this.profiles[this.currentIndex]; if (!current) { return { rotated: false }; } current.failureCount += 1; current.lastFailureReason = reason; current.cooldownUntil = this.now() + this.cooldownMs; const nextIndex = this.findFirstAvailableIndex(); if (nextIndex === -1) { return { rotated: false }; } this.currentIndex = nextIndex; return { rotated: true, profile: this.getCurrentProfile() }; } private resetExpiredCooldowns(): void { const now = this.now(); for (const profile of this.profiles) { if (profile.cooldownUntil !== undefined && profile.cooldownUntil <= now) { profile.cooldownUntil = undefined; } } const nextIndex = this.findFirstAvailableIndex(); if (nextIndex !== -1) { this.currentIndex = nextIndex; } } private findFirstAvailableIndex(): number { const now = this.now(); return this.profiles.findIndex((profile) => profile.cooldownUntil === undefined || profile.cooldownUntil <= now); } }