From 3361b671edce74644e3c530494a4357609397901 Mon Sep 17 00:00:00 2001 From: Simbiot Harness Date: Wed, 30 Sep 2026 17:23:46 +0000 Subject: [PATCH] =?UTF-8?q?fix(worker+scheduler+inbox):=20=D1=82=D1=80?= =?UTF-8?q?=D0=B8=20=D1=84=D0=B8=D0=BA=D1=81=D0=B0=20=D1=87=D0=B0=D1=81?= =?UTF-8?q?=D0=BE=D0=B2=D0=BE=D0=B9=20=D1=81=D0=B2=D0=BE=D0=B4=D0=BA=D0=B8?= =?UTF-8?q?=20(t-260930162615-fb58be)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1) per-call timeout LLM-вызова (SIMBIOT_LLM_CALL_TIMEOUT, 90с default, max_retries=1): роутер-лог показал — зависший тик = POST в ollama принят, но байт стрима не пришёл (DONE без TTFT), соседние вызовы ходили за 1-3с. Один висящий вызов больше не убивает весь тик по уровневому таймауту (300/2880с) — режется сам вызов, event-loop завершает тик штатно, spend-хвост и задачи не теряются (e-260930161900-baaecb, e-260930162600-cbb7e2, e-260930153143-85246b). 2) обрезка TG-инбокса (e-260930154125-082a2a): приём 4000→10000 симв., показ в контексте тика 300→1200 — длинные сообщения Alex больше не теряют хвост. 3) loss-of-claim в батче: TimeoutExpired от isolated_runner пролетал мимо TaskFailed-блока run_level_tick — claimed-задачи висели в running до сирот-чистки (час+). Теперь claim сразу возвращается TaskPromoted в очередь (reason с причиной смерти воркера). Тесты 8/8 (hermetic), живой вызов модели с timeout=90 — ok. --- harness/_llm_worker.py | 19 +++++++++++++++++-- harness/scheduler.py | 27 +++++++++++++++++++++------ harness/tg_inbox.py | 2 +- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/harness/_llm_worker.py b/harness/_llm_worker.py index f162536..65fa428 100644 --- a/harness/_llm_worker.py +++ b/harness/_llm_worker.py @@ -88,7 +88,16 @@ def _wire_tool_budget(agent, max_calls: int = 30): def build_level_model_from_payload(payload: dict): """Модель для воркера — та же сборка, что и в runtime.build_level_model: - 9router понимает chat/completions (usage там же), Responses API — шим без usage.""" + 9router понимает chat/completions (usage там же), Responses API — шим без usage. + + SIMBIOT_LLM_CALL_TIMEOUT (e-260930161900-6da5c9, e-260930153143-85246b): + per-call таймаут LLM-вызова в сек. Роутер-лог показал: зависший тик = POST + в ollama-апстрим принят, но байт стрима так и не пришёл (DONE без TTFT), + при этом соседние вызовы ходили за 1-3с. Таймаут уровня (300/2880с) убивает + ВЕСЬ тик из-за одного висящего вызова; теперь вызов режется (до 90с, + hour 180с), исключение отлавливает event-loop — тик получает результат + с пометкой «не успел», spend-хвост и задачи не теряются. + """ from strands.models.openai import OpenAIModel import os params = {} @@ -98,9 +107,15 @@ def build_level_model_from_payload(payload: dict): model_id = payload.get("model") or "auto" if model_id == "none": model_id = "auto" + # per-call таймаут: один зависший апстрим-вызов не должен убивать весь тик + call_timeout = int(os.environ.get("SIMBIOT_LLM_CALL_TIMEOUT", "90")) return OpenAIModel( + # timeout открытого клиента (openai SDK) режет каждый chat.completions + # вызов; при зависании апстрима получаем APITimeoutError за N сек и + # strands event-loop завершает тик штатно (без потери предыдущих шагов) client_args={"api_key": os.environ.get("OPENAI_API_KEY", ""), - "base_url": os.environ.get("OPENAI_BASE_URL", "http://127.0.0.1:20128/v1")}, + "base_url": os.environ.get("OPENAI_BASE_URL", "http://127.0.0.1:20128/v1"), + "timeout": call_timeout, "max_retries": 1}, model_id=model_id, **({"params": params} if params else {}), ) diff --git a/harness/scheduler.py b/harness/scheduler.py index 7dcc599..f50bf69 100644 --- a/harness/scheduler.py +++ b/harness/scheduler.py @@ -317,7 +317,7 @@ def build_context(level_cfg, model: dict, batch=None): tag = (" [голосовое, расшифровка не удалась — попроси прислать текстом]" if m.get("transcribed") else " [голосовое, ещё не расшифровано]") - lines.append(f"{i}. [{m_id}] от {who}{tag}: {m.get('text','')[:300]}") + lines.append(f"{i}. [{m_id}] от {who}{tag}: {m.get('text','')[:1200]}") batch_ids = ",".join(ids) # t-260930064924: батч-триаж — ВСЕ неотвеченные обрабатываются ОДНИМ тиком. # Раньше мин-уровень отвечал по одному за тик (m-516173222..228 — 7 тиков @@ -417,11 +417,26 @@ def run_level_tick(level_cfg, seq: int, model: dict): if not claimed: claimed = None prompt = build_context(level_cfg, model, batch) - text, usage, trace_id = isolated_runner.run_llm_tick( - level_cfg, prompt, - session_id=f"s-{level_cfg.name}-{seq}", - trace_prefix=f"{level_cfg.name}-{seq}", - ) + try: + text, usage, trace_id = isolated_runner.run_llm_tick( + level_cfg, prompt, + session_id=f"s-{level_cfg.name}-{seq}", + trace_prefix=f"{level_cfg.name}-{seq}", + ) + except Exception as e: + # e-260930162600-cbb7e2/cbb7e2-семейство: TimeoutExpired от isolated_runner + # пролетал МИМО блока text-is-None ниже — claimed-задачи висели в running + # до сирот-чистки (час+), очередь казалась «не стартующей» (85246b). + # Возвращаем claim в очередь сразу (TaskPromoted — тот же механизм, что + # orphan-requeue: ready + сброс уровня; следующий тик возьмёт заново), + # исключение летит дальше — CycleError/эскалация как раньше. + for tid in (claimed or []): + 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, + "reason": f"тик-воркер умер ({str(e)[:120]}) — " + f"claim возвращён в очередь без потери"}) + raise if text is None: # воркер упал (таймаут/краш) — НИ одна задача не выполнена с его # точки зрения, но executor мог частично поработать инструментами. diff --git a/harness/tg_inbox.py b/harness/tg_inbox.py index 18ecd04..f18e42d 100644 --- a/harness/tg_inbox.py +++ b/harness/tg_inbox.py @@ -102,7 +102,7 @@ def receive_message(update: dict): "user_id": user.get("id"), "username": user.get("username"), "first_name": user.get("first_name"), - "text": (msg.get("text") or msg.get("caption") or "")[:4000], + "text": (msg.get("text") or msg.get("caption") or "")[:10000], "kind": kind, "file_id": file_id, # file_unique_id: ключ аудио-кэша и идемпотентности расшифровки