@calmhive/calmhive-cli
Version:
🐝 Intelligent Claude CLI wrapper with rule injection, AFk background processing, voice interface, and adaptive retry
208 lines (190 loc) • 6.86 kB
JavaScript
/**
* Adaptive retry mechanism for handling Claude usage limits with exponential backoff
*
* Implements sophisticated retry logic with exponential backoff to handle transient failures,
* particularly useful for API rate limiting and usage limit scenarios.
*
* @example
* const retry = new AdaptiveRetry();
*
* // Simple usage
* retry.recordFailure(); // Usage limit hit
* const delay = retry.getNextDelay(); // Returns increasing delay
*
* // With retry wrapper
* const result = await retry.waitWithRetry(async () => {
* return await someAPICall();
* }, 3);
*/
class AdaptiveRetry {
/**
* Creates a new AdaptiveRetry instance with default configuration
*
* @param {Object} options - Configuration options
* @param {number} options.baseDelay - Base delay in milliseconds (default: 30000)
* @param {number} options.maxDelay - Maximum delay in milliseconds (default: 3600000)
* @param {number} options.backoffMultiplier - Exponential backoff multiplier (default: 2)
*/
constructor(options = {}) {
this.baseDelay = options.baseDelay || 30000; // 30 seconds base
this.maxDelay = options.maxDelay || 3600000; // 1 hour max
this.backoffMultiplier = options.backoffMultiplier || 2;
this.consecutiveFailures = 0;
}
/**
* Calculates the next delay using exponential backoff algorithm
*
* The delay progression follows: 30s, 1m, 2m, 4m, 8m, 16m, 32m, 60m (max)
* Each failure doubles the delay until maxDelay is reached.
*
* @returns {number} Delay in milliseconds for the next retry
*
* @example
* const retry = new AdaptiveRetry();
* retry.recordFailure(); // First failure
* console.log(retry.getNextDelay()); // 30000 (30 seconds)
* retry.recordFailure(); // Second failure
* console.log(retry.getNextDelay()); // 60000 (1 minute)
*/
getNextDelay() {
// Calculate exponential backoff: 30s, 1m, 2m, 4m, 8m, 16m, 32m, 60m (max)
const delay = Math.min(
this.baseDelay * Math.pow(this.backoffMultiplier, this.consecutiveFailures),
this.maxDelay
);
return delay;
}
/**
* Records a successful operation and resets the failure count
*
* This should be called when an operation succeeds to reset the exponential
* backoff progression. Future failures will start from the base delay again.
*
* @example
* const retry = new AdaptiveRetry();
* retry.recordFailure(); // Failure count: 1
* retry.recordSuccess(); // Failure count: 0 (reset)
*/
recordSuccess() {
// Reset on successful iteration
this.consecutiveFailures = 0;
}
/**
* Records a failed operation and increments the failure count
*
* This increases the consecutive failure count, which affects the delay
* calculation for subsequent retry attempts. Each failure increases the
* exponential backoff delay.
*
* @example
* const retry = new AdaptiveRetry();
* retry.recordFailure(); // Failure count: 1, next delay: 30s
* retry.recordFailure(); // Failure count: 2, next delay: 60s
*/
recordFailure() {
this.consecutiveFailures++;
}
/**
* Executes a function with automatic retry logic and exponential backoff
*
* This method wraps any async function with intelligent retry behavior.
* It automatically handles failures with exponential backoff delays and
* tracks success/failure state for optimal retry timing.
*
* @param {Function} spawner - Async function to execute with retry logic
* @param {number} maxRetries - Maximum number of retry attempts (default: 5)
* @returns {Promise<*>} Result from the spawner function if successful
* @throws {Error} If all retry attempts are exhausted
*
* @example
* const retry = new AdaptiveRetry();
*
* // Retry an API call with automatic backoff
* const result = await retry.waitWithRetry(async () => {
* const response = await fetch('/api/data');
* if (!response.ok) return false; // Trigger retry
* return await response.json();
* }, 3);
*
* // Retry with Claude AFk session
* const session = await retry.waitWithRetry(async () => {
* return await processManager.runSingleIteration(sessionData);
* });
*/
async waitWithRetry(spawner, maxRetries = 5) {
if (typeof spawner !== 'function') {
throw new Error('Spawner must be a function');
}
if (!Number.isInteger(maxRetries) || maxRetries < 1) {
throw new Error('Max retries must be a positive integer');
}
let attempts = 0;
while (attempts < maxRetries) {
try {
const result = await spawner();
// If successful, reset failure count
if (result) {
this.recordSuccess();
return result;
}
// If spawn returned false (likely usage limit)
attempts++;
this.recordFailure();
const delay = this.getNextDelay();
// console.log(`⏳ Usage limit likely hit. Waiting ${delay/1000}s before retry (attempt ${attempts}/${maxRetries})...`);
await new Promise(resolve => setTimeout(resolve, delay));
} catch (error) {
console.error('❌ Error in retry loop:', error);
attempts++;
if (attempts >= maxRetries) {
throw error;
}
const delay = this.getNextDelay();
// console.log(`⏳ Error occurred. Waiting ${delay/1000}s before retry...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error(`Failed after ${maxRetries} attempts`);
}
/**
* Gets the current state of the retry mechanism
*
* @returns {Object} Current retry state
* @returns {number} returns.consecutiveFailures - Number of consecutive failures
* @returns {number} returns.nextDelay - Next delay in milliseconds
* @returns {number} returns.baseDelay - Base delay configuration
* @returns {number} returns.maxDelay - Maximum delay configuration
*
* @example
* const retry = new AdaptiveRetry();
* retry.recordFailure();
* const state = retry.getState();
* console.log(`Next retry in ${state.nextDelay / 1000} seconds`);
*/
getState() {
return {
consecutiveFailures: this.consecutiveFailures,
nextDelay: this.getNextDelay(),
baseDelay: this.baseDelay,
maxDelay: this.maxDelay,
backoffMultiplier: this.backoffMultiplier
};
}
/**
* Resets the retry state to initial conditions
*
* Clears all failure history and resets delays to base level.
* Useful when starting a new operation context.
*
* @example
* const retry = new AdaptiveRetry();
* retry.recordFailure();
* retry.recordFailure();
* retry.reset(); // Back to clean state
* console.log(retry.getNextDelay()); // Back to base delay
*/
reset() {
this.consecutiveFailures = 0;
}
}
module.exports = AdaptiveRetry;