docs(task): A5 — persist the telemetry the router already measures

Latency, token usage and failover counts flow through router_metadata on every
call and are then discarded; nothing in the project accumulates them. Four of
the seven headline numbers on the mockups are therefore honestly derivable from
our own calls rather than fabricated. RPS, SLA and host resource metrics stay in
Active Limitations.

Also carries the three debts forward for the third time: HKCU in the installer,
fastapi/uvicorn as required dependencies, and YAML comment loss.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hermes Team 2026-08-21 16:53:15 +07:00
parent 31e4e7f66c
commit f5d002c46f

View file

@ -0,0 +1,142 @@
# Задание A5 (Antigravity): настоящая телеметрия вызовов и закрытие долгов
## Дата поступления
2026-08-21
## База
Проверочный HEAD: **`31e4e7f`**, `origin/main` = `31e4e7f`. `git fetch`, зафиксировать `BASE_SHA`.
## Ветка
`antigravity/telemetry`
---
## Что принято по A4
Проверено исполнением:
- **Утечка учётных данных закрыта — самый старый дефект проекта.** Подставил в окружение `OPENAI_API_KEY`, `CODEX_TOKEN_X`, `DEEPSEEK_API_KEY`, `ANTHROPIC_API_KEY`, перехватил окружение дочернего процесса `agy`: 31 переменная, **ноль ключей провайдеров**, изоляция профиля через `USERPROFILE` сохранена.
- `selection_trace` — объяснение выбора провайдера появилось в метаданных ответа.
- Изоляция реестра в тестах установщика доработана.
- Граница соблюдена. Прогон: headless 175 passed, с UI-зависимостями **222 passed**, ruff чисто, гейт PASSED.
---
## Контекст: почему это задание
Владелец сравнил установленную сборку с утверждёнными макетами. На макетах ключевое место занимают показатели:
```
98 ms P95 · 842 rps · 99.98% успешные · 1.23% ошибки
1.2M / 3.0M токенов · история использования в USD
CPU 28% · Память 61% · Диск 39% · Сеть 42 Мбит/с
```
Сейчас контракт объявляет их отсутствующими (Gap 12), и UI обязан показывать «Н/Д». **Но это верно не для всех.** Разберём по источнику данных:
| Показатель | Источник | Вердикт |
|---|---|---|
| Латентность вызова | `router_engine.py:254-256` уже замеряет `elapsed` | **измеримо, данные выбрасываются** |
| Токены | `agy_subprocess.py:623` уже извлекает `prompt_tokens`/`completion_tokens`; HTTP-адаптеры получают `usage` от провайдера | **измеримо, данные выбрасываются** |
| Количество переключений | `failover_count` и `failover_trail` уже считаются | **измеримо, данные выбрасываются** |
| Доля ошибок | классификация ошибок уже есть | **измеримо** |
| Стоимость | нужна таблица цен по моделям | измеримо **при наличии прайса** |
| RPS провайдера, SLA, uptime | провайдеры не отдают | **неизмеримо** |
| CPU, память, диск, сеть | к работе роутера отношения не имеют | **неизмеримо и не нужно** |
То есть четыре показателя из семи — настоящие, и мы их просто теряем. Задание: перестать их терять.
---
## P0-1. Сохранять телеметрию собственных вызовов
Сейчас `router_metadata` собирается и отдаётся в ответе, после чего пропадает. Ни одного места, где эти цифры накапливаются, в проекте нет.
Завести хранилище телеметрии — по одной записи на вызов:
```
timestamp
role
profile_id
provider
model
outcome (success | failover | error | quota_exhausted | rate_limited)
latency_seconds
prompt_tokens
completion_tokens
failover_count
error_category (если применимо)
```
Требования:
- запись не должна замедлять вызов — писать асинхронно либо буферизовать;
- ограниченный размер: кольцевой буфер в памяти плюс ротация файла, без неограниченного роста;
- ни одного секрета и ни одного фрагмента содержимого запроса или ответа — только метаданные;
- переживать перезапуск приложения (агрегаты можно хранить в файле рядом с `router_state.json`, атомарно, как уже сделано там).
**Токены брать только из `usage`, который вернул провайдер.** Если провайдер `usage` не вернул — поля пустые, а не оценка по длине текста.
## P0-2. Агрегаты, которые честно выводятся из записей
Поверх хранилища — агрегаты по провайдеру, профилю, модели и роли за окно времени:
- латентность: P50, P95, максимум, число вызовов;
- токены: суммарно на вход и на выход;
- переключения: сколько раз уходили с профиля и по какой причине;
- доля ошибок: доля неуспешных вызовов от общего числа.
Это **наши собственные наблюдения за нашими же вызовами**, а не заявления провайдера. В контракте так и записать: источник `own_measurement`, а не `*_api`. Разница принципиальна и должна быть видна в данных.
Если вызовов в окне не было — отдавать отсутствие данных, а не нули. «Ноль вызовов» и «ноль миллисекунд» — разные вещи, ровно как «нет данных» и «квота исчерпана».
## P1-3. Стоимость — только при наличии прайса
Стоимость считать, если в конфигурации задана таблица цен по моделям (вход/выход за миллион токенов). Нет таблицы — нет стоимости, поле пустое.
Не зашивать цены в код: они меняются, а зашитый прайс через месяц станет тихой ложью. Формат — в конфиге пользователя, с примером в `config/`.
## P1-4. Обновить контракт
Gap 12 переформулировать: часть показателей переходит из «неизмеримо» в «измеряется нами». Для каждого нового поля указать источник (`own_measurement`), окно агрегации и поведение при отсутствии вызовов. Разделы «Closed Gaps» и «Active Limitations» сохранить в текущем виде — они удачные.
RPS провайдера, SLA, uptime, CPU/память/диск/сеть остаются в «Active Limitations» с прежним требованием к UI: «Н/Д» либо блок отсутствует.
## P1-5. Закрыть три долга
Проверено на `31e4e7f`, всё ещё открыто и не зафиксировано как осознанный долг:
| Долг | Состояние |
|---|---|
| `Registry.CurrentUser` в `HermesHubSetup.cs` | 2 вызова |
| `fastapi` / `uvicorn` в обязательных зависимостях | `gui_server` живёт в `legacy/` и не поставляется |
| Комментарии `router_profiles.yaml` | 5 строк → 2 при сохранении |
По каждому — закрыть либо записать обоснование в отчёте. Третий раз переношу их из задания в задание; молчаливый перенос дальше не годится.
---
## Ограничения
- Граница прежняя: зона Codex (`router/ui/**`, `hermes_hub_app.py`, `tests/test_ui_*.py`) — не трогать.
- Никаких оценок вместо измерений. Нет данных — нет числа.
- Секреты и содержимое запросов в телеметрию не попадают.
- Тег `v0.1.1` не создавать.
## Критерии приёмки
1. Ни один файл зоны Codex не изменён.
2. После серии вызовов (моками) в телеметрии есть записи с латентностью, токенами и исходом; проверено тестом.
3. Агрегаты считаются корректно: тест с известным набором значений проверяет P50/P95 и суммы токенов.
4. При отсутствии вызовов агрегаты отдают отсутствие данных, а не нули; проверено тестом.
5. Токены не появляются, если провайдер не вернул `usage`; проверено тестом.
6. В телеметрии нет секретов и содержимого запросов; проверено тестом с подставным ключом и текстом.
7. Размер хранилища ограничен; ротация проверена тестом.
8. Контракт обновлён: новые поля с источником `own_measurement`, неизмеримое осталось в «Active Limitations».
9. Три долга закрыты либо зафиксированы с обоснованием.
10. Прогон **в обоих окружениях**; обе команды и оба результата в отчёте.
11. `ruff check .` чисто; release gate PASSED на финальном коммите.
12. Отчёт: `START_HEAD`, `FINAL_HEAD`, `origin/main`, `git status`, точный `X passed / Y skipped / Z failed`.
## Порядок сдачи
Передать точный `FINAL_COMMIT_SHA`.