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.
- гейт (e-260930062242/2510/2607/2741): collect_signals ранжирует события
(unreplied_inbox/ready_task/critical-эскалация = critical; warn-эскалации,
high_load = warn), gate_allows_llm решает по порогу уровня
(SIMBIOT_GATE_<LEVEL>, дефолты min=critical/hour=warn/day=warn);
тихий такт — GateSkipped + дайджест-заглушка, без LLM и без расхода роутера
- model в BudgetSpent/InvocationStarted (e-260930062120): видно, какая модель
и уровень тратили кредиты (strands не выносит фактическую модель в usage —
пишем запрошенный model_id)
- per-level лимиты тул-колов уже в дереве: resolve_tool_budget (min=30/hour=300,
env-оверрайд) (e-260930060717/060904)
- tests/test_gate.py: 28 hermetic-проверок, ALL PASS
- simbiot-presentation.html: презентация Simbiot (t-260930053538), 10 слайдов
Репродукция: мин-тик с реальным контекстом → модель (auto/low effort)
каждый ход делала tool-вызов, никогда не выдавая чистый текст;
limits={'turns': 15} обрывал цикл на границе хода, result.message
оставался toolResult-сообщением → str(result)='' → DigestWritten len 0.
limit_turns — нативный стоп SDK, но он не кладёт в историю итоговый
ассистентский текст. Наш end_turn (AfterToolsEvent) кладёт: дайджест
не пустеет. Защита от таймаутов остаётся двойной: cancel_tool на
вызовы сверх лимита + end_turn после партии.
E2E-подтверждение: тот же промпт, stop_reason=end_turn, текст 465
символов ('Тик чистый. Проверил: ...').
Симптом: DigestWritten min с пустым text при живом агенте (15 tool-вызовов,
BudgetSpent писались). Корень: PrintingCallbackHandler стримит в stdout
воркера; scheduler читает capture_output=True; длинный вывод (reasoning +
Tool #N на каждый вызов) переполнял pipe → BrokenPipeError внутри
stream_async → ok=False, текст тика потерян. Воркер перезапускался каждый
тик — падения шли серией, мин-дайджесты пустые.
Результат тика живёт в JSON-файле (result_path) — печать не нужна вовсе:
create_harness(callback_handler=null_callback_handler).
E2E: воркер с 2 shell-вызовами, stdout 0 байт, ok=True, текст на месте.
Два корня пустого spend за час:
1) usage-добыча в воркере смотрела m["message"]["usage"] — у strands usage живёт
в message["metadata"]["usage"] (event_loop.py), поэтому usage всегда был пуст
(тот же баг, что уже чинили в runtime._extract_usage).
2) BudgetSpent писал scheduler ПОСЛЕ успешного возврата воркера — таймаут убивал
воркер раньше записи, spend терял весь тик.
3) Модель: strands_harness строит OpenAIResponsesModel, у 9router /responses — шим
без usage (проверено: response.completed приходит без usage; non-stream возвращает
chat-completion-объект). Перевёл на OpenAIModel (chat/completions, include_usage) —
usage приходит и с инструментами, и в reasoning-режиме.
Фикс:
- воркер пишет BudgetSpent ИНКРЕМЕНТАЛЬНО после каждого модельного вызова
(hook на MessageAddedEvent, assistant с metadata.usage, дедуп по объекту) —
таймаут теряет максимум хвост текущего вызова, не весь тик;
- _extract_usage: корректная структура + суммирование по шагам (model_calls);
- scheduler дублирует запись только если воркер не смог разобрать usage;
- события помечены source: worker|runner и trace_id тика.
Проверено: дельты воркера == итоговый usage тика (39-шаговый тик сходится);
убитый по таймауту тик сохраняет все завершённые шаги; spend виден в
read model/dashboard.