# Задание 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`.
