All files / src/agent/lifecycle retry-policy.ts

80% Statements 24/30
90.9% Branches 10/11
50% Functions 2/4
80% Lines 24/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121                  16x                                                         35x 35x     19x 5x         5x     14x     2x 2x       2x 2x       2x 2x       4x 4x           4x       2x 2x         2x 2x         5x 5x   5x 5x                                                
/**
 * RetryPolicy — Comprehensive retry logic with error-specific handling.
 *
 * Implements intelligent retry strategies for different types of agent failures.
 * Replaces scattered retry logic with unified, configurable policies.
 */
 
import { createModuleLogger } from '../../logger/index.js';
 
const log = createModuleLogger('retry-policy');
 
export type AgentErrorType =
  | 'auth_failure'     // 401 errors, invalid tokens
  | 'usage_limit'      // Rate limiting, quota exceeded
  | 'missing_signal'   // Process completed but no signal.json
  | 'process_crash'    // Process exited with error code
  | 'timeout'          // Process timed out
  | 'unknown';         // Unclassified errors
 
export interface AgentError {
  type: AgentErrorType;
  message: string;
  isTransient: boolean;         // Can this error be resolved by retrying?
  requiresAccountSwitch: boolean; // Should we switch to next account?
  shouldPersistToDB: boolean;   // Should this error be saved for debugging?
  exitCode?: number | null;
  signal?: string | null;
  originalError?: Error;
}
 
export interface RetryPolicy {
  readonly maxAttempts: number;
  readonly backoffMs: number[];
  shouldRetry(error: AgentError, attempt: number): boolean;
  getRetryDelay(attempt: number): number;
}
 
export class DefaultRetryPolicy implements RetryPolicy {
  readonly maxAttempts = 3;
  readonly backoffMs = [1000, 2000, 4000]; // 1s, 2s, 4s exponential backoff
 
  shouldRetry(error: AgentError, attempt: number): boolean {
    if (attempt >= this.maxAttempts) {
      log.debug({
        errorType: error.type,
        attempt,
        maxAttempts: this.maxAttempts
      }, 'max retry attempts reached');
      return false;
    }
 
    switch (error.type) {
      case 'auth_failure':
        // Retry auth failures - tokens might be refreshed
        log.debug({ attempt, errorType: error.type }, 'retrying auth failure');
        return true;
 
      case 'usage_limit':
        // Don't retry usage limits - need account switch
        log.debug({ attempt, errorType: error.type }, 'not retrying usage limit - requires account switch');
        return false;
 
      case 'missing_signal':
        // Retry missing signal - add instruction prompt
        log.debug({ attempt, errorType: error.type }, 'retrying missing signal with instruction');
        return true;
 
      case 'process_crash':
        // Only retry transient crashes
        const shouldRetryTransient = error.isTransient;
        log.debug({
          attempt,
          errorType: error.type,
          isTransient: error.isTransient,
          shouldRetry: shouldRetryTransient
        }, 'process crash retry decision');
        return shouldRetryTransient;
 
      case 'timeout':
        // Retry timeouts up to max attempts
        log.debug({ attempt, errorType: error.type }, 'retrying timeout');
        return true;
 
      case 'unknown':
      default:
        // Don't retry unknown errors by default
        log.debug({ attempt, errorType: error.type }, 'not retrying unknown error');
        return false;
    }
  }
 
  getRetryDelay(attempt: number): number {
    const index = Math.min(attempt - 1, this.backoffMs.length - 1);
    const delay = this.backoffMs[index] || this.backoffMs[this.backoffMs.length - 1];
 
    log.debug({ attempt, delay }, 'retry delay calculated');
    return delay;
  }
}
 
/**
 * AgentExhaustedError - Special error indicating account needs switching.
 * When thrown, caller should attempt account failover rather than retry.
 */
export class AgentExhaustedError extends Error {
  constructor(message: string, public readonly originalError?: AgentError) {
    super(message);
    this.name = 'AgentExhaustedError';
  }
}
 
/**
 * AgentFailureError - Terminal failure that cannot be retried.
 * Indicates all retry attempts have been exhausted or error is non-retriable.
 */
export class AgentFailureError extends Error {
  constructor(message: string, public readonly originalError?: AgentError) {
    super(message);
    this.name = 'AgentFailureError';
  }
}