prodigy 0.4.4

Turn ad-hoc Claude sessions into reproducible development pipelines with parallel AI agents
Documentation
## Conditional Retry with Error Matchers

By default, Prodigy retries all errors when `retry_on` is empty. Use the `retry_on` field to retry only specific error types, allowing fine-grained control over which failures should trigger retries.

**Source**: `src/cook/retry_v2.rs:100-151` (ErrorMatcher enum definition)

**Case Sensitivity Behavior**:
- **Built-in matchers** (Network, Timeout, ServerError, RateLimit): **Case-insensitive** - error messages are normalized to lowercase before matching
- **Pattern matcher**: **Case-sensitive by default** - matches against original error message case (src/cook/retry_v2.rs:142-148)
  - Use regex flag `(?i)` for case-insensitive pattern matching
  - Example: `pattern: '(?i)database locked'` matches "Database Locked", "DATABASE LOCKED", etc.

### Available Error Matchers

The `ErrorMatcher` enum provides five built-in matchers for common error categories:

#### 1. Network Errors

Matches network connectivity issues:

```yaml
retry_config:
  attempts: 5
  retry_on:
    - network
```

**Matches** (case-insensitive):
- "network"
- "connection"
- "refused"
- "unreachable"

**Source**: `src/cook/retry_v2.rs:128-132`

**Use Case**: Retrying HTTP requests, database connections, or API calls that fail due to network issues.

#### 2. Timeout Errors

Matches timeout-related failures:

```yaml
retry_config:
  attempts: 3
  retry_on:
    - timeout
```

**Matches** (case-insensitive):
- "timeout"
- "timed out"

**Source**: `src/cook/retry_v2.rs:133-137`

**Use Case**: Retrying slow external services or operations with strict time limits.

#### 3. Server Errors

Matches HTTP 5xx server errors:

```yaml
retry_config:
  attempts: 4
  retry_on:
    - server_error
```

**Matches** (case-insensitive):
- "500"
- "502"
- "503"
- "504"
- "server error"

**Source**: `src/cook/retry_v2.rs:138-142`

**Use Case**: Retrying API requests during transient server failures or deployments.

#### 4. Rate Limit Errors

Matches rate limiting responses:

```yaml
retry_config:
  attempts: 10
  initial_delay: "60s"
  retry_on:
    - rate_limit
```

**Matches** (case-insensitive):
- "rate limit"
- "429"
- "too many requests"

**Source**: `src/cook/retry_v2.rs:143-147`

**Use Case**: Retrying API calls with exponential backoff when hitting rate limits.

#### 5. Custom Pattern Matching

Match specific error messages using regex patterns:

```yaml
retry_config:
  attempts: 3
  retry_on:
    - pattern: "(?i)database locked"    # Case-insensitive: matches any case
    - pattern: "SQLITE_BUSY"            # Case-sensitive: exact match only
    - pattern: "(?i)temporary failure"  # Case-insensitive
```

**Pattern Syntax**:
- Regex patterns matched against original error message (case-sensitive by default)
- Use `(?i)` flag at start of pattern for case-insensitive matching
- Invalid regex patterns return false (no match)

**Source**: `src/cook/retry_v2.rs:142-148` (Pattern variant implementation)

**Use Case**: Matching application-specific error messages or database-specific errors.

**Case Sensitivity Examples**:
- `pattern: "SQLITE_BUSY"` - Only matches "SQLITE_BUSY" (not "sqlite_busy")
- `pattern: "(?i)SQLITE_BUSY"` - Matches "SQLITE_BUSY", "sqlite_busy", "Sqlite_Busy", etc.
- `pattern: "(?i)database.*locked"` - Case-insensitive regex with wildcards

### Combining Multiple Matchers

You can specify multiple error matchers to retry on any of them:

```yaml
retry_config:
  attempts: 5
  backoff: exponential
  initial_delay: "2s"
  retry_on:
    - network
    - timeout
    - server_error
```

This configuration retries if the error matches **any** of:
- Network errors
- Timeout errors
- Server errors (5xx)

**Behavior**: Matchers are evaluated with OR logic - if any matcher matches, the error is retryable.

### Empty retry_on (Retry All Errors)

When `retry_on` is empty or omitted, **all errors trigger retry**:

```yaml
retry_config:
  attempts: 3
  # retry_on is empty - retries all errors
```

**Source**: `src/cook/retry_v2.rs:42-43` (retry_on field with default `Vec::new()`)

This is equivalent to having no error filtering - every failure triggers the retry logic.

### Selective Retry Example

Only retry transient network and timeout issues, but fail immediately on other errors:

```yaml
commands:
  - shell: "curl -f https://api.example.com/data"
    retry_config:
      attempts: 5
      backoff: exponential
      initial_delay: "1s"
      max_delay: "30s"
      retry_on:
        - network
        - timeout
```

**Behavior**:
- If curl fails with "connection refused" → Retry
- If curl fails with "timeout" → Retry
- If curl fails with "404 Not Found" → **Fail immediately** (no retry)
- If curl fails with "401 Unauthorized" → **Fail immediately** (no retry)

