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

19 KiB
Raw Blame History

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

curl -sS http://127.0.0.1:7030/health | python3 -m json.tool

Очікування:

{
  "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

curl -sS http://127.0.0.1:7030/bridge/mappings | python3 -m json.tool

1.3 Logs (останні 30 рядків)

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)

# Логін як 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)

curl -sS https://matrix.daarion.space/.well-known/matrix/client
# Очікування: {"m.homeserver":{"base_url":"https://matrix.daarion.space"}}

3. Операційні Метрики

3.1 Traffic counters

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

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

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)

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 не відповідає"

# 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"

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):

# На 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 велике"

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 малий.

Фікс:

# Збільшити 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"

# 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"

# 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"

# 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

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

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

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 (термін дії)

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

# Поточний 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:

# Змінна для зручності
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 метрик

# 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, або відповідає не той агент.

Діагностика:

# 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)

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.

# Перевірити 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:

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. Що прикріпити до інциденту

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'