-
Notifications
You must be signed in to change notification settings - Fork 528
feat: Add graceful handling for Claude Code usage limits #352
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
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 |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ import { | |
| buildPromptWithImages, | ||
| isAbortError, | ||
| classifyError, | ||
| isAmbiguousCLIExit, | ||
| loadContextFiles, | ||
| } from '@automaker/utils'; | ||
| import { resolveModelString, DEFAULT_MODELS } from '@automaker/model-resolver'; | ||
|
|
@@ -33,6 +34,7 @@ import { | |
| import { FeatureLoader } from './feature-loader.js'; | ||
| import type { SettingsService } from './settings-service.js'; | ||
| import { pipelineService, PipelineService } from './pipeline-service.js'; | ||
| import { ClaudeUsageService } from './claude-usage-service.js'; | ||
| import { | ||
| getAutoLoadClaudeMdSetting, | ||
| getEnableSandboxModeSetting, | ||
|
|
@@ -238,14 +240,19 @@ export class AutoModeService { | |
| return true; | ||
| } | ||
|
|
||
| // Immediately pause for ambiguous CLI exits (often quota issues in disguise) | ||
| if (isAmbiguousCLIExit(new Error(errorInfo.message))) { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Signal that we should pause due to repeated failures or quota exhaustion. | ||
| * This will pause the auto loop to prevent repeated failures. | ||
| */ | ||
| private signalShouldPause(errorInfo: { type: string; message: string }): void { | ||
| private async signalShouldPause(errorInfo: { type: string; message: string }): Promise<void> { | ||
| if (this.pausedDueToFailures) { | ||
| return; // Already paused | ||
| } | ||
|
|
@@ -256,6 +263,59 @@ export class AutoModeService { | |
| `[AutoMode] Pausing auto loop after ${failureCount} consecutive failures. Last error: ${errorInfo.type}` | ||
| ); | ||
|
|
||
| // Fetch current usage data to provide suggested resume time | ||
| let suggestedResumeAt: string | undefined; | ||
| let lastKnownUsage: Record<string, unknown> | undefined; | ||
|
|
||
| try { | ||
| const usageService = new ClaudeUsageService(); | ||
| if (await usageService.isAvailable()) { | ||
| const usage = await usageService.fetchUsageData(); | ||
| if (usage && !('error' in usage)) { | ||
| lastKnownUsage = usage as Record<string, unknown>; | ||
| // Calculate suggested resume time based on reset times | ||
| const sessionReset = (usage as { sessionResetTime?: string }).sessionResetTime; | ||
| const sessionResetText = (usage as { sessionResetText?: string }).sessionResetText; | ||
| const weeklyReset = (usage as { weeklyResetTime?: string }).weeklyResetTime; | ||
| const weeklyResetText = (usage as { weeklyResetText?: string }).weeklyResetText; | ||
|
|
||
| if (sessionReset) { | ||
| // Check if it's already an ISO date string | ||
| const resetDate = new Date(sessionReset); | ||
| if (!isNaN(resetDate.getTime())) { | ||
| suggestedResumeAt = sessionReset; | ||
| } else if (sessionResetText) { | ||
| // Parse "Resets in Xh Ym" format from text | ||
| const match = sessionResetText.match(/(\d+)h\s*(\d+)?m?/); | ||
| if (match) { | ||
| const hours = parseInt(match[1], 10); | ||
| const minutes = parseInt(match[2] || '0', 10); | ||
| const resetMs = (hours * 60 + minutes) * 60 * 1000; | ||
| suggestedResumeAt = new Date(Date.now() + resetMs).toISOString(); | ||
| } | ||
| } | ||
| } else if (weeklyReset) { | ||
| // Check if it's already an ISO date string | ||
| const resetDate = new Date(weeklyReset); | ||
| if (!isNaN(resetDate.getTime())) { | ||
| suggestedResumeAt = weeklyReset; | ||
| } else if (weeklyResetText) { | ||
| // Parse text format | ||
| const match = weeklyResetText.match(/(\d+)h\s*(\d+)?m?/); | ||
| if (match) { | ||
| const hours = parseInt(match[1], 10); | ||
| const minutes = parseInt(match[2] || '0', 10); | ||
| const resetMs = (hours * 60 + minutes) * 60 * 1000; | ||
| suggestedResumeAt = new Date(Date.now() + resetMs).toISOString(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+276
to
+313
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. The logic for calculating For example, you could create a helper function: private parseResumeTime(resetTime?: string, resetText?: string): string | undefined {
// ... parsing logic ...
}And then use it in both places. |
||
| } | ||
| } catch (e) { | ||
| console.warn('[AutoMode] Failed to fetch usage data for pause info:', e); | ||
| } | ||
|
|
||
| // Emit event to notify UI | ||
| this.emitAutoModeEvent('auto_mode_paused_failures', { | ||
| message: | ||
|
|
@@ -266,6 +326,8 @@ export class AutoModeService { | |
| originalError: errorInfo.message, | ||
| failureCount, | ||
| projectPath: this.config?.projectPath, | ||
| suggestedResumeAt, | ||
| lastKnownUsage, | ||
| }); | ||
|
|
||
| // Stop the auto loop | ||
|
|
@@ -298,6 +360,73 @@ export class AutoModeService { | |
| // Reset failure tracking when user manually starts auto mode | ||
| this.resetFailureTracking(); | ||
|
|
||
| // Proactive usage limit check before starting the loop | ||
| try { | ||
| const usageService = new ClaudeUsageService(); | ||
| if (await usageService.isAvailable()) { | ||
| const usage = await usageService.fetchUsageData(); | ||
| if (usage && !('error' in usage)) { | ||
| const isAtSessionLimit = usage.sessionPercentage >= 100; | ||
| const isAtWeeklyLimit = usage.weeklyPercentage >= 100; | ||
|
|
||
| if (isAtSessionLimit || isAtWeeklyLimit) { | ||
| console.log('[AutoMode] Usage limit detected at startup, not starting loop'); | ||
|
|
||
| // Use the reset time directly if it's an ISO string, otherwise parse the text | ||
| let suggestedResumeAt: string | undefined; | ||
| if (isAtSessionLimit && usage.sessionResetTime) { | ||
| // Check if it's already an ISO date string | ||
| const resetDate = new Date(usage.sessionResetTime); | ||
| if (!isNaN(resetDate.getTime())) { | ||
| suggestedResumeAt = usage.sessionResetTime; | ||
| } else { | ||
| // Try to parse "Resets in Xh Ym" format from sessionResetText | ||
| const match = (usage.sessionResetText || '').match(/(\d+)h\s*(\d+)?m?/); | ||
| if (match) { | ||
| const hours = parseInt(match[1], 10); | ||
| const minutes = parseInt(match[2] || '0', 10); | ||
| suggestedResumeAt = new Date( | ||
| Date.now() + (hours * 60 + minutes) * 60 * 1000 | ||
| ).toISOString(); | ||
| } | ||
| } | ||
| } else if (isAtWeeklyLimit && usage.weeklyResetTime) { | ||
| // Check if it's already an ISO date string | ||
| const resetDate = new Date(usage.weeklyResetTime); | ||
| if (!isNaN(resetDate.getTime())) { | ||
| suggestedResumeAt = usage.weeklyResetTime; | ||
| } else { | ||
| // Try to parse text format | ||
| const match = (usage.weeklyResetText || '').match(/(\d+)h\s*(\d+)?m?/); | ||
| if (match) { | ||
| const hours = parseInt(match[1], 10); | ||
| const minutes = parseInt(match[2] || '0', 10); | ||
| suggestedResumeAt = new Date( | ||
| Date.now() + (hours * 60 + minutes) * 60 * 1000 | ||
| ).toISOString(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Emit pause event instead of starting | ||
| this.emitAutoModeEvent('auto_mode_paused_failures', { | ||
| message: | ||
| 'Auto Mode cannot start: Usage limit reached. Please wait for your quota to reset.', | ||
| errorType: 'quota_exhausted', | ||
| projectPath, | ||
| suggestedResumeAt, | ||
| lastKnownUsage: usage as Record<string, unknown>, | ||
| }); | ||
|
|
||
| return; // Don't start the loop | ||
| } | ||
| } | ||
| } | ||
| } catch (error) { | ||
| console.warn('[AutoMode] Failed to check usage limits at startup:', error); | ||
| // Continue starting the loop anyway | ||
| } | ||
|
|
||
| this.autoLoopRunning = true; | ||
| this.autoLoopAbortController = new AbortController(); | ||
| this.config = { | ||
|
|
||
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.
🛠️ Refactor suggestion | 🟠 Major
Duplicated reset time parsing logic violates DRY principle.
The same logic for parsing session/weekly reset times from usage data appears twice: once in
signalShouldPause(lines 282-312) and once instartAutoLoop(lines 384-408). This duplication makes maintenance harder and increases bug risk.🔎 Proposed fix: Extract helper method
Then use
this.getSuggestedResumeTime(usage)in both locations.Also applies to: 376-408
🤖 Prompt for AI Agents