fix(scheduler+bot): сироты-чистка не рвёт свежие клеймы (grace 90с); алиас «list» → без-LLM команда /задачи; авто-уведомление Alex при падении тика (e-261001124837-ab3fa7, e-261001124424-d9295f, e-261001134429-c68495)
This commit is contained in:
parent
0523135bf1
commit
fa634ce526
3 changed files with 45 additions and 1 deletions
|
|
@ -581,6 +581,23 @@ def process_level(level_cfg, model: dict):
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # эскалация — best effort: не маскировать исходную ошибку
|
pass # эскалация — best effort: не маскировать исходную ошибку
|
||||||
|
# авто-уведомление Alex в TG (e-261001134429-c68495): падение тика
|
||||||
|
# не должно ждать, пока человек заметит на дашборде или спросит сам.
|
||||||
|
# Идемпотентно (ledger по esc_id — как tg-notify -k): рестарт демона
|
||||||
|
# или повторный проход дубль в чат не пришлёт. Best effort: ошибка
|
||||||
|
# доставки не маскирует исходное падение.
|
||||||
|
try:
|
||||||
|
from . import tg_inbox as _tgi
|
||||||
|
from . import commands as _cmds
|
||||||
|
_cmds.deliver_artifact(
|
||||||
|
f"alert:{esc_id}", kind="message",
|
||||||
|
send=lambda: _tgi.notify_chat(
|
||||||
|
f"🚨 Тик {level_cfg.name} упал: {str(e)[:300]}\n"
|
||||||
|
f"Диагноз: {traceback.format_exc()[-400:]}\n"
|
||||||
|
f"Эскалация {esc_id} поднята наверх — часовой/дневной уровень разберёт."),
|
||||||
|
owner=f"scheduler:{level_cfg.name}")
|
||||||
|
except Exception:
|
||||||
|
pass # уведомление — best effort, не валить обработку ошибки
|
||||||
return "error: " + str(e)[:120]
|
return "error: " + str(e)[:120]
|
||||||
finally:
|
finally:
|
||||||
st["busy"] = False
|
st["busy"] = False
|
||||||
|
|
@ -638,6 +655,16 @@ def _reflex_tick(level_cfg, seq: int, model: dict):
|
||||||
|
|
||||||
DELIVERY_POLL = 15 # сек: проход delivery-демона (push уже сделал большую часть)
|
DELIVERY_POLL = 15 # сек: проход delivery-демона (push уже сделал большую часть)
|
||||||
|
|
||||||
|
# Окно на первый след воркера (e-261001124424-d9295f / e-261001124837-ab3fa7):
|
||||||
|
# воркер после клейма грузится секунды-десятки секунд, ПЕРВЫЙ trace появляется
|
||||||
|
# не мгновенно. Сирота-чистка без окна считала свежевзятую задачу мёртвой
|
||||||
|
# («мёртв: рестарт/таймаут») через 1-2с после клейма и возвращала в очередь —
|
||||||
|
# задача ходила по кругу ready↔running, а мин-LLM соседних тиков видел её
|
||||||
|
# ready и ловил ValueError «завершать можно только running». Grace 90с:
|
||||||
|
# живой воркер успевает начать писать след, умерший-при-старте ловится сразу
|
||||||
|
# после окна (не хуже прежнего мгновенного requeue).
|
||||||
|
CLAIM_GRACE_SEC = 90
|
||||||
|
|
||||||
|
|
||||||
def orphan_requeue():
|
def orphan_requeue():
|
||||||
"""Сироты рестартов: задачи в running без живого воркера → очередь.
|
"""Сироты рестартов: задачи в running без живого воркера → очередь.
|
||||||
|
|
@ -663,6 +690,18 @@ def orphan_requeue():
|
||||||
sess = t.get("session_id") or ""
|
sess = t.get("session_id") or ""
|
||||||
level = t.get("level") or "hour"
|
level = t.get("level") or "hour"
|
||||||
cfg = LEVELS.get(level)
|
cfg = LEVELS.get(level)
|
||||||
|
# GRACE на первый след (e-261001124837-ab3fa7): задача, взятая ТОЛЬКО
|
||||||
|
# ЧТО (клейм свежее окна) — не сирота: воркер ещё грузится и первый
|
||||||
|
# trace/BudgetSpent не успел появиться. Без окна чистка рвала клейм
|
||||||
|
# через 1-2с и гоняла задачу по кругу ready↔running (кейс «list»
|
||||||
|
# 12:43-12:48). Окно от claimed_at (если есть) — не от created.
|
||||||
|
claimed_at = t.get("claimed_at")
|
||||||
|
if claimed_at:
|
||||||
|
try:
|
||||||
|
if (now - datetime.fromisoformat(claimed_at)) < timedelta(seconds=CLAIM_GRACE_SEC):
|
||||||
|
continue # воркер ещё не обязан был показать след
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
# Порог молчания = таймаут убийства воркера (isolated_runner:
|
# Порог молчания = таймаут убийства воркера (isolated_runner:
|
||||||
# max(tick_timeout, every*0.8)) + запас. Пока воркер жив, он пишет
|
# max(tick_timeout, every*0.8)) + запас. Пока воркер жив, он пишет
|
||||||
# след (BudgetSpent/Tool*) каждые секунды-минуты; тишина дольше
|
# след (BudgetSpent/Tool*) каждые секунды-минуты; тишина дольше
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ _last_fail_ts = 0.0
|
||||||
_ALIASES = {
|
_ALIASES = {
|
||||||
"лестница": ("лестница", "ladder", "lestnica"),
|
"лестница": ("лестница", "ladder", "lestnica"),
|
||||||
"статус": ("статус", "status"),
|
"статус": ("статус", "status"),
|
||||||
"задачи": ("задачи", "tasks", "zadachi"),
|
"задачи": ("задачи", "tasks", "zadachi", "list"),
|
||||||
"помощь": ("помощь", "help", "pomoshch", "start", "начать", "команды", "commands"),
|
"помощь": ("помощь", "help", "pomoshch", "start", "начать", "команды", "commands"),
|
||||||
}
|
}
|
||||||
_ALIAS_TO_CMD = {}
|
_ALIAS_TO_CMD = {}
|
||||||
|
|
@ -54,6 +54,8 @@ def parse(text) -> "str | None":
|
||||||
Команда: "/имя" (первый токен; @бот отбрасывается) или ОДИНОЧНОЕ
|
Команда: "/имя" (первый токен; @бот отбрасывается) или ОДИНОЧНОЕ
|
||||||
слово-алиас без слэша («лестница»). Фраза с пробелами без слэша —
|
слово-алиас без слэша («лестница»). Фраза с пробелами без слэша —
|
||||||
не команда (обычная речь, её слышит минутный LLM-уровень).
|
не команда (обычная речь, её слышит минутный LLM-уровень).
|
||||||
|
Алиас "list" (e-261001124837-ab3fa7): «доска» из чата — частая
|
||||||
|
реплика; без алиаса мин-уровень рожал из неё S-клоны на доске.
|
||||||
"""
|
"""
|
||||||
t = (text or "").strip()
|
t = (text or "").strip()
|
||||||
if not t:
|
if not t:
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,9 @@ check("parse обычная речь", P("привет, как дела?") is No
|
||||||
check("parse пусто", P("") is None)
|
check("parse пусто", P("") is None)
|
||||||
check("parse /неизвестная", P("/фыва") is None)
|
check("parse /неизвестная", P("/фыва") is None)
|
||||||
check("parse /cmd с аргументом", P("/status сейчас") == "статус")
|
check("parse /cmd с аргументом", P("/status сейчас") == "статус")
|
||||||
|
check("parse aлиас list → задачи (e-261001124837-ab3fa7)", P("list") == "задачи")
|
||||||
|
check("parse LIST регистр", P("LIST") == "задачи")
|
||||||
|
check("parse list с пробелами — НЕ задача-клон", P("list please") is None)
|
||||||
|
|
||||||
# ── 2. хендлеры: непустой текст, без исключений, без сырого HTML-рискa ──
|
# ── 2. хендлеры: непустой текст, без исключений, без сырого HTML-рискa ──
|
||||||
es.init_db()
|
es.init_db()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue