Approve завис и вот-вот протухнет
Human-in-the-loop закрывает момент «агент встал, нужен ваш approve прямо сейчас». Но реальная боль приходит позже: push прилетел, вы были за рулём, потом забыли — и approve висит. Через час пайплайн так и не поехал, задача вот-вот протухнет (авто-deny или блокировка деплоя), а вы даже не помните, что там ждали.
Здесь фокус не на «нужен approve», а на дедлайне и эскалации: approve висит слишком долго и скоро истечёт. Один push при постановке в очередь — мало. Нужен sweeper, который смотрит на просроченные approve и напоминает всё громче по мере приближения дедлайна.
Хранилище pending-approvals с дедлайнами
Заголовок раздела «Хранилище pending-approvals с дедлайнами»Каждый approve — строка со своим дедлайном и лестницей уже отправленных напоминаний. Агент кладёт запрос, дальше живёт своей жизнью.
import os, time, json, sqlite3, requests
DB = os.environ.get("APPROVALS_DB", "/state/approvals.db")
def _db(): db = sqlite3.connect(DB) db.execute("""CREATE TABLE IF NOT EXISTS approvals( id TEXT PRIMARY KEY, what TEXT, created REAL, deadline REAL, status TEXT, rung INTEGER)""") return db
def request_approval(req_id, what, ttl_sec=3600): """Агент паркует задачу: approve нужен до created+ttl.""" now = time.time() db = _db() db.execute("INSERT OR REPLACE INTO approvals VALUES (?,?,?,?,?,?)", (req_id, what, now, now + ttl_sec, "pending", 0)) db.commit() notify("🤖 Нужен approve", f"{what}\nОтветить до {time.strftime('%H:%M', time.localtime(now + ttl_sec))}.", priority=6)
def resolve(req_id, verdict): """Вызывается из вашего callback (approve/deny), см. human-in-the-loop.""" db = _db() db.execute("UPDATE approvals SET status=? WHERE id=?", (verdict, req_id)) db.commit()
def notify(title, message, priority): requests.post(f"{os.environ['NOTIFLY_URL']}/message", params={"token": os.environ["NOTIFLY_TOKEN"]}, json={"title": title, "message": message, "priority": priority}, timeout=5)Как принять сам вердикт (кнопка / callback-URL / WebScript) — целиком в
human-in-the-loop. Здесь важно
только, что resolve() перекладывает строку из pending в approve/deny.
Sweeper: эскалируем просроченные растущим приоритетом
Заголовок раздела «Sweeper: эскалируем просроченные растущим приоритетом»Отдельный процесс (cron / Yandex Cloud function по
таймеру) раз в несколько минут пробегает по pending и решает, кого пора
пнуть. Чем ближе дедлайн — тем громче.
def sweep(): now = time.time() db = _db() rows = db.execute( "SELECT id, what, deadline, rung FROM approvals WHERE status='pending'" ).fetchall()
for req_id, what, deadline, rung in rows: left = deadline - now
# лестница напоминаний: чем меньше времени, тем выше приоритет и rung if left <= 0: ladder = (3, 10) # дедлайн пробит — громкий финал elif left <= 5 * 60: ladder = (2, 9) # < 5 мин — почти истекло elif left <= 15 * 60: ladder = (1, 7) # < 15 мин — пора else: continue # ещё рано напоминать
want_rung, prio = ladder if rung >= want_rung: continue # эту ступень уже отправляли
mins = max(int(left // 60), 0) notify("⏳ Approve скоро протухнет", f"{what}\nОсталось ~{mins} мин. Ответьте, иначе авто-deny.", priority=prio) db.execute("UPDATE approvals SET rung=? WHERE id=?", (want_rung, req_id)) db.commit()
if __name__ == "__main__": sweep()Поле rung — та же идея, что флаг _warned в
budget-guard: каждую ступень
лестницы шлём ровно один раз, чтобы sweeper не спамил одинаковым push
каждые пять минут.
Авто-deny на истечении (fail-safe)
Заголовок раздела «Авто-deny на истечении (fail-safe)»Молчание — это ответ «нет». Если approve нужен для необратимого действия
(git push --force, миграция, выкат), безопаснее по истечении дедлайна
авто-денайить и разблокировать пайплайн отказом, а не висеть вечно:
def expire_overdue(): now = time.time() db = _db() dead = db.execute( "SELECT id, what FROM approvals WHERE status='pending' AND deadline < ?", (now,)).fetchall() for req_id, what in dead: db.execute("UPDATE approvals SET status='auto-deny' WHERE id=?", (req_id,)) notify("🛑 Approve авто-отклонён", f"{what}\nНикто не ответил вовремя — задача отменена (safe default).", priority=8) db.commit()Вешаете expire_overdue() в тот же sweeper после sweep(). Агент,
опрашивая своё хранилище, увидит статус auto-deny и корректно откатит
задачу вместо бесконечного ожидания.
Лестница напоминаний вместо стены
Заголовок раздела «Лестница напоминаний вместо стены»Если хочется мягче — не привязывайте эскалацию к дедлайну жёстко, а напоминайте по расписанию: первый пинг тихий/обычный, дальше — громче.
| Прошло с запроса | Приоритет | Смысл |
|---|---|---|
| сразу | 6 | «есть задача на approve» |
| +20 мин | 7 | «всё ещё висит» |
| +45 мин | 9 | «дедлайн близко, ответьте» |
| дедлайн | 10 | «истекло, авто-deny» |
Такой ладдер уважает ваше внимание: вы не тонете в громких push, но задачу физически невозможно проспать.
Что положить в push
Заголовок раздела «Что положить в push»- что именно ждёт approve — команда, файл, ветка, имя миграции;
- сколько осталось до дедлайна и что случится на истечении (авто-deny?);
req_id— чтобы найти запрос в логах и в хранилище;- ссылка на approve/deny — чтобы ответить прямо из push, не открывая ноутбук.
Связанные рецепты
Заголовок раздела «Связанные рецепты»- Нужен human-in-the-loop — как поймать approve прямо сейчас; этот рецепт — про его дедлайн.
- Зависший агент / loop — агент молчит не потому, что ждёт вас, а потому что завис.
- Heartbeat (dead-man-switch) — чтобы запустить sweeper по таймеру.