Расписание common.check в 00:35 совпало с выкаткой: старый процесс успел отправить цепочку, новый после старта отправил её ещё раз. Теперь каждая отработанная минута атомарно отмечается в bot.schedule_runs; если отметка уже есть, рассылка пропускается. Старые отметки чистятся через 7 дней.
В админке появился раздел «Расписания»: время, дни недели, таймзона, шаблон и вкл/выкл, с историей изменений и удалением. Бот раз в минуту читает активные строки из БД, поэтому правки применяются без перезапуска; при сбое базы он переживает его на последнем прочитанном наборе или на расписании из кода. Стартовые рассылки перенесены миграцией V16, так что поведение не меняется. Заодно в зависимости добавлен tzdata: в slim-образах без него падает ZoneInfo на Europe/Moscow.