guard(tasks): «бюджет исчерпан» — не результат (e-261001074527): complete со стабом → ValueError + авто-requeue в ready; батч-финал тика со стабом → requeue всего клея; tests/test_budget_stub_guard.py 14 checks (FAILS 0)

This commit is contained in:
Simbiot Harness 2026-10-01 08:25:05 +00:00
parent ed7ffd4461
commit 4e7f62d35a
3 changed files with 150 additions and 0 deletions

View file

@ -138,6 +138,27 @@ def mark_awaiting_delivery(task_id: str, reason: str = "", reply_to: str = None)
return task_id
import re as _re
# Guard (e-261001074527-f0c1b4): ночной тик 20727 пачкой закрыл 5 L-задач
# шаблонной отпиской воркера «бюджет исчерпан» — это стоп-фраза тика, а не
# результат. Такие «результаты» закрывать задачу не могут.
_BUDGET_STUB_RE = _re.compile(r"бюджет\s+(?:шагов\s+)?исчерпан", _re.IGNORECASE)
_BUDGET_STUB_MARKERS_RE = _re.compile(r"тик\s+завершён|tool-вызов|шагов", _re.IGNORECASE)
def looks_like_budget_stub(answer: str) -> bool:
"""True, если answer — шаблонная отписка воркера о бюджете шагов.
Требование маркера («тик завершён», «tool-вызов», «шагов») отсекает
ложные срабатывания: честный результат работы, где по делу упомянут
исчерпанный бюджет («проверил, бюджет месяца исчерпан»), под гвард
не попадает — а в стоп-фразе воркера маркер всегда есть.
"""
s = answer or ""
return bool(_BUDGET_STUB_RE.search(s) and _BUDGET_STUB_MARKERS_RE.search(s))
def complete_task_with_delivery(task_id: str, answer: str, level: str,
result_ref: str = None):
"""TaskCompleted + немедленная доставка результата в TG-чат.
@ -157,6 +178,12 @@ def complete_task_with_delivery(task_id: str, answer: str, level: str,
raise ValueError(f"задача {task_id} в статусе {t['status']} — завершать можно только running")
if (answer or "").strip() == "":
raise ValueError("пустой результат — доставлять нечего, заверши как failed/requeue")
if looks_like_budget_stub(answer):
requeue_task(task_id, reason="guard: 'бюджет исчерпан' — не результат, задача в ready (инцидент e-261001074527)")
raise ValueError(
f"задача {task_id} ПЕРЕВОДЖЕНА в ready: отпиской «бюджет исчерпан» "
f"задачу закрывать нельзя. Либо доделай и завершай с реальным "
f"результатом, либо явно requeue/failed.")
# TaskCompleted — независимо от доставки (событие-истина о завершении)
es.append_event("task", task_id, "TaskCompleted", {
"result_ref": result_ref, "answer": answer[:2000],

View file

@ -468,6 +468,14 @@ def run_level_tick(level_cfg, seq: int, model: dict):
# исполнитель пишет в дайджесте). Доставка — той же командой
# commands.deliver_result (идемпотентна: без флага/уже sent — Skip).
# Ответ урезаем на задачу, чтобы 6 задач не раздули 6×2000.
# GUARD (e-261001074527-f0c1b4): финальный текст тика «бюджет исчерпан» —
# стоп-сообщение воркера, а не результат; ночь 20727 им фейково закрылись
# 5 L-задач. Такой тик работы не завершал — задачи возвращаются в ready.
from .commands import looks_like_budget_stub, requeue_task as _requeue
if (text or "") and looks_like_budget_stub(text):
for tid in (claimed or []):
_requeue(tid, reason="guard: финал тика «бюджет исчерпан» — работа не завершена, задача в ready (e-261001074527)")
return text, []
for tid in (claimed or []):
es.append_event("task", tid, "TaskCompleted", {
"result_ref": trace_id, "answer": (text or "")[:2000],

View file

@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-
"""Guard бюджет-стабов (e-261001074527-f0c1b4, hermetic, без LLM/TG).
Запуск: ./venv/bin/python tests/test_budget_stub_guard.py
Ночной тик 20727 пачкой закрыл 5 L-задач отпиской воркера «бюджет исчерпан».
Гвард: complete_task_with_delivery со стоп-фразой → ValueError + авто-requeue
в ready (TaskPromoted), TaskCompleted НЕ пишется. Честные результаты со
встречным упоминанием бюджета гвард не трогает.
Проверяет: looks_like_budget_stub на ночных текстах (стоп-фраза воркера,
end_turn-сообщение tool-budget, шаблон ночной доставки) — true; честные
результаты («уточнил бюджет», «бюджет рекламной кампании исчерпан») — false;
complete со стабом → ValueError + задача вернулась в ready; повторный
complete с реальным результатом → done. CLI requeue по guard-сообщению
работает (задача не теряется).
"""
import os
import sys
_DST = "/tmp/simbiot-test-guard.db"
for p in (_DST, _DST + "-wal", _DST + "-shm"):
if os.path.exists(p):
os.unlink(p)
os.environ["SIMBIOT_DB"] = _DST
sys.path.insert(0, "/root/simbiot-harness")
from harness import es
from harness import commands
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 task_state(tid):
model = es.run_projections()
t = model["tasks"].get(tid)
return t["status"] if t else None
def task_events(tid):
db = es._conn()
try:
rows = db.execute("SELECT event_type FROM events WHERE aggregate_id=?", (tid,)).fetchall()
return [r["event_type"] for r in rows]
finally:
db.close()
# ── 1. Predicate: ночные шаблоны распознаются ──
nightly = [
"✅ Готово, тик завершён: бюджет исчерпан",
"тик завершён: бюджет шагов исчерпан (30 tool-вызовов). Выше — результат того, что успело выполниться.",
"тиковый бюджет шагов исчерпан (30 tool-вызовов): вызов отменён; новые вызовы инструментов отключены до конца тика",
]
for i, txt in enumerate(nightly):
check(f"1.{i}: stub распознан", commands.looks_like_budget_stub(txt), txt[:50])
# ── 2. Predicate: честные результаты не трогаем ──
honest = [
"Утверждён оффер: 10 мест × $99.9/мес. Задача закрыта, счётчик на лендинг — следующая задача.",
"Проверил рекламный кабинет: бюджет рекламной кампании исчерпан, переключил на резервный.",
"Проанализировал траты за месяц; бюджет месяца исчерпан частично — отчёт готов.",
"Рефакторинг завершён из 3 шагов. Всё влезло, бюджет не трогали.",
]
for i, txt in enumerate(honest):
check(f"2.{i}: честный не стаб", not commands.looks_like_budget_stub(txt), txt[:50])
# ── 3. Живая задача: complete со стабом → ValueError + requeue в ready ──
tid, _sz = commands.submit_task(title="Тест guard", minutes=30,
details="тестовая задача гвирда бюджет-стабов")
es.append_event("task", tid, "TaskClaimed", {"level": "hour", "session_id": "t-guard"})
es.run_projections()
check("3.0: задача running", task_state(tid) == "running")
stub = "тик завершён: бюджет исчерпан"
try:
commands.complete_task_with_delivery(tid, stub, level="hour")
check("3.1: complete со стабом отклонён", False, "нет ValueError")
except ValueError as e:
check("3.1: complete со стабом отклонён", "ПЕРЕВЕД" in str(e).upper() or "ready" in str(e), str(e)[:80])
check("3.2: после guard — задача в ready", task_state(tid) == "ready", task_state(tid))
check("3.3: TaskCompleted НЕ записан", "TaskCompleted" not in task_events(tid), task_events(tid))
check("3.4: requeue-событие есть", "TaskPromoted" in task_events(tid))
# ── 4. Тот же тик: повторный complete с реальным результатом → done ──
es.append_event("task", tid, "TaskClaimed", {"level": "hour", "session_id": "t-guard2"})
tid2, delivered = commands.complete_task_with_delivery(
tid, "Реальный результат: расчёт суммы бюджета проекта завершён", level="hour")
check("4.0: честный complete после requeue", task_state(tid) == "done", task_state(tid))
check("4.1: TaskCompleted записан", task_events(tid).count("TaskCompleted") == 1)
# ── 5. Честный «бюджет … исчерпан» в реальном результате → TaskCompleted ок ──
tid3, _sz3 = commands.submit_task(title="Тест честного", minutes=30,
details="тест: результат содержит бюджет-фразу по делу")
es.append_event("task", tid3, "TaskClaimed", {"level": "hour", "session_id": "t-guard3"})
try:
commands.complete_task_with_delivery(
tid3, "Проверил кабинет: бюджет рекламной кампании исчерпан, отчёт готов",
level="hour")
check("5.0: честный результат проходит", task_state(tid3) == "done", task_state(tid3))
except ValueError as e:
check("5.0: честный результат проходит", False, str(e)[:80])
print()
print("FAILS:", len(fails))
sys.exit(1 if fails else 0)