### Regex Pattern Syntax and Case Sensitivity

Understanding case sensitivity is critical when using Pattern matchers:

| Matcher Type | Case Sensitivity | Normalization |
|-------------|------------------|---------------|
| Network | Case-insensitive | Error converted to lowercase |
| Timeout | Case-insensitive | Error converted to lowercase |
| ServerError | Case-insensitive | Error converted to lowercase |
| RateLimit | Case-insensitive | Error converted to lowercase |
| Pattern | **Case-sensitive by default** | No normalization (original case) |

**Making Pattern Matching Case-Insensitive**:

Use the `(?i)` flag at the start of your regex pattern:

```yaml
retry_on:
  # Case-insensitive patterns (recommended)
  - pattern: "(?i)connection refused"  # Matches any case variation
  - pattern: "(?i)database.*locked"    # Case-insensitive with wildcards

  # Case-sensitive patterns (use with caution)
  - pattern: "SQLITE_BUSY"             # Only matches exact case
  - pattern: "ERROR: Authentication"   # Must match exact case
```

**Invalid Regex Handling**:

If a pattern contains invalid regex syntax, it returns `false` (no match):

```yaml
retry_on:
  - pattern: "[invalid(regex"  # Invalid syntax → returns false → no retry
```

**Source**: src/cook/retry_v2.rs:142-148

### Advanced Pattern Matching

Use regex patterns for precise error matching:

```yaml
retry_config:
  attempts: 3
  retry_on:
    # Case-insensitive patterns (recommended for flexible matching)
    - pattern: "(?i)SQLite.*database is locked"
    - pattern: "(?i)deadlock detected"

    # Case-sensitive pattern (exact match required)
    - pattern: "SQLITE_BUSY"
```

**Pattern Matching Logic** (src/cook/retry_v2.rs:116-150):
1. Each matcher's `matches()` method is called with the error message
2. Built-in matchers normalize error to lowercase before checking
3. Pattern matcher applies regex to **original case** of error message
4. Invalid regex patterns return false (no match, no retry)
5. If any matcher returns true, error is retryable

### Implementation Details

The matching logic is implemented in `ErrorMatcher::matches()`:

```rust
// Simplified implementation (src/cook/retry_v2.rs:116-150)
impl ErrorMatcher {
    pub fn matches(&self, error_msg: &str) -> bool {
        let error_lower = error_msg.to_lowercase();
        match self {
            Self::Network => {
                // Case-insensitive matching via lowercase normalization
                error_lower.contains("network")
                    || error_lower.contains("connection")
                    || error_lower.contains("refused")
                    || error_lower.contains("unreachable")
            }
            Self::Timeout => {
                error_lower.contains("timeout") || error_lower.contains("timed out")
            }
            Self::ServerError => {
                error_lower.contains("500")
                    || error_lower.contains("502")
                    || error_lower.contains("503")
                    || error_lower.contains("504")
                    || error_lower.contains("server error")
            }
            Self::RateLimit => {
                error_lower.contains("rate limit")
                    || error_lower.contains("429")
                    || error_lower.contains("too many requests")
            }
            Self::Pattern(pattern) => {
                // Case-sensitive by default - matches against original error_msg
                // Use (?i) flag in pattern for case-insensitive matching
                if let Ok(re) = regex::Regex::new(pattern) {
                    re.is_match(error_msg)  // Uses original case, not error_lower
                } else {
                    false  // Invalid regex = no match
                }
            }
        }
    }
}
```

### Testing Error Matchers

The retry_v2 module includes tests for built-in matchers (src/cook/retry_v2.rs:463-502):

```rust
#[test]
fn test_error_matcher_network() {
    let matcher = ErrorMatcher::Network;
    assert!(matcher.matches("Connection refused"));
    assert!(matcher.matches("Network unreachable"));
    assert!(matcher.matches("connection timeout"));  // Case-insensitive
    assert!(!matcher.matches("Syntax error"));
}

#[test]
fn test_error_matcher_timeout() {
    let matcher = ErrorMatcher::Timeout;
    assert!(matcher.matches("Operation timeout"));
    assert!(matcher.matches("Request timed out"));
    assert!(!matcher.matches("Network error"));
}

#[test]
fn test_error_matcher_rate_limit() {
    let matcher = ErrorMatcher::RateLimit;
    assert!(matcher.matches("Rate limit exceeded"));
    assert!(matcher.matches("Error 429"));
    assert!(matcher.matches("Too many requests"));
    assert!(!matcher.matches("Server error"));
}
```

**Note**: Pattern matcher tests are not yet implemented in the test suite. The above tests cover Network, Timeout, and RateLimit matchers only.

### See Also

- [Basic Retry Configuration]./basic-retry-configuration.md - Core retry configuration
- [Failure Actions]./failure-actions.md - What happens when all retries are exhausted
- [Best Practices]./best-practices.md - When to use selective retry
- [Complete Examples]./complete-examples.md - Full workflow examples with error matchers