From 3f2300700cded1192ebdd3ce5133d9cfad81154d Mon Sep 17 00:00:00 2001 From: Simbiot Harness Date: Sun, 4 Oct 2026 15:07:31 +0000 Subject: [PATCH] =?UTF-8?q?fix(e-261004144805):=20=D1=81=D0=BC=D0=B5=D1=80?= =?UTF-8?q?=D1=82=D1=8C=20=D1=82=D0=B8=D0=BA=D0=B0=20=D0=BD=D0=B5=20=D0=B2?= =?UTF-8?q?=D0=BE=D1=81=D0=BA=D1=80=D0=B5=D1=88=D0=B0=D0=B5=D1=82=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BA=D1=80=D1=8B=D1=82=D1=8B=D0=B5=20=D0=B7=D0=B0=D0=B4?= =?UTF-8?q?=D0=B0=D1=87=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Слепой TaskPromoted/TaskFailed по списку claimed возвращал в очередь задачи, уже закрытые воркером ВНУТРИ тика до таймаута (кейс: полная миграция Гермеса→Vybirai complete+delivery в 14:31, тик убит 14:48, задача воскрешена — двойная работа и риск дублей). _task_still_active(): живая проекция как истина; только running ре-кьюится/фейлится. Ошибка чтения → прежнее поведение (True). --- harness/scheduler.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/harness/scheduler.py b/harness/scheduler.py index ba855cb..3acdc33 100644 --- a/harness/scheduler.py +++ b/harness/scheduler.py @@ -423,6 +423,23 @@ def _guard_budget_stub_requeue(claimed, text: str): return text, [] +def _task_still_active(task_id: str) -> bool: + """Живая ли задача для целей requeue/fail при смерти воркера тика. + + e-261004144805-b9d6aa: воркер мог успеть закрыть задачу (TaskCompleted) + ВНУТРИ длинного тика до таймаута — тогда слепой TaskPromoted/TaskFailed + по списку claimed воскрешал готовую работу. Истина — свежая проекция, + а не список claim'ов на старте тика. Только running считается активной; + ошибка чтения → консервативно True (прежнее поведение). + """ + try: + model = es.run_projections() + t = (model.get("tasks") or {}).get(task_id) + return bool(t) and t.get("status") == "running" + except Exception: + return True + + def run_level_tick(level_cfg, seq: int, model: dict): """Один тик уровня с LLM: дайджест + (опц.) БАТЧ задач. Возвращает (text, [task_id]). @@ -491,7 +508,15 @@ def run_level_tick(level_cfg, seq: int, model: dict): # Возвращаем claim в очередь сразу (TaskPromoted — тот же механизм, что # orphan-requeue: ready + сброс уровня; следующий тик возьмёт заново), # исключение летит дальше — CycleError/эскалация как раньше. + # + # e-261004144805-b9d6aa: слепой TaskPromoted по ВСЕМУ claimed + # воскрешал задачи, которые воркер успел закрыть ВНУТРИ тика + # до таймаута (кейс: 131358 complete+delivery в 14:31, тик убит + # в 14:48 → TaskPromoted вернул готовую задачу в очередь). + # Фильтруем по живой проекции: возвращаем только реально running. for tid in (claimed or []): + if not _task_still_active(tid): + continue size = next((t.get("size", "M") for t in batch if t.get("id") == tid), "M") es.append_event("task", tid, "TaskPromoted", {"to": size, "from": size, @@ -503,7 +528,10 @@ def run_level_tick(level_cfg, seq: int, model: dict): # точки зрения, но executor мог частично поработать инструментами. # По одной TaskFailed на каждую claimed — иначе батч висит в running # до orphan_requeue (час+). Сироты всё равно подстрахуют. + # e-261004144805-b9d6aa: закрытые внутри тика не трогаем (см. выше). for tid in (claimed or []): + if not _task_still_active(tid): + continue es.append_event("task", tid, "TaskFailed", { "error": str(usage.get("error", "llm worker failed"))[:500], })