harness/compact.py + cli.py compact [--dry-run|--vacuum|--verify-archives]:
шум сек-уровня (CycleTicked sec, ReflexObserved без алерта/команд/каналов)
старше часа → архив jsonl.gz (0600, строки целиком со sig) → мост цепочки
(подписи унесённых строк, на которые ссылаются остающиеся, + маркеры
boundary:<id> для legacy-границ) → подписанный StoreCompacted с sha256
архива/строк/моста → удаление пачками. audit.verify_events принимает мост
только при совпадении хеша с подписанным событием; проекции подгружают
мост лениво, если хвост перекрыл сжатый диапазон (rebuild с нуля).
Репетиция на копии прода: 1389 → 200 МБ, verify 484 с/3.4 ГБ → 29 с/420 МБ,
архив 246 МБ — все 2.47 млн подписей проверены; rebuild_projections OK.
Найдено репетицией: legacy-граница (id 466633) и коллизия id сжатия в
одну секунду — оба покрыты тестом. scripts/compact-store.sh — процедура
с VACUUM под остановленными писателями.
мин-уровень: если до ответа нужна работа >30 с — сначала короткий
tg-reply, итог — cli.py tg-reply <id> --followup (в чат входящего:
группа → группа; идемпотентно по тексту). p90 «старт LLM → ответ» был 223 с.
hour: лимит 300 вызовов оставлен — реальный пик 205 (p90 169), 100
оборвал бы 8 из 19 тиков.
Тесты: test_compact 25 PASS, test_followup 10 PASS; полный набор — без регрессий.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Разрешены конфликты: isolated_runner (тщательный upstream-вариант killpg-wait), cli.py task-диспетчеризация upstream + guard ValueError, aum.py paths-изоляция + STORE_DB fallback.
Тесты после мерджа: 5 устарели к новой реальности — починены по интентам:
- test_aum 9c: hermetic-мок OPENROUTER_KEY (ключ уехал со старого хоста)
- test_router_env_fallback: патчит paths.env_path (runtime больше не читает _HARNESS_DIR)
- test_submit_title_guard: контракт task list = показ доски (exit 0), а не ERROR
- test_tg_commands: доска задач от тестовой ready-задачи (в прод-копии ready пусто)
- test_instance_isolation: SKIP RAG-контура без вендора /root/rag (хост мигрировал)
Итог: 19/19 зелёные
title, совпадающий с формой t-NNNNNNNNNNNN-XXXX, теперь показывает задачу
(или ERROR exit 2, если не найдена), а не создаёт само-клон. Корень
эскалации: мин-агент звал 'cli.py task t-...' для чтения задачи.
- es: TaskClosed → статус closed (терминальный, обратимый через requeue; closed_by manual|archive)
- commands: close_task (ручной, только failed, идемпотентен), archive_stale_failed(max_age_hours=24, now=None) — демон-архиватор
- scheduler._delivery_and_orphans_loop: каждый проход (15с) архивирует failed>24ч
- requeue принимает closed → ready (доработка из архива)
- cli: simbiot close <id> [-r reason]
- dashboard: порядок ready→running→failed→done→архив, счётчик «архив» в статусбаре, схема закрытых фейлов под одной строкой-счётчиком (тумблер показывает полностью), pill.closed
- тест tests/test_close_stale_failed.py: 15 проверок (закрытие, порог через симуляцию +25ч, done не трогается, идемпотентность, полный цикл closed→ready→failed→closed)
Причина: find_task возвращал ОДНУ ready-задачу за такт — при 5+ ready
очередь росла быстрее исполнения (1 задача/такт).
Теперь:
- select_task_batch: ВСЕ ready-задачи класса уровня, старейшая первой;
- кап пачки MAX_TASK_BATCH (min=3, hour=6, day=6; env SIMBIOT_BATCH_MAX_<LEVEL>);
- бюджет контекста: секция задач <= ctx_tokens*3*0.5 символов,
мандат/выжимки/эскалации/инбокс остаются в тике; старейшая берётся всегда;
- run_level_tick: claim всего батча ДО воркера; TaskCompleted/TaskFailed
ПО каждой отдельно (смерть воркера не теряет батч в running);
- build_context: секция 'ТЕКУЩАЯ РАБОТА — БАТЧ из N' с постановками,
инструкция закрывать каждую задачу cli.py complete <id> -a;
- CLI complete: закрытие running-задачи с результатом
(commands.complete_task_with_delivery — доставка в TG идемпотентна).
Тесты: tests/test_task_batch.py — 28 проверок (ALL PASS), набор 8/8 зелёный.
Инцидент: 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 тестов ОК.
- 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), все зелёные
- 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 доведён, протестирован, закоммичен.
- 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 в легитимном)
Часть 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()