diff --git a/.gitignore b/.gitignore index 6d2e4585..e0acc0dc 100644 --- a/.gitignore +++ b/.gitignore @@ -168,3 +168,4 @@ OPUS_ANALYSIS_AND_IDEAS.md # Auto Claude generated files .security-key +/shared_docs \ No newline at end of file diff --git a/apps/frontend/src/main/claude-profile/profile-scorer.ts b/apps/frontend/src/main/claude-profile/profile-scorer.ts index fc0f8ecc..25b58816 100644 --- a/apps/frontend/src/main/claude-profile/profile-scorer.ts +++ b/apps/frontend/src/main/claude-profile/profile-scorer.ts @@ -35,12 +35,19 @@ export function getBestAvailableProfile( // 2. Lower weekly usage (more important than session) // 3. Lower session usage // 4. More recently authenticated + const isDebug = process.env.DEBUG === 'true'; + + if (isDebug) { + console.warn('[ProfileScorer] Evaluating', candidates.length, 'candidate profiles (excluding:', excludeProfileId, ')'); + } const scoredProfiles: ScoredProfile[] = candidates.map(profile => { let score = 100; // Base score + if (isDebug) console.warn('[ProfileScorer] Scoring profile:', profile.name, '(', profile.id, ')'); // Check rate limit status const rateLimitStatus = isProfileRateLimited(profile); + if (isDebug) console.warn('[ProfileScorer] Rate limit status:', rateLimitStatus); if (rateLimitStatus.limited) { // Severely penalize rate-limited profiles if (rateLimitStatus.type === 'weekly') { @@ -73,10 +80,14 @@ export function getBestAvailableProfile( } // Check if authenticated - if (!isProfileAuthenticated(profile)) { + const isAuth = isProfileAuthenticated(profile); + if (isDebug) console.warn('[ProfileScorer] isProfileAuthenticated:', isAuth, 'hasOAuthToken:', !!profile.oauthToken, 'hasConfigDir:', !!profile.configDir); + if (!isAuth) { score -= 500; // Severely penalize unauthenticated profiles + if (isDebug) console.warn('[ProfileScorer] Applied -500 penalty for no auth'); } + if (isDebug) console.warn('[ProfileScorer] Final score:', score); return { profile, score }; }); diff --git a/apps/frontend/src/main/claude-profile/profile-utils.ts b/apps/frontend/src/main/claude-profile/profile-utils.ts index 80a3c048..e6d8ceea 100644 --- a/apps/frontend/src/main/claude-profile/profile-utils.ts +++ b/apps/frontend/src/main/claude-profile/profile-utils.ts @@ -56,9 +56,16 @@ export async function createProfileDirectory(profileName: string): Promise = new Map(); // profileId -> timestamp + private static AUTH_FAILURE_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes cooldown + + // Debug flag for verbose logging + private readonly isDebug = process.env.DEBUG === 'true'; private constructor() { super(); @@ -40,7 +47,7 @@ export class UsageMonitor extends EventEmitter { const settings = profileManager.getAutoSwitchSettings(); if (!settings.enabled || !settings.proactiveSwapEnabled) { - console.warn('[UsageMonitor] Proactive monitoring disabled'); + console.warn('[UsageMonitor] Proactive monitoring disabled. Settings:', JSON.stringify(settings, null, 2)); return; } @@ -118,6 +125,15 @@ export class UsageMonitor extends EventEmitter { const weeklyExceeded = usage.weeklyPercent >= settings.weeklyThreshold; if (sessionExceeded || weeklyExceeded) { + if (this.isDebug) { + console.warn('[UsageMonitor:TRACE] Threshold exceeded', { + sessionPercent: usage.sessionPercent, + weekPercent: usage.weeklyPercent, + activeProfile: activeProfile.id, + hasToken: !!decryptedToken + }); + } + console.warn('[UsageMonitor] Threshold exceeded:', { sessionPercent: usage.sessionPercent, sessionThreshold: settings.sessionThreshold, @@ -130,8 +146,48 @@ export class UsageMonitor extends EventEmitter { activeProfile.id, sessionExceeded ? 'session' : 'weekly' ); + } else { + if (this.isDebug) { + console.warn('[UsageMonitor:TRACE] Usage OK', { + sessionPercent: usage.sessionPercent, + weekPercent: usage.weeklyPercent + }); + } } } catch (error) { + // Check for auth failure (401/403) from fetchUsageViaAPI + if ((error as any).statusCode === 401 || (error as any).statusCode === 403) { + const profileManager = getClaudeProfileManager(); + const activeProfile = profileManager.getActiveProfile(); + + if (activeProfile) { + // Mark this profile as auth-failed to prevent swap loops + this.authFailedProfiles.set(activeProfile.id, Date.now()); + console.warn('[UsageMonitor] Auth failure detected, marked profile as failed:', activeProfile.id); + + // Clean up expired entries from the failed profiles map + const now = Date.now(); + this.authFailedProfiles.forEach((timestamp, profileId) => { + if (now - timestamp > UsageMonitor.AUTH_FAILURE_COOLDOWN_MS) { + this.authFailedProfiles.delete(profileId); + } + }); + + try { + const excludeProfiles = Array.from(this.authFailedProfiles.keys()); + console.warn('[UsageMonitor] Attempting proactive swap (excluding failed profiles):', excludeProfiles); + await this.performProactiveSwap( + activeProfile.id, + 'session', // Treat auth failure as session limit for immediate swap + excludeProfiles + ); + return; + } catch (swapError) { + console.error('[UsageMonitor] Failed to perform auth-failure swap:', swapError); + } + } + } + console.error('[UsageMonitor] Check failed:', error); } finally { this.isChecking = false; @@ -190,6 +246,12 @@ export class UsageMonitor extends EventEmitter { if (!response.ok) { console.error('[UsageMonitor] API error:', response.status, response.statusText); + // Throw specific error for auth failures so we can trigger a swap + if (response.status === 401 || response.status === 403) { + const error = new Error(`API Auth Failure: ${response.status}`); + (error as any).statusCode = response.status; + throw error; + } return null; } @@ -220,7 +282,12 @@ export class UsageMonitor extends EventEmitter { ? 'weekly' : 'session' }; - } catch (error) { + } catch (error: any) { + // Re-throw auth failures to be handled by checkUsageAndSwap + if (error?.statusCode === 401 || error?.statusCode === 403) { + throw error; + } + console.error('[UsageMonitor] API fetch failed:', error); return null; } @@ -270,22 +337,34 @@ export class UsageMonitor extends EventEmitter { /** * Perform proactive profile swap + * @param currentProfileId - The profile to switch from + * @param limitType - The type of limit that triggered the swap + * @param additionalExclusions - Additional profile IDs to exclude (e.g., auth-failed profiles) */ private async performProactiveSwap( currentProfileId: string, - limitType: 'session' | 'weekly' + limitType: 'session' | 'weekly', + additionalExclusions: string[] = [] ): Promise { const profileManager = getClaudeProfileManager(); - const bestProfile = profileManager.getBestAvailableProfile(currentProfileId); - - if (!bestProfile) { - console.warn('[UsageMonitor] No alternative profile for proactive swap'); + + // Get all profiles to swap to, excluding current and any additional exclusions + const allProfiles = profileManager.getProfilesSortedByAvailability(); + const excludeIds = new Set([currentProfileId, ...additionalExclusions]); + const eligibleProfiles = allProfiles.filter(p => !excludeIds.has(p.id)); + + if (eligibleProfiles.length === 0) { + console.warn('[UsageMonitor] No alternative profile for proactive swap (excluded:', Array.from(excludeIds), ')'); this.emit('proactive-swap-failed', { - reason: 'no_alternative', - currentProfile: currentProfileId + reason: additionalExclusions.length > 0 ? 'all_alternatives_failed_auth' : 'no_alternative', + currentProfile: currentProfileId, + excludedProfiles: Array.from(excludeIds) }); return; } + + // Use the best available from eligible profiles + const bestProfile = eligibleProfiles[0]; console.warn('[UsageMonitor] Proactive swap:', { from: currentProfileId, diff --git a/apps/frontend/src/main/index.ts b/apps/frontend/src/main/index.ts index 8ee2eaf7..612e26a2 100644 --- a/apps/frontend/src/main/index.ts +++ b/apps/frontend/src/main/index.ts @@ -359,24 +359,34 @@ app.whenReady().then(() => { }); }); - // Pre-initialize Claude profile manager in background (non-blocking) - // This ensures profile data is loaded before user clicks "Start Claude Code" - setImmediate(() => { - initializeClaudeProfileManager().catch((error) => { - console.warn('[main] Failed to pre-initialize profile manager:', error); + // Initialize Claude profile manager, then start usage monitor + // We do this sequentially to ensure profile data (including auto-switch settings) + // is loaded BEFORE the usage monitor attempts to read settings. + // This prevents the "UsageMonitor disabled" error due to race condition. + initializeClaudeProfileManager() + .then(() => { + // Only start monitoring if window is still available (app not quitting) + if (mainWindow) { + // Setup event forwarding from usage monitor to renderer + initializeUsageMonitorForwarding(mainWindow); + + // Start the usage monitor + const usageMonitor = getUsageMonitor(); + usageMonitor.start(); + console.warn('[main] Usage monitor initialized and started (after profile load)'); + } + }) + .catch((error) => { + console.warn('[main] Failed to initialize profile manager:', error); + // Fallback: try starting usage monitor anyway (might use defaults) + if (mainWindow) { + initializeUsageMonitorForwarding(mainWindow); + const usageMonitor = getUsageMonitor(); + usageMonitor.start(); + } }); - }); - // Initialize usage monitoring after window is created if (mainWindow) { - // Setup event forwarding from usage monitor to renderer - initializeUsageMonitorForwarding(mainWindow); - - // Start the usage monitor - const usageMonitor = getUsageMonitor(); - usageMonitor.start(); - console.warn('[main] Usage monitor initialized and started'); - // Log debug mode status const isDebugMode = process.env.DEBUG === 'true'; if (isDebugMode) {