```python
from typing import Callable, Dict, Any

class JobQueue:
    """
    Manages jobs and processes them with built-in retry logic 
    and exponential backoff.
    """
    def __init__(self):
        # Stores job payloads: {job_id: job_data}
        self.jobs: Dict[str, Dict[str, Any]] = {}
        # Tracks the number of times a job has failed and needs retrying: {job_id: retry_count}
        self.job_attempts: Dict[str, int] = {}
        self.MAX_RETRIES = 3

    def add_job(self, job_id: str, data: Dict[str, Any]):
        """Adds a job to the queue."""
        self.jobs[job_id] = data
        self.job_attempts[job_id] = 0

    def process_job(self, job_id: str, processor: Callable) -> bool:
        """
        Processes a job, retrying up to MAX_RETRIES times upon failure.
        
        Returns True if the job succeeds, False otherwise.
        """
        if job_id not in self.jobs:
            raise ValueError(f"Job ID {job_id} not found in the queue.")

        job_data = self.jobs[job_id]
        
        for attempt in range(self.MAX_RETRIES):
            current_attempts = self.job_attempts[job_id]
            
            try:
                # Attempt to run the job processor
                processor(job_data)
                
                # Success
                print(f"[{job_id}] Job succeeded on attempt {current_attempts + 1}.")
                return True
            
            except Exception as e:
                print(f"[{job_id}] Attempt {current_attempts + 1} failed: {e}")
                
                # Check if this was the last allowed attempt
                if current_attempts >= self.MAX_RETRIES - 1:
                    print(f"[{job_id}] All {self.MAX_RETRIES} attempts exhausted. Job failed permanently.")
                    return False
                
                # Increment attempt count
                self.job_attempts[job_id] += 1
                
                # Calculate exponential backoff delay (1s, 2s, 4s...)
                # The delay is based on the number of previous failures (current_attempts)
                delay = 2 ** current_attempts
                
                # Simulate waiting/delaying
                print(f"[{job_id}] Retrying in {delay}s...")
                # In a real system, time.sleep(delay) would occur here.
                # We simulate the delay by continuing the loop.

        # Should only be reached if MAX_RETRIES was 0 or logic error occurred
        return False

# --- Example Usage ---

# Mock function to simulate a job processor that fails a specific number of times
failure_count = {}

def mock_processor(data: Dict[str, Any]):
    """
    Simulates a network request that fails the first 2 times, then succeeds.
    """
    job_id = data.get("job_id")
    
    if job_id not in failure_count:
        failure_count[job_id] = 0

    if failure_count[job_id] < 2:
        failure_count[job_id] += 1
        raise ConnectionError(f"Simulated network failure (Attempt {failure_count[job_id]})")
    else:
        print(f"--- Successfully fetched {data['url']} ---")
        return True

def failing_processor(data: Dict[str, Any]):
    """Simulates a job that always fails."""
    raise ValueError("Critical processing error.")

# 1. Setup Queue
queue = JobQueue()

# 2. Add jobs
job_id_success = "job_success"
queue.add_job(job_id_success, {"job_id": job_id_success, "url": "https://example.com/success"})

job_id_fail = "job_fail"
queue.add_job(job_id_fail, {"job_id": job_id_fail, "url": "https://example.com/failure"})


# --- Test Case 1: Success after retries ---
print("\n=======================================")
print("Running job_success (Expected: Success)")
success_result = queue.process_job(job_id_success, mock_processor)
print(f"Final result for job_success: {success_result}")


# --- Test Case 2: Exhausted retries ---
print("\n=======================================")
print("Running job_fail (Expected: Failure)")
fail_result = queue.process_job(job_id_fail, failing_processor)
print(f"Final result for job_fail: {fail_result}")
```