Перейти к содержимому

Approve завис и вот-вот протухнет

Human-in-the-loop закрывает момент «агент встал, нужен ваш approve прямо сейчас». Но реальная боль приходит позже: push прилетел, вы были за рулём, потом забыли — и approve висит. Через час пайплайн так и не поехал, задача вот-вот протухнет (авто-deny или блокировка деплоя), а вы даже не помните, что там ждали.

Здесь фокус не на «нужен approve», а на дедлайне и эскалации: approve висит слишком долго и скоро истечёт. Один push при постановке в очередь — мало. Нужен sweeper, который смотрит на просроченные approve и напоминает всё громче по мере приближения дедлайна.

Каждый 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 каждые пять минут.

Молчание — это ответ «нет». Если 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, но задачу физически невозможно проспать.

  • что именно ждёт approve — команда, файл, ветка, имя миграции;
  • сколько осталось до дедлайна и что случится на истечении (авто-deny?);
  • req_id — чтобы найти запрос в логах и в хранилище;
  • ссылка на approve/deny — чтобы ответить прямо из push, не открывая ноутбук.