Общие метрики NeMo Relay
Hermes подключает NeMo Relay как обычную runtime-зависимость на тех платформах,
для которых Relay публикует нативный wheel. Интеграция общих метрик встроена
в Hermes и не требует плагина наблюдаемости. На остальных нативных целях
Hermes по-прежнему импортируется без Relay: там используется явный no-op хост
с урезанными возможностями. Выполнение Hermes остаётся доступным, а области
видимости Relay, middleware, плагины и подписчики — нет. Экстра
hermes-agent[nemo-relay] сохранена как no-op псевдоним для совместимости
с уже существующими командами установки.
Плагин Hermes observability/nemo_relay при этом удалён. Тем, у кого он был,
нужно убрать observability/nemo_relay (или его старый псевдоним
nemo_relay) из plugins.enabled и перенести настройку экспортёров
в plugins.toml Relay. Явный файл можно выбрать переменной
HERMES_NEMO_RELAY_PLUGINS_TOML, а hermes update или hermes migrate relay
создаёт такой файл при переносе старых настроек
HERMES_NEMO_RELAY_ATOF_* и HERMES_NEMO_RELAY_ATIF_*. Сами эти старые
переменные экспортёры Relay больше не настраивают.
На поддерживаемых платформах для управляемых вызовов провайдера и инструментов Hermes требует NeMo Relay 0.10.
Runtime-зависимость и граница данных
Hermes устанавливает платформенный нативный wheel nemo-relay из ограниченного
диапазона зависимости >=0.10,<0.11. Опубликованный пакет собирается из
репозитория NVIDIA NeMo Relay.
На неподдерживаемых платформах работает описанный выше явный no-op runtime;
никакая другая реализация не скачивается.
Relay 0.10 требует конфигурацию наблюдаемости с version = 4. Собственные
Python-перехватчики выполнения LLM получают (name, request, context, next_call); контекст отдаёт выбранный кодек запроса и, для нестримингового
выполнения, кодек ответа. Нативные плагины нужно пересобрать под ABI v7 из
Relay 0.10 и поднять нижнюю границу compat.relay до 0.10.0. Воркерам,
которые регистрируют перехватчики выполнения LLM, нужно обновить коллбэки
и перегенерировать привязки протокола. Полный чек-лист обновления — в
руководстве по миграции на 0.10
от Relay.
Кроме того, Relay 0.10 сам запускает экспорт OpenTelemetry, когда активная
конфигурация Relay встречает непустой OTEL_EXPORTER_OTLP_ENDPOINT (или
отдельный OTEL_EXPORTER_OTLP_*_ENDPOINT для сигнала) в окружении процесса
Hermes. Эти span-ы gen_ai несут сообщения промптов и ответов. Файлы,
которые создаёт hermes migrate relay, задают
[components.config.opentelemetry] enabled = false, чтобы сохранить поведение
0.9; добавьте такую же секцию в файл, написанный вручную, или задайте
OTEL_SDK_DISABLED=true, если этот экспорт вам не нужен.
Когда активно управляемое выполнение Relay, запрос к провайдеру и его ответ
проходят через этот нативный модуль внутри процесса Hermes, поэтому
настроенные перехватчики работают с реальным вызовом. Это отдельная история,
не связанная с контрактом данных общих метрик. Режим общих метрик не ставит
никакого сетевого экспортёра расширенной наблюдаемости, а его подписчик
принимает только версионированную проекцию по списку разрешений, описанную
ниже. Единственный исходящий путь — опциональный отправитель пакетов из
приложения A: он ничего не передаёт, пока пользователь не включит и
enabled, и send, и отправляет целые пакеты, а не живые span-ы. Отдельно
настроенный плагин расширенной наблюдаемости или динамический плагин может
создать другой путь данных, и такой путь требует собственной проверки
политики.
Сбор выключен, пока его не включит политика Hermes:
telemetry:
shared_metrics:
enabled: true
Этот выбор читается из собственного config.yaml профиля. Управляемый
машиной оверлей конфигурации не может включить или выключить общие метрики от
имени профиля.
Hermes пользуется обычным обнаружением плагинов Relay на весь процесс. Relay читает эти файлы, от низшего приоритета к высшему:
| Слой | Linux и macOS | Windows |
|---|---|---|
| Пользовательский | $XDG_CONFIG_HOME/nemo-relay/plugins.toml или ~/.config/nemo-relay/plugins.toml | %USERPROFILE%\.config\nemo-relay\plugins.toml (если заданы, приоритет у XDG_CONFIG_HOME, затем у HOME) |
| Системный | /etc/nemo-relay/plugins.toml | %ProgramData%\nemo-relay\plugins.toml |
HERMES_NEMO_RELAY_PLUGINS_TOML заменяет пользовательский файл явно выбранным;
системный файл по-прежнему применяется поверх него. Конфигурация внутри
репозитория игнорируется. Если явно выбранный файл загрузить не удалось,
Hermes сообщает об ошибке и продолжает работу без плагинов Relay, а не
откатывается к другой конфигурации.
Запустите hermes doctor, чтобы увидеть, какие файлы применяются. Его раздел
NeMo Relay Plugins перечисляет каждый файл, который находит Relay, говорит,
включён ли хоть один плагин, и показывает любые проблемы, о которых сообщает
Relay, — не загружая код плагинов.
Сегментация session-span для непрерывных сессий
Relay экспортирует span, когда закрывается его область видимости. Непрерывная сессия шлюза может жить сутками, поэтому её span сессии остаётся открытым, хотя span каждого хода экспортируется как обычно. Опциональная сегментация вращает на границе хода только область видимости сессии:
gateway:
telemetry:
session_segments:
on_compaction: false # вращать после компактизации контекста
max_turns: 0 # 0 = без ограничений; N = ходов на сегмент
| Ключ | По умолчанию | Поведение |
|---|---|---|
on_compaction | false | Вращать после завершения компактизации, на следующей границе хода. |
max_turns | 0 | Вращать после каждых N завершённых ходов; 0 отключает ограничение. |
Оба значения по умолчанию сохраняют одну область видимости сессии на всё её
время жизни. У span-ов после вращения остаётся тот же session_id, к нему
добавляются hermes.session.segment и hermes.session.segment_reason
(compaction или max_turns).
Рабочий каталог в данных области
Когда Hermes знает логический рабочий каталог сессии или задачи, стартовые
области hermes.session и hermes.turn включают его в ATOF как data.cwd.
Поэтому у хода, выполняемого в worktree задачи, каталог может отличаться от
каталога сессии, которой он принадлежит. Неизвестные каталоги опускаются, а
данные конца области по-прежнему зарезервированы под исход.
Рабочий каталог — входные данные области Relay, так что его видит каждый включённый подписчик Relay, а не только ATOF. По путям можно узнать имена пользователей, названия репозиториев или устройство точек монтирования. Relay не фильтрует события по рабочему каталогу; если путь не должен покидать хост, используйте доверенный локальный коллектор или не включайте удалённый экспортёр для этого процесса.
Марки компактизации
Когда runtime Relay жив, Hermes отправляет одну марку Relay с
data_schema: {"name": "hermes.compaction", "version": "1"} для каждого из
этих источников:
- каждая попытка
compress_context, которая вернулась, включая заблокированные, направленные в Codex и прерванные; - каждый проход микрокомпактизации;
- каждая зафиксированная проактивная обрезка результатов инструментов;
- каждая компактизация треда Codex app-server, которую Codex выполнил сам, а Hermes её заметил.
Для марки настройки не нужны, потому что экспортёры Relay настраиваются отдельно.
Другие перезаписи истории и запроса марок пока не порождают: нативная
компактизация OpenAI Responses и обрезка элементов перед её контрольной точкой,
уменьшение изображений после отказа провайдера, усечение только запроса и
автоматический сброс сессии в шлюзе после хода, закончившегося
compression_exhausted: он заменяет сессию новой пустой, а не компактизирует её.
| Марка | Когда | Эффект в Relay |
|---|---|---|
compaction | outcome: committed при scope: history или provider | Сбрасывает актуальность истории LLM для области агента этой сессии, так что следующий старт LLM запишет всю компактизированную историю |
compaction.attempt | Любой другой исход (aborted, failed, skipped, blocked, other) | Нет |
Фиксация, у которой не удалось разделение сессии уже после сохранения
компактизированной истории, всё равно считается committed (модель дальше видит
компактизированную историю), с failure_class: session_split_failed
и split_status: failed_not_indexed.
Родитель марки — живой hermes.turn той же сессии либо область
hermes.session, когда компактизация идёт вне хода (гигиена шлюза). При
compression.in_place: false фиксация с ротацией переводит агента в дочернюю
сессию, у которой нет области Relay, пока её не откроет более поздний вызов
LLM под управлением Relay или следующий ход. Пока области нет, компактизация
в дочерней сессии (второй предварительный проход или проход микрокомпактизации
перед концом хода) получает в родители живой hermes.turn, в котором агент
стартовал, а data.session_id по-прежнему называет дочернюю сессию. Марка не
отправляется ни из хода, который Relay не инструментирует (второй параллельный
ход той же сессии), ни из форка с отвязанным сохранением вроде фонового
ревью: их компактизация переписывает только их собственную расшифровку.
data плоская, чтобы OpenTelemetry разворачивал каждое поле в
nemo_relay.mark.data.<key>. Каждый ключ v1 присутствует в каждой марке;
неприменимое значение — null. Категориальные поля берутся из этих закрытых
множеств, кроме двух идентификаторов суммаризатора:
| Поле | Значения v1 |
|---|---|
kind | summarize, micro_summarize, prune_tool_results, provider_native |
scope | history, provider |
official | true только для попыток compress_context |
method | llm_summary, aux_fallback_main, deterministic_fallback, deterministic_prune, provider, none |
trigger | manual, turn_start_threshold, pre_api, post_tool, idle, engine_preflight, overflow, gateway_hygiene, between_turns, proactive_prune, provider, unknown |
trigger_class | auto, manual, overflow, provider, unknown |
outcome | committed, aborted, failed, skipped, blocked |
split_status | not_applicable, in_place_committed, rotated_committed, failed_not_indexed, aborted или null |
token_count_method | estimate_rough или null |
overflow_reason | context_overflow, payload_too_large, long_context_tier (причина от классификатора ошибок для отказа провайдера, после которого восстанавливается попытка с trigger: overflow) или null для любого другого триггера |
summarizer_provider, summarizer_model | Свободная форма, не закрытое множество: идентификаторы провайдера и модели, которые Hermes выбрал для вызова суммаризации, или null, если попытка их не записала. Марка микрокомпактизации несёт только summarizer_model — настроенную модель суммаризации |
Значение вне закрытого множества читается как other. failure_class берёт
классы hermes.compression.count плюс классы, существующие только у попытки
(выходы защитных проверок вроде blocked:cooldown, выходы маршрута Codex вроде
codex_auto_native, выходы аренды session_ownership_lost,
session_ownership_unreadable и cooldown_state_unreadable, empty_transcript
(движок контекста не вернул сообщений), summary_model_benched и исходы
микрокомпактизации), none, когда класса нет, и other для всего остального.
Автоматический вызов, не передавший метку триггера, читается как
trigger: unknown при trigger_class: auto; запись вообще без триггера
читается как unknown в обоих полях. in_place и session_rotated
выставляются, только когда split_status равен in_place_committed или
rotated_committed; cache_break равен null при scope: provider.
Числовые поля несут эффект (tokens_before, tokens_after,
tokens_reclaimed, messages_before, messages_after, items_dropped)
и тайминги (duration_ms, summary_generation_ms, aux_call_duration_ms,
queue_wait_ms, commit_ms). Число токенов — грубая оценка только по
сообщениям (token_count_method: estimate_rough). Полезная нагрузка никогда
не содержит текст сообщений, текст сводки, тему фокуса (только
has_focus_topic), текст ошибок и пути к файлам. attempt_id и session_id
нужны для корреляции; не используйте их как метки метрик.
Ходы Codex app-server выполняют вызовы модели внутри Codex, поэтому Relay не
записывает для них старт LLM. Перезапись фиксирует их марка compaction;
сброс актуальности влияет только на более поздний вызов LLM на стороне Hermes
в той же области сессии. Компактизация, которую Hermes запускает принудительно,
записывается один раз — её попыткой compress_context: следующий ход Codex
отбрасывает уведомления хода компактизации и не может сообщить о ней повторно.
Известные пробелы в v1:
- При
gateway.telemetry.session_segments.on_compaction: trueфиксация с ротацией вне любого хода закрывает старую сессию Relay до отправки марки, и марка теряется. Сейчас ни один вызов так не делает: ручной/compressоткладывает это уведомление до момента после марки, а гигиена шлюза компактизирует на месте. - Зарезервировано для будущих версий и в v1 никогда не отправляется:
kindrequest_truncation/window_reduction,scope: request,trigger: recovery,outcome: noopиtoken_count_method: provider_usage.
Политика плагинов на весь процесс и изоляция профилей
Конфигурация плагинов Relay — это решение уровня развёртывания процесса, а не настройка профиля Hermes. Первый обслуживаемый профиль запускает ленивую инициализацию, и каждый следующий профиль, который обслуживает этот процесс Hermes, разделяет получившиеся статические middleware, динамические плагины, подписчиков, экспортёры и политику ограничений (guardrails). После успешной инициализации Hermes пишет в лог загруженные файлы:
The Relay plugin host is active process-wide and applies to all profiles hosted by this Hermes process. Configuration files: /home/user/.config/nemo-relay/plugins.toml; /etc/nemo-relay/plugins.toml
Области видимости профилей внутри этой общей политики по-прежнему сохраняют причинную изоляцию. ATIF группирует события по их верхнеуровневой области видимости Agent, так что одновременные сессии разных профилей дают отдельные траектории, а не одну смешанную. ATOF и другие глобальные подписчики видят события всех обслуживаемых профилей. Статические и динамические middleware точно так же выполняются для управляемых вызовов из каждого профиля.
Плагин-воркер в отдельном процессе воркера не образует границы безопасности между профилями. Одна активация на весь процесс направляет в этот воркер вызовы из всех обслуживаемых профилей, сохраняя при этом стек областей видимости Relay вызывающего профиля. Нативные динамические плагины загружаются в процесс Hermes и делят с ним ту же границу политики.
Если профилям нужны разные уровни доверия, учётные данные плагинов, адресаты экспорта или политики ограничений — запускайте их в отдельных процессах Hermes. Этот процессный контракт плагинов не меняет независимое согласие каждого профиля на общие метрики, его локальное состояние SQLite и группировку траекторий ATIF.
Ядро Hermes владеет одним хостом Relay и одной изолированной областью
видимости сессии Relay на каждую сессию Hermes. Продюсеры жизненного цикла
в ядре получают через agent.relay_runtime общий дескриптор сессии или
выполняют в контексте этой сессии API областей видимости, LLM, инструментов
и марок Relay. Новые продуктовые марки не требуют регистрации плагина Hermes.
Марки общих метрик по-прежнему должны содержать только поля, одобренные
версионированным списком разрешений; жёсткая зависимость не меняет политику
сбора и приватности.
Текущие срезы
Текущие вертикальные срезы фиксируют псевдонимную активность профиля, логические вызовы модели, верхнеуровневые запуски задач, исходы инструментов и одобрений, а также жизненный цикл и повторное использование навыков:
Hermes turn, API, tool, and approval hooks
-> Relay session, task, LLM, tool, and mark lifecycle
-> Hermes shared-metrics subscriber
-> SQLite counters
-> immutable JSON delta package
Hermes отправляет пустой LLMRequest в жизненный цикл, принадлежащий
метрикам. Это не тот отдельный управляемый вызов через нативный runtime,
который описан выше. Терминальное событие метрик содержит идентификатор
модели и маршрут провайдера, которые Hermes использовал для логического
вызова, например nvidia/nemotron-3-ultra через openrouter. Эти
идентификаторы приводятся к нижнему регистру и ограничены структурно, но не
нормализуются по каталогу моделей из репозитория. Ценообразование и
классификация по семействам моделей — забота бэкенда метрик. Промпты, ответы,
эндпоинты, ошибки, идентификаторы сессий, задач и запросов в событие метрик
и в пакет не попадают. Новые вызовы используют hermes.model_route.count. Начиная со схемы пакета v3,
каждая строка маршрута также несёт call_role (primary или auxiliary),
outcome (success, failed, cancelled) и error_class: собственное
значение FailoverReason классификатора ошибок (rate_limit, auth,
context_overflow, …) для последней неудачной попытки этого логического
вызова либо none. Строка success с классом, отличным от none, — это вызов,
который восстановился после такой ошибки. Вспомогательные вызовы (заголовки,
сжатие, зрение, …) подчиняются тем же правилам: одна строка на логический
вызов независимо от числа попыток через запасные варианты, классификация тем же
классификатором (тело HTTP-200 с объектом error провайдера классифицируется
по этому объекту), cancelled с none, если Hermes прервал вызов (/stop,
Ctrl+C, прерывание, завершение работы), и unknown только когда классификатор
не может назвать сбой. Вспомогательный вызов, идущий рядом с ходом (генерация
заголовка) и завершающийся под собственными живыми областями хода, всё равно
считается: его результат закрывает область, когда ход его вычерпывает.
У вспомогательных строк ttft_bucket равен unknown: большинство
вспомогательных вызовов не стримятся. Прежний контракт
hermes.model_call.count остаётся только читаемым — чтобы ожидающие локальные
счётчики, созданные старыми сборками, можно было экспортировать без потери
данных.
Первый старт сессии с согласием порождает пустую марку Relay
hermes.client.active. Подписчик уровня профиля создаёт случайный UUID
идентичности установки и через транзакционный compare-and-set записывает
не более одного счётчика активного клиента в любом скользящем 24-часовом окне.
У метрики нет измерений; версия Hermes, семейство ОС, архитектура и способ
установки остаются ограниченными ресурсами пакета. Параллельные процессы
Hermes делят одну защёлку SQLite, поэтому одновременные старты не могут
посчитать одну установку дважды. Более поздняя сессия или задача может снова
попытаться поставить марку, но подписчик подавляет её до истечения скользящего
окна.
Каждый запуск задачи — это область видимости Relay Function с именем
hermes.task_run, вложенная в сессию Hermes, которой она принадлежит.
Стартовый счётчик содержит только ограниченные значения поверхности выполнения
и точки входа плюс, для задач шлюза, встроенную platform обмена сообщениями
(telegram, discord, slack, …; платформы, которые Hermes поставляет
в plugins/platforms/, — по имени; платформа из plugin-catalog/ — по имени
записи каталога, только если собственная запись установщика доказывает
установку из каталога; любая другая платформа-плагин — plugin; любая другая
поверхность — none). Терминальный счётчик (hermes.task_run.finished)
содержит поля старта плюс ограниченные исход, причину завершения, статус
прерывания и failure_class для упавших задач: FailoverReason провайдера,
если ход умер на классифицированной ошибке API, иначе локальный класс
(empty_response, context_compression, repeated_errors, exception,
other, …). То же событие завершения питает hermes.task_run.duration с
поверхностью выполнения, исходом, корзиной длительности и корзиной числа
повторов провайдера. Пакет v2 нёс длительность, повторы и число вызовов
модели и инструментов на задачу прямо в терминальной строке, из-за чего почти
каждая задача становилась отдельной строкой; число вызовов на ход живёт
в hermes.task_cost.count. Сырые причины выхода машину не покидают. Повторы —
это дополнительные попытки провайдера для одного и того же идентификатора
API-запроса Hermes; они не раздувают число логических вызовов модели. Вызовы
инструментов дедуплицируются по идентификатору вызова инструмента Hermes после
того, как замечен терминальный результат инструмента. Внешняя граница
выполнения AIAgent закрывает задачу при обычном возврате, раннем возврате,
исключении и отмене. Если Hermes вращает сессию разговора во время сжатия
контекста, владение активной задачей следует за идентификатором задачи.
Измерение entrypoint (в hermes.task_run.started, hermes.task_run.finished
и hermes.session.count) говорит, кто запустил выполнение; значения берутся
из закрытого множества:
| Значение | Смысл |
|---|---|
interactive | Человек в чат-интерфейсе: REPL hermes, hermes chat -q, который запускает REPL на TTY, --tui, Desktop, редакторы ACP. |
one_shot | Конечный запуск CLI, который отвечает на один промпт и завершается: hermes -z / --oneshot, hermes chat -q вне TTY или с --oneshot, -Q / --quiet. Строка shell человека и скрипт, который гоняет её в цикле, выглядят одинаково, поэтому оба читаются как one_shot. Ходы доставки Bot Chat (hermes -p <profile> chat -c "Bot Chat" -Q) — тоже one-shot запуски: их автором может быть человек на другом соединении. Поверхность остаётся cli. |
background | Автономный запуск, порождённый диспетчером Hermes: воркер kanban (HERMES_SESSION_SOURCE=kanban) или пересылка A2A (--source a2a). |
delegated | Запуск субагента под родительской задачей или сессией (имеет приоритет над значениями выше). |
gateway_message | Сообщение с платформы обмена сообщениями. |
scheduled_task, batch, api, python | Cron, пакетный раннер, сервер API, встраивание в Python. |
other, unknown | Не поддаётся атрибуции. |
Запуск считается one_shot или background, когда его процесс несёт маркер
HERMES_SINGLE_QUERY_SESSION, который выставляют one-shot пути (тот же маркер
читают источник сессии и cache_ttl: auto). Вовлечённость
(hermes.engagement.*) и строки только для наблюдаемых запусков (стоимость
задач, использование инструментов за сессию, трение моделей) относятся к
one_shot так же, как к interactive, — как до появления этого значения;
запуски background и delegated неконтролируемые и там исключены. Пакеты,
записанные до появления one_shot, несут такие запуски как interactive и
по-прежнему проходят проверку. hermes -z выходит через os._exit, поэтому
закрывает свою сессию метрик перед выходом, а не полагается на хук atexit.
Каждый вызов инструмента представлен жизненным циклом инструмента Relay
с именем hermes.tool_call. Терминальный счётчик содержит только ограниченные
корзины категории инструмента, исхода и исхода одобрения; то же событие питает
hermes.tool_call.latency с категорией инструмента, корзиной задержки и явной
корзиной числа повторов (пакет v2 нёс задержку и повторы в терминальной строке,
по одной строке на несколько вызовов). Категорию Hermes выводит из набора
инструментов, уже объявленного в его runtime-реестре; пользовательские и
нераспознанные наборы схлопываются в other, а имена инструментов и плагинов
не экспортируются. То же терминальное событие питает и
hermes.tool.usage.count с tool_name, outcome и error_class.
tool_name экспортируется только для инструментов, объявленных в статическом
toolsets.TOOLSETS репозитория (toolsets.BUILTIN_TOOL_NAMES, снятый до
создания любого пользовательского набора в runtime); инструменты MCP дают
mcp, а любой инструмент плагина или пользовательский — plugin.
error_class отображает собственные значения error_type Hermes
(tool_error, timeout, interrupted, invalid_arguments, blocked,
contract_violation); любое другое значение, например имя класса исключения,
схлопывается в exception. Повторы Hermes не угадывает по повторяющимся именам
инструментов или соседним вызовам: если хук не даёт явной связи повтора,
корзина повторов получает значение unknown. Решения об одобрении порождают
марки hermes.tool_approval и записываются либо с привязкой к вызову
инструмента, либо явно как unattributed. Имена не встроенных инструментов,
идентификаторы вызовов, аргументы, результаты, команды, описания и текст
ошибок в события и пакеты общих метрик не входят. Запущенный инструмент, который
всё ещё открыт на момент завершения задачи, закрывается как упавший,
просроченный или отменённый и остаётся в корзине счётчика инструментов этой
задачи.
Успешные изменения навыков порождают марки hermes.skill.lifecycle только
с ограниченным действием и происхождением. Успешные загрузки порождают марки
hermes.skill.load с ограниченным происхождением, состоянием
«первое использование / повторное», состоянием «повторное после патча»,
корзиной числа использований и skill_name: имя навыка — только если это
навык, который поставляет сам Hermes (skills/ или optional-skills/), иначе
custom. Повторное использование и непрерывность поколений патчей Hermes
вычисляет транзакционно в своём существующем состоянии skills/.usage.json;
имена локальных навыков и навыков, созданных агентом, а также точные счётчики
и поколения никогда не попадают в события метрик Relay, измерения SQLite или
пакеты. Использование после нового патча считается один раз как
reused_after_patch; последующие использования остаются обычным повторным
использованием до следующего патча. Привязка исхода задачи к патчу отложена,
пока не определены её окно и семантика для нескольких навыков.
Раз в скользящие 24 часа первая активация также порождает марку
hermes.install.snapshot, описывающую, как настроен профиль: провайдер памяти
(имя встроенного провайдера, builtin или plugin), корзины числа серверов
MCP, включённых плагинов, установленных навыков, включённых заданий cron,
профилей и подключённых платформ обмена сообщениями, идентификатор основного
провайдера, терминальный бэкенд (local, docker, ssh, … или other),
язык интерфейса (поставляемая локаль или other) и install_age_bucket:
как давно началась самая первая сессия профиля. Возраст установки позволяет
бэкенду отличить нового пользователя от старого, который только что дал
согласие. Имена серверов, плагинов, навыков, заданий и профилей в событие не
читаются никогда. Та же защёлка compare-and-set, что и у hermes.client.active,
держит его в одной строке на установку в сутки, а продюсер проверяет защёлку
до обхода дерева навыков.
Снимок также несёт шесть полей об отставании версий, канале и оборудовании; все читаются офлайн (без сетевых вызовов и подпроцессов):
release_channel(stable,main,dev,unknown): канал, зашитый в упакованную сборку (канареечные сборки main читаются какmain), запись о канале в установке из исходников, иначе ветка checkout (mainдля main/master,devдля любой другой ветки). URL git-remote и имена веток в событие не читаются никогда.version_age_bucket(lt_7d…gte_90d,unknown): возраст установленной версии по дате её собственного коммита в штампе установки или checkout.behind_bucket(0,1,2,3_to_5,6_to_10,gte_11,unknown): на сколько коммитов или релизов отстаёт версия, только по кэшированному результату проверки обновлений для этой самой ревизии не старше 7 дней; иначеunknown.ram_bucket(lt_8g…gte_128g,unknown): установленная память, округлённая до номинального размера (итог ОС, умноженный на 1,1 на резервирование под прошивку).gpu_class(nvidia,amd,intel,apple_silicon,none,unknown): вендор GPU с наивысшим приоритетом по/proc/driver/nvidiaили PCI-идентификаторам вендоров DRM в Linux, по классу в реестре видеоадаптеров в Windows, по нативной arm64 в macOS. Никогда не название модели, не версия драйвера и не объём VRAM.local_model_provider_used(yes/no): работает ли основная модель или любая вспомогательная задача на локальном или self-hosted сервере (идентификатор локального провайдера вроде Ollama/LM Studio/llama.cpp или базовый URL в loopback/частной сети). Сам URL остаётся на машине.
Метрики для продуктовых решений
Они отвечают на продуктовые вопросы, которые счётчики активности не покрывают: что удерживает людей, где отваливаются новые пользователи, какие поверхности и модели несут реальную нагрузку и в какие расширения стоит вкладываться. Каждое измерение — закрытое перечисление, корзина, идентификатор провайдера/модели (как в маршрутах моделей) или публичное имя, которое поставляет сама Nous.
| Метрика | Измерения | На какой вопрос отвечает |
|---|---|---|
hermes.session.count | entrypoint, surface, platform, корзины ходов и неудачных ходов, корзина активной длительности, последний исход, корзины числа сообщений / вызовов модели / вызовов инструментов (0 … 101_to_250, 251_to_1000, gte_1001) | Насколько глубоко реально используют каждую поверхность; заканчиваются ли сессии сразу после сбоя? Одна строка на разговор, пишется, когда поверхность его закрывает: идентификаторы сессий, на которые разговор передаёт ротация сжатия, сливаются в одну строку (выбывший идентификатор закрывается, как только завершается его ход в процессе); сброс шлюза, истечение простоя, /new или /branch начинают новый разговор, хотя и записывают старую сессию как родительскую. Форки фонового ревью, которые повторно используют идентификатор сессии, не добавляют ходов, вызовов и сообщений. Сообщения — это ходы пользователя + ответы основной модели + результаты инструментов; вызовы модели — логические запросы к основному API (повторы не считаются). |
hermes.install.milestone | milestone, корзина возраста установки | Сколько проходит от установки до первого успеха, первого сообщения шлюза, первого запуска cron, первого делегирования, первого созданного навыка (созданный фоновым ревью Hermes не считается), первой длинной сессии? Записывается один раз на установку. |
hermes.setup.completed | surface (cli/desktop), provider | Каких провайдеров выбирают при настройке и на какой поверхности. |
hermes.model_tokens.sum | роль вызова, модель, провайдер, вспомогательная задача, тип токенов | Объём токенов по моделям и провайдерам, доля кэша промптов и во сколько обходится вспомогательная работа (сжатие, заголовки, зрение, …). Значение — сумма токенов, а не число событий. |
hermes.model_route.count ttft_bucket | время до первого токена | Воспринимаемая задержка по провайдерам и моделям. |
hermes.compression.count | trigger, исход, корзина заполнения контекста, класс сбоя | Как часто запускается компактизация, насколько заполняются контексты и падает ли она. skipped = сбоить было нечему: блокировка удерживается в другом месте, нечего суммаризировать (вызова модели нет), остановка пользователем или попытку заменила более новая. Начиная со схемы пакета v4 строка также несёт failure_class, имя класса из журнала попыток по закрытому списку: причины пропуска (lock_contended, insufficient_messages, no_compressible_window, empty_post_handoff_window, explicit_interrupt, attempt_superseded, snapshot_stale) в пропущенных строках; причины сбоя (summary_auth_failure, summary_network_failure, summary_truncated_failure, summary_empty_content_failure, summary_overload_failure, summary_generation_aborted, summary_generation_failed, summary_overload_degraded, aux_model_fallback, feasibility_skip, stall_deterministic_fallback, stall_interrupted, would_grow, no_progress, commit_fence_cancelled, pool_saturated, session_split_failed, empty_transcript, exception, rollback) в строках сбоев; none при чистом успехе. Успех, зафиксированный через запасной вариант, сохраняет класс своей строки, поэтому failure_class, отличный от none, в строке success значит «зафиксировано через запасной вариант»: детерминированная сводка (summary_generation_failed, summary_overload_degraded, stall_deterministic_fallback или feasibility_skip, который покрывает и модель суммаризации, отправленную на скамейку после повторных откатов) или основная модель, заменившая упавшую модель суммаризации (aux_model_fallback). Имя типа Python у исключения или отката отбрасывается (exception:TimeoutError читается как exception), любое другое значение читается как other, сбой без класса — unknown. |
hermes.model_switch.count | провайдер «из»/«в», surface | От каких провайдеров уходят и к каким переходят. |
hermes.fallback.count | провайдер «из»/«в», класс ошибки | Как часто запасные провайдеры выручают ход и в каких случаях. |
hermes.slash_command.count | command, surface | Какие встроенные команды используются (/retry, /undo, /new — сигналы трения). Команды навыков и плагинов дают skill/plugin. |
hermes.extension.install.count | kind, source, name, исход | Какие навыки из каталога, серверы MCP и плагины устанавливают. name — встроенный/опциональный навык, запись optional-mcps/ или plugin-catalog/, иначе custom. |
hermes.memory.op.count | op (add/replace/remove/read/search/other), provider (builtin, встроенный плагин памяти, иначе plugin), origin (foreground/background_review), исход (success/failed/rejected), класс сбоя | Пишет ли цикл обучения в память, кто этого просит (ход пользователя или фоновое ревью), как часто записи отклоняются или падают и почему. Начиная со схемы пакета v4 failure_class называет причину из закрытого списка, сопоставленного с каждым отказом/сбоем встроенного хранилища: no_match (old_text не совпал ни с одной записью), ambiguous (совпал с несколькими), over_budget (запись превысила бы лимит символов), would_empty (пакет удалил бы последнюю запись), drift (файл содержит текст, который хранилище не может пройти туда и обратно), read_failed (файл существует, но не читается), retry_cap (исчерпан бюджет повторов на ход), scan_blocked (содержимое совпало с шаблоном угрозы), missing_content, missing_old_text, invalid_args, stale_entry (запись отложенной записи изменилась), disabled (эта цель выключена в конфиге), staged (удержано до одобрения пользователя), gate_refused (шлюз одобрения записи заблокировал её); provider_error / exception — для инструмента провайдера-плагина, вернувшего ошибку или упавшего; none при успехе; other/unknown в остальных случаях. Пакет применяется целиком или никак, поэтому каждая строка op отклонённого пакета несёт класс той операции, которая его остановила. Никогда не текст памяти, не old_text и не сообщение об ошибке. |
hermes.curator.run.count | trigger (scheduled/manual), исход (success/failed/skipped), корзины archived/merged/patched/created | Запускается ли куратор навыков и консолидирует ли он что-нибудь на деле. Пробные запуски дают skipped; плановая проверка, обнаружившая, что другой процесс уже выполняет проход, ничего не записывает. Никогда не имена навыков. |
hermes.delegation.run.count | корзина числа субагентов, глубина (1–3, gte_4), режим (foreground/background), исход (success/partial/failed/cancelled) | Насколько широко и глубоко разветвляется delegate_task и как часто завершаются все дочерние. Одна строка на вызов, на сколько бы единиц завершения он ни делился. |
hermes.execution_backend.count | kind (terminal/browser/code), бэкенд, исход, класс ошибки | Какие песочницы несут реальную работу и насколько каждая надёжна. Терминальные бэкенды — это значения terminal.backend (иначе other); бэкенды браузера — local, lightpanda, cdp, camofox, extension или встроенный облачный провайдер (иначе other); execute_code — local или remote. Ненулевой код выхода самой команды по-прежнему успех бэкенда, но команда на переднем плане, упёршаяся в таймаут, — это failed/timeout; вызовы terminal и execute_code, которые защита отклоняет до бэкенда, собственные листинги Hermes для автодополнения путей в TUI/Desktop и вызовы форков фонового ревью и куратора не считаются. |
hermes.platform.health | platform, event (connect_ok/connect_failed/reconnect/disconnect), класс ошибки (auth/network/rate_limited/config/other) | Какие платформы обмена сообщениями не подключаются или отваливаются и почему. connect_failed считает эпизод неудачного подключения один раз на профиль, платформу и сутки UTC (повторы наблюдателя переподключения — не новые строки; следующий успех завершает эпизод, а платформа, которая продолжает падать и на следующий день, считается снова), с классом ошибки первого сбоя эпизода. Классификация идёт по типам исключений, статусам HTTP и собственным фатальным кодам Hermes, никогда по тексту ошибки. |
hermes.platform.delivery | platform, исход (sent/failed), класс сбоя (rate_limited/too_long/auth/network/forbidden/other) | Как часто ответы не доходят до пользователя, по платформам (один счёт на логический ответ, повторы включены). |
hermes.gateway.reply_latency | platform, корзина первого ответа (lt_2s … gte_60s) | Время от принятого входящего сообщения до первого видимого текста ответа (первый фрагмент стрима или итоговое сообщение). |
hermes.cron.run | исход (success/failed/missed/skipped), вид доставки (local/platform/webhook/none/other), корзина длительности | Запускаются ли плановые задания, падают, пропускаются из-за ограничителя или наложения, либо пропускаются, пока Hermes был выключен. Имена заданий, промпты, расписания и цели не включаются никогда. |
hermes.startup.latency | surface (cli, tui, desktop_attach, gateway_boot, serve_boot), корзина задержки (lt_500ms … gte_10s) | Сколько времени каждая поверхность идёт от запуска до готовности, чтобы регрессии старта были видны по поверхностям и релизам. Одна строка на старт процесса: CLI = от старта процесса до первого отрисованного промпта (или отправленного запроса -q; воркеры Kanban исключены), TUI = от старта процесса Ink до готовности шлюза, Desktop = от старта приложения до подключённого бэкенда, шлюз = от старта процесса до подключённых адаптеров, hermes serve = от старта процесса до начала прослушивания. Не считаются: процесс, перезапущенный на месте через re-exec (например, hermes sessions browse, возобновляющий сессию), и терминал каждой вкладки Chat в дашборде; повторное подключение TUI/Desktop к тому же бэкенду заново не считается. |
hermes.update.run | kind, исход, failed_stage, duration_bucket, from_version_age_bucket, apply_mode | Успешны ли обновления, сколько они длятся, где падают и насколько устарела версия, с которой обновляются. outcome — success, noop, failed, refused или partial (зафиксировано, затем прервано или оставило отложенные изменения пользователя в stash; только строки hermes update). Запуск partial считается попыткой и сбоем в показателе обновлений на пользователя (раньше такие запуски читались как failed, так что тренд остаётся непрерывным), а его причины лежат рядом с запусками, которые применили код, а затем провалили последующий шаг. Строки hermes update выводятся из итоговой квитанции обновления, один раз на запуск (kind — desktop, если запуск сделал hand-off Desktop из checkout исходников); запуск, который завершает интерпретатор до обновления или голый интерпретатор начальной загрузки после провала подготовки зависимостей (он не может загрузить читатель конфига), паркуется локально только с ограниченными полями, нужными строке, и считается при следующем старте, который выбрасывает его без записи, если сбор выключен (ничего не паркуется, когда этот интерпретатор уже может определить, что сбор выключен); упакованные самообновления Desktop (apply_mode=package) приложение сообщает один раз, после перезапуска, который их применяет. Отказ по блокировке (lock_held) дочернего процесса обновления, запущенного hand-off Desktop (маркер обновления называет его или его родителя делегатом hand-off), читается как failed, а не refused: Desktop уже закрылся ради этого обновления и сообщает о нём как о неудачном. Сюда входит и блокировка checkout, удерживаемая другим мутатором (сборка Desktop, синхронизация venv), а не только запущенное другое обновление. |
hermes.update.stage | stage, исход, duration_bucket | Результат и время по стадиям hermes update (plan, snapshot, apply, deps, build, restart, verify) по временным меткам стадий из квитанции. |
hermes.process.exit | process_kind, exit_kind, crash_class | Как завершаются процессы CLI / TUI / шлюза / serve / cron-tick (clean, crash только с семейством исключения, killed, watchdog); о них сообщает следующий старт в том же профиле по локальному маркеру (старт с выключенным сбором удаляет эти маркеры и ожидающие provider_setup без отчёта). Ходы, прерванные сторожем хода, тоже считаются как exit_kind=watchdog. |
Ответы, которые несёт relay-коннектор, сообщают платформу, на которой идёт
разговор (платформа входящего сообщения, а когда коннектор представляет ровно
одну платформу — она), но никогда не relay; relay остаётся, только когда
неизвестно ни то ни другое. Ход, входящее сообщение которого коннектор не
пометил, всё равно считается сообщением шлюза (execution_surface=gateway,
у задачи/сессии platform=relay). hermes.platform.health для relay-коннектора
остаётся relay: его единственный сокет представляет несколько платформ,
поэтому подключение или обрыв не принадлежат ни одной из них в отдельности.
Десктопное приложение: что используют, что мешает, что отключают
Записывается приложением Desktop в хранилище профиля в фокусе, только пока у этого профиля включён переключатель сбора. Когда он выключен, приложение не хранит локальной записи (выключение удаляет то, что было сохранено) и ничего не отправляет. Приложение держит одну локальную запись на подключение к шлюзу и профиль, общую для окон этого профиля; после смены профиля ничего не хранится и не отправляется, пока не прочитан переключатель нового профиля, а сам переключатель перечитывается, когда окно вновь получает фокус (так что отказ через CLI или в другом окне действует и здесь). Запросов оценки и другого нового UI нет; каждый факт берётся из взаимодействия, которое в приложении уже есть. Каждое значение — закрытый идентификатор, определённый в коде приложения (область, действие, уведомление, сценарий, переключатель, шаг), опубликованный ключ конфигурации или корзина. Текст сообщений, текст тостов, имена сессий/ботов/профилей, пути и значения настроек не уходят никогда.
| Метрика | Измерения | На какой вопрос отвечает |
|---|---|---|
hermes.desktop.feature_use | area (панели, палитра команд, выбор модели/сессии, голос, Bot Mode, скины, проекты, каждая страница настроек, полные страницы, other) | Какие области Desktop используются вообще; считается не чаще раза на область за сутки UTC на профиль (защёлка в локальной базе профиля, так что второе окно или перезапуск бэкенда не пересчитывают). |
hermes.desktop.action_use | action (встроенные идентификаторы команд/сочетаний клавиш приложения плюс несколько именованных кнопок; команды плагинов и нумерованные быстрые клавиши слотов — other, схлопываются в приложении до того, как что-либо сохранено), via (click/shortcut/palette/menu), корзина числа | Какие кнопки и команды нажимают и как: агрегируется в приложении по профилю и сообщается один раз за завершённые сутки (строк на каждое нажатие нет). Строка ложится в период суток, которые она описывает, а не суток отправки; каждые сутки записываются один раз на профиль, сколько бы окон или перезапусков бэкенда о них ни сообщали, а сутки старше 8 дней отбрасываются. |
hermes.desktop.mode_use | mode (sessions/bots), корзина активных минут, корзина отправленных сообщений, корзина числа ботов | Как время в Desktop делится между Bot Mode и обычными сессиями. Одна строка на использованный за день режим, в период этого дня (та же защёлка «раз на профиль в сутки», что у action_use); активное время суммирует паузы между взаимодействиями до 5 минут. |
hermes.desktop.friction | kind (notice_dismissed, error_toast, renderer_crash, backend_disconnect, slow_frame), detail (идентификатор уведомления, категория ошибки, причина сбоя, причина обрыва, корзина длительности кадра) | Что мешает. Тосты ошибок несут только категорию, заданную в коде; падения рендерера записывает оболочка приложения, только когда собирает данные профиль самого упавшего окна, а сообщает о них окно этого профиля после возвращения; медленные кадры — это долгие кадры, пока окно видимо, с ограничением в сутки. |
hermes.desktop.dislike | signal (quick_close, cancelled, setting_off_default, rage_click, undo, feature_disabled), target, setting, direction | Сигналы, что функция не нужна: панель закрыта в течение 5 с после открытия, диалог/сценарий, из которого вышли назад, настройка, переведённая к значению по умолчанию или от него (только ключ; бэкенд сам сравнивает сохранённое значение со значением по умолчанию), три клика по одному элементу за секунду, отмена действия, выключенная поставляемая функция. Ограничено на сигнал в сутки. |
hermes.desktop.onboarding | step (шаги первого запуска: выбор провайдера, вход, ключ API, локальный эндпоинт, выбор модели, «выбрать позже», экран бесплатного тарифа, карточки управляемой настройки, согласие, первое сообщение), event (reached/completed/abandoned) | Где останавливается первый запуск. Каждое событие шага — один раз на профиль (защёлка в небольшом файле профиля); abandoned — шаг, всё ещё открытый, когда приложение стартует в следующий раз. Первый запуск происходит до вопроса о согласии, поэтому пока на него не ответили, приложение держит события шагов только в памяти (не пишет на диск и не отправляет) и записывает их, если пользователь соглашается в эту сессию приложения; «нет» или выход раньше отбрасывают их. Выключение сбора в Desktop удаляет эти защёлки вместе с собственной копией приложения. |
Сессии подытоживаются при закрытии (финализация, сброс или выход процесса); делегированные дочерние сессии отдельно не считаются. Вехи фиксируются защёлками в локальной базе, поэтому каждая срабатывает один раз на установку, сколько бы процессов её ни достигло.
Качество, трение и давление контекста по моделям
Провайдер и модель подчиняются правилам маршрута модели: провайдер, который
поставляет Hermes (встроенный, профиль plugins/model-providers/ из дерева
исходников или публичный идентификатор models.dev), и его идентификатор модели;
пользовательские эндпоинты, плагины провайдеров, установленные в
$HERMES_HOME/plugins/model-providers/ или из pip (включая имена и алиасы),
локальные алиасы серверов для custom (ollama, local, vllm, llamacpp,
llama-cpp, llama.cpp) и серверы на loopback читаются как custom.
Поставляемый провайдер, чей эндпоинт — сервер на loopback (lmstudio под любым
из его алиасов), сохраняет своё имя, но его модель читается как custom.
Модель, чей провайдер неизвестен или чей идентификатор — URL, путь к файлу,
сетевой адрес (host:port, IP-адрес, localhost) или ARN AWS (он содержит
идентификатор аккаунта), читается как custom. У провайдеров Azure
идентификатор модели — это имя развёртывания, выбранное владельцем, поэтому он
проходит только когда это публичный идентификатор модели (каталоги моделей
Hermes или локальный кэш models.dev, например gpt-4o); acme-legal-prod
читается как custom. Локальный подписчик заново применяет эти правила к полям
провайдера и модели каждой марки и отбрасывает строку, которую они переписали
бы.
| Метрика | Измерения | На какой вопрос отвечает |
|---|---|---|
hermes.model_tool_quality.count | provider, model, роль вызова, issue (none, invalid_json, unknown_tool, schema_mismatch, empty_arguments, repaired) | Какие модели выдают сломанные вызовы инструментов и как часто Hermes приходилось их чинить. Каждый выданный вызов считается один раз (чистые — как none), так что значение служит знаменателем доли. empty_arguments считается только для инструментов с обязательными параметрами; repaired значит, что Hermes исправил имя инструмента или JSON аргументов и выполнил вызов. |
hermes.model_friction.count | provider, model, signal (retry, undo, interrupt, quick_abandon, switch_away) | С какими моделями пользователи борются. Относится к модели, выдавшей ход: /retry и /undo там, где они выполняются, прерывание пользователем интерактивного хода, сессия, закончившаяся в течение 60 секунд после неудачного хода, и переключение /model прочь от модели. |
hermes.context_peak.count | provider, model, корзина пика заполнения, корзина окна (lt_32k … gte_1m), достигнут ли лимит (yes/no) | Насколько близко сессии подходят к контекстному окну каждой модели и как часто его переполняют. Одна строка на закрытый разговор: идентификаторы сессий, на которые разговор передаёт ротация сжатия, сообщаются один раз, по самому заполненному сегменту; limit_hit значит, что основной вызов отклонён как слишком большой (переполнение контекста или HTTP 413), то есть отказы, на которые Hermes отвечает принудительным сжатием. |
Точность обвязки агента
Эти метрики настраивают сам цикл агента. Собственные циклы фонового ревью и куратора Hermes не считаются никогда; делегированные субагенты — считаются (их вызовы инструментов, циклы и ответы — тоже поведение модели). Текст команд, пути к файлам, аргументы инструментов и текст ответов не уходят никогда — только закрытые значения ниже.
| Метрика | Измерения | На какой вопрос отвечает |
|---|---|---|
hermes.file_edit.count | tool (patch, write_file), mode (replace, v4a, whole_file), исход (applied, already_applied, no_match, ambiguous, failed), стратегия совпадения (цепочка нечёткого сопоставления инструмента patch: exact, line_trimmed, whitespace_normalized, indentation_flexible, escape_normalized, trimmed_boundary, unicode_normalized, block_anchor, context_aware; none, когда ничего не совпало) | Какие стратегии нечёткого сопоставления оправдывают себя и как часто правки промахиваются или неоднозначны. Одна строка на вызов инструмента правки; многофрагментный патч V4A сообщает самую свободную стратегию, какая понадобилась любому фрагменту. |
hermes.loop_guard.count | provider, model, signal (repeated_tool_call, loop_detected, iteration_cap), detector (exact_failure, idempotent_no_progress, same_tool_failure, identical_call_streak, identical_cycle, web_search_cap, subagent_cap, iteration_budget) | Как часто срабатывает каждая защита от зацикливания, по моделям. repeated_tool_call — предупреждение, с которым вызов всё же выполнился, loop_detected — блокировка или остановка, iteration_cap — ход, исчерпавший бюджет итераций. Не чаще раза на ход на сигнал и детектор. |
hermes.tool_recovery.count | provider, model, tool (имя встроенного, иначе mcp / plugin), следующий инструмент (same, different, none), следующий исход (success, error, no_tool_call, gave_up) | Восстанавливаются ли модели после неудачного вызова инструмента. Одна строка на неудачный вызов, разрешается по следующему раунду модели: её следующий вызов того же инструмента, иначе первый её вызов; no_tool_call, когда она ответила текстом, gave_up, когда ход закончился без её ответа (остановлен, бюджет исчерпан, прерван, ошибка). |
hermes.terminal.outcome.count | backend (терминальные бэкенды), вид команды (git, package_manager, build, test_runner, python, node, shell_builtin, shell, file_ops, network, container, other), исход (ok, nonzero, timeout, killed) | Какие виды команд падают или упираются в таймаут, по бэкендам. Вид берётся из фиксированной таблицы по первому слову программы (после присваиваний окружения и обёрток вроде sudo). Одна строка на команду на переднем плане, дошедшую до статуса выхода; timeout / killed берутся из собственных дедлайна и флагов прерывания Hermes, поэтому собственный exit 124 команды — это nonzero. hermes.execution_backend.count считает те же вызовы по признаку, обслужил ли их бэкенд, — непересекающиеся измерения, а не второй счёт исходов. |
hermes.model_reply_issue.count | provider, model, issue (none, empty, reasoning_only, refusal, truncated_length) | Какие модели возвращают непригодные ответы. Одна строка на ответ основной модели (пригодные — как none, знаменатель доли). refusal и truncated_length берутся только из структурированной причины завершения (content_filter, length); empty — корректный ответ без видимого текста, вызова инструмента и рассуждений. |
Эффективность: стоимость хода, потери, накладные расходы инструментов и разрывы кэша промптов
Провайдер и модель подчиняются правилам маршрута модели выше. «Ход пользователя»
— одно сообщение пользователя вплоть до итогового ответа; работа, которой
владеет Hermes (фоновое ревью памяти/навыков, куратор, собственные ходы
делегированных субагентов), ходом пользователя не считается. cache_break и
tool_output_truncation описывают поведение модели и инструментов, поэтому
делегированные субагенты там считаются; фоновое ревью и куратор — никогда.
| Метрика | Измерения | На какой вопрос отвечает |
|---|---|---|
hermes.task_cost.count | provider, model, корзина токенов (lt_2k … gte_1m, unknown), корзина вызовов инструментов, корзина вызовов API (0 … 51_to_100, gte_101), исход (completed, interrupted, failed) | Сколько стоит ход пользователя по моделям. Токены — это промпт (включая чтение и запись кэша) плюс завершение по основным вызовам хода; unknown, если провайдер не сообщил расход. Одна строка на интерактивный ход, окончание которого увидел пользователь (аборт при закрытии сессии ходом не считается). |
hermes.wasted_tokens.count | provider, model, reason (interrupt, retry, undo), корзина токенов | Сколько токенов пользователи выбрасывают. Одна строка на ход, который отбросили прерывание, /retry или /undo (/undo N считает N ходов), относится к модели, выдавшей этот ход; ход, прерванный и затем отменённый, считается один раз. unknown, когда этот процесс ход не видел (перезапуск, удалённый хост). |
hermes.tool_output_truncation.count | tool (имя поставляемого инструмента, иначе mcp / plugin), truncated (yes/no), корзина исходного размера (в символах: lt_1k … gte_500k) | Какие инструменты выдают вывод, слишком большой для хранения в строке. Одна строка на результат инструмента; yes, когда инструмент сам обрезал вывод (обрезка начала/конца в terminal, execute_code и MCP; размер тогда исходный) или его сбросили на диск ограничение на результат либо бюджет на ход. |
hermes.tool_overhead.count | корзина числа включённых инструментов, корзина токенов схем инструментов (0, lt_2k … gte_40k), поверхность выполнения | Во что обходится ношение определений инструментов. Одна строка на закрытый интерактивный разговор: включённые в нём инструменты и собственная оценка Hermes числа токенов, которые их определения добавляют к каждому запросу. |
hermes.tool_enabled_unused.count | toolset (набор, который поставляет Hermes; серверы MCP, плагины и пользовательские наборы читаются как custom), used (yes/no) | За какие наборы инструментов по умолчанию платят, но не пользуются. Одна строка на включённый набор на закрытый интерактивный разговор (ограничена поставляемыми наборами). |
hermes.cache_break.count | provider, model, cause (compression, model_switch, toolset_change, system_prompt_rebuild, provider_reported_miss, cache_expired) | Как часто Hermes выбрасывает прогретый кэш промптов и почему. compression ожидаема; model_switch, toolset_change (массив инструментов изменился посреди разговора) и system_prompt_rebuild (продолжающийся разговор пересобрал системный промпт вместо воспроизведения сохранённых байтов) — причины, известные Hermes; provider_reported_miss — основной вызов, прочитавший ноль токенов из кэша сразу после прогретого чтения на той же модели без известной Hermes причины, cache_expired — то же после как минимум пяти минут простоя. Известная причина повторно как промах не считается. |
Вовлечённость и неявная удовлетворённость моделью
| Метрика | Измерения | На какой вопрос отвечает |
|---|---|---|
hermes.engagement.surface_day.count | surface (cli, tui, desktop, gateway, acp), корзина активных минут (0, lt_5m, 5m_to_30m, 30m_to_2h, 2h_to_6h, gte_6h) | Как долго на самом деле используют каждую поверхность в день. Одна строка на поверхность, использованную в закрытые сутки UTC. |
hermes.engagement.day.count | корзина активных минут, число использованных поверхностей (0–3, gte_4), основной провайдер, основная модель, корзина числа активных профилей | Дни активности в неделю, использование нескольких поверхностей и возврат на следующий день / следующую неделю по моделям. Одна строка на закрытые сутки UTC, в которые человек пользовался Hermes. Корневой (по умолчанию) профиль также пишет строку хоста в дни, когда были активны только другие профили: surfaces_used_count равен 0, активные минуты 0, и строка несёт число активных профилей; при подсчёте дней активности исключайте строки с surfaces_used_count=0. |
hermes.model_switch_after.count | provider, model (модель, от которой ушли), корзина числа ходов до переключения (1, 2_to_3, 4_to_10, 11_to_30, gte_31) | Как долго пользователи остаются на модели, прежде чем /model её покидает. Считает ходы пользователя, отправленные на старой модели в разговоре (включая сегменты сжатия; ход, ушедший на запасную модель, всё равно относится к модели, на которую был отправлен; форки фонового ревью ходами не считаются); переключение до первого хода на текущей модели не считается. |
Активное время накапливается локально по суткам UTC: сумма пауз между
последовательными взаимодействиями (ход пользователя, начинающийся или
заканчивающийся на интерактивной поверхности, либо сообщение шлюза;
неконтролируемые запуски cron, которые считает hermes.cron.run, делегированные
дочерние, фоновое ревью, куратор, пакетный режим и встраивание через сервер
API / Python исключены), каждая пауза ограничена 5 минутами. Строки суток
записываются, когда сутки закрываются первым взаимодействием в более поздний
день, в одной транзакции базы данных, так что сутки сообщаются ровно один раз
на профиль, сколько бы процессов ни увидели смену суток; они датируются
сутками, которые описывают. Основная модель — та, что обслужила больше всего
ходов пользователя в этот день (none, если таких не было), её имя задают
правила маршрута модели. Дни активности в неделю и возврат по моделям
выводятся на сервере из этих суточных строк и существующего install_id:
Hermes не хранит для них ни недельного окна, ни идентификатора, кроме
install_id.
active_profile_count_bucket считает различные профили хоста с ходом,
которым владеет пользователь (взаимодействия выше), в эти сутки UTC. Каждый
профиль сворачивает свои ходы в один накопитель хоста, который лежит в базе
корневого (по умолчанию) профиля, как непрозрачные локальные хеши домашнего
каталога каждого профиля, которые его никогда не покидают, так что профиль
считается один раз, какой бы процесс или мультиплексированный runtime его ни
обслуживал. Число несёт только строка суток корневого профиля (он сообщает
сутки, даже когда сам корневой простаивал, с 0 активных минут и
поверхностей); строка любого другого профиля читается как 0. Когда у
корневого профиля сбор выключен, в его базу ничего не пишется, и число не
сообщается.
Онбординг и сигналы о функциях
| Метрика | Измерения | На какой вопрос отвечает |
|---|---|---|
hermes.tool_unavailable.count | provider, model, имя инструмента (только поставляемые встроенные) | Какие наборы инструментов должны быть включены по умолчанию: модель вызвала поставляемый Hermes инструмент, который эта сессия не включала. Любое другое неизвестное имя (плагин, MCP, выдуманное) остаётся только проблемой unknown_tool в model_tool_quality. Встроенный инструмент, включённый сессией, но отложенный за tool_search (доступный через tool_call), недоступным не считается. Фоновые ревью, делегированные дочерние и задания cron, чьи наборы инструментов сужены намеренно, исключены. |
hermes.provider_setup.count | provider (имя из каталога; пользовательские эндпоинты читаются как custom), surface (cli_setup, cli_model, tui, desktop, dashboard), event (started, completed, failed, abandoned), класс сбоя (auth, network, no_models, other; none, если не было сбоя; cancelled больше не записывается, отмена — это abandoned) | Где срывается подключение провайдера. started считается, как только провайдер выбран; конец сценария записывает поверхность, которая его вела. Сценарий, который пользователь бросил, — это abandoned, но не failed: Esc или Ctrl-C в выборе CLI, Cancel/Back при входе в Desktop или дашборде, отказ от согласия на странице провайдера и код входа, оставленный истекать (у любого провайдера). Back (стрелка влево) в CLI оставляет сценарий открытым, так что повторный выбор того же провайдера продолжает его (один started), а выбор другого провайдера или выход из команды завершает его как abandoned. Сценарий, который никто не довёл до конца, оставляет локальный маркер, о котором следующий старт настройки или Hermes в профиле сообщает как об abandoned (его процесса уже нет или он ждёт больше часа). Вход в Desktop/дашборде, оборвавшийся посреди опроса, сохраняет класс ошибки, которая его завершила (network для оборванного соединения, auth для отказа), а не голый other. Новый или изменённый ключ API провайдера, сохранённый из формы (TUI/Desktop/дашборд), и вновь добавленный пользовательский эндпоинт начинаются и завершаются одним действием; очистка ключа, повторное сохранение того же ключа, правка существующего эндпоинта, токены экосистем (GITHUB_TOKEN, GH_TOKEN, HF_TOKEN) и ключи, которые запрашивает и панель настроек инструмента (например, GEMINI_API_KEY, XAI_API_KEY, DEEPINFRA_API_KEY), из общей формы ключей не считаются (онбординг и настройки модели в Desktop помечают свои сохранения как подключение провайдера, поэтому те считаются). Никогда не ключ, не токен, не базовый URL и не текст ошибки. Выход из выбора провайдера до выбора не считается. |
hermes.feature_adoption.count | feature (memory, skills_created, delegation, cron, gateway_platform, desktop, tui, mcp, plugins, browser, voice, kanban, projects, bot_mode, curator), дни с установки (same_day, 1d_to_7d, 7d_to_30d, 30d_to_90d, gte_90d, unknown) | Через сколько после установки каждой крупной функцией впервые реально пользуются. Один раз на функцию на установку, защёлка в локальной базе, выводится из счётчиков выше (запись в память на переднем плане, навык, созданный по просьбе пользователя (не фоновым ревью Hermes), успешный вызов инструмента MCP/плагина/браузера/TTS/kanban, задача Desktop/TUI/шлюза, запуск cron, ручной запуск куратора; плановый проход куратора не считается) плюс прямые отчёты о первом использовании для сообщений Bot Mode и создания проекта. Возраст берётся у профиля-владельца (его первая сессия). |
hermes.feature_disabled.count | kind (toolset, skill, plugin, platform, setting, memory, curator, compression), name, surface (cli_tools, cli_config, cli_slash, tui, desktop, dashboard), event (disabled, re_enabled) | Что пользователи отключают. Сверяется при самой записи конфига: удалённый набор инструментов, включённый по умолчанию, навык или плагин, добавленный в список отключённых, настройка со значением по умолчанию true, переведённая в false (и каждый возврат обратно). Имена публичны, только когда поставляются: ключ набора инструментов, встроенный навык или навык из каталога, встроенный плагин или плагин из каталога (плагины платформ обмена сообщениями сообщаются как platform), путь ключа DEFAULT_CONFIG (никогда не значение) — иначе custom. Удаление навыка из каталога считается как disabled. Записывают только пользовательские точки входа (hermes tools / config / skills / plugins, слэш-команды в чате, TUI/Desktop, дашборд); настройка и миграции не записывают, даже когда миграция запускается внутри одной из них (hermes config migrate, профиль, созданный из дашборда). Настройка, чьё значение — шаблон ${VAR}, не сравнивается. Сверка и запись идут в фоновом потоке после записи, вне всех блокировок конфига. Не чаще раза на (kind, name, event) в сутки. |
Причины сбоев установки и обновления
Начиная со схемы пакета v4 (расширена на месте; только добавления, так что любая уже отправленная форма остаётся допустимой), две существующие метрики говорят, почему они упали. Строки, записанные до изменения, сохраняют прежний набор полей и по-прежнему упаковываются.
| Метрика | Добавленные измерения | На какой вопрос отвечает |
|---|---|---|
hermes.extension.install.count | класс сбоя, registry | Почему падают установки навыков, плагинов и MCP и какой источник skills-hub даёт сбои. failure_class — none при успехе, иначе закрытое имя выхода по сбою, остановившему установку. Навыки: ambiguous, auth_rejected, fetch_failed, invalid_bundle, invalid_name, not_found, rate_limited, scan_blocked, stale_index. Плагины: already_installed, clone_failed, deps_declined, deps_failed, git_missing, incompatible, invalid_source, manifest_invalid, non_interactive, removed_from_catalog, scan_blocked. Серверы MCP: auth_required, bootstrap_failed, clone_failed, config_invalid, config_rejected, connect_failed, git_missing, missing_credentials, server_start_failed. Любой вид может также читаться как exception, filesystem_error, network, other, permission. Выброшенная ошибка без названного класса классифицируется только по её типу Python (PermissionError читается как permission, ошибки соединения и таймаута — network, прочие OSError — filesystem_error, всё остальное — exception, имя типа отбрасывается). registry задан только в строках навыков: адаптер skills-hub, который разрешил навык или отдал его, один из browse-sh, clawhub, github, hermes-index, lobehub, official, skills-sh, url, well-known (идентификаторы адаптеров, которые строит tools/skills_hub_search.py::create_source_router), unresolved, когда ни один адаптер не ответил, none для восстановлений встроенных и всех строк плагинов/MCP, иначе other. Строка, у которой failure_class вне набора её вида, равен none при сбое или задан при успехе, либо которая называет registry в строке плагина/MCP, недопустима. Никогда не идентификатор, не URL, не текст ошибки и не путь; name сохраняет прежнее правило «имя из каталога или custom». |
hermes.update.run | класс сбоя | Почему падает hermes update, рядом с тем, где (failed_stage). none, если исход не failed, refused или partial. Выводится только из итоговой квитанции обновления: build_failed, deps_failed, exception, fleet_stale, fleet_unverified, git_failed, interrupted, lock_held, managed_install, os_error, disk_full, permission_denied, restart_failed, subprocess_failed, иначе other; упакованные самообновления Desktop сообщают unknown (их RPC несёт стадию, но не причину). Исключение, завершившее запуск, читается только по имени своего типа, никогда по сообщению (PermissionError читается как permission_denied, OSError с errno ENOSPC — как disk_full); неудачное возобновление шлюза Windows после обновления читается как restart_failed. Каждый выход, останавливающий запуск до отметки стадии apply, записывает на самом выходе один закрытый токен, и запуск его читает (несколько из них, syntax_rollback, stash_restore_rejected, unexpected_branch, head_moved, срабатывают после того, как git сдвинул дерево и вернул его обратно или оставил следующему запуску для восстановления): fetch_failed, git_timeout (сетевой вызов git упёрся в лимит времени обновлятора), channel_unresolved (также недопустимое имя канала в config.yaml), gateway_pause_failed (Windows: шлюзы не удалось безопасно приостановить до перемещения venv), git_in_progress (merge/rebase/… уже выполнялся), git_index_locked, merge_conflict (локальные коммиты в пользовательской ветке), checkout_move_failed, branch_missing, parked_branch_blocked, detached_head, unexpected_branch, head_moved (ref сдвинулся посреди обновления), target_unresolved, target_syntax_error (отказ до перемещения), syntax_rollback (переместили, не прошло проверку синтаксиса, откатили), commit_point_refused (точку фиксации не удалось взвести; ничего не сдвинулось), venv_foreign_owner, local_changes_blocked (локальные изменения, которые git не смог убрать в stash, или грязное дерево, которое запасной вариант с ZIP не станет перезаписывать), stash_restore_rejected, not_git_checkout, old_version_handoff (hand-off старого обновлятора не удалось завершить), branch_unsupported, download_failed, zip_failed, disk_full, lock_held, managed_install. aborted_before_apply теперь служит лишь запасным значением для такого выхода без записанного токена. Предварительная проверка синтаксиса при старте читает критичные файлы цели из хранилища объектов git; когда это чтение упирается в таймаут (установка без блобов лениво подтягивает каждый блоб), проверка пропускает их как отсутствующие файлы, а решает последующая проверка синтаксиса после перемещения, так что запуск больше не падает как subprocess_failed. Отказ по блокировке обновления, отказ из-за операции Git и отказ управляемой установки срабатывают до открытия квитанции запуска; квитанции они по-прежнему не пишут (хранилище квитанций принадлежит обновлению, которое держит блокировку) и записывают одну строку с этим токеном, с kind desktop, когда маркер обновления называет hand-off Desktop этого запуска (как его делегата или партнёра по hand-off), но никогда лишь потому, что маркер удерживает другое обновление. Ошибка git, завершившая перемещение checkout, читается как git_index_locked только когда git сообщает, что блокировка индекса уже существует; отказ git в праве записи в него (или в любой файл) читается как permission_denied. Токен читается, только пока у запуска нет отметки apply, так что причина не может пережить выход, который её записал. Запуск, который зафиксировал, а затем был прерван, или чьи отложенные в stash локальные изменения не удалось применить заново, читается как исход partial (interrupted / local_changes_parked, failed_stage apply для последнего), но не failed; одни лишь отложенные последующие шаги оставляют запуск success (контракт C3). Запуск, который завершает интерпретатор до обновления, паркуется с токеном, кодом выхода, ведущим типом исключения причины остановки (или одной из двух фиксированных фраз Hermes) и флагами перезапуска/действия пользователя (никогда argv, пути и сообщения), так что он классифицируется точно так же, как тот же запуск, завершённый внутри процесса. Неудачный запуск, у которого все отметки стадий прошли, теперь читается как failed_stage verify (проверка после перезапуска записала partial) или restart (пропущенный перезапуск оставил флоту долг) вместо other; запуск partial с неудачной отметкой build или restart сохраняет эту стадию, а его строка стадии verify считается проваленной, только когда во флоте есть устаревшая или лежащая строка. |
Свежие установки
| Метрика | Измерения | На какой вопрос отвечает |
|---|---|---|
hermes.install.run | installer (install_sh, install_ps1, other), исход (success, failed), failed_stage (prerequisites, repository, venv, python_deps, config, products, setup, gateway, complete, other; none при успехе), failure_class (unsupported_platform, download_failed, download_digest_mismatch, uv_unusable, git_missing, curl_missing, libstdcxx_missing, git_extract_failed, dir_not_checkout, git_clone_failed, git_fetch_failed, local_changes_blocked, git_checkout_failed, git_reset_failed, commit_not_on_branch, filesystem_error, python_install_failed, deps_install_failed, products_build_failed, setup_failed, gateway_failed, interrupted, other; none при успехе), корзина длительности (lt_30s … gte_15m) | Как часто удаётся свежий запуск install.sh / install.ps1 и на какой стадии и по какой закрытой причине он падает. Установщик работает до вопроса о согласии и ничего не отправляет: полный запуск (не отдельный вызов --stage / -Stage, так что пошаговые вызовы начального установщика Desktop не считаются) оставляет один небольшой локальный файл в $HERMES_HOME/telemetry/shared_metrics/pending_installs/, который хранит только эти токены, случайный идентификатор и время начала и конца (никогда текст ошибок, пути, URL и имена хостов). Более поздний старт Hermes в этом профиле считает его один раз и только если в этот момент сбор включён; старт с выключенным сбором или любой ответ с отказом удаляет файлы без отчёта. При включённой отправке строка записывается только в сутки UTC, пакет которых может пройти шлюз согласия на отправку, то есть когда окно согласия на отправку открылось не позже 00:00 UTC этого дня: согласие, данное во время самой установки (стадия setup), открывает окно посреди дня, так что квитанция ждёт первого старта в более поздний день, а не попадает в пакет дня согласия, который никогда не отправляется. При включённом сборе и выключенной отправке строка записывается при первом старте и остаётся на этой машине. Квитанция, которую ни один старт не учёл за 7 дней, удаляется без отчёта, чтобы старая установка никогда не считалась под более поздней версией Hermes; нечитаемая квитанция тоже удаляется. Ctrl-C, SIGTERM или закрытый терминал (SIGHUP), остановившие установку, читаются как interrupted, даже если дочерний процесс стадии перехватил сигнал и завершился со своей ошибкой; Ctrl-C, который стадия обрабатывает и после которого восстанавливается (выбор провайдера в setup), установку не останавливает. Поэтому неудачная установка считается лишь тогда, когда более поздняя установка или старт Hermes в том же профиле завершается успешно при включённом сборе; неудачная установка, за которой такой не последовало, не считается никогда. |
Локальное состояние пишется в:
$HERMES_HOME/telemetry/shared_metrics/metrics.sqlite3
$HERMES_HOME/telemetry/shared_metrics/outbox/*.json
База хранит транзакционные агрегаты и состояние outbox пакетов. Файлы пакетов
— неизменяемые дельта-документы, соответствующие закрытой JSON-схеме
и записываемые атомарной заменой как компактный JSON (jq . выведет его с
отступами). Когда сервис приёма принял или отклонил пакет, база хранит только
его состояние отправки и сбрасывает свою копию тела; локальной историей
остаётся файл. Каждый пакет фиксирует версию Hermes,
семейство ОС, архитектуру и способ установки как ограниченные ресурсы клиента.
Нераспознанные значения платформы или способа установки экспортируются как
unknown; сырые строки платформы, имена хостов и пути не включаются никогда.
Полностью упакованные строки агрегатов, а также строки и файлы успешно
экспортированных пакетов хранятся локально 30 дней. Ожидающие строки пакетов
и счётчики с неэкспортированными дельтами не удаляются никогда.
Схемы пакетов v1, v2 и v3 для существующих файлов outbox не меняются. Новые
пакеты используют v4, которая добавляет failure_class в
hermes.compression.count и hermes.memory.op.count и по-прежнему принимает
их наборы полей из v3 (а также, будучи расширена на месте только
добавлением значений, и после выхода v4 в стабильном релизе, —
failure_class в hermes.extension.install.count / hermes.update.run и
registry в первой из них, с приёмом их прежних наборов полей v4), так же как
v3 принимала наборы полей v2 для hermes.model_route.count,
hermes.tool_call.count и счётчиков задач, — чтобы счётчики, записанные до
обновления, безопасно сливались. Словари, выведенные из реестров в репозитории
(имена инструментов, платформы, провайдеры памяти, классы ошибок), ограничены
в схеме JSON шаблонами; авторитетный список разрешений — это
shared_metrics_contract.py.
Каждый пакет содержит install_id, сгенерированный как случайный UUID.
Несмотря на имя поля в схеме, сейчас его область — один HERMES_HOME, так что
точнее называть его постоянным псевдонимным идентификатором профиля. Он не
выводится из данных оборудования, аккаунта, хоста, пути или учётных данных.
Он стабилен между пакетами этого профиля и потому может связывать эти
локальные пакеты между собой. Удаление $HERMES_HOME/telemetry/shared_metrics
сбрасывает идентификатор вместе со всеми агрегатами и файлами пакетов.
Удалённая доставка — opt-in и по умолчанию выключена. Повторное использование постоянного локального идентификатора на удалённой стороне требовало отдельного продуктового и приватностного решения, охватывающего согласие, область идентичности, поведение при сбросе, хранение и удаление — это решение принято.
Эти решения зафиксированы в приложении A, а реализующий их экспортёр уже выпущен. Сам по себе сбор всё так же ничего не передаёт: отправитель работает только при истинном
telemetry.shared_metrics.send. Каждый переданный пакет несёт стабильныйinstall_idкак есть (продуктовое решение от 2026-08-27 — см. запись в A.2, включая отменённую схему с HMAC-псевдонимом).
Идентичность установки ограничена одним HERMES_HOME. Чтобы сбросить её,
остановите процессы Hermes и удалите $HERMES_HOME/telemetry/shared_metrics.
Так намеренно удаляются вместе старая идентичность, база агрегатов и
локальные пакеты в очереди; следующая сессия с согласием создаёт новую
идентичность. Отключение общих метрик останавливает новый сбор, но не удаляет
молча ранее собранное локальное состояние.
Smoke-тест
Прогоните настоящий ход CLI Hermes против детерминированного локального сервера модели:
./.venv/bin/python scripts/smoke_nemo_relay_shared_metrics.py
По умолчанию скрипт использует установленную зависимость nemo-relay.
Передавайте --relay-python ../nemo-relay/python только при тестировании
локально собранной привязки Relay.
Smoke-тест заставляет локальную модель сделать настоящий вызов инструмента
read_file перед финальным ответом, а затем прогоняет через установленную
привязку Relay переходы навыков: create, load, reuse, patch, edit, stale,
archive, restore и install. Он проверяет счётчики модели, провайдера, задач,
инструментов и навыков в SQLite, валидирует все экспортированные
дельта-пакеты по закрытой схеме, проверяет псевдонимный счётчик активного
клиента и убеждается, что в пакетах нет «канареек» — промпта, ответа,
идентификатора вызова инструмента, результата инструмента и имени навыка.
Приложение A: решения по удалённому экспортёру (фаза 2)
Статус: реализовано. Это приложение отвечает на продуктовые и приватностные вопросы, которые раздел «Текущие срезы» откладывал до будущего удалённого экспортёра. Здесь записано, что решено и почему, — чтобы обоснование пережило реализацию.
Отправка по умолчанию выключена и требует одновременно
telemetry.shared_metrics.enabled и telemetry.shared_metrics.send.
Экспортёр отправляет файлы пакетов, уже записанные в
$HERMES_HOME/telemetry/shared_metrics/outbox/, в сервис приёма телеметрии
Hermes. Этот сервис проверяет только конверт (schema_version плюс UUID
package_id) и сохраняет тело дословно в S3.
A.1 Согласие
Передача — отдельный opt-in, независимый от сбора, под новым ключом конфигурации:
telemetry:
shared_metrics:
enabled: false # собирать локально
send: false # НОВОЕ: передавать в сервис телеметрии Nous
sendпо умолчанию false. Сам по себе сбор ничего не передаёт.sendтребуетenabled, но не подразумевает его: флаг передачи не должен молча включать сбор.send: trueприenabled: falseвыдаёт предупреждение и ничего не делает.- Как и
enabled,sendпринадлежит профилю и не переопределяется конфигурацией управляемой области.
Оба ключа спрашивают один раз на профиль, с одними и теми же тремя ответами везде («Send to Nous» / «Local only» / «No thanks»):
| Поверхность | Где появляется предложение |
|---|---|
hermes setup | В конце каждого сценария (Quick, Full, Blank Slate, Portal, --quick). |
hermes / hermes --tui | Один раз перед началом интерактивного чата. Пропускается для -q, перенаправленного вывода или JSON, порождённых действий и панелей, размещённых в Desktop. |
| Hermes Desktop | Полоса над полем ввода, после первого онбординга. Она не блокирует поле ввода и не забирает фокус. |
| Веб-дашборд | Баннер над каждой страницей, для управляемого профиля. |
«No thanks» — вариант по умолчанию в терминале, так что нажатие Enter никого
не включает. Esc в терминале и ✕ на баннере дашборда оставляют вопрос
открытым, и его зададут снова в следующий раз. Полоса Desktop появляется только
после того, как первый онбординг завершён или пропущен, и никогда не
показывается во внутреннем профиле настройки, где идёт приветственный чат.
Ответ на любой поверхности записывает оба ключа в config.yaml профиля плюс
offer_version, а профиль, у которого уже есть любой из ключей, больше не
спрашивают. Управляемой установке предложение не показывают никогда.
Одно исключение: до исправления буфера опережающего ввода нажатие Enter во
время запуска hermes chat могло сохранить «No thanks» до появления вопроса.
Поэтому «No thanks», записанный без offer_version, предлагают ещё раз на
каждой поверхности, с пояснением почему. Любой ответ закрывает вопрос
навсегда, включая Esc или ✕ на баннере, которые сохраняют «No thanks». Ответы
с согласием не спрашивают повторно. Чтобы позже изменить ответ, используйте
hermes setup telemetry, hermes tools или Settings › Safety › Privacy &
network в Desktop.
Пакет отправляется только тогда, когда весь его период попадает внутрь
записанного окна согласия. Согласие хранится как явные интервалы
в SQLite-хранилище общих метрик (send_consent_windows): окно открывается,
когда send: true замечен впервые, подтверждается вперёд каждым следующим
наблюдением и закрывается — в последний подтверждённый момент, а не по
настенным часам, — когда замечен send: false. Единственный согласователь
выводит эту таблицу из конфигурации при каждом старте процесса, так что
изменения через мастер настройки, ручные правки config.yaml и отзывы
согласия посреди прохода идут одним и тем же путём, и ни один из них не может
пропустить переход.
Любой пакет, период которого предшествует первому окну, попадает между окнами или выходит за новейший подтверждённый момент, исключается — шлюз закрывается при отказе (fail closed). Поэтому свежий пакет ждёт не больше одного старта процесса после завершения своего периода, прежде чем станет пригодным к отправке.
Шлюз проверяет период, а не время создания пакета. Один период раскладывается на несколько пакетов, созданных в разные дни: первый пакет дня пишется в тот же день, а хвостовой пакет за тот же период обычно появляется на следующий. Если бы проверялось время создания, хвост периода ушёл бы, а голова отбросилась — и день оказался бы молча недосчитан. Проверка по периоду держит согласие направленным только вперёд, а каждый переданный период — полным.
Локальная история может накапливаться до 30 дней, и эти данные собирались под обещание, что ничего не выгружается. Уважение к согласию только вперёд стоит максимум 30 дней накопившегося бэклога, на отправку которого разрешения у нас никогда не было.
A.2 Область идентичности — стабильный install_id передаётся как есть
Запись о решении. Исходная схема этого экспортёра (и редакции 1–8 этого
приложения) передавала вместо идентификатора ключевой псевдоним:
HMAC-SHA256(key = локально хранимая ротируемая соль, message = install_id),
с ротацией соли каждые 30 дней. 2026-08-27, до выпуска фичи (ноль
согласившихся пользователей, ноль передач в продакшене), владелец продукта
решил, что аналитике нужна стабильная идентичность сквозь окна — кривые
удержания, поведение установки во времени, — а ротация по своей природе
именно это и разрушает. Слой псевдонимизации убрали целиком, а не ослабили
на месте.
Что передаётся теперь:
- Каждый пакет несёт
install_idдословно: постоянный случайный UUID уровня профиля, описанный выше. - Он генерируется локально (
uuid4), не содержит сведений об оборудовании, аккаунте, пользователе или машине и идентифицирует профиль, а не человека. - Он стабилен, пока пользователь не удалит каталог общих метрик, что генерирует его заново (см. A.4).
Последствия — названы прямо, а не замазаны:
- Пакеты одного профиля коррелируют бессрочно, а не в пределах окна. Долгосрочная связываемость последовательности ежедневных конвертов одной установки теперь спроектированное поведение, а не побочный остаток.
- Анализ остаточных рисков старой схемы из A.3 (стабильный кортеж
resourceплюс смежные периоды, перекидывающие мост между окнами ротации) потерял смысл — границ окон, через которые можно было бы перекинуть мост, больше нет. - Текст согласия в мастере настройки описывает эту модель идентичности явно; его обновили в том же изменении, которое убрало вывод псевдонима, так что ни в одной выпущенной сборке согласие не собиралось под старой формулировкой.
Побайтово идентичные повторные отправки сохраняются. Передаваемый
идентификатор записывается в строку (sent_install_id) при первой подготовке
пакета, а тело для передачи всегда собирается из этого записанного значения,
поэтому повтор даёт идентичные байты. Этого требует контракт: повторная
отправка package_id с другим содержимым — неопределённое поведение.
(Со стабильным идентификатором записанная копия уже не защищает от ротации —
она остаётся как столбец аудита и дешёвая страховка от любых будущих изменений
семантики идентичности.)
A.3 Ротация — убрана (запись о решении)
Ротацию соли удалили вместе с выводом псевдонима (продуктовое решение, 2026-08-27). Этот раздел сохранён как запись о том, что делала прежняя схема и почему её удаление приняли:
- Ротация существовала, чтобы ограничить долгосрочную связываемость: одна идентичность на 30-дневное окно, несвязанные идентичности между окнами.
- Задокументированный остаточный риск (полный анализ — в истории git):
стабильный низкоэнтропийный кортеж
resourceв конверте плюс смежные суточные периоды и так правдоподобно позволяли связать окна для редких конфигураций, так что граница лишь повышала цену, а стеной не была. - Продуктовая потребность, которая её и добила: непрерывность сквозь окна — ровно то, что нужно анализу удержания. Границу, которая в основном мешает честному анализу и лишь повышает цену для настойчивого корреляционщика, сочли неверным компромиссом, как только стабильная идентичность стала требованием.
В хранилище нет соли, нет расписания ротации, и нигде в конвейере нет выведенного идентификатора.
A.4 Поведение при сбросе
Удаление $HERMES_HOME/telemetry/shared_metrics по-прежнему сбрасывает
локальную идентичность, агрегаты и файлы пакетов — ровно так, как описано
выше. Теперь действуют две честные оговорки:
- Сброс генерирует
install_idзаново, так что последующие пакеты передают новую идентичность. Локальный сброс действительно даёт новую удалённую идентичность. - Сброс не отменяет отправленное. Уже переданные пакеты остаются в хранилище сервиса приёма под тем идентификатором, с которым были отправлены. В контракте v1 нет API ни для чтения, ни для удаления.
send: false останавливает передачу немедленно: согласие перечитывается
перед каждым пакетом, так что уже идущий проход останавливается после
пакета, который отправляет в этот момент, а не сливает всю партию. Ранее
переданные пакеты при этом не удаляются, а локальный сбор не прекращается.
Отключение отправки также закрывает окно согласия — в последний момент, когда согласие реально наблюдалось, а не по настенным часам. Пакеты, чьи периоды попадают между одним окном и следующим, не передаются никогда, даже если отправку позже включить снова, — и это верно при любом числе циклов вкл/выкл, при ручных правках без запущенного процесса и при часах, прыгающих в любую сторону (открытие окна прижимается сверху ко всем меткам времени, уже имеющимся в хранилище; отметки наблюдения продвигаются на ограниченный шаг за вызов, так что один сбойный выброс вперёд не утащит горизонт подтверждения на годы вперёд; закрытие никогда не ложится позже часов самого закрывающего наблюдения). В отличие от прежней единственной подвижной даты opt-in, закрытие и повторное открытие НЕ отбрасывает ещё не доставленный бэклог из предыдущего окна согласия — те пакеты остаются внутри своего интервала и сохраняют право на отправку.
Одно намеренное последствие для пути обновления: пакеты, экспортированные
при доинтервальной модели согласия (до появления send_consent_windows),
предшествуют первому записанному окну и потому после обновления никогда не
передаются. Это направление «закрыться при отказе» — вернуть старую подвижную
суточную отметку, чтобы их выпустить, значило бы вернуть семантику, которую
пять раундов ревью показали несостоятельной, — и стоит оно максимум
недоставленного бэклога, но никогда не собранных данных.
A.5 Хранение
- Локально: без изменений — 30 дней для успешно экспортированной истории, а ожидающие дельты хранятся до экспорта. Состояние отправки не продлевает локальное хранение: пакет, который так и не удалось отправить, всё равно удаляется через 30 дней. Неограниченный локальный рост при навсегда недоступном эндпоинте — худший отказ, чем потеря метрик с установки, которая сломана уже месяц.
- Удалённо: сырые пакеты хранятся в S3 бессрочно в продакшене и 30 дней на staging.
A.6 Удаление
В контракте v1 нет пути удаления на удалённой стороне, и это приложение его не выдумывает. Что может сделать пользователь:
| Действие | Эффект |
|---|---|
send: false | Новые пакеты не покидают машину |
enabled: false | Сбор останавливается; существующее локальное состояние сохраняется |
Удалить .../shared_metrics | Локальная идентичность, агрегаты и файлы сбрасываются; будущие отправки используют новый install_id |
| Удалить уже отправленные данные | Не самообслуживание — нужен оператор, работающий с бакетом S3 |
Если когда-нибудь появится обязательство удалять данные по запросу, путь
поиска теперь прямой: install_id пользователя (его можно прочитать из
локального хранилища) — это ключ, под которым хранятся его данные. Построение
серверного API удаления остаётся новым продуктовым решением, а не деталью
реализации.
A.7 Что такое каталог outbox
Записано потому, что при планировании фазы 2 его однажды прочитали неверно — так, что это удалило бы данные пользователя.
Каталог — это локальная история, а не очередь на отправку.
package_outbox — таблица SQLite; её столбец exported_at означает «записан
на диск», а не «отправлен». Файлы неизменяемы и удаляются только по
возрасту.
Контракт приёма говорит, что отправителям следует удалять пакет из своего
outbox по 202. Экспортёр этого не делает. Удаление по подтверждению
превратило бы 30-дневную локальную историю пользователя в очередь передачи
и уничтожило бы состояние, которое ему обещали. Вместо этого состояние
отправки живёт в новых столбцах таблицы package_outbox; передача файлов не
касается.
A.8 Замечание об области действия
Поле install_id внутри тела пакета передаётся так, как его записал
генератор (переписывается из замороженного sent_install_id строки, где
хранится то же значение). Ни одно другое поле полезной нагрузки не меняется,
ничего не добавляется, и сервис рассматривает всё тело как непрозрачное.
Поэтому эволюция схемы полезной нагрузки, как и раньше, остаётся заботой
отправителя.