# Voice Incidents Runbook
**Version:** 1.0 | **Node:** NODA2 | **SLO doc:** `config/slo_policy.yml`

---

## Перший крок для БУДЬ-ЯКОГО алерту (30 секунд)

```bash
# 1. Репро пакет — весь контекст в одному запиті
curl -s http://localhost:8002/api/voice/degradation_status | python3 -m json.tool

# 2. Canary живий синтез
python3 ops/scripts/voice_canary.py --mode preflight --memory-url http://localhost:8000

# 3. Логи останніх 2 хвилин
docker logs sofiia-console --since 2m 2>&1 | grep -E "ERROR|WARNING|TTS|LLM|502|429|503"
docker logs dagi-memory-service-node2 --since 2m 2>&1 | grep -E "ERROR|403|edge.tts|synthesiz"
```

**Поля `repro` у відповіді** дають: `last_5_tts_errors`, `last_5_llm_errors`, `node_id`, `last_model`, `concurrent_tts_slots_free`.

---

## Alert 1: `VoiceTTFA_P95_Breach_Fast`
**Умова:** TTFA p95 > 5000ms за 10 хвилин | **Severity:** warning

**Що значить:** LLM відповідає повільно — черга Ollama переповнена, модель cold-start, або qwen3.5 вибрана замість gemma3.

### Крок 1 — Діагностика (2 хв)
```bash
# Ollama поточний стан
curl -s http://localhost:11434/api/ps | python3 -m json.tool
# Метрики LLM по моделях (якщо є Prometheus)
# promql: histogram_quantile(0.95, rate(voice_llm_ms_bucket[5m])) by (model)

# Деградаційний стан
curl -s http://localhost:8002/api/voice/degradation_status | python3 -c \
  "import sys,json; d=json.load(sys.stdin); print(d['repro']['last_model'], d['p95'])"
```

### Крок 2 — Mitigation
```bash
# A. Примусово переключити на gemma3 (якщо qwen3.5 завантажений)
# В UI: зняти галочку "Якісно" → fast profile автоматично обере gemma3

# B. Якщо Ollama завантажений запитами — зупинити важкі моделі
curl -s -X POST http://localhost:11434/api/generate \
  -d '{"model":"qwen3.5:35b-a3b","keep_alive":0}'  # вивантажити з GPU

# C. Якщо Ollama не відповідає — перезапуск
docker restart ollama && sleep 10
curl -s http://localhost:11434/api/tags | python3 -m json.tool
```

### Крок 3 — Verify
```bash
python3 ops/scripts/voice_canary.py --mode runtime --memory-url http://localhost:8000
# Очікування: overall=ok, Polina/Ostap < 3000ms
```

---

## Alert 2: `VoiceTTFA_P95_Breach_Quality`
**Умова:** quality profile TTFA p95 > 7000ms | **Severity:** warning

**Що значить:** qwen3.5 або qwen3:14b надто повільні. Часто — конкурентні запити або cold token generation.

