# Cursor Paginator (generic)

Fuente conceptual: MiroFish `backend/app/utils/zep_paging.py` (AGPL-3.0)
Reimplementar desde cero. No copiar codigo.

## Funcion a implementar

### cursor_paginate

- **Dominio:** core
- **Lang:** Python
- **Purity:** impure (llama API externa)
- **Signature:**
  ```python
  def cursor_paginate(
      fetch_page: Callable[..., list[T]],
      get_cursor: Callable[[T], str | None],
      page_size: int = 100,
      max_items: int = 2000,
      max_retries: int = 3,
      retry_delay: float = 2.0,
      retryable_exceptions: tuple[type[Exception], ...] = (ConnectionError, TimeoutError, OSError),
  ) -> list[T]:
  ```
- **Descripcion:** Paginador generico basado en cursor que funciona con cualquier API que use cursor-based pagination. Cada pagina se obtiene con retry automatico. Se detiene cuando: pagina vacia, batch < page_size, o se alcanza max_items.
- **Algoritmo:**
  1. `all_items = []`, `cursor = None`
  2. Loop:
     - Llamar `fetch_page(limit=page_size, cursor=cursor)` con retry (exponential backoff)
     - Si batch vacio: break
     - Extend all_items
     - Si `len(all_items) >= max_items`: truncar y break
     - Si `len(batch) < page_size`: break (ultima pagina)
     - Obtener cursor del ultimo item: `cursor = get_cursor(batch[-1])`
     - Si cursor es None: break
  3. Retornar all_items
- **Patron:** Abstrae completamente el mecanismo de paginacion. El caller solo provee:
  - `fetch_page`: funcion que recibe `limit` y `cursor` kwargs, retorna lista
  - `get_cursor`: funcion que extrae el cursor del ultimo item
- **Deps:** `time`
- **Error type:** Exception (si todos los retries fallan en una pagina)
- **Tests:**
  - API que retorna 3 paginas de 10 items
  - API que falla 1 vez por pagina (retry funciona)
  - max_items limita correctamente
  - API que retorna pagina parcial (ultima pagina)
  - Cursor None en ultimo item (se detiene)
