# 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';
      }
    }