fix(scheduler+bot): сироты-чистка не рвёт свежие клеймы (grace 90с); алиас «list» → без-LLM команда /задачи; авто-уведомление Alex при падении тика (e-261001124837-ab3fa7, e-261001124424-d9295f, e-261001134429-c68495)

This commit is contained in:
Simbiot Harness 2026-10-01 15:11:53 +00:00
parent 0523135bf1
commit fa634ce526
3 changed files with 45 additions and 1 deletions

View file

@ -581,6 +581,23 @@ def process_level(level_cfg, model: dict):
})
except Exception:
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]
finally:
st["busy"] = False
@ -638,6 +655,16 @@ def _reflex_tick(level_cfg, seq: int, model: dict):
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():
"""Сироты рестартов: задачи в running без живого воркера → очередь.
@ -663,6 +690,18 @@ def orphan_requeue():
sess = t.get("session_id") or ""
level = t.get("level") or "hour"
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:
# max(tick_timeout, every*0.8)) + запас. Пока воркер жив, он пишет
# след (BudgetSpent/Tool*) каждые секунды-минуты; тишина дольше

View file

@ -30,7 +30,7 @@ _last_fail_ts = 0.0
_ALIASES = {
"лестница": ("лестница", "ladder", "lestnica"),
"статус": ("статус", "status"),
"задачи": ("задачи", "tasks", "zadachi"),
"задачи": ("задачи", "tasks", "zadachi", "list"),
"помощь": ("помощь", "help", "pomoshch", "start", "начать", "команды", "commands"),
}
_ALIAS_TO_CMD = {}
@ -54,6 +54,8 @@ def parse(text) -> "str | None":
Команда: "/имя" (первый токен; @бот отбрасывается) или ОДИНОЧНОЕ
слово-алиас без слэша («лестница»). Фраза с пробелами без слэша —
не команда (обычная речь, её слышит минутный LLM-уровень).
Алиас "list" (e-261001124837-ab3fa7): «доска» из чата — частая
реплика; без алиаса мин-уровень рожал из неё S-клоны на доске.
"""
t = (text or "").strip()
if not t:

View file

@ -58,6 +58,9 @@ check("parse обычная речь", P("привет, как дела?") is No
check("parse пусто", P("") is None)
check("parse /неизвестная", P("/фыва") is None)
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 ──
es.init_db()