Разбор гейта (трассы с 15.10): hour — 0% пустых тиков, min — 6% (15% токенов уровня) → модель-гейт не нужен. Пустые мин-тики — зацикливание: «бюджет исчерпан» возвращал S как S, мин брал её снова с нуля (S-задача 33 захвата за 2 ч; 45 из 272 задач ≥ 2 мин-захватов). - requeue_or_escalate: после 2 захватов на уровне (SIMBIOT_TASK_ATTEMPTS_ PER_LEVEL) размер растёт выше классов уровня (S→M, к часу, 300 шагов) — и для guard «бюджет исчерпан», и для смерти воркера; счётчик попыток best effort (сбой → обычный requeue, не ломает путь падения воркера); - kodi_core — несколько голов (общие: хранение/обучение/промоут/откат, nf в модели; SIMBIOT_CORE_<ГОЛОВА>). Голова task_fit: влезет ли S в мин-тик (метки — из истории задач); shadow — прогноз перед первым мин-захватом, on — уверенное «не влезет» сразу S→M. Честно: на 93 задачах 5 фолдов 0.58–0.62 при базе 0.52, уверенных 0% — пока инертна; - рефлексия (как reflexion.py): ≥ 3 расхождений вердикта адресации с меткой человека/чата или ≥ 3 S-задач, не влезших в мин → задача часу с автодоставкой: разобрать и предложить Alex правило (код не трогать); - недельный отчёт ядра (понедельник, без LLM, идемпотентно по неделе); ночью: train_all → reflect → weekly_report; cli.py core reflect|report|--head. Тесты: test_task_escalation 8, test_kodi_core_tasks 13, test_kodi_core_ reflect 10, test_kodi_core 28 PASS; полный набор — без регрессий. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
89 lines
3.8 KiB
Python
89 lines
3.8 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Эскалация недоделанных задач по уровням (аудит 2026-10-17). Hermetic.
|
||
|
||
Запуск: ./venv/bin/python tests/test_task_escalation.py
|
||
|
||
Кейс: «бюджет исчерпан» возвращал S-задачу как S — мин-уровень брал её
|
||
по кругу с нуля (33 захвата одной задачи за 2 ч). Проверяет: 1-я неудача
|
||
на min — остаётся S; 2-я — S→M, мин её больше не берёт, час берёт;
|
||
guard «бюджет исчерпан» идёт тем же путём; закрытую внутри тика задачу
|
||
guard не трогает; на часу (берёт S/M/L) размер не растёт — расти некуда;
|
||
порог настраивается env.
|
||
"""
|
||
import os
|
||
import sys
|
||
import tempfile
|
||
|
||
sys.path.insert(0, "/root/simbiot-harness")
|
||
os.environ["SIMBIOT_DB"] = os.path.join(tempfile.mkdtemp(prefix="simbiot_esc_"), "harness.db")
|
||
os.environ.pop("SIMBIOT_TASK_ATTEMPTS_PER_LEVEL", None)
|
||
|
||
from harness import es, scheduler
|
||
from harness.levels import LEVELS
|
||
|
||
es.init_db()
|
||
fails = []
|
||
|
||
|
||
def check(name, cond, extra=""):
|
||
print(("PASS" if cond else "FAIL"), name, ("| " + str(extra)) if not cond else "")
|
||
if not cond:
|
||
fails.append(name)
|
||
|
||
|
||
def submit(tid, size="S"):
|
||
es.append_event("task", tid, "TaskSubmitted", {"id": tid, "title": f"задача {tid}",
|
||
"size": size, "created": "2026-10-17T08:00:00"})
|
||
|
||
|
||
def claim(tid, lvl="min"):
|
||
assert es.claim_task_if_ready(tid, {"level": lvl, "session_id": f"s-{lvl}-1"})
|
||
|
||
|
||
def task(tid):
|
||
return es.run_projections()["tasks"][tid]
|
||
|
||
|
||
MIN, HOUR = LEVELS["min"], LEVELS["hour"]
|
||
submit("t-1")
|
||
claim("t-1")
|
||
scheduler.requeue_or_escalate("t-1", MIN, "S", "бюджет исчерпан")
|
||
check("1-я неудача на min — остаётся S, ready", task("t-1")["size"] == "S"
|
||
and task("t-1")["status"] == "ready")
|
||
claim("t-1")
|
||
scheduler.requeue_or_escalate("t-1", MIN, "S", "бюджет исчерпан")
|
||
t = task("t-1")
|
||
check("2-я неудача на min — S→M", t["size"] == "M" and t["status"] == "ready", t)
|
||
check("мин больше не берёт", "t-1" not in scheduler.select_task_batch(MIN, es.run_projections()))
|
||
check("час берёт", "t-1" in scheduler.select_task_batch(HOUR, es.run_projections()))
|
||
|
||
# guard «бюджет исчерпан» с уровнем — тот же путь
|
||
submit("t-2")
|
||
for _ in range(2):
|
||
claim("t-2")
|
||
scheduler._guard_budget_stub_requeue(["t-2"], "бюджет исчерпан", MIN,
|
||
[{"id": "t-2", "size": task("t-2")["size"]}])
|
||
check("guard: после 2 попыток S→M", task("t-2")["size"] == "M", task("t-2"))
|
||
|
||
# закрытую внутри тика guard не воскрешает
|
||
submit("t-3")
|
||
claim("t-3")
|
||
es.append_event("task", "t-3", "TaskCompleted", {"result": "сделано"})
|
||
scheduler._guard_budget_stub_requeue(["t-3"], "бюджет исчерпан", MIN, [{"id": "t-3", "size": "S"}])
|
||
check("guard: закрытая задача не возвращается", task("t-3")["status"] == "done", task("t-3")["status"])
|
||
|
||
# на часу размер не растёт: час берёт S/M/L
|
||
submit("t-4", "M")
|
||
for _ in range(3):
|
||
claim("t-4", "hour")
|
||
scheduler.requeue_or_escalate("t-4", HOUR, "M", "бюджет исчерпан")
|
||
check("час: размер не растёт (выше брать некому)", task("t-4")["size"] == "M")
|
||
|
||
os.environ["SIMBIOT_TASK_ATTEMPTS_PER_LEVEL"] = "1"
|
||
submit("t-5")
|
||
claim("t-5")
|
||
scheduler.requeue_or_escalate("t-5", MIN, "S", "x")
|
||
check("порог из env (1 попытка)", task("t-5")["size"] == "M")
|
||
|
||
print(f"\n{'OK' if not fails else 'FAILED'}: {len(fails)} fail(s)")
|
||
sys.exit(1 if fails else 0)
|