Комментарии 2
Хороший разбор, особенно место про фоновые уведомления: проверка chat_id против текущей сессии — ровно та защита, которую легко забыть.
Про глобальный monkey-patch: есть вариант, который убирает его и для «чужих» ботов, где Bot создаётся глубоко внутри. Поднять рядом маленький HTTP-сервер, который отвечает в формате Bot API, и направить бота на него через base_url (в PTB это ApplicationBuilder().base_url(...), в aiogram — TelegramAPIServer). Если бот даёт переопределить адрес API конфигом или переменной окружения, исходный код не трогается вообще.
Побочный плюс такого разреза: ответы маршрутизируются по chat_id из самого запроса, а не по ContextVar. Тогда фоновые уведомления перестают быть отдельным классом проблем — сервер просто кладёт sendMessage в очередь нужного пользователя, есть у него открытая вкладка или нет. И исчезает зависимость от версии PTB: работает с 20.7, 21.x и с ботами не на PTB.
Ещё одно место, где жизненные циклы расходятся так же, как у кнопок: ConversationHandler. Если бот держит состояния диалога без persistence, после рестарта адаптера пользователь окажется в середине сценария в браузере и в начале — в памяти бота. Для веб-режима я бы включал PicklePersistence или свою persistence хотя бы для conversations, иначе часть «устаревших кнопок» будет на самом деле устаревшими состояниями.

Как я запустил Telegram-бота в браузере, не переписывая его обработчики