# Shared Error Handling Patterns

This document provides standardized error handling patterns that all agents MUST use to ensure consistent, reliable operation.

## Core Principles

1. **Fail Fast** - Detect errors early and fail immediately
2. **Be Specific** - Use appropriate exception types
3. **Log Context** - Include relevant information for debugging
4. **Clean Up** - Always clean up resources on failure
5. **Retry Intelligently** - Use exponential backoff for transient errors

## Standard Error Types

```python
# Network/API errors that might be transient
class NetworkError(Exception):
    """Network or API communication failure."""
    pass

# Git operation failures
class GitError(Exception):
    """Git command or operation failure."""
    pass

class GitConflictError(GitError):
    """Git merge or rebase conflict."""
    pass

# Forgejo API specific errors
class ForgejoError(Exception):
    """Forgejo API operation failure."""
    pass

# Agent coordination errors
class CoordinationError(Exception):
    """Agent coordination or claiming failure."""
    pass

class AlreadyClaimedError(CoordinationError):
    """Work item is already claimed by another agent."""
    pass

# Configuration/setup errors
class ConfigurationError(Exception):
    """Missing or invalid configuration."""
    pass

# Operation failures after retries
class OperationFailedError(Exception):
    """Operation failed after all retry attempts."""
    def __init__(self, operation_name, last_error):
        self.operation_name = operation_name
        self.last_error = last_error
        super().__init__(f"{operation_name} failed: {last_error}")
```

## Standard Recovery Pattern

```python
def safe_operation_with_recovery(operation_name, operation_func, 
                                recovery_func=None, max_retries=3):
    """
    Standard pattern for operations that might fail.
    
    Args:
        operation_name: Human-readable operation description
        operation_func: Function to execute (callable)
        recovery_func: Optional recovery function(error_type, error)
        max_retries: Maximum retry attempts
        
    Returns:
        Result of operation_func
        
    Raises:
        OperationFailedError: If all attempts fail
    """
    last_error = None
    
    for attempt in range(max_retries):
        try:
            print(f"[ATTEMPT {attempt+1}/{max_retries}] {operation_name}")
            result = operation_func()
            print(f"[SUCCESS] {operation_name} completed")
            return result
            
        except GitConflictError as e:
            print(f"[GIT CONFLICT] {operation_name}: {e}")
            if recovery_func:
                try:
                    recovery_func("git_conflict", e)
                except Exception as recovery_error:
                    print(f"[RECOVERY FAILED] {recovery_error}")
            last_error = e
            
        except NetworkError as e:
            print(f"[NETWORK ERROR] {operation_name}: {e}")
            if attempt < max_retries - 1:
                wait_time = 2 ** attempt  # Exponential backoff: 1, 2, 4...
                print(f"[RETRY] Waiting {wait_time}s before retry...")
                time.sleep(wait_time)
            last_error = e
            
        except ForgejoError as e:
            print(f"[FORGEJO ERROR] {operation_name}: {e}")
            # Forgejo errors might be rate limits
            if "rate limit" in str(e).lower() and attempt < max_retries - 1:
                wait_time = 60  # Wait longer for rate limits
                print(f"[RATE LIMIT] Waiting {wait_time}s...")
                time.sleep(wait_time)
            last_error = e
            
        except Exception as e:
            print(f"[UNEXPECTED ERROR] {operation_name}: {type(e).__name__}: {e}")
            last_error = e
            # Don't retry unexpected errors unless recovery possible
            if recovery_func:
                try:
                    recovery_func("unexpected", e)
                    continue  # Try again after recovery
                except:
                    break
            break
    
    # All attempts failed
    print(f"[FAILED] {operation_name} after {attempt + 1} attempts")
    raise OperationFailedError(operation_name, last_error)
```

## Cleanup Manager

```python
class CleanupManager:
    """Context manager that ensures cleanup happens even on failure."""
    
    def __init__(self, cleanup_func, cleanup_description="cleanup"):
        """
        Args:
            cleanup_func: Function to call for cleanup
            cleanup_description: Human-readable description
        """
        self.cleanup_func = cleanup_func
        self.cleanup_description = cleanup_description
        self.cleaned = False
        self.error_during_cleanup = None
    
    def __enter__(self):
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if not self.cleaned:
            try:
                print(f"[CLEANUP] Performing {self.cleanup_description}")
                self.cleanup_func()
                self.cleaned = True
                print(f"[CLEANUP] {self.cleanup_description} completed")
            except Exception as e:
                self.error_during_cleanup = e
                print(f"[CLEANUP FAILED] {self.cleanup_description}: {e}")
                # Don't suppress the original exception
        
        # Don't suppress any exception
        return False
    
    def cleanup_now(self):
        """Manual cleanup trigger."""
        if not self.cleaned:
            self.cleanup_func()
            self.cleaned = True
```

## Usage Examples

### Git Operations

