Два корня пустого 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.
- harness/dashboard.py: stdlib-only HTTP-сервер, /api/state (read-only из event store через run_projections — та же проекция, что у scheduler/CLI), /api/health на отдельном порту 8766 (без auth — для systemd healthcheck)
- harness/dashboard.html: страница в стиле architecture.html (paper/ink/signal), fetch каждые 5с, честный errbox при недоступности API, XSS-экранирование всех полей
- auth: SIMBIOT_DASH_TOKEN из /etc/simbiot/dashboard.env (env-файл юнита, 600); без/с неверным токеном — 401; токен в query → cookie
- systemd-юнит simbiot-dashboard.service (после scheduler, restart=always), порт 8765/tcp в UFW
Часть 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()