Skip to content

fix(whatsapp): не дать ассистенту молча умирать — токен и реконнект - #835

Merged
ShaerWare merged 1 commit into
mainfrom
local/fix/whatsapp-assistant-resilience
Aug 20, 2026
Merged

fix(whatsapp): не дать ассистенту молча умирать — токен и реконнект#835
ShaerWare merged 1 commit into
mainfrom
local/fix/whatsapp-assistant-resilience

Conversation

@ShaerWare

Copy link
Copy Markdown
Owner

Что случилось

WhatsApp-ассистент лежал двумя независимыми поломками:

  • с 15 августа — отвечал ошибкой на каждое сообщение при живом коннекте;
  • с 18 августа — канал отвалился целиком и не поднялся сам.

Обе починены; прод уже восстановлен вручную, этот PR закрывает причины.

1. Внутренний токен бота: 24 часа против недель жизни процесса

Бот-подпроцесс получает JWT один раз, в env, при спавне и не умеет его обновлять. create_session() при этом всегда выдавала admin-TTL в 24 часа (ADMIN_JWT_EXPIRATION_HOURS). Процесс живёт неделями → через сутки каждый запрос к /admin/chat/sessions отдаёт 401, llm_router._ensure_session кидает HTTPStatusError, клиент видит «ошибка». Бридж при этом connected, сообщения принимаются, квитанции о прочтении уходят.

На проде: токен iat 14 авг 20:44 → exp 15 авг 20:44. Ровно +24ч.

Поломку не поймали, потому что мониторится статус коннекта, а не доходимость ответа.

  • create_access_token() / create_session() принимают expires_hours
  • BOT_INTERNAL_TOKEN_HOURS (env, default 1 год) — для внутренних сессий; человеческий admin-TTL не тронут
  • сессии по-прежнему регистрируются в user_sessions → отзыв и аудит работают, рестарт бота ротирует токен
  • revoke_internal_sessions() гасит прошлый токен инстанса, чтобы годовые admin-токены не копились при каждом рестарте

Тонкость в ротации: внутренний токен выписывается на admin user_id, поэтому отзыв по user_id вышиб бы админа из браузера, а отзыв по общему user_agent — соседние боты. Теперь user_agent несёт instance_id, и отзыв точечный. Кэш JTI чистится первым: хит в SessionCache короткозамыкает проверку в БД, так что токен, отозванный только в БД, продолжал бы работать до рестарта процесса.

2. Бридж сдавался навсегда после 12 реконнектов

MAX_RECONNECT_ATTEMPTS = 12 при backoff 2s→4s→8s→16s→32s→60s×7 даёт бюджет ~8 минут. 18 августа десятиминутная просадка DNS его сожгла, бридж написал giving up reconnecting, manual start required и остался мёртвым до ручного старта.

Дефект в том, что лимит бьёт только по восстановимым ошибкам: loggedOut и connectionReplaced уходят по раннему return и до этой ветки не доходят, restartRequired сбрасывает счётчик. То есть единственное, что он реально убивал, — транзиентные сбои, от которых сессия восстановилась бы сама.

Теперь реконнект бесконечный, прижатый к максимальной задержке — одна попытка в минуту стоит ничего, а сессия лечится сама, как только чинится сеть. После старого бюджета лог поднимается до warn, чтобы реально залипшая сессия оставалась видимой оператору.

Отдельно, на проде (не в git)

/etc/resolv.conf имел options timeout:1 attempts:1 без резервного резолвера — одна медленная выдача сразу давала EAI_AGAIN (те же ошибки видны в RSS-логах как «Temporary failure in name resolution»). Смягчено до timeout:2 attempts:2 + фолбэк 1.1.1.1, бэкап рядом. Проверено: VPN amn0 не в пути host-трафика, и DNS, и WhatsApp идут напрямую через eth0.

Проверка

  • tests/unit/test_bot_internal_token.py — 6 новых тестов, пройдены: TTL-override, неизменность admin-TTL, точечность отзыва по user_agent, чистка кэша только своего JTI
  • полный юнит-набор: 244 passed, 2 failed; оба падения (test_dataset_synced.py) воспроизводятся на чистом main — предсуществующие, не из этого PR
  • ruff check + ruff format --check чисто, node --check на бридже чисто

🤖 Generated with Claude Code

Два независимых дефекта уронили WhatsApp-канал: с 15 августа ассистент
отвечал ошибкой при живом коннекте, с 18 августа лежал целиком.

Внутренний токен бота (24ч → жизнь процесса)

Бот-подпроцесс получает JWT один раз, в env, при спавне и не умеет его
обновлять, а create_session() всегда выдавала admin-TTL в 24 часа. Процесс
живёт неделями → через сутки каждый запрос к /admin/chat/sessions отдаёт
401, клиент видит «ошибка», при этом бридж connected и сообщения приходят.
Поэтому и не заметили: мониторится статус коннекта, а не доходимость ответа.

- create_access_token()/create_session() принимают expires_hours
- BOT_INTERNAL_TOKEN_HOURS (env, default 1 год) для внутренних сессий;
  человеческий admin-TTL не меняется
- сессии по-прежнему в user_sessions → отзыв и аудит работают
- revoke_internal_sessions() ротирует прошлый токен инстанса: user_agent
  теперь несёт instance_id, поэтому отзыв не задевает ни другие боты, ни
  браузерные сессии админа (внутренний токен выписан на admin user_id).
  Кэш JTI чистится первым — хит в кэше короткозамыкает проверку в БД,
  и отозванный только в БД токен продолжал бы работать

Бридж сдавался навсегда после 12 реконнектов

MAX_RECONNECT_ATTEMPTS=12 при backoff 2s→60s = бюджет ~8 минут. 18 августа
десятиминутная просадка DNS его сожгла, сессия осталась мёртвой до ручного
старта — двое суток простоя.

Дефект в том, что лимит бьёт только по восстановимым ошибкам: loggedOut и
connectionReplaced уходят по раннему return, restartRequired сбрасывает
счётчик. То есть он убивал ровно те сбои, от которых сессия восстановилась
бы сама. Теперь реконнект бесконечный, прижатый к максимальной задержке
(одна попытка в минуту), а после старого бюджета лог поднимается до warn,
чтобы реально залипшая сессия оставалась видимой.

Отдельно, на проде (не в git): /etc/resolv.conf имел timeout:1 attempts:1
без резервного резолвера — одна медленная выдача давала EAI_AGAIN. Смягчено
до timeout:2 attempts:2 + фолбэк 1.1.1.1, бэкап рядом.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ShaerWare
ShaerWare merged commit 3cc6d15 into main Aug 20, 2026
3 checks passed
@ShaerWare
ShaerWare deleted the local/fix/whatsapp-assistant-resilience branch August 20, 2026 20:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant