- voice.py: kind=video в TRANSCRIBABLE_KINDS; extract_audio_track() — ffmpeg
вытягивает дорожку (моно 16к opus) из видеоконтейнера; речь ≤60с
(voice/video_note/video) сразу идёт в faster-whisper-large-v3-turbo
(base на видео давал кашу). E2E: m-516173698 расшифрован.
- tg_inbox.py: поллер транскрибирует и msg.video, не только voice/audio/video_note.
- tools/tui_chat.py: TUI-чат (t-261004132716) лежал только у Volya (33301d3) —
в origin его не было, из-за этого терминал не грузился у 6 агентов.
Тестовое TUI-сообщение (t-261004132716, chat_id=tui:local) стало
последним в inbox → notify_chat/send_document целились в несуществующий
Telegram-чат → уведомления тихо SKIP'ались при живом чате. Фильтр
локальных каналов расширен: portal:* И tui:*.
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.
Инцидент: Alex получал презентацию трижды (m-516173239/241). Две причины:
1) гонка push (тик) vs pull (delivery-демон): оба проходили проверку
delivery_status != 'sent', пока TG-отправка (до 35с) была в полёте;
2) ad-hoc curl sendDocument из мин-тиков вообще без метки delivered.
Фикс — delivery_ledger (INSERT OR IGNORE: факт доставки раз и навсегда)
+ delivery_locks (атомарный claim ДО отправки, TTL 120с; одно SQLite-
выражение = нет гонок между потоками и процессами). deliver_result /
deliver_artifact: ledger → claim → отправка → запись; сбой → release,
демон досыплет. Requeue сбрасывает ledger (доработка доставляется заново).
Новое: cli.py tg-deliver-file (идемпотентная отправка файлов, --force),
tg-notify -k <ключ>, tg_inbox.send_document (multipart на чистом urllib),
deliver_pending pre-filter по ledger, мандаты уровней: прямые
curl/api.telegram отправки запрещены. DeliverySkipped логирует пропуски.
Тесты: tests/test_delivery_idempotency.py — 49 проверок (гонка потоков,
межпроцессный claim, сбои+ретраи, requeue, CLI). Регрессия: 6/6 тестов ОК.
Минутный тик отвечал по одному сообщению за тик (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), все зелёные
- 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
- 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 доведён, протестирован, закоммичен.
busy в read model всегда False — истинная занятость живёт в памяти
планировщика, событий для неё нет. Срез показывал «ожидает», пока
часовой уровень держал running-задачу. TaskClaimed закрепляет уровень
за задачей в проекции — теперь срез показывает «занят (running-задача)».
- tg_inbox.notify_chat(): отправка в чат харнеса без входящего-родителя
(чат = последний MessageReceived от Alex)
- tg_inbox.ladder_snapshot(): срез лестницы — занятость/тики/ошибки уровней,
готово/в работе, открытые эскалации; read-only через run_projections()
- scheduler._notify_busy(): уровень сообщает в TG о взятии задачи
(анти-спам 15м на уровень) — TaskClaimed в scheduler.run_level_tick
- cli.py: команда ladder + tg-notify; /лестница|/ladder|/статус|/status
в tg-reply заменяется на срез лестницы — запрос из TG самодостаточен
- guard в __main__ tg_inbox: единственный легитимный поллер — systemd-сервис
(раньше python -m harness.tg_inbox с любыми аргументами поднимал
конкурирующего поллера → 409 Conflict в легитимном)