```python
def clone_with_retry(repo_url, target_dir):
    """Clone a repository with automatic retry and cleanup."""
    
    def do_clone():
        result = subprocess.run(
            ['git', 'clone', repo_url, target_dir],
            capture_output=True, text=True
        )
        if result.returncode != 0:
            if "fatal: unable to access" in result.stderr:
                raise NetworkError(f"Network error cloning: {result.stderr}")
            elif "already exists and is not an empty directory" in result.stderr:
                raise GitError(f"Directory already exists: {target_dir}")
            else:
                raise GitError(f"Clone failed: {result.stderr}")
        return target_dir
    
    def recovery(error_type, error):
        if error_type == "git_conflict" and os.path.exists(target_dir):
            print(f"[RECOVERY] Removing existing directory {target_dir}")
            shutil.rmtree(target_dir)
    
    with CleanupManager(
        lambda: shutil.rmtree(target_dir) if os.path.exists(target_dir) else None,
        f"remove {target_dir}"
    ):
        return safe_operation_with_recovery(
            f"clone {repo_url}",
            do_clone,
            recovery
        )
```

### Forgejo API Operations

```python
def create_issue_with_retry(owner, repo, title, body):
    """Create a Forgejo issue with retry logic."""
    
    def do_create():
        try:
            issue = forgejo_create_issue(owner, repo, title, body)
            return issue
        except Exception as e:
            if "rate limit" in str(e).lower():
                raise ForgejoError(f"Rate limit exceeded: {e}")
            elif "network" in str(e).lower():
                raise NetworkError(f"Network error: {e}")
            else:
                raise ForgejoError(f"API error: {e}")
    
    return safe_operation_with_recovery(
        f"create issue '{title}'",
        do_create,
        max_retries=5  # More retries for API operations
    )
```

### Work Claiming with Cleanup

```python
def work_with_claim(owner, repo, issue_number, work_func):
    """Execute work with proper claim management."""
    
    claim_id = None
    
    def cleanup_claim():
        if claim_id:
            try:
                release_claim(owner, repo, issue_number, claim_id, "cleanup")
            except:
                pass  # Best effort
    
    with CleanupManager(cleanup_claim, "release work claim"):
        # Claim the work
        success, claim_id, error = claim_work_item(
            owner, repo, "issue", issue_number, "my-agent", "session-123"
        )
        
        if not success:
            raise AlreadyClaimedError(error)
        
        # Do the actual work
        try:
            result = work_func()
            # Success - release with completed status
            release_claim(owner, repo, issue_number, claim_id, "completed")
            return result
        except Exception as e:
            # Failure - release with failed status
            release_claim(owner, repo, issue_number, claim_id, "failed")
            raise
```

### Complex Operation with Multiple Steps

```python
def implement_feature_with_recovery(issue_number, subtasks):
    """Implement a feature with recovery for each step."""
    
    completed_subtasks = []
    clone_dir = f"/tmp/impl-{issue_number}"
    
    def cleanup_all():
        if os.path.exists(clone_dir):
            shutil.rmtree(clone_dir)
    
    with CleanupManager(cleanup_all, "remove working directory"):
        # Step 1: Clone
        safe_operation_with_recovery(
            "clone repository",
            lambda: git_clone(repo_url, clone_dir)
        )
        
        # Step 2: Implement each subtask
        for subtask in subtasks:
            try:
                safe_operation_with_recovery(
                    f"implement {subtask['title']}",
                    lambda: implement_subtask(subtask, clone_dir),
                    recovery_func=lambda t, e: rollback_subtask(subtask, clone_dir)
                )
                completed_subtasks.append(subtask)
            except OperationFailedError as e:
                print(f"[ERROR] Subtask {subtask['title']} failed: {e}")
                # Decide whether to continue or abort
                if subtask.get('required', True):
                    raise  # Abort on required subtask failure
        
        # Step 3: Commit and push
        if completed_subtasks:
            safe_operation_with_recovery(
                "commit and push changes",
                lambda: git_commit_and_push(clone_dir, completed_subtasks)
            )
        
        return completed_subtasks
```

## Best Practices

1. **Always use specific exception types** - Never catch bare `Exception` unless re-raising
2. **Include context in errors** - Add relevant details (file paths, URLs, etc.)
3. **Log before operations** - So you know what was attempted if it fails
4. **Use with statements** - For automatic resource cleanup
5. **Implement recovery** - But only for errors you can actually recover from
6. **Set appropriate timeouts** - Don't wait forever for operations
7. **Test error paths** - Error handling code needs testing too

## Anti-Patterns to Avoid

```python
# ❌ WRONG - Swallowing all errors silently
try:
    do_something()
except:
    pass

# ✓ CORRECT - Specific handling with logging
try:
    do_something()
except NetworkError as e:
    print(f"[NETWORK ERROR] Operation failed: {e}")
    raise

# ❌ WRONG - No cleanup on error
temp_dir = create_temp_dir()
do_work(temp_dir)  # If this fails, temp_dir is leaked
cleanup(temp_dir)

# ✓ CORRECT - Guaranteed cleanup
with CleanupManager(lambda: cleanup(temp_dir)):
    do_work(temp_dir)

# ❌ WRONG - Infinite retry
while True:
    try:
        do_operation()
        break
    except:
        continue

# ✓ CORRECT - Limited retries with backoff
safe_operation_with_recovery("operation", do_operation, max_retries=3)
```

## Integration with Agents

All agents should import and use these patterns:

```python
from shared.error_handling import (
    safe_operation_with_recovery,
    CleanupManager,
    NetworkError,
    GitError,
    ForgejoError,
    OperationFailedError
)

# Then use throughout the agent code
```