- InitiativeSent (aggregate cycle, id proactive:<slot>) — идемпотентно UNIQUE-стора
- es-проекция: последний слот + история sent (анти-спам стейт)
- harness/proactive.py: окно 5-21 UTC (история чата), интервал 8ч, тишина 2ч
после сообщения Alex, не при неотвеченных входящих/critical-эскалациях
- банк из 10 тем без LLM (ноль кредитов), round-robin по использованным
- хук в delivery-демоне (15с, best-effort); гейт важности не затронут
- CLI: proactive / --plan / --force; env-оверрайды интервала/тишины/слота
- tests/test_proactive.py: 34 проверки (окно/анти-спам/round-robin/идемпотентность/CLI)
Минутный тик отвечал по одному сообщению за тик (m-516173222..228 —
7 тиков подряд на один разговор): контекст показывал все, но мандат
говорил «ответь на него», и главное — нечем было закрыть несколько
входящих одним ответом (tg-reply закрывает одно, «хвосты» снова будили
гейт важности).
- tg_inbox.send_batch_reply: одна TG-отправка → ReplySent на КАЖДОЕ
входящее батча (полный текст у головного, у хвостовых — ссылка);
гонки (replied/ignored выпадают, пустой батч → ValueError), разные
чаты делят батч — каждому своя отправка
- cli tg-reply <id1>,<id2>,… -c: батч-режим; несколько id без -c — отказ
(защита от случайного закрытия); одиночный режим обратно совместим
- build_context: секция инбокса = «БАТЧ из N» с готовой командой
(все id через запятую) и инструкцией закрыть ВСЁ за этот тик; кап
10 → 50 (показываем весь батч, а не хвост)
- мандат min: батч-семантика (сводный ответ по смыслу разговора)
- тесты tests/test_inbox_batch.py: 32 проверки (hermetic), все зелёные
- гейт (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 слайдов
- harness/tg_commands.py: реестр команд, парсер (кириллица/транслит/@бот/без слэша),
хендлеры из read model, answer_pending (идемпотентно, cooldown 60с при лежащем TG),
setMyCommands с фолбэком кириллица→транслит (Bot API латиницу только)
- scheduler.py: сек-тик отвечает на команды без LLM (payload.commands),
build_context НЕ показывает команды мин-уровню (гонки/лежащий TG не тратят LLM)
- tg_inbox.py: send_message/send_reply принимают короткий timeout для рефлекса;
поллер при старте авторегистрирует команды бота
- cli.py: tg-commands --register/--list
- события TaskAwaitDelivery / DeliverySent: флаг 'результат доставить в чат'
на задаче, доставка идемпотентна (один раз sent — повторов нет)
- commands.mark_awaiting_delivery / deliver_result (единственный путь отправки) /
complete_task_with_delivery: завершение и отправка идут парой —
«сделал и промолчал» больше не случается
- scheduler: push после TaskCompleted + delivery-демон (pull каждые 15с —
досылает, если тик умер между завершением и отправкой); requeue сбрасывает
доставку — доработка снова уйдёт в чат
- orphan_requeue: running-задачи с мёртвым воркером (рестарт демона убивает
воркеров, TaskCompleted не пишется) возвращаются в очередь автоматически;
корень эскалации e-260929234911: 'повторный таймаут часового воркера'
был убийством живого воркера рестартом в 23:12/00:03, не таймаутом
(трейс и BudgetSpent шли вплоть до секунды рестарта)
- cli: task -a (подать с доставкой) + task-await (пометить существующую);
мин-уровень помечает задачи-просьбы из TG
- 18 тестов на копии БД: submit→await→complete→deliver→idempotency→
requeue→re-deliver→pull→orphan→rebuild
- voice.py: download по file_id (getFile→file, кэш ogg по file_unique_id),
два ASR-яруса: whisper-asr-webservice base (быстро ~1с) → fallback
faster-whisper large-v3-turbo (CPU int8, ~11с, точный на русском;
короткие voice ≤30с идут сразу в точный ярус — base коверкает фразы)
- _fw_asr_worker.py: faster-whisper в системном python3 (в venv нет),
subprocess JSON-обёртка (stdin JSON → stdout JSON)
- tg_inbox.py: file_unique_id в MessageReceived + расшифровка сразу
при приёме (поллер пишет MessageTranscribed до первого мин-тика;
сбой транскрипции не валит поллер) + video_note как транскрибируемый kind
- es.py: проекция MessageTranscribed (transcript→text, original_text,
reset для --force), cli tg-transcribe <id> [--force] — ручная расшифровка
- scheduler.py: голосовые в контексте мин-уровня даже с пустым текстом
(неотвеченное не исчезает): пометки «расшифровано/не расшифровано/
расшифровка не удалась», сортировка по ts, срез 10
Е2Е проверено: m-516173163 — кэш-хит → turbo → «Ты теперь слышишь мои
голосовые, да?» в стор; base на том же файле давал «Туше, что ты тебя
слышишь» — выбор ярусов подтверждён фактом. Воркер past-hour умер по
2880с-таймауту до коммита; WIP доведён, протестирован, закоммичен.
Два корня пустого 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.
Часть 1 (часовой уровень, задача t-260929155421-9f7bff):
- EscalationRaised/Acknowledged в ES-спину, проекция escalations
- CLI: escalate / escalations / escalation-ack
- уровни видят открытые эскалации в контексте; scheduler перезапускает
критическую эскалацию сразу (не дожидаясь границы такта)
Часть 2 (минутный, этот тик):
- fix(cli): 'events -n N' показывал первые N событий вместо хвоста стора
(README обещает хвост) — теперь хвост; --after N — от id N вперёд
- + es.count_events()