# Spec: LLM Timeout Enforcement

## Objetivo

Fazer os config params `extraction_timeout_sec` e `reflexion_timeout_sec` funcionarem de verdade — hoje existem no `MemoryConfig` mas nenhuma chamada LLM é limitada por timeout.

## Contexto

O `MemoryConfig` define `extraction_timeout_sec = 30.0` e `reflexion_timeout_sec = 10.0`, mas nenhuma chamada `llm.complete()` no write pipeline usa `asyncio.wait_for()`. Se o LLM travar, o pipeline inteiro trava sem limite. A doc lista esses params como funcionais — são API pública mentirosa.

## Definition of Done

1. [ ] `extraction_timeout_sec` é enforced em todas as chamadas `llm.complete()` dentro de `extract.py` via `asyncio.wait_for()`
2. [ ] `reflexion_timeout_sec` é enforced na chamada de reflexion em `extract.py`
3. [ ] Quando timeout ocorre, a chamada retorna resultado vazio (fail-safe) em vez de propagar `TimeoutError` — consistente com o comportamento fail-safe já existente
4. [ ] Testes existentes continuam passando (277 testes)
5. [ ] Novo teste verifica que timeout é enforced (mock LLM que nunca retorna + timeout curto)

## Escopo

- `src/arandu/write/extract.py` — wrap todas as chamadas `llm.complete()` com `asyncio.wait_for(timeout)`
- `tests/test_timeout.py` — novo teste

## Anti-escopo

- NÃO adicionar timeout no read pipeline (outra spec)
- NÃO adicionar timeout em entity resolution ou reconciliation (a gente removeu esses params)
- NÃO mudar a interface do `LLMProvider` protocol
- NÃO alterar docs (o param já está documentado corretamente)

## Decisões Técnicas

1. **`asyncio.wait_for()` em cada chamada** — não um timeout global no pipeline. Cada chamada LLM tem seu próprio timeout. Razão: multi-pass faz ~5 chamadas, cada uma deve ter seu limite individual.

2. **Fail-safe no timeout** — `asyncio.TimeoutError` é capturado no mesmo try/except que já existe pra erros de LLM. Retorna resultado vazio. Razão: consistente com o padrão fail-safe da extraction ("better to miss than to crash").

3. **Config params existentes** — usar `config.extraction_timeout_sec` e `config.reflexion_timeout_sec` que já existem. Zero mudança na API pública.

## Budget

≤ 2 arquivos (extract.py + test_timeout.py)

## Riscos

- **Cancelamento de task async**: `asyncio.wait_for` cancela a coroutine. Se o LLMProvider tem cleanup (ex: fecha conexão HTTP), precisa lidar com `CancelledError`. Mitigação: a maioria dos providers (openai, httpx) já lida com isso.
- **Testes flaky**: timeout tests podem ser flaky em CI com runners lentos. Mitigação: usar timeout muito curto (0.001s) com mock que usa `asyncio.sleep(999)`.
