fix(whatsapp): не дать ассистенту молча умирать — токен и реконнект - #835
Merged
Conversation
Два независимых дефекта уронили 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Что случилось
WhatsApp-ассистент лежал двумя независимыми поломками:
Обе починены; прод уже восстановлен вручную, этот PR закрывает причины.
1. Внутренний токен бота: 24 часа против недель жизни процесса
Бот-подпроцесс получает JWT один раз, в env, при спавне и не умеет его обновлять.
create_session()при этом всегда выдавала admin-TTL в 24 часа (ADMIN_JWT_EXPIRATION_HOURS). Процесс живёт неделями → через сутки каждый запрос к/admin/chat/sessionsотдаёт 401,llm_router._ensure_sessionкидаетHTTPStatusError, клиент видит «ошибка». Бридж при этомconnected, сообщения принимаются, квитанции о прочтении уходят.На проде: токен
iat14 авг 20:44 →exp15 авг 20:44. Ровно +24ч.Поломку не поймали, потому что мониторится статус коннекта, а не доходимость ответа.
create_access_token()/create_session()принимаютexpires_hoursBOT_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, бэкап рядом. Проверено: VPNamn0не в пути host-трафика, и DNS, и WhatsApp идут напрямую через eth0.Проверка
tests/unit/test_bot_internal_token.py— 6 новых тестов, пройдены: TTL-override, неизменность admin-TTL, точечность отзыва поuser_agent, чистка кэша только своего JTItest_dataset_synced.py) воспроизводятся на чистомmain— предсуществующие, не из этого PRruff check+ruff format --checkчисто,node --checkна бридже чисто🤖 Generated with Claude Code