---
name: avtero_api_hardening
description: avtero API защита от зависания (watchdog + bounded gate); НЕ добавлять uvicorn-воркеров
metadata: 
  node_type: memory
  type: project
  originSessionId: 4eea915e-3b47-4cd5-8a0e-bfa29b78a87c
---

avtero v0.8.1 (commit `0d98695`): одно-воркерный uvicorn API (`local.avtero`, :8201) мог зависать (`healthz http=000`, все запросы в таймаут) при подвисшем LLM-вызове. Захарденено тремя слоями:

1. **Watchdog** — launchd `local.avtero.watchdog` (`~/Library/LaunchAgents/`, скрипт `scripts/api_watchdog.sh`, интервал 30с): пробит локальный `127.0.0.1:8201/healthz`, после 2 подряд неудач (счётчик в `logs/api_watchdog.state`) → `launchctl kickstart -k local.avtero`. **Это главный слой — гарантирует авто-восстановление.** Если API завис — он сам поднимется за ~60-90с, ручной kickstart больше не нужен.
2. `llm.make_client`: `httpx.Timeout(connect=10, pool=5, read=60)`.
3. `_preflight_gate`: `asyncio.wait_for(decode_only, GATE_DECODE_TIMEOUT=25с)` → фолбэк на `needs_confirmation`.

**ВАЖНО — НЕ добавлять `uvicorn --workers >1`:** job-движок (`app/services/jobs.py`) хранит очереди событий в памяти процесса (`_QUEUES` dict). При >1 воркере job создаётся на одном воркере, а SSE `/events` попадает на другой → job не найден, стрим пуст, весь async-подбор ломается. Масштабировать можно только вынеся job-store в redis (большой рефактор, не нужен — проблему решает watchdog).

Разведано: исходящие клиенты уже bounded (LLM 60s, exist/emex 15s), блокирующих sync-вызовов на горячем пути НЕТ. Async-loop одним await не морозится — проверено black-hole тестом на изолированном :8299 (healthz 200/17-34мс при висящем job). Наблюдавшийся `http=000` = насыщение/стэкинг, не блокировка.

Подробности: Obsidian Decisions D-api-hung-llm-hardening, Changelog v0.8.1. См. [[avtero_overview]], [[mac_mini_upterm_access]].