### Дії
1. Перевірити `degradation_status.repro.last_model` — підтвердити що це quality profile.
2. Якщо це ізольована сесія — ігнорувати (quality SLO м'якший).
3. Якщо 5+ хвилин стабільно → переключити всіх на fast: в `router-config.yml` тимчасово видалити `voice_quality_uk` з `agent_voice_profiles.sofiia.quality_option`.
4. Після нормалізації — повернути.

```bash
# Підтвердити що fast profile нормальний
curl -s -X POST http://localhost:8002/api/voice/chat/stream \
  -H "Content-Type: application/json" \
  -d '{"message":"ping","model":"ollama:gemma3:latest","voice_profile":"voice_fast_uk"}' \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print('llm_ms:', d['meta']['llm_ms'])"
```

---

## Alert 3: `VoiceQueueUnderflow_Spike`
**Умова:** underflow rate > 1/хв за 5 хвилин | **Severity:** warning

**Що значить:** браузер відтворює аудіо швидше ніж BFF синтезує `rest_chunks`. Користувач чує тишу між реченнями.

### Діагностика
```bash
# Перевірити TTS latency (чи сповільнилось edge-tts?)
curl -s http://localhost:8000/voice/health | python3 -c \
  "import sys,json; d=json.load(sys.stdin); [print(v['voice'], v['ms'],'ms') for v in d['voices']]"

# Перевірити concurrent TTS slots
curl -s http://localhost:8002/api/voice/degradation_status | python3 -c \
  "import sys,json; d=json.load(sys.stdin); print('free slots:', d['repro']['concurrent_tts_slots_free'])"
```

### Mitigation
- **Якщо TTS slow** (> 2s) → Alert 4 (edge-tts). Дивись нижче.
- **Якщо concurrent slots = 0** → TTS DOS. Перевірити `docker stats dagi-memory-service-node2`. Збільшити `MAX_CONCURRENT_TTS` або перезапустити memory-service.
- **Якщо slots OK** → перший чанк надто короткий (~1 речення). Тимчасове рішення — зменшити `MIN_CHUNK_CHARS` у `voice_utils.py` щоб більше тексту йшло у перший чанк.

---

## Alert 4: `VoiceTTS_P95_Degraded`
**Умова:** TTS synthesis p95 > 2000ms за 10 хвилин | **Severity:** **critical**

**Що значить:** edge-tts сповільнився або починає отримувати 403. Типова причина — Microsoft endpoint зміна auth або rate limiting.

### Крок 1 — Визначити тип помилки (1 хв)
```bash
# Подивитись last_5_tts_errors
curl -s http://localhost:8002/api/voice/degradation_status | python3 -c \
  "import sys,json; d=json.load(sys.stdin); [print(e) for e in d['repro']['last_5_tts_errors']]"

# Живий тест
python3 ops/scripts/voice_canary.py --mode preflight --memory-url http://localhost:8000
```

### Якщо 403 errors:
```bash
# Перевірити версію edge-tts
docker exec dagi-memory-service-node2 pip show edge-tts | grep Version
# Очікується: 7.2.7

# Якщо версія не 7.2.7 — оновити
docker exec dagi-memory-service-node2 pip install edge-tts==7.2.7
docker restart dagi-memory-service-node2
sleep 10 && python3 ops/scripts/voice_canary.py --mode preflight
```

### Якщо timeout / network:
```bash
# Тест від сервера до Microsoft endpoint
docker exec dagi-memory-service-node2 python3 -c \
  "import asyncio, edge_tts; asyncio.run(edge_tts.list_voices())"

# Якщо мережева проблема — тимчасово переключити на espeak (fallback)
# В memory-service env: TTS_FALLBACK_ENGINE=espeak
# Увага: якість значно гірша, але голос є
```

### Нотувати в incident log:
```bash
curl -s -X POST http://localhost:9102/v1/tools/execute \
  -H "Content-Type: application/json" \
  -d '{"tool":"oncall_tool","action":"incident_log_append","params":{"severity":"sev2","title":"TTS degraded — edge-tts","body":"VoiceTTS_P95_Degraded alert fired. Last errors: ..."}}'
```

---

## Alert 5: `VoiceTTS_ErrorRate_High`
**Умова:** TTS errors > 0.05/s за 3 хвилини | **Severity:** **critical**

**Що значить:** масові відмови TTS синтезу. Користувачі або не чують нічого, або чують espeak-fallback.

### Перший крок (30 секунд)
```bash
# Скільки помилок і якого типу
docker logs dagi-memory-service-node2 --since 5m 2>&1 | grep -c "ERROR\|403\|edge.tts"
docker logs dagi-memory-service-node2 --since 5m 2>&1 | grep "ERROR" | tail -5
```

### Mitigation tree:
```
error_type = 403         → Крок "Якщо 403 errors" з Alert 4
error_type = timeout     → Перевірити мережу, перезапустити memory-service
error_type = synthesis   → pip install edge-tts==7.2.7 --force-reinstall
error_type = OOM         → docker stats → перезапустити memory-service з більшим RAM limit
```

### Аварійний fallback (якщо нічого не допомогло):
```bash
# Вимкнути автоспік у UI — щоб не показувало помилки
# Або тимчасово вимкнути streaming
docker exec sofiia-console env VOICE_STREAM_ENABLED=false \
  uvicorn app.main:app --host 0.0.0.0 --port 8002 &
# (не рекомендовано на prod без rebuild, але як аварійний захід)
```

### Повідомити користувачів (якщо > 10 хвилин):
- Додати banner у UI: змінна `VOICE_DEGRADED_BANNER` у env → відобразити через degradation badge "🔴 TTS DEGRADED"

---

## Escalation

| Тривалість | Дія |
|------------|-----|
| < 10 хв    | Автоматичний деградаційний badge у UI, моніторинг |
| 10–30 хв   | Mitigation з цього runbook, canary preflight |
| > 30 хв    | Escalate до @IvanTytar, записати incident в ops/incidents.jsonl |
| > 2 год    | Post-mortem draft (Sofiia-supervisor `postmortem_draft_graph`) |

```bash
# Записати incident
echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","sev":"sev2","title":"Voice TTS degraded","status":"open"}' \
  >> ops/incidents.jsonl
```

---

## Корисні команди (bookmark)

```bash
# Швидкий статус всього voice стеку
curl -s http://localhost:8002/api/voice/degradation_status | python3 -m json.tool
curl -s http://localhost:8000/voice/health | python3 -c "import sys,json; d=json.load(sys.stdin); print('TTS:', d['edge_tts'], '| Polina:', [v for v in d['voices'] if 'Polina' in v['voice']][0]['ms'], 'ms')"
python3 ops/scripts/voice_canary.py --mode preflight

# Browser console для активних сесій
# _voiceStats()                — p50/p95 по останніх 20 турнах
# _voice_degradation_sm        — поточний стан на сервері

# Prometheus queries (якщо є)
# histogram_quantile(0.95, rate(voice_ttfa_ms_bucket[5m])) by (voice_profile)
# rate(voice_tts_errors_total[5m])
# rate(voice_queue_underflows_total[5m]) * 60
```
