-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat: Limit auto-claude window usage based on budget #1907
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -927,32 +927,55 @@ export class UsageMonitor extends EventEmitter { | |
| const profileManager = getClaudeProfileManager(); | ||
| const settings = profileManager.getAutoSwitchSettings(); | ||
|
|
||
| if (!settings.enabled || !settings.proactiveSwapEnabled) { | ||
| this.debugLog('[UsageMonitor:TRACE] Proactive swap disabled, skipping threshold check'); | ||
| // Budget policy (budgetCapPercent / noExtraUsage) runs independently of the | ||
| // auto-switch master toggle — a single-account user can still enforce a hard stop. | ||
| const hasBudgetPolicy = settings.budgetCapPercent !== undefined || settings.noExtraUsage; | ||
| const isProactiveEnabled = settings.enabled && settings.proactiveSwapEnabled; | ||
|
|
||
| if (!hasBudgetPolicy && !isProactiveEnabled) { | ||
| this.debugLog('[UsageMonitor:TRACE] Proactive swap and budget policy both disabled, skipping threshold check'); | ||
| return; | ||
| } | ||
|
|
||
| const thresholds = this.checkThresholdsExceeded(usage, settings); | ||
|
|
||
| if (thresholds.anyExceeded) { | ||
| const limitLabel = thresholds.sessionExceeded ? 'session' : 'weekly'; | ||
| const limitPercent = thresholds.sessionExceeded ? usage.sessionPercent : usage.weeklyPercent; | ||
| const capLabel = settings.noExtraUsage | ||
| ? 'noExtraUsage (100%)' | ||
| : settings.budgetCapPercent !== undefined | ||
| ? `budgetCap (${settings.budgetCapPercent}%)` | ||
| : `threshold (${thresholds.sessionExceeded ? (settings.sessionThreshold ?? 95) : (settings.weeklyThreshold ?? 99)}%)`; | ||
| console.warn( | ||
| `[UsageMonitor] Budget limit reached: ${limitLabel} usage at ${limitPercent.toFixed(1)}% exceeds ${capLabel} for profile "${profileId}". ${hasBudgetPolicy ? 'Will stop agents if no alternative account.' : 'Will attempt account switch.'}` | ||
| ); | ||
|
|
||
| this.debugLog('[UsageMonitor:TRACE] Threshold exceeded', { | ||
| sessionPercent: usage.sessionPercent, | ||
| weekPercent: usage.weeklyPercent, | ||
| activeProfile: profileId, | ||
| hasCredential: !!credential | ||
| hasCredential: !!credential, | ||
| hasBudgetPolicy, | ||
| isProactiveEnabled | ||
| }); | ||
|
|
||
| this.debugLog('[UsageMonitor] Threshold exceeded:', { | ||
| sessionPercent: usage.sessionPercent, | ||
| sessionThreshold: settings.sessionThreshold ?? 95, | ||
| weeklyPercent: usage.weeklyPercent, | ||
| weeklyThreshold: settings.weeklyThreshold ?? 99 | ||
| weeklyThreshold: settings.weeklyThreshold ?? 99, | ||
| budgetCapPercent: settings.budgetCapPercent, | ||
| noExtraUsage: settings.noExtraUsage | ||
| }); | ||
|
|
||
| // Attempt proactive swap | ||
| // Attempt proactive swap; pass stopIfExhausted=true when a budget policy is active | ||
| // so that running agents are killed if no alternative account is available. | ||
| await this.performProactiveSwap( | ||
| profileId, | ||
| thresholds.sessionExceeded ? 'session' : 'weekly' | ||
| thresholds.sessionExceeded ? 'session' : 'weekly', | ||
| [], | ||
| hasBudgetPolicy | ||
| ); | ||
| } else { | ||
| this.debugLog('[UsageMonitor:TRACE] Usage OK', { | ||
|
|
@@ -1081,10 +1104,24 @@ export class UsageMonitor extends EventEmitter { | |
| */ | ||
| private checkThresholdsExceeded( | ||
| usage: ClaudeUsageSnapshot, | ||
| settings: { sessionThreshold?: number; weeklyThreshold?: number } | ||
| settings: { sessionThreshold?: number; weeklyThreshold?: number; budgetCapPercent?: number; noExtraUsage?: boolean } | ||
| ): { sessionExceeded: boolean; weeklyExceeded: boolean; anyExceeded: boolean } { | ||
| const sessionExceeded = usage.sessionPercent >= (settings.sessionThreshold ?? 95); | ||
| const weeklyExceeded = usage.weeklyPercent >= (settings.weeklyThreshold ?? 99); | ||
| const baseSession = settings.sessionThreshold ?? 95; | ||
| const baseWeekly = settings.weeklyThreshold ?? 99; | ||
|
|
||
| // Budget cap acts as a ceiling on both thresholds | ||
| const effectiveSession = settings.budgetCapPercent !== undefined | ||
| ? Math.min(baseSession, settings.budgetCapPercent) | ||
| : baseSession; | ||
| const effectiveWeekly = settings.budgetCapPercent !== undefined | ||
| ? Math.min(baseWeekly, settings.budgetCapPercent) | ||
| : baseWeekly; | ||
|
Comment on lines
+1113
to
+1118
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The UI allows setting Suggested FixPrevent the Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. |
||
|
|
||
| // noExtraUsage: also flag when hitting 100% | ||
| const sessionExceeded = usage.sessionPercent >= effectiveSession || | ||
| (!!settings.noExtraUsage && usage.sessionPercent >= 100); | ||
| const weeklyExceeded = usage.weeklyPercent >= effectiveWeekly || | ||
| (!!settings.noExtraUsage && usage.weeklyPercent >= 100); | ||
|
Comment on lines
1105
to
+1124
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Budget-only mode is still enforcing proactive thresholds. Line 1109 and Line 1110 always seed 95/99 (or configured) thresholds, so Suggested fix- private checkThresholdsExceeded(
+ private checkThresholdsExceeded(
usage: ClaudeUsageSnapshot,
- settings: { sessionThreshold?: number; weeklyThreshold?: number; budgetCapPercent?: number; noExtraUsage?: boolean }
+ settings: { sessionThreshold?: number; weeklyThreshold?: number; budgetCapPercent?: number; noExtraUsage?: boolean },
+ useProactiveThresholds: boolean
): { sessionExceeded: boolean; weeklyExceeded: boolean; anyExceeded: boolean } {
- const baseSession = settings.sessionThreshold ?? 95;
- const baseWeekly = settings.weeklyThreshold ?? 99;
+ const baseSession = useProactiveThresholds ? (settings.sessionThreshold ?? 95) : 100;
+ const baseWeekly = useProactiveThresholds ? (settings.weeklyThreshold ?? 99) : 100;- const thresholds = this.checkThresholdsExceeded(usage, settings);
+ const thresholds = this.checkThresholdsExceeded(usage, settings, isProactiveEnabled);🤖 Prompt for AI Agents |
||
|
|
||
| return { | ||
| sessionExceeded, | ||
|
|
@@ -1867,7 +1904,8 @@ export class UsageMonitor extends EventEmitter { | |
| private async performProactiveSwap( | ||
| currentProfileId: string, | ||
| limitType: 'session' | 'weekly', | ||
| additionalExclusions: string[] = [] | ||
| additionalExclusions: string[] = [], | ||
| stopIfExhausted: boolean = false | ||
| ): Promise<void> { | ||
| const profileManager = getClaudeProfileManager(); | ||
| const excludeIds = new Set([currentProfileId, ...additionalExclusions]); | ||
|
|
@@ -1924,11 +1962,20 @@ export class UsageMonitor extends EventEmitter { | |
|
|
||
| if (unifiedAccounts.length === 0) { | ||
| this.debugLog('[UsageMonitor] No alternative profile for proactive swap (excluded:', Array.from(excludeIds)); | ||
| this.emit('proactive-swap-failed', { | ||
| reason: additionalExclusions.length > 0 ? 'all_alternatives_failed_auth' : 'no_alternative', | ||
| currentProfile: currentProfileId, | ||
| excludedProfiles: Array.from(excludeIds) | ||
| }); | ||
| if (stopIfExhausted) { | ||
| // Budget policy: no account to switch to — emit budget-exhausted so running agents are stopped. | ||
| this.emit('budget-exhausted', { | ||
| reason: 'budget_cap_no_alternative', | ||
| currentProfile: currentProfileId, | ||
| limitType | ||
| }); | ||
| } else { | ||
| this.emit('proactive-swap-failed', { | ||
| reason: additionalExclusions.length > 0 ? 'all_alternatives_failed_auth' : 'no_alternative', | ||
| currentProfile: currentProfileId, | ||
| excludedProfiles: Array.from(excludeIds) | ||
| }); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To improve maintainability and adhere to the DRY (Don't Repeat Yourself) principle, consider extracting the logic for calculating effective thresholds into a helper function. This logic is duplicated in several places in this file (
calculateFallbackScore,scoreUnifiedAccount,shouldProactivelySwitch) and also inapps/frontend/src/main/claude-profile/usage-monitor.tsin thecheckThresholdsExceededfunction.A shared utility function could look like this: