Files
microdao-daarion/docs/runbook/matrix-bridge-dagi-ops.md
Apple d40b1e87c6 feat(matrix-bridge-dagi): harden mixed rooms with safe defaults and ops visibility (M2.2)
Guard rails (mixed_routing.py):
  - MAX_AGENTS_PER_MIXED_ROOM (default 5): fail-fast at parse time
  - MAX_SLASH_LEN (default 32): reject garbage/injection slash tokens
  - Unified rejection reasons: unknown_agent, slash_too_long, no_mapping
  - REASON_REJECTED_* constants (separate from success REASON_*)

Ingress (ingress.py):
  - per-room-agent concurrency semaphore (MIXED_CONCURRENCY_CAP, default 1)
  - active_lock_count property for /health + prometheus
  - UNKNOWN_AGENT_BEHAVIOR: "ignore" (silent) | "reply_error" (inform user)
  - on_routed(agent_id, reason) callback for routing metrics
  - on_route_rejected(room_id, reason) callback for rejection metrics
  - matrix.route.rejected audit event on every rejection

Config + main:
  - max_agents_per_mixed_room, max_slash_len, unknown_agent_behavior, mixed_concurrency_cap
  - matrix_bridge_routed_total{agent_id, reason} counter
  - matrix_bridge_route_rejected_total{room_id, reason} counter
  - matrix_bridge_active_room_agent_locks gauge
  - /health: mixed_guard_rails section + total_agents_in_mixed_rooms
  - docker-compose: all 4 new guard rail env vars

Runbook: section 9 — mixed room debug guide (6 acceptance tests, routing metrics, session isolation, lock hang, config guard)

Tests: 108 pass (94 → 108, +14 new tests for guard rails + callbacks + concurrency)
Made-with: Cursor
2026-03-05 01:41:20 -08:00

531 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Matrix Bridge DAGI — Ops Runbook (H4)
**Сервіс:** `matrix-bridge-dagi` | **Нода:** NODA1 | **Порт:** 7030 (localhost)
**Stack:** Matrix (Synapse) → bridge → Router `/v1/agents/{id}/infer` → Matrix reply
**Фаза:** M2.2 (N rooms + mixed room routing), H1/H2/H3 hardening активний
---
## 0. Purpose
Операційні перевірки та troubleshooting для `matrix-bridge-dagi` на NODA1:
- Matrix ↔ Bridge ↔ Router ↔ Agent ↔ Matrix
- Audit events в sofiia-console (`POST /api/audit/internal`)
- Rate limit (room/sender RPM)
- Backpressure queue (drops/queue wait)
---
## 1. Quick Status (30 секунд)
### 1.1 Health
```bash
curl -sS http://127.0.0.1:7030/health | python3 -m json.tool
```
Очікування:
```json
{
"ok": true,
"matrix_reachable": true,
"gateway_reachable": true,
"mappings_count": 1,
"queue": {"size": 0, "max": 100, "workers": 2},
"rate_limiter": {"room_rpm_limit": 20, "sender_rpm_limit": 10, "active_rooms": 0, "active_senders": 0}
}
```
| Поле | Норма | Тривога |
|------|-------|---------|
| `ok` | `true` | `false` → дивитись `error` |
| `matrix_reachable` | `true` | `false` → I4 |
| `gateway_reachable` | `true` | `false` → I1 |
| `queue.size` | ≈0 idle | >50 → I3 |
| `mappings_count` | ≥1 | 0 → перевірити `BRIDGE_ROOM_MAP` |
### 1.2 Mappings
```bash
curl -sS http://127.0.0.1:7030/bridge/mappings | python3 -m json.tool
```
### 1.3 Logs (останні 30 рядків)
```bash
docker logs matrix-bridge-dagi-node1 --tail 30
```
Шукати: `ERROR`, `Rate limited`, `Queue full`, `Invoke ok`, `Reply sent`
---
## 2. Smoke Test (23 хв)
### 2.1 E2E через Element (рекомендовано)
1. Element → "Change homeserver" → `matrix.daarion.space`
2. Логін: `test_user` / `TestUser_2026!`
3. Room: **DAGI — Sofiia**
4. Відправити: `ping`
5. Очікування: reply ≤ 5s (Mistral/DeepSeek залежний)
### 2.2 Smoke через curl (без Element)
```bash
# Логін як test_user
TOKEN=$(curl -sS -X POST http://localhost:8008/_matrix/client/v3/login \
-H 'Content-Type: application/json' \
-d '{"type":"m.login.password","identifier":{"type":"m.id.user","user":"test_user"},"password":"TestUser_2026!"}' \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')
# Відправити повідомлення
ROOM_ID=$(grep 'SOFIIA_ROOM_ID' /opt/microdao-daarion/.env | cut -d= -f2)
TXN_ID="smoke-$(date +%s)"
curl -sS -X PUT "http://localhost:8008/_matrix/client/v3/rooms/$ROOM_ID/send/m.room.message/$TXN_ID" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"msgtype":"m.text","body":"smoke test ping"}' | python3 -c 'import sys,json; print("sent:", json.load(sys.stdin).get("event_id","ERROR"))'
# Чекати reply (~15-30s залежить від sync cycle)
sleep 30
# Перевірити health metrics
curl -sS http://127.0.0.1:7030/metrics | grep -E 'matrix_bridge_(messages_received|messages_replied)_total'
```
### 2.3 Перевірити .well-known (для Element auto-discovery)
```bash
curl -sS https://matrix.daarion.space/.well-known/matrix/client
# Очікування: {"m.homeserver":{"base_url":"https://matrix.daarion.space"}}
```
---
## 3. Операційні Метрики
### 3.1 Traffic counters
```bash
curl -sS http://127.0.0.1:7030/metrics | grep -E \
"matrix_bridge_messages_(received|replied)_total|matrix_bridge_rate_limited_total|matrix_bridge_queue_dropped_total"
```
| Метрика | Значення | Що означає |
|---------|----------|------------|
| `messages_received_total` | росте | повідомлення надходять |
| `messages_replied_total{status="ok"}` | росте | агент відповідає |
| `messages_replied_total{status="error"}` | >0 | проблема send_text → I1 |
| `rate_limited_total` | ≈0 | норма; >0 → I2 |
| `queue_dropped_total` | 0 | норма; >0 → I3 |
### 3.2 Latency histograms
```bash
curl -sS http://127.0.0.1:7030/metrics | grep -E \
"(invoke_duration|send_duration|queue_wait)_seconds_(bucket|count|sum)"
```
Орієнтири:
| Histogram | Норма (p95) | Тривога |
|-----------|-------------|---------|
| `invoke_duration_seconds` | 15s | >20s → LLM деградація |
| `send_duration_seconds` | <200ms | >2s → Synapse проблема |
| `queue_wait_seconds` | <50ms idle | >5s → I3 (workers перевантажені) |
### 3.3 Queue state
```bash
curl -sS http://127.0.0.1:7030/health | python3 -c \
'import sys,json; d=json.load(sys.stdin); print("queue:", d.get("queue"))'
```
---
## 4. Audit Events (sofiia-console)
### 4.1 Перевірити audit ingest (internal endpoint)
```bash
INT_TOKEN=$(grep 'SOFIIA_INTERNAL_TOKEN' /opt/microdao-daarion/.env | cut -d= -f2)
curl -sS -X POST http://127.0.0.1:8002/api/audit/internal \
-H 'Content-Type: application/json' \
-H "X-Internal-Service-Token: $INT_TOKEN" \
-d '{"event":"matrix.smoke_check","agent_id":"sofiia","node_id":"NODA1","status":"ok"}' | python3 -m json.tool
# Очікування: {"ok": true, "event": "matrix.smoke_check"}
```
### 4.2 Події для пошуку в audit log
| Event | Коли | Норма |
|-------|------|-------|
| `matrix.message.received` | при кожному вхідному | завжди |
| `matrix.agent.replied` | при успішній відповіді | завжди |
| `matrix.rate_limited` | при перевищенні RPM | рідко/ніколи |
| `matrix.queue_full` | при переповненні черги | ніколи |
| `matrix.error` | при помилці invoke/send | ніколи |
---
## 5. Common Incidents
### I1: "Element підключений, але Sofiia не відповідає"
```bash
# Step 1: health check
curl -sS http://127.0.0.1:7030/health | python3 -c 'import sys,json; d=json.load(sys.stdin); print("matrix:", d.get("matrix_reachable"), "gw:", d.get("gateway_reachable"), "maps:", d.get("mappings_count"))'
# Step 2: metrics — received vs replied
curl -sS http://127.0.0.1:7030/metrics | grep -E 'matrix_bridge_messages_(received|replied)_total'
# Step 3: logs
docker logs matrix-bridge-dagi-node1 --tail 50 | grep -E 'ERROR|invoke|reply|error'
# Step 4: router direct test
curl -sS -X POST http://127.0.0.1:9102/v1/agents/sofiia/infer \
-H 'Content-Type: application/json' \
-d '{"prompt":"ping","session_id":"smoke-check","user_id":"ops"}' | python3 -c 'import sys,json; d=json.load(sys.stdin); print("router:", d.get("response","ERROR")[:80])'
```
**Фікс:**
- `gateway_reachable: false` → перевірити `DAGI_GATEWAY_URL=http://dagi-router-node1:8000`
- `mappings_count: 0` → перевірити `BRIDGE_ROOM_MAP` в env
- received росте, replied ні → router або send_text проблема (Step 4)
---
### I2: "Bagato rate_limited в metrics"
```bash
curl -sS http://127.0.0.1:7030/metrics | grep rate_limited_total
curl -sS http://127.0.0.1:7030/health | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d.get("rate_limiter"))'
```
**Причина:** RPM занадто низький або room flood (bot/скрипт).
**Фікс** (тимчасово, через env):
```bash
# На NODA1:
cd /opt/microdao-daarion
# Відредагувати .env:
# RATE_LIMIT_ROOM_RPM=40
# RATE_LIMIT_SENDER_RPM=20
docker compose -f docker-compose.matrix-bridge-node1.yml up -d
```
---
### I3: "Queue drops ростуть / queue.size велике"
```bash
curl -sS http://127.0.0.1:7030/metrics | grep queue_dropped_total
curl -sS http://127.0.0.1:7030/metrics | grep 'invoke_duration_seconds_bucket'
```
**Причина:** модель/Router повільні або `WORKER_CONCURRENCY` малий.
**Фікс:**
```bash
# Збільшити workers і queue (в .env):
# WORKER_CONCURRENCY=4
# QUEUE_MAX_EVENTS=300
docker compose -f docker-compose.matrix-bridge-node1.yml up -d
```
Паралельно перевірити latency histogram — якщо `invoke_duration` >20s, проблема на стороні LLM.
---
### I4: "matrix_reachable: false у /health"
```bash
# Synapse публічний ендпоінт
curl -sS https://matrix.daarion.space/_matrix/client/versions | python3 -c 'import sys,json; d=json.load(sys.stdin); print("ok, versions:", list(d.get("versions",[]))[:3])'
# Synapse внутрішній (з NODA1)
curl -sS http://localhost:8008/_matrix/client/versions
# Synapse контейнер
docker ps | grep synapse
docker logs dagi-synapse-node1 --tail 20
# Nginx
curl -sI https://matrix.daarion.space/_matrix/client/versions | head -3
```
**Фікс:**
- Synapse контейнер впав → `docker compose -f docker-compose.synapse-node1.yml up -d`
- TLS / .well-known → перевірити nginx (`nginx -t && nginx -s reload`)
- Токен невалідний → I5 (rotation)
---
### I5: "Замінити MATRIX_ACCESS_TOKEN"
```bash
# 1. Отримати новий токен (з NODA1)
NEW_TOKEN=$(curl -sS -X POST http://localhost:8008/_matrix/client/v3/login \
-H 'Content-Type: application/json' \
-d '{"type":"m.login.password","identifier":{"type":"m.id.user","user":"dagi_bridge"},"password":"DAGIbr1dge_M4tr1x_2026!"}' \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')
echo "New token: ${#NEW_TOKEN} chars"
# 2. Оновити в .env
sed -i "s/^MATRIX_ACCESS_TOKEN=.*/MATRIX_ACCESS_TOKEN=$NEW_TOKEN/" /opt/microdao-daarion/.env
# 3. Restart bridge
cd /opt/microdao-daarion
docker compose -f docker-compose.matrix-bridge-node1.yml up -d
# 4. Verify
sleep 10
curl -sS http://127.0.0.1:7030/health | python3 -c 'import sys,json; d=json.load(sys.stdin); print("matrix:", d.get("matrix_reachable"))'
```
---
### I6: "Замінити SOFIIA_INTERNAL_TOKEN"
```bash
# 1. Генерувати новий токен
NEW_INT_TOKEN=$(openssl rand -base64 32 | tr -d '/+=')
# 2. Оновити в .env
sed -i "s/^SOFIIA_INTERNAL_TOKEN=.*/SOFIIA_INTERNAL_TOKEN=$NEW_INT_TOKEN/" /opt/microdao-daarion/.env
# 3. Restart обох сервісів
cd /opt/microdao-daarion
docker compose -f docker-compose.node1.yml up -d dagi-sofiia-console-node1
docker compose -f docker-compose.matrix-bridge-node1.yml up -d
sleep 15
# 4. Verify audit ingest
INT_TOKEN=$(grep 'SOFIIA_INTERNAL_TOKEN' .env | cut -d= -f2)
curl -sS -X POST http://127.0.0.1:8002/api/audit/internal \
-H 'Content-Type: application/json' \
-H "X-Internal-Service-Token: $INT_TOKEN" \
-d '{"event":"matrix.token_rotated","agent_id":"sofiia","node_id":"NODA1","status":"ok"}' | python3 -c 'import sys,json; print("audit ok:", json.load(sys.stdin).get("ok"))'
```
---
## 6. Restart / Rollback
### Restart bridge
```bash
cd /opt/microdao-daarion
docker compose -f docker-compose.matrix-bridge-node1.yml up -d --force-recreate
sleep 15
curl -sS http://127.0.0.1:7030/health | python3 -c 'import sys,json; d=json.load(sys.stdin); print("ok:", d["ok"])'
```
### Restart Synapse
```bash
docker compose -f docker-compose.synapse-node1.yml up -d
sleep 20
curl -sS http://localhost:8008/_matrix/client/versions | python3 -c 'import sys,json; print("synapse ok:", "versions" in json.load(sys.stdin))'
```
### Мінімальний smoke після restart
```bash
curl -sS http://127.0.0.1:7030/health | python3 -c \
'import sys,json; d=json.load(sys.stdin); print("bridge ok:", d["ok"], "| matrix:", d.get("matrix_reachable"), "| gw:", d.get("gateway_reachable"))'
# Потім: 1 повідомлення в Element → reply
```
---
## 7. Release Checklist (перед деплоєм bridge)
```
[ ] health ok (ok: true, matrix: true, gw: true)
[ ] queue drops == 0 (curl /metrics | grep queue_dropped_total → 0)
[ ] rate_limited == 0 (curl /metrics | grep rate_limited_total → 0)
[ ] smoke test: Element → ping → reply ≤ 5s
[ ] audit events з'являються (matrix.message.received, matrix.agent.replied)
[ ] .well-known → JSON з base_url
[ ] TLS cert valid (не <7 days до expiry)
```
### Перевірка TLS cert (термін дії)
```bash
echo | openssl s_client -connect matrix.daarion.space:443 -servername matrix.daarion.space 2>/dev/null \
| openssl x509 -noout -dates
# notAfter — запас не менше 7 днів (auto-renew certbot)
```
---
---
## 9. Mixed Room Debug Guide (M2.2)
### 9.1 Перевірка конфігурації mixed rooms
```bash
# Поточний mapping (regular + mixed)
curl -sS http://127.0.0.1:7030/bridge/mappings | python3 -m json.tool
# Guard rail параметри (з /health)
curl -sS http://127.0.0.1:7030/health | python3 -m json.tool | python3 -c "
import sys, json; h=json.load(sys.stdin)
print('mixed_rooms:', h.get('mixed_rooms_count',0))
print('total_agents:', h.get('total_agents_in_mixed_rooms',0))
print('guard_rails:', json.dumps(h.get('mixed_guard_rails',{}), indent=2))
"
```
### 9.2 Smoke test для mixed room (6 acceptance test cases)
Відправляємо з `test_user` у mixed room `!roomX:daarion.space`:
```bash
# Змінна для зручності
ROOM_ID="!roomX:daarion.space"
TOKEN="@test_user_token" # або через Element UI
# 1. Slash → Sofiia
curl -sX POST "https://matrix.daarion.space/_matrix/client/v3/rooms/${ROOM_ID}/send/m.room.message/txn1" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"msgtype":"m.text","body":"/sofiia ping"}' | jq '.event_id'
# 2. Slash → Helion
curl -sX POST "...txn2" -d '{"msgtype":"m.text","body":"/helion ping"}' | jq '.event_id'
# 3. @mention → Sofiia
curl -sX POST "...txn3" -d '{"msgtype":"m.text","body":"@sofiia status"}' | jq '.event_id'
# 4. colon-mention → Sofiia
curl -sX POST "...txn4" -d '{"msgtype":"m.text","body":"sofiia: status"}' | jq '.event_id'
# 5. Plain text → default agent
curl -sX POST "...txn5" -d '{"msgtype":"m.text","body":"ping"}' | jq '.event_id'
# 6. Unknown slash → audit matrix.route.rejected
curl -sX POST "...txn6" -d '{"msgtype":"m.text","body":"/unknown test"}' | jq '.event_id'
```
**Очікувана поведінка у кімнаті:**
| Команда | Reply prefix | Router agent |
|---------|-------------|--------------|
| `/sofiia ping` | `Sofiia: ...` | sofiia |
| `/helion ping` | `Helion: ...` | helion |
| `@sofiia status` | `Sofiia: ...` | sofiia |
| `sofiia: status` | `Sofiia: ...` | sofiia |
| `ping` (plain) | `<DefaultAgent>: ...` | перший у списку |
| `/unknown test` | `⚠️ Unknown agent...` або тиша (залежно від `UNKNOWN_AGENT_BEHAVIOR`) | — |
### 9.3 Перевірка routing метрик
```bash
# Successful routing breakdown by reason
curl -sS http://127.0.0.1:7030/metrics | grep 'matrix_bridge_routed_total'
# Очікування:
# matrix_bridge_routed_total{agent_id="sofiia",reason="slash_command"} N
# matrix_bridge_routed_total{agent_id="helion",reason="slash_command"} N
# matrix_bridge_routed_total{agent_id="sofiia",reason="at_mention"} N
# matrix_bridge_routed_total{agent_id="sofiia",reason="default"} N
# Rejections
curl -sS http://127.0.0.1:7030/metrics | grep 'matrix_bridge_route_rejected_total'
# Очікується > 0 лише якщо були /unknown або занадто довгі токени
# Active concurrency locks
curl -sS http://127.0.0.1:7030/metrics | grep 'active_room_agent_locks'
# Зазвичай 0 (між повідомленнями)
```
### 9.4 Debug: "Wrong agent responds"
**Симптом:** У mixed room `/helion ...` → відповідає sofiia, або відповідає не той агент.
**Діагностика:**
```bash
# 1. Перевірити audit events в sofiia-console
# (через psql або API)
curl -sS http://127.0.0.1:8002/api/audit \
| python3 -m json.tool | grep -A5 '"event":"matrix.message.received"' \
| grep '"routing_reason"'
# routing_reason має бути "slash_command", "at_mention", "colon_mention" або "default"
# 2. Перевірити логи bridge
docker logs matrix-bridge-dagi-node1 --tail 100 2>&1 | grep -E 'route|Route|routing'
# Очікування: "Slash route: /helion → helion" або "Default route: → sofiia"
# 3. Перевірити BRIDGE_MIXED_ROOM_MAP в .env
grep BRIDGE_MIXED_ROOM_MAP /opt/microdao-daarion/.env
# Формат: "!roomX:server=sofiia,helion"
# Перший у списку = default agent
```
**Виправлення:**
- Якщо порядок агентів неправильний — змінити `BRIDGE_MIXED_ROOM_MAP` або встановити `BRIDGE_MIXED_DEFAULTS`
- Перезапустити bridge: `docker restart matrix-bridge-dagi-node1`
### 9.5 Debug: "Session context змішується між агентами"
**Симптом:** Helion "пам'ятає" контекст розмови sofiia.
**Перевірка:** Session key у логах (`session_id` у invoke payload)
```bash
docker logs matrix-bridge-dagi-node1 --tail 50 2>&1 | grep session_id
# Очікування для mixed room:
# session_id = "matrix:roomX_daarion_space:sofiia" ← ізольований per-agent
# session_id = "matrix:roomX_daarion_space:helion" ← окремий контекст
```
Якщо обидва агенти мають однаковий `session_id` — це баг рефакторингу, відкати на M2.0.
### 9.6 Debug: Concurrency lock "застрявання"
**Симптом:** Запит зависає, не відповідає, active_lock_count > 0 протягом >60s.
```bash
# Перевірити active locks
curl -sS http://127.0.0.1:7030/health | python3 -m json.tool | python3 -c \
"import sys,json; h=json.load(sys.stdin); print(h.get('mixed_guard_rails',{}).get('active_room_agent_locks',0))"
# Якщо > 0 протягом довгого часу — Router застряг
curl -sS http://127.0.0.1:9102/health | jq '.status'
# Якщо Router недоступний — перезапуск bridge звільнить locks (graceful shutdown + cancel)
docker restart matrix-bridge-dagi-node1
```
### 9.7 Guard rail: перевірка MAX_AGENTS_PER_MIXED_ROOM
Якщо у `.env` є рядок з 6+ агентами і `MAX_AGENTS_PER_MIXED_ROOM=5`:
```bash
docker logs matrix-bridge-dagi-node1 --tail 20 2>&1 | grep 'Config error\|MAX_AGENTS'
# Очікується: "❌ Config error: BRIDGE_MIXED_ROOM_MAP parse errors: Room ... has 6 agents > MAX..."
# Bridge не стартує → /health поверне {"ok":false,"error":"..."}
```
**Виправлення:** Зменшити кількість агентів або збільшити `MAX_AGENTS_PER_MIXED_ROOM`.
---
## 8. Що прикріпити до інциденту
```bash
echo "=== /health ===" && curl -sS http://127.0.0.1:7030/health | python3 -m json.tool
echo "=== /bridge/mappings ===" && curl -sS http://127.0.0.1:7030/bridge/mappings | python3 -m json.tool
echo "=== /metrics traffic ===" && curl -sS http://127.0.0.1:7030/metrics \
| grep -E 'matrix_bridge_(messages|rate_limited|queue_dropped|gateway_errors|routed|route_rejected)'
echo "=== /metrics latency ===" && curl -sS http://127.0.0.1:7030/metrics \
| grep -E '(invoke|send|queue_wait)_duration_seconds_(count|sum)'
echo "=== logs ===" && docker logs matrix-bridge-dagi-node1 --tail 50 2>&1 \
| grep -E 'ERROR|WARN|rate_limited|queue_full|Reply sent|invoke ok|route|rejected'
```