---
license: "MIT. Translation of the Hermes Agent documentation, Copyright (c) 2025 Nous Research. See https://wiki.winsmux.dev/hermes/licenses.txt"
title: "Relay 共有メトリクス"
description: "NeMo Relay の共有メトリクス。何を出力するか、同意と保持期間、ステージングでの検証"
upstream_path: developer-guide/relay-shared-metrics.md
upstream_blob: f0eb605665f3434512c8de9acfea782b8b2f750c
sources:
  - https://hermes-agent.nousresearch.com/docs/developer-guide/relay-shared-metrics
---

# NeMo Relay 共有メトリクス {#nemo-relay-shared-metrics}

Hermes は、Relay がネイティブ wheel を公開しているプラットフォームでは、NeMo Relay を通常の実行時依存として含んでいます。
共有メトリクスの連携は Hermes に組み込まれており、Hermes の observability プラグインは必要ありません。
それ以外のネイティブ環境でも、Hermes は Relay なしで import できます。
そうした環境では、機能を明示的に絞った何もしないホストを使います。
Hermes の実行はそのまま使えますが、Relay のスコープ、ミドルウェア、プラグイン、サブスクライバーは使えません。
`hermes-agent[nemo-relay]` extra は、既存のインストールコマンドとの互換のための、何もしない別名として残っています。

> [!WARNING]
> この変更で、Hermes の `observability/nemo_relay` プラグインは削除されます。既存の利用者は、
> `plugins.enabled` から `observability/nemo_relay`（または旧名の `nemo_relay`）を外し、
> エクスポーターの設定を Relay の
> `plugins.toml` へ移す必要があります。`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-dependency-and-data-boundary}

Hermes は、範囲を限った `>=0.10,<0.11` の依存指定から、プラットフォーム別の `nemo-relay` ネイティブ wheel をインストールします。
公開パッケージは [NVIDIA NeMo Relay リポジトリ](https://github.com/NVIDIA/NeMo-Relay) からビルドされています。
対応していないプラットフォームでは、別の実装をダウンロードするのではなく、上で説明した明示的な何もしないランタイムを使います。

Relay 0.10 では、observability の設定に `version = 4` が必要です。独自の Python 製 LLM 実行インターセプトは `(name, request, context, next_call)` を受け取ります。
context からは選ばれたリクエストのコーデックと、ストリーミングでない実行ではレスポンスのコーデックも参照できます。
ネイティブプラグインは Relay 0.10 の ABI v7 向けにビルドし直し、`compat.relay` の下限を `0.10.0` にする必要があります。
LLM 実行インターセプトを登録するワーカーは、コールバックを更新し、プロトコルのバインディングを生成し直す必要があります。
アップグレードの確認項目の全体は、Relay の [0.10 移行ガイド](https://docs.nvidia.com/nemo/relay/reference/migration-guides) を参照してください。

Relay 0.10 は、有効な Relay の設定があり、Hermes プロセスの環境に空でない `OTEL_EXPORTER_OTLP_ENDPOINT`（またはシグナル別の `OTEL_EXPORTER_OTLP_*_ENDPOINT`）があると、OpenTelemetry へのエクスポートを自動で始めます。
その `gen_ai` スパンにはプロンプトとレスポンスのメッセージが含まれます。
`hermes migrate relay` が生成するファイルは、0.9 と同じ動きを保つために `[components.config.opentelemetry] enabled = false` を設定します。
このエクスポートを望まない場合は、手書きのファイルにも同じセクションを足すか、`OTEL_SDK_DISABLED=true` を設定してください。

Relay の管理下実行が有効なときは、設定したインターセプターが実際の呼び出しに作用できるよう、プロバイダーへのリクエストとレスポンスが Hermes プロセス内のそのネイティブモジュールを通ります。
これは共有メトリクスのデータの取り決めとは別のものです。
共有メトリクスのモードでは、詳細な observability 用のネットワークエクスポーターは入らず、そのサブスクライバーが受け付けるのは、後述するバージョン付きで許可リストに載った射影だけです。
付録 A で説明する、利用者が自分で有効にするパッケージ送信器が唯一の外向きの経路です。
利用者が `enabled` と `send` の両方を有効にしない限り何も送信せず、送るのもリアルタイムのスパンではなくパッケージ単位です。
別途設定した詳細な observability プラグインや動的プラグインを有効にすると、別のデータ経路ができることがあり、それぞれ独自のポリシー審査が必要です。

収集は、Hermes のポリシーで有効にしない限りオフのままです。

```yaml
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-segmentation-for-continuous-sessions}

Relay は、スコープが閉じたときにスパンを出力します。
継続するゲートウェイのセッションは何日も開いたままになることがあるので、各ターンのスパンは通常どおり出力されても、セッションのスパンは開いたままです。
任意の分割機能を使うと、ターンの区切りでセッションのスコープだけを切り替えられます。

```yaml
gateway:
  telemetry:
    session_segments:
      on_compaction: false  # rotate after context compaction
      max_turns: 0          # 0 = unlimited; N = turns per segment
```

| キー | 既定値 | 動き |
|---|---:|---|
| `on_compaction` | `false` | 圧縮が完了したあと、次のターンの区切りで切り替えます。 |
| `max_turns` | `0` | 完了したターンが N 回に達するごとに切り替えます。`0` は上限なしです。 |

どちらも既定値のままなら、セッション全体で1つのセッションスコープが保たれます。
切り替えたスパンは同じ `session_id` を持ち、`hermes.session.segment` と `hermes.session.segment_reason`（`compaction` または `max_turns`）が加わります。

## 作業ディレクトリのスコープデータ {#working-directory-scope-data}

Hermes がセッションやタスクの論理的な作業ディレクトリを把握している場合、
`hermes.session` と `hermes.turn` の開始スコープには、ATOF でそれが `data.cwd` として含まれます。
そのため、タスクの worktree で動くターンは、それを持つセッションと値が異なることがあります。
作業ディレクトリが分からない場合は省かれます。スコープ終了時のデータは、引き続き結果のために予約されています。

作業ディレクトリは Relay のスコープ入力なので、ATOF だけでなく、有効になっているすべての
Relay サブスクライバーから見えます。パスからはユーザー名、リポジトリ名、マウントの構成が分かることがあります。
Relay は作業ディレクトリでイベントを絞り込みません。パスをホストの外に出したくない場合は、
信頼できるローカルのコレクターを使うか、そのプロセスではリモートのエクスポーターを有効にしないでください。

## 圧縮マーク {#compaction-marks}

Relay のランタイムが動いているとき、Hermes は次の発生源ごとに、
`data_schema: {"name": "hermes.compaction", "version": "1"}` を持つ Relay マークを1つ出します。

- 戻ってきた `compress_context` の試行ごと（ブロックされたもの、
  Codex に回されたもの、中断されたものを含みます）
- マイクロ圧縮の各パス
- 確定した、先回りのツール結果の刈り込みごと
- Codex が自分で行い、Hermes が観測した Codex app-server の
  スレッド圧縮ごと

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 のスコープを持ちません。
スコープがない間に子セッションで起きた圧縮（2回目のプリフライトのパスや、ターンの終わる前に走るマイクロ圧縮のパス）は、
エージェントが開始した、動いている `hermes.turn` を親にします。そのときも
`data.session_id` は子セッションを指します。
Relay が計測しないターン（同じセッションで同時に走る2つ目のターン）や、
バックグラウンドのレビューのような永続化から切り離されたフォークからはマークを出しません。
それらの圧縮が書き換えるのは自分のトランスクリプトだけだからです。

`data` は平らな構造なので、OpenTelemetry はすべてのフィールドを
`nemo_relay.mark.data.<key>` に展開します。v1 のキーはすべてのマークに必ずあり、当てはまらない値は `null` です。
分類を表すフィールドは、次の決まった集合から値を取ります。2つの要約側の識別子だけは例外です。

| フィールド | v1 の値 |
|---|---|
| `kind` | `summarize`、`micro_summarize`、`prune_tool_results`、`provider_native` |
| `scope` | `history`、`provider` |
| `official` | `compress_context` の試行でだけ `true` |
| `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_auto_native` のような Codex への経路による終了、リースによる終了の `session_ownership_lost`、`session_ownership_unreadable`、
`cooldown_state_unreadable`、`empty_transcript`（コンテキストエンジンがメッセージを1つも返さなかった）、
`summary_model_benched`、マイクロ圧縮の結果）を使います。分類がないときは `none`、それ以外は `other` です。
きっかけのラベルを渡さない自動の呼び出し元は `trigger: unknown` と
`trigger_class: auto` になります。きっかけがまったくない記録は、
両方とも `unknown` になります。`in_place` と `session_rotated` は、`split_status` が
`in_place_committed` か `rotated_committed` のときだけ設定されます。`cache_break` は
`scope: provider` では `null` です。

数値のフィールドには効果（`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` マークは書き換えを記録しますが、鮮度の
リセットが効くのは、同じセッションスコープで後から Hermes 側が行う LLM 呼び出しだけです。
Hermes が強制した圧縮は、その `compress_context` の試行で1回だけ記録されます。
次の Codex のターンは圧縮ターンからの通知を捨てるので、
同じ圧縮をもう一度報告することはありません。

v1 の既知の制約:

- `gateway.telemetry.session_segments.on_compaction: true` のとき、ターンの外で
  ローテーションする確定は、マークを出す前に古い Relay セッションを閉じてしまうため、
  マークが失われます。現在この動きをする呼び出し元はありません。手動の
  `/compress` はその通知をマークのあとまで遅らせ、ゲートウェイの
  衛生処理はその場で圧縮します。
- 後のバージョン向けに予約されていて v1 では出さないもの: `kind` の
  `request_truncation` / `window_reduction`、`scope: request`、
  `trigger: recovery`、`outcome: noop`、そして
  `token_count_method: provider_usage`。

## プロセス全体のプラグインポリシーとプロファイルの分離 {#process-wide-plugin-policy-and-profile-isolation}

Relay プラグインの設定は、Hermes のプロファイル設定ではなく、プロセス単位のデプロイ上の選択です。
最初にホストされたプロファイルが遅延初期化のきっかけになり、同じ Hermes プロセスがホストするほかのプロファイルはすべて、その結果できた静的ミドルウェア、動的プラグイン、サブスクライバー、エクスポーター、ガードレールのポリシーを共有します。
初期化に成功すると、Hermes は読み込んだファイルを次のようにログに出します。

```text
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 スコープごとにまとめるので、同時に動くプロファイルのセッションは、1本に混ざった軌跡ではなく別々の軌跡になります。
ATOF などのグローバルなサブスクライバーは、ホストされているすべてのプロファイルのイベントを観測します。
静的・動的なミドルウェアも同様に、すべてのプロファイルの管理下の呼び出しで動きます。

別のワーカープロセスで動くワーカープラグインは、プロファイルごとのセキュリティ境界にはなりません。
プロセス全体で1回の有効化により、ホストされているすべてのプロファイルからの呼び出しが、呼び出し元プロファイルの Relay スコープのスタックを保ったまま、そのワーカーへ振り分けられます。
ネイティブの動的プラグインは Hermes プロセスに読み込まれ、同じポリシーの境界を共有します。

信頼レベル、プラグインの認証情報、エクスポーターの送り先、ガードレールのポリシーが異なるプロファイルは、別々の Hermes プロセスで動かしてください。
このプロセス全体のプラグインの取り決めは、各プロファイルが独立して持つ共有メトリクスへの同意、ローカルの SQLite の状態、ATIF の軌跡のまとまりには影響しません。

Hermes のコアは、Relay ホストを1つ持ち、Hermes のセッションごとに分離された Relay セッションスコープを1つ持ちます。
コアのライフサイクルを生成する側は、`agent.relay_runtime` を使って共有のセッションハンドルを取得するか、そのセッションの文脈で Relay のスコープ、LLM、ツール、mark の API を実行します。
新しい製品向けの mark に、Hermes プラグインとしての登録は必要ありません。
共有メトリクスの mark には、引き続きバージョン付きの許可リストで承認されたフィールドだけを含める必要があります。
必須の依存になったからといって、収集やプライバシーのポリシーが変わるわけではありません。

## 現在の対象範囲 {#current-slices}

現在の縦割りの対象範囲では、仮名化したプロファイルの活動、論理的なモデル呼び出し、最上位のタスク実行、ツールと承認の結果、スキルのライフサイクルと再利用を記録します。

```text
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` を送ります。
これは、上で説明したネイティブランタイムを通る管理下実行の呼び出しとは別のものです。
終端のメトリクスイベントには、Hermes がその論理的な呼び出しに使ったモデル識別子とプロバイダーの経路が含まれます。
たとえば `openrouter` 経由の `nvidia/nemotron-3-ultra` です。
これらの識別子は小文字にそろえ、構造上の長さも制限していますが、リポジトリに含まれるモデル一覧による正規化はしていません。
料金やモデルファミリーの分類はメトリクスのバックエンドの役割です。
プロンプト、レスポンス、エンドポイント、エラーの文言、セッション ID、タスク ID、リクエスト ID は、メトリクスイベントにもパッケージにも含まれません。
新しい呼び出しは `hermes.model_route.count` を使います。
パッケージスキーマ v3 からは、経路の各行に `call_role`（`primary` または `auxiliary`）、`outcome`（`success`、`failed`、`cancelled`）、`error_class` も入ります。
この分類は、その論理的な呼び出しで最後に失敗した試行について、エラー分類器自身が付けた `FailoverReason` の値（`rate_limit`、`auth`、`context_overflow` など）で、失敗がなければ `none` です。
`success` の行で分類が `none` 以外なら、そのエラーのあとに回復した呼び出しという意味です。
補助の呼び出し（タイトル、圧縮、画像の読み取りなど）も同じ規則に従います。フォールバックの試行が何回あっても論理的な呼び出し 1 つにつき 1 行で、同じ分類器で分類します（HTTP 200 の本文にプロバイダーの `error` オブジェクトが入っている場合は、そのオブジェクトから分類します）。
Hermes が中断した場合（`/stop`、Ctrl+C、割り込み、終了処理）は `cancelled` で分類は `none` になり、`unknown` になるのは分類器が失敗を言い当てられないときだけです。
ターンと並行して走り（タイトルの生成）、ターン自身の生きているスコープの下で終わる補助の呼び出しも数えます。その結果は、ターンがそれを受け取って片づけるときにスコープを閉じます。
補助の行の `ttft_bucket` は `unknown` になります。補助の呼び出しの多くはストリーミングされないためです。
以前の `hermes.model_call.count` の取り決めは、古いビルドで作られた未送出のローカルカウンターをデータを失わずに出力できるよう、読み取り専用で残しています。

同意したうえでの最初のセッション開始時に、空の `hermes.client.active` Relay mark を出します。
プロファイル単位のサブスクライバーは、ランダムな UUID のインストール識別子を作り、トランザクションの compare-and-set を使って、直近24時間の範囲ごとに client-active のカウンターを最大1回だけ記録します。
このメトリクスには次元がありません。
Hermes のバージョン、OS の種類、アーキテクチャ、インストール方法は、値の範囲を限ったパッケージのリソースとして扱います。
同時に動く Hermes プロセスは SQLite のラッチを共有するので、同時に起動しても1つのインストールを二重に数えることはありません。
後のセッションやタスクが再び mark を出そうとすることはありますが、直近24時間の範囲が過ぎるまでサブスクライバーが抑止します。

タスク実行はそれぞれ、`hermes.task_run` という名前の Relay `Function` スコープで、所有する Hermes セッションを親に持ちます。
開始時のカウンターに含まれるのは、範囲を限った実行面と入口の値に加えて、ゲートウェイのタスクでは組み込みのメッセージングの `platform`（`telegram`、`discord`、`slack` など）だけです。
`plugins/platforms/` の下で Hermes が同梱しているプラットフォームは名前で、`plugin-catalog/` のプラットフォームはインストーラー自身の記録でカタログからのインストールと確認できる場合に限りカタログの項目名で表し、それ以外のプラグインのプラットフォームは `plugin`、それ以外の実行面は `none` になります。
終端のカウンター（`hermes.task_run.finished`）には、開始時のフィールドに加えて、範囲を限った結果、終了理由、終了状態と、失敗したタスクの `failure_class` が含まれます。
この分類は、分類済みの API エラーでターンが終わった場合はプロバイダーの `FailoverReason`、それ以外はローカルの分類（`empty_response`、`context_compression`、`repeated_errors`、`exception`、`other` など）です。
同じ終了イベントから、実行面、結果、所要時間の区分、プロバイダーへの再試行回数の区分を持つ `hermes.task_run.duration` も記録します。
パッケージ v2 では、所要時間、再試行、タスクごとのモデル呼び出し回数とツール呼び出し回数を終端の行そのものに持たせていたため、ほぼすべてのタスクがそれぞれ別の行になっていました。ターンごとの呼び出し回数は `hermes.task_cost.count` にあります。
生の終了理由がマシンの外に出ることはありません。
再試行とは、同じ Hermes API リクエスト ID に対する追加のプロバイダー試行のことで、論理的なモデル呼び出し回数を水増ししません。
ツール呼び出しは、終端のツール結果を観測したあと、Hermes のツール呼び出し ID で重複を除きます。
外側の `AIAgent` 実行の境界が、通常の戻り、早期の戻り、例外、キャンセルのいずれでもタスクを閉じます。
コンテキスト圧縮の途中で Hermes が会話のセッションを切り替えた場合も、実行中のタスクの所有はタスク ID に従います。

`entrypoint` の次元（`hermes.task_run.started`、`hermes.task_run.finished`、`hermes.session.count` にあります）は、誰がその実行を送り出したかを、決まった値の集合から示します。

| 値 | 意味 |
|---|---|
| `interactive` | チャットの画面にいる人です。`hermes` の REPL、TTY 上で REPL の最初の入力になる `hermes chat -q`、`--tui`、Desktop、ACP のエディターが当たります。 |
| `one_shot` | プロンプトに 1 回答えて終わる、有限の CLI 実行です。`hermes -z` / `--oneshot`、TTY の外または `--oneshot` 付きの `hermes chat -q`、`-Q` / `--quiet` が当たります。人がシェルに打った 1 行と、それを繰り返し回すスクリプトは見分けがつかないので、どちらも `one_shot` になります。Bot Chat の配信ターン（`hermes -p <profile> chat -c "Bot Chat" -Q`）も 1 回きりの実行です。書いたのは別の接続にいる人かもしれません。実行面は `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` になるのは、そのプロセスが、1 回きりの経路が設定する `HERMES_SINGLE_QUERY_SESSION` の目印を持っているときです（セッションの出どころと `cache_ttl: auto` が読むのと同じ目印です）。
エンゲージメント（`hermes.engagement.*`）と、人が見ている実行だけを数える行（タスクの費用、セッションごとのツールの使用、モデルとの摩擦）は、この値ができる前と同じく `one_shot` を `interactive` と同じに扱います。`background` と `delegated` の実行は人が見ていないので、そこから外します。
`one_shot` ができる前に書かれたパッケージは、こうした実行を `interactive` として持っており、それでも検証を通ります。
`hermes -z` は `os._exit` で抜けるので、atexit のフックに頼らず、終了する前に自分でメトリクスのセッションを閉じます。

ツールの呼び出しはそれぞれ、`hermes.tool_call` という名前の Relay ツールライフサイクルで表します。
終端のカウンターに含まれるのは、範囲を限ったツールの分類、結果、承認の結果だけです。
同じイベントから、ツールの分類、待ち時間の区分、明示的な再試行回数の区分を持つ `hermes.tool_call.latency` も記録します（パッケージ v2 では待ち時間と再試行を終端の行に持たせていたため、数回の呼び出しごとに1行になっていました）。
Hermes は、実行時のレジストリにすでに宣言されているツールセットから分類を導きます。
独自のツールセットや認識できないツールセットは、ツール名やプラグイン名を出力するのではなく `other` にまとめます。
同じ終端イベントからは、`tool_name`、`outcome`、`error_class` を持つ `hermes.tool.usage.count` も記録します。
`tool_name` を出力するのは、リポジトリの静的な `toolsets.TOOLSETS` で宣言されたツール（実行時の独自ツールセットが作られる前に取得した `toolsets.BUILTIN_TOOL_NAMES`）と、Tool Search の橋渡しのツール（`tool_search`、`tool_describe`、`tool_call`）だけです。MCP のツールは `mcp`、ホスト型コネクターのツール（`connectors__*`）はツールの分類も `connector` として `connector`、プラグインや独自のツールはすべて `plugin` として報告します。コネクターのスラッグとツール名は出力しません。コネクターの呼び出しをまとめた `tool_call` の一括実行では、各項目をそれぞれ1回の呼び出しとして扱います。項目ごとに1行を記録し、`tool_call` の外側の呼び出しは重ねて数えません（どの項目も実行される前に拒否された一括実行は、`tool_call` として1回だけ数えます）。
`error_class` は Hermes 自身の `error_type` の値（`tool_error`、`timeout`、`interrupted`、`invalid_arguments`、`blocked`、`contract_violation`）を対応付けたもので、例外クラス名などそれ以外の値は `exception` にまとめます。失敗したコネクターの項目は、その結果に入っているツールゲートウェイのエラーコードで分類します。`connection_required`（接続が必要、または期限切れ）、`not_allowed`、`alias`（別名が必要、または不明）、`invalid_arguments`、そしてバックエンド系のすべてのコード（提供元のエラー、レート制限、タイムアウト、ポリシーやゲートウェイの失敗）は `connector_backend` です。それ以外のコードは `tool_error` のままです。Hermes 自身がファイル入力を拒否した場合も同じように数えます。拒否されたパスは `not_allowed`、存在しないファイルや大きすぎるファイルは `invalid_arguments`、アップロードの失敗は `connector_backend` です。
Hermes は、同じツール名の繰り返しや隣り合う呼び出しから再試行を推測しません。
フックが明示的な再試行の関係を渡さない場合、再試行の区分は `unknown` になります。
承認の判断は `hermes.tool_approval` mark として出し、ツール呼び出しに紐づくもの、または明示的に `unattributed` として記録します。
組み込み以外のツール名、呼び出し ID、引数、結果、コマンド、説明、エラーの文言は、共有メトリクスのイベントにもパッケージにも含まれません。
開始したツールがタスク終了時にまだ開いていた場合は、失敗、タイムアウト、キャンセルのいずれかとして閉じ、タスクのツール回数の区分に含めたままにします。

スキルの変更に成功すると、範囲を限った操作と由来だけを持つ `hermes.skill.lifecycle` mark を出します。
スキルの読み込みに成功すると、範囲を限った由来、初回利用か再利用かの状態、パッチ後の再利用の状態、利用回数の区分、`skill_name` を持つ `hermes.skill.load` mark を出します。
この名前は、Hermes が同梱しているスキル（`skills/` または `optional-skills/`）の場合だけスキル名で、それ以外は `custom` です。
Hermes は、再利用とパッチ世代の連続性を、既存の `skills/.usage.json` の状態の中でトランザクションとして導きます。
ローカルのスキルやエージェントが作ったスキルの名前、正確な回数・世代が、Relay のメトリクスイベント、SQLite の次元、パッケージに入ることはありません。
新しいパッチ後の利用は `reused_after_patch` として1回だけ数え、その後の利用は次のパッチまで通常の再利用として扱います。
パッチ後のタスク結果との結び付けは、その範囲と複数スキルの場合の意味が決まるまで先送りしています。

直近24時間の範囲ごとに1回、最初の有効化では、プロファイルの構成を表す `hermes.install.snapshot` mark も出します。
中身は、メモリプロバイダー（同梱のプロバイダー名、`builtin`、`plugin` のいずれか）、MCP サーバー・有効なプラグイン・インストール済みのスキル・有効な cron ジョブ・プロファイル・接続済みのメッセージングプラットフォームそれぞれの数の区分、メインのプロバイダー ID、ターミナルのバックエンド（`local`、`docker`、`ssh` など、または `other`）、表示言語（同梱のロケールか `other`）、そして `install_age_bucket` です。
これは、そのプロファイルで最初のセッションが始まってからどれだけたったかを表します。
インストールからの期間があるので、バックエンドは新しい利用者と、今になって有効にしただけの既存の利用者を見分けられます。
サーバー、プラグイン、スキル、ジョブ、プロファイルの名前をイベントに読み込むことはありません。
`hermes.client.active` と同じ compare-and-set のラッチで1インストールにつき1日1行に抑え、生成側はスキルのツリーをたどる前にラッチを確認します。

<!-- ---- v4 install ---- -->
スナップショットには、バージョンの遅れ、チャンネル、ハードウェアに関する6つのフィールドも入ります。どれもオフラインで読み取ります（ネットワーク呼び出しもサブプロセスも使いません）。

- `release_channel`（`stable`、`main`、`dev`、`unknown`）: パッケージ版ならビルド時に埋め込んだチャンネル（main の canary ビルドは `main`）、ソースからのインストールならそのチャンネルの記録、
  どちらもなければチェックアウトのブランチ（main/master は `main`、それ以外のブランチは `dev`）です。
  git のリモート URL やブランチ名をイベントに読み込むことはありません。
- `version_age_bucket`（`lt_7d` … `gte_90d`、`unknown`）: *インストール済み* のバージョンがどれだけ古いかで、
  インストールの記録またはチェックアウトにある、そのバージョン自身のコミット日から求めます。
- `behind_bucket`（`0`、`1`、`2`、`3_to_5`、`6_to_10`、`gte_11`、`unknown`）: 何コミット、または何リリース遅れているかです。
  このリビジョンそのものに対する更新確認の結果がキャッシュにあり、7日以内の場合だけ使います。それ以外は `unknown` です。
- `ram_bucket`（`lt_8g` … `gte_128g`、`unknown`）: 搭載メモリを公称のサイズに丸めたものです
  （ファームウェアの予約分を見込み、OS が報告する合計を1.1倍します）。
- `gpu_class`（`nvidia`、`amd`、`intel`、`apple_silicon`、`none`、`unknown`）: 優先度の最も高い GPU ベンダーです。
  Linux では `/proc/driver/nvidia` か DRM の PCI ベンダー ID、Windows ではディスプレイアダプターのレジストリのクラス、macOS ではネイティブの arm64 かどうかから判定します。
  モデル名、ドライバーのバージョン、VRAM の容量は含めません。
- `local_model_provider_used`（`yes`/`no`）: メインのモデルか補助タスクのどれかが、ローカルまたは自前のサーバーで動いているかどうかです
  （Ollama/LM Studio/llama.cpp のようなローカルのプロバイダー ID、またはループバックやプライベートネットワークのベース URL）。
  URL そのものはローカルにとどまります。

### 判断材料のメトリクス {#decision-data-metrics}

これらは、活動のカウンターでは答えられない製品上の問いに答えるためのものです。人が使い続ける理由、新しい利用者がどこで離れるか、
実際に使われている実行面とモデルはどれか、投資する価値のある拡張はどれか、といった問いです。
次元はどれも、閉じた列挙値、区分、（モデル経路と同じ）プロバイダー・モデルの識別子、Nous 自身が
同梱している公開の名前のいずれかです。

| メトリクス | 次元 | 答える問い |
|---|---|---|
| `hermes.session.count` | 入口、実行面、プラットフォーム、ターン数・失敗したターン数の区分、アクティブ時間の区分、最後の結果、メッセージ数・モデル呼び出し数・ツール呼び出し数の区分（`0` … `101_to_250`、`251_to_1000`、`gte_1001`） | 実行面ごとに実際どれだけ深く使われているか。失敗の直後にセッションが終わっていないか。1つの会話につき1行で、実行面がその会話を閉じたときに書き込みます。圧縮による切り替えで引き継がれたセッション ID は1行にまとめます（引退した ID は、処理中のターンが終わった時点で閉じます）。ゲートウェイのリセット、アイドルによる期限切れ、`/new`、`/branch` は、古いセッションを親として記録していても新しい会話として始まります。セッション ID を使い回す背景レビューの fork は、ターン、呼び出し、メッセージを増やしません。メッセージはユーザーのターン、主モデルの返答、ツール結果の合計で、モデル呼び出しは論理的な主 API リクエストの数です（再試行は除きます）。 |
| `hermes.install.milestone` | マイルストーン、インストールからの期間の区分 | インストールから、最初の成功、最初のゲートウェイのメッセージ、最初の cron 実行、最初の委任、最初に作ったスキル（Hermes の背景レビューが作ったものは数えません）、最初の長いセッションまで、どれだけかかったか。1インストールにつき1回だけ記録します。 |
| `hermes.setup.completed` | 実行面（`cli`/`desktop`）、プロバイダー | セットアップでどのプロバイダーが選ばれ、それがどの実行面か。 |
| `hermes.model_tokens.sum` | 呼び出しの役割、モデル、プロバイダー、補助タスク、トークンの種類 | モデル・プロバイダーごとのトークン量、プロンプトキャッシュの割合、補助的な処理（圧縮、タイトル、画像認識など）にかかる量。値はイベントの回数ではなくトークンの合計です。 |
| `hermes.model_route.count` `ttft_bucket` | 最初のトークンまでの時間 | プロバイダー・モデルごとの体感の待ち時間。 |
| `hermes.compression.count` | きっかけ、結果、コンテキストの埋まり具合の区分、失敗の分類 | 圧縮がどれくらいの頻度で動くか、コンテキストがどこまで埋まるか、失敗していないか。`skipped` は、失敗しようがなかった場合です。ロックをほかが持っていた、要約できるものがなかった（モデルを呼んでいない）、利用者が止めた、新しい試行に置き換えられた、のいずれかです。パッケージスキーマ v4 からは、この行に `failure_class` も載ります。試行ログ自身の分類名で、決まった一覧から選びます。skipped の行ではスキップの理由（`lock_contended`、`insufficient_messages`、`no_compressible_window`、`empty_post_handoff_window`、`explicit_interrupt`、`attempt_superseded`、`snapshot_stale`）、failed の行では失敗の理由（`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` です。フォールバックを通して確定した成功は、その行の分類を保ちます。そのため、`success` の行で `failure_class` が `none` 以外なら「フォールバックで確定した」という意味です。決定的な要約（`summary_generation_failed`、`summary_overload_degraded`、`stall_deterministic_fallback`、または `feasibility_skip`。最後のものは、フォールバックが続いて要約モデルが外された場合も含みます）か、失敗した要約モデルの代わりにメインモデルを使った場合（`aux_model_fallback`）です。例外や巻き戻しの Python の型名は落とし（`exception:TimeoutError` は `exception` になります）、それ以外の値は `other`、分類のない失敗は `unknown` になります。 |
| `hermes.model_switch.count` | 切り替え元・切り替え先のプロバイダー、実行面 | どのプロバイダーから離れ、どこへ移っているか。 |
| `hermes.fallback.count` | 切り替え元・切り替え先のプロバイダー、エラーの分類 | 代替プロバイダーがどれくらいの頻度で、何が原因のターンを救っているか。 |
| `hermes.slash_command.count` | コマンド、実行面 | どの組み込みコマンドが使われているか（`/retry`、`/undo`、`/new` は使いにくさのシグナルです）。スキルとプラグインのコマンドは `skill`/`plugin` として報告します。 |
| `hermes.extension.install.count` | 種類、入手元、名前、結果 | カタログのどのスキル、MCP サーバー、プラグインがインストールされているか。`name` は、同梱・任意のスキル、`optional-mcps/` か `plugin-catalog/` の項目で、それ以外は `custom` です。 |
| `hermes.memory.op.count` | 操作（`add`/`replace`/`remove`/`read`/`search`/`other`）、プロバイダー（`builtin`、同梱のメモリプラグイン、それ以外は `plugin`）、起点（`foreground`/`background_review`）、結果（`success`/`failed`/`rejected`）、失敗の分類 | 学習のループがメモリに書き込んでいるか、誰が求めたか（ユーザーのターンか背景レビューか）、書き込みが拒否されたり失敗したりする頻度と、その理由。パッケージスキーマ v4 からは、`failure_class` が理由を示します。組み込みのストアが返す拒否・失敗の1つずつに対応させた、決まった一覧から選びます。`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` です。まとめた操作は全部実行するか何もしないかのどちらかなので、拒否された一括操作では、どの操作の行にも、止めた操作の分類が載ります。メモリの本文、old_text、エラーの文言は含めません。 |
| `hermes.curator.run.count` | きっかけ（`scheduled`/`manual`）、結果（`success`/`failed`/`skipped`）、アーカイブ・統合・パッチ・作成の件数の区分 | スキルのキュレーターが動いているか、実際に何かをまとめているか。試行のみの実行は `skipped` として報告します。定期の確認で、ほかのプロセスがすでに処理中だと分かった場合は何も記録しません。スキル名は含めません。 |
| `hermes.delegation.run.count` | サブエージェント数の区分、深さ（`1`–`3`、`gte_4`）、モード（`foreground`/`background`）、結果（`success`/`partial`/`failed`/`cancelled`） | delegate_task の分岐がどれだけ広く深くなるか、子がすべて終わる頻度。いくつの完了単位に分かれても、1回の呼び出しにつき1行です。 |
| `hermes.execution_backend.count` | 種類（`terminal`/`browser`/`code`）、バックエンド、結果、エラーの分類 | 実際の作業をどのサンドボックスが担い、それぞれどれだけ安定しているか。ターミナルのバックエンドは `terminal.backend` の値（それ以外は `other`）、ブラウザーのバックエンドは `local`、`lightpanda`、`cdp`、`camofox`、`extension`、同梱のクラウドプロバイダー（それ以外は `other`）、execute_code は `local` か `remote` です。コマンド自身が 0 以外で終わってもバックエンドとしては成功ですが、フォアグラウンドのコマンドがタイムアウトに達した場合は `failed`/`timeout` です。バックエンドに届く前にガードが拒否したターミナルと execute_code の呼び出し、TUI/Desktop のパス補完のために Hermes 自身が行う一覧取得、背景レビューとキュレーターの fork による呼び出しは数えません。 |
| `hermes.platform.health` | プラットフォーム、イベント（`connect_ok`/`connect_failed`/`reconnect`/`disconnect`）、エラーの分類（`auth`/`network`/`rate_limited`/`config`/`other`） | どのメッセージングプラットフォームで接続の失敗や切断が起きているか、その理由。`connect_failed` は、接続に失敗し続ける一続きの出来事を、プロファイル・プラットフォーム・UTC の日ごとに1回だけ数えます（再接続の見張り役によるやり直しは新しい行になりません。次に成功するとその一続きは終わり、翌日も失敗しているプラットフォームはまた数えます）。エラーの分類は、その一続きで最初に失敗したときのものを使います。例外の型、HTTP ステータス、Hermes 自身の致命的なコードから分類し、エラーの文言は使いません。 |
| `hermes.platform.delivery` | プラットフォーム、結果（`sent`/`failed`）、失敗の分類（`rate_limited`/`too_long`/`auth`/`network`/`forbidden`/`other`） | プラットフォームごとに、返答がユーザーに届かない頻度（論理的な返答1件につき1回で、再試行も含みます）。 |
| `hermes.gateway.reply_latency` | プラットフォーム、最初の応答までの区分（`lt_2s` … `gte_60s`） | 受け付けた受信メッセージから、目に見える最初の返答テキスト（ストリームの最初の塊か最終メッセージ）までの時間。 |
| `hermes.cron.run` | 結果（`success`/`failed`/`missed`/`skipped`）、配信の種類（`local`/`platform`/`webhook`/`none`/`other`）、所要時間の区分 | 定期ジョブが動いているか、失敗していないか、ゲートや重複で飛ばされていないか、Hermes が止まっている間に取りこぼされていないか。ジョブ名、プロンプト、スケジュール、送り先は含めません。 |
| `hermes.startup.latency` | 実行面（`cli`、`tui`、`desktop_attach`、`gateway_boot`、`serve_boot`）、待ち時間の区分（`lt_500ms` … `gte_10s`） | 各実行面が起動から使える状態になるまでどれだけかかるか。起動の遅れを実行面とリリースごとに見られます。プロセスの起動1回につき1行です。CLI はプロセスの起動から最初のプロンプトが表示されるまで（または `-q` の問い合わせが送られるまで。Kanban のワーカーは除きます）、TUI は Ink のプロセスの起動からゲートウェイの準備完了まで、Desktop はアプリの起動からバックエンドへの接続まで、ゲートウェイはプロセスの起動からアダプターの接続まで、`hermes serve` はプロセスの起動から待ち受け開始までです。その場で再 exec したプロセス（たとえばセッションを再開する `hermes sessions browse`）と、ダッシュボードの Chat タブの各ターミナルは数えません。TUI/Desktop が同じバックエンドへ再接続しても数え直しません。 |
| `hermes.update.run` | 種類、結果、失敗した段階、所要時間の区分、更新前のバージョンの古さの区分、適用方式 | 更新が成功しているか、どれだけかかるか、どこで失敗するか、更新前のバージョンがどれだけ古かったか。`outcome` は `success`、`noop`、`failed`、`refused`、`partial` のいずれかです（partial はコミットしたあとに中断されたか、ユーザーが退避した変更を退避したまま残した実行で、`hermes update` の行だけに出ます）。`partial` の実行は、ユーザーごとの更新率では試行1回かつ失敗1回として数えます（これまでこうした実行は `failed` と読まれていたので、推移は途切れません）。その理由は、コードを適用したあとで後続の処理に失敗した実行と同じ側に並びます。`hermes update` の行は、最終的な更新の記録から1回の実行につき1回導きます（Desktop のソースチェックアウトからの引き継ぎで実行した場合、`kind` は `desktop` です）。更新前のインタープリターが完了させた実行、または依存関係の準備に失敗したあと素のブートストラップ用インタープリターが完了させた実行（こちらは設定の読み込み処理を読み込めません）は、行に必要な範囲の限られたフィールドだけを持ってローカルに保留し、次の起動で数えます。収集が無効なら、次の起動はそれを記録せずに消します（そのインタープリターの時点で収集が無効だと分かる場合は、何も保留しません）。Desktop のパッケージ版の自己更新（`apply_mode=package`）は、それを適用する再起動のあとにアプリが1回報告します。Desktop の引き継ぎが起動した更新の子プロセス（更新マーカーが、それかその親を引き継ぎの代理として示しているもの）がロックで拒否された場合（`lock_held`）は、`refused` ではなく `failed` になります。Desktop はその更新を実行するためにすでに終了しているので、失敗した更新として報告します。これには、別の更新が実行中の場合だけでなく、別の変更処理（Desktop のビルドや venv の同期）がチェックアウトのロックを握っている場合も含まれます。 |
| `hermes.update.stage` | 段階、結果、所要時間の区分 | `hermes update` の段階（plan、snapshot、apply、deps、build、restart、verify）ごとの結果と実時間。記録にある段階のタイムスタンプから求めます。 |
| `hermes.process.exit` | プロセスの種類、終了の種類、クラッシュの分類 | CLI / TUI / ゲートウェイ / serve / cron の tick の各プロセスがどう終わったか（`clean`、例外の系統だけを持つ `crash`、`killed`、`watchdog`）。同じプロファイルの次の起動が、ローカルの目印から報告します（収集がオフの状態で起動すると、これらの目印と保留中の `provider_setup` の目印は報告されずに削除されます）。ターンの watchdog で打ち切られたターンも `exit_kind=watchdog` として数えます。 |

relay コネクターが運ぶ返答は、会話が行われているプラットフォーム（受信メッセージのプラットフォーム、
それがなければ、コネクターがちょうど1つのプラットフォームを受け持っている場合はそのプラットフォーム）を報告し、`relay` にはしません。`relay` が残るのは、どちらも分からない場合だけです。
コネクターが印を付けなかった受信メッセージのターンも、ゲートウェイのメッセージとして扱います
（`execution_surface=gateway`、タスク・セッションの `platform=relay`）。relay コネクターの `hermes.platform.health`
は `relay` のままです。1本のソケットで複数のプラットフォームを受け持つので、
接続や切断はどれか1つのプラットフォームだけのものではないからです。

<!-- ---- v5 desktop ---- -->
#### Desktop アプリ: 何が使われ、何が邪魔になり、何がオフにされるか {#desktop-app-what-gets-used-what-gets-in-the-way-what-gets-turned-off}

Desktop アプリが、フォーカスしているプロファイルのストアに記録します。記録するのは、そのプロファイルの
収集のスイッチがオンの間だけです。オフのときはローカルに何も残さず
（オフにすると残していたものを削除します）、何も送りません。アプリは、ゲートウェイの接続とプロファイルごとに
ローカルの記録を1つ持ち、そのプロファイルのウィンドウで共有します。プロファイルを切り替えたあとは、新しいプロファイルの
スイッチを読み取るまで何も残さず、何も送りません。スイッチはウィンドウがフォーカスを取り戻すたびに読み直します
（CLI やほかのウィンドウで無効にすると、そこにも反映されます）。評価を求める
プロンプトなどの新しい UI はありません。どの事実も、アプリにもとからある操作から得ます。
値はどれも、アプリのコードで定義した閉じた ID（エリア、操作、通知、フロー、
トグル、ステップ）、公開されている設定キー、区分のいずれかです。メッセージの本文、トーストの本文、
セッション・ボット・プロファイルの名前、パス、設定の値が外に出ることはありません。

| メトリクス | 次元 | 答える問い |
|---|---|---|
| `hermes.desktop.feature_use` | エリア（ペイン、コマンドパレット、モデル・セッションの選択、音声、Bot Mode、スキン、プロジェクト、各設定ページ、全画面のページ、`other`） | Desktop のどのエリアがそもそも使われているか。エリアごとに、プロファイルにつき UTC の1日1回までしか数えません（プロファイルのローカルデータベースにラッチするので、2つ目のウィンドウやバックエンドの再起動で数え直すことはありません）。 |
| `hermes.desktop.action_use` | 操作（アプリの組み込みのコマンド・キーバインドの ID と、いくつかの名前付きボタン。プラグインのコマンドと番号付きスロットのショートカットは `other` で、何かを残す前にアプリ内でまとめます）、経路（`click`/`shortcut`/`palette`/`menu`）、回数の区分 | どのボタンやコマンドが、どうやって押されているか。アプリ内でプロファイルごとに集計し、1日が終わるごとに1回報告します（押すたびの行はありません）。行は、送った日ではなく、その行が表す日の期間に入ります。ウィンドウやバックエンドの再起動がいくつ報告しても、各日はプロファイルにつき1回だけ記録し、8日より古い日は捨てます。 |
| `hermes.desktop.mode_use` | モード（`sessions`/`bots`）、アクティブ時間（分）の区分、送信したメッセージ数の区分、ボット数の区分 | Desktop で過ごす時間が Bot Mode と通常のセッションにどう分かれているか。その日に使ったモードごとに、その日の期間へ1行です（`action_use` と同じ、プロファイルにつき1日1回のラッチ）。アクティブ時間は、最大5分までの操作の間隔を合計したものです。 |
| `hermes.desktop.friction` | 種類（`notice_dismissed`、`error_toast`、`renderer_crash`、`backend_disconnect`、`slow_frame`）、詳細（通知 ID、エラーの分類、クラッシュの理由、切断の理由、フレーム時間の区分） | 何が邪魔になっているか。エラーのトーストが持つのは、コードで定義した分類だけです。レンダラーのクラッシュは、クラッシュしたウィンドウ自身のプロファイルが収集している場合だけアプリのシェルが記録し、復帰したあとにそのプロファイルのウィンドウが報告します。遅いフレームは、ウィンドウが表示されている間の長いフレームで、1日あたりの上限があります。 |
| `hermes.desktop.dislike` | シグナル（`quick_close`、`cancelled`、`setting_off_default`、`rage_click`、`undo`、`feature_disabled`）、対象、設定、方向 | 機能が望まれていないことを示すシグナル。開いて5秒以内に閉じたペイン、途中でやめたダイアログやフロー、既定値へ、または既定値から動かした設定（キーだけで、保存した値と既定値の比較はバックエンド自身が行います）、1秒以内に同じ操作部品を3回クリック、元に戻す操作、同梱の機能をオフにする操作です。シグナルごとに1日あたりの上限があります。 |
| `hermes.desktop.onboarding` | ステップ（初回起動のステップ: プロバイダーの選択、サインイン、API キー、ローカルのエンドポイント、モデルの選択、あとで選ぶ、無料枠の画面、セットアップの案内カード、同意、最初のメッセージ）、イベント（`reached`/`completed`/`abandoned`） | 初回起動がどこで止まるか。各ステップのイベントはプロファイルにつき1回です（プロファイルごとの小さなファイルにラッチします）。`abandoned` は、次にアプリを起動したときにまだ開いていたステップです。初回起動は同意の質問より前に行われるので、答えが出るまでアプリはステップのイベントをメモリにだけ持ち（ディスクには書かず、送りもしません）、そのアプリのセッション中に利用者が有効にした場合に記録します。「いいえ」を選ぶか、先に終了すると破棄します。Desktop で収集をオフにすると、アプリ自身の記録と一緒にそれらのラッチも削除します。 |

セッションは閉じたとき（確定、リセット、プロセスの終了）に要約します。
委任した子のセッションは別には数えません。マイルストーンは
ローカルデータベースにラッチするので、いくつのプロセスが到達しても1インストールにつき1回だけ記録します。

#### モデルごとの品質、使いにくさ、コンテキストの圧迫 {#per-model-quality-friction-and-context-pressure}

プロバイダーとモデルは、モデル経路の規則に従います。Hermes が同梱しているプロバイダー（組み込み、
リポジトリ内の `plugins/model-providers/` のプロファイル、公開されている models.dev の ID）はそのまま、モデルもその
モデル ID で表します。独自のエンドポイント、
`$HERMES_HOME/plugins/model-providers/` の下や pip からインストールしたプロバイダープラグイン（名前と別名を含む）、
`custom` のローカルサーバー用の別名（`ollama`、`local`、`vllm`、`llamacpp`、
`llama-cpp`、`llama.cpp`）、ループバックのサーバーは `custom` になります。同梱のプロバイダーでも、
エンドポイントがループバックのサーバーのもの（`lmstudio`。どの別名でも同じ）は、名前はそのまま残し、
モデルは `custom` になります。プロバイダーが分からないモデルや、ID が URL、ファイルパス、
ネットワークアドレス（`host:port`、IP アドレス、`localhost`）、AWS ARN（アカウント ID を含むため）のモデルも
`custom` になります。Azure のプロバイダーでは、モデル ID は所有者が選んだデプロイ名なので、
公開されているモデル ID（Hermes のモデル一覧か
ローカルの models.dev のキャッシュにあるもの。たとえば `gpt-4o`）の場合だけそのまま通します。`acme-legal-prod` は `custom` になります。ローカルの
サブスクライバーは、すべての mark のプロバイダー・モデルのフィールドにこれらの規則をもう一度当て、
書き換えが必要になる行は捨てます。

| メトリクス | 次元 | 答える問い |
|---|---|---|
| `hermes.model_tool_quality.count` | プロバイダー、モデル、呼び出しの役割、問題（`none`、`invalid_json`、`unknown_tool`、`schema_mismatch`、`empty_arguments`、`repaired`） | どのモデルが壊れたツール呼び出しを出すか、Hermes がどれくらいの頻度で直す必要があったか。出力された呼び出しはすべて1回ずつ数える（問題のないものは `none`）ので、値は割合の分母になります。`empty_arguments` は必須パラメーターを持つツールの場合だけ数えます。`repaired` は、Hermes がツール名か引数の JSON を直して呼び出しを実行したという意味です。 |
| `hermes.model_friction.count` | プロバイダー、モデル、シグナル（`retry`、`undo`、`interrupt`、`quick_abandon`、`switch_away`） | 利用者がどのモデルに手こずっているか。そのターンを出力したモデルに結び付けます。`/retry` と `/undo` は実行された場所で、対話的なターンを利用者が中断した場合、失敗したターンから60秒以内に終わったセッション、`/model` でそのモデルから切り替えた場合です。 |
| `hermes.context_peak.count` | プロバイダー、モデル、最大の埋まり具合の区分、ウィンドウの区分（`lt_32k` … `gte_1m`）、上限への到達（`yes`/`no`） | セッションが各モデルのコンテキストウィンドウにどこまで近づくか、どれくらいの頻度であふれるか。閉じた会話1つにつき1行です。圧縮による切り替えで引き継がれたセッション ID は、最も埋まった区間の値で1回だけ報告します。`limit_hit` は、主の呼び出しが大きすぎるとして拒否された（コンテキストのあふれ、または HTTP 413）という意味で、Hermes が強制的な圧縮で対応する拒否です。 |

#### Tool Search とコネクター {#tool-search-and-connectors}

パッケージスキーマ v4 の中で追加しました。どの次元にも、コネクターのスラッグ、ツール名、検索語は現れません。

| 指標 | 次元 | 答える問い |
|---|---|---|
| `hermes.tool_search.count` | 範囲（`local`、`connector`、`both`: 検索がどのカタログを読んだか）、ヒット数の区分（`0` … `gte_11`、返した別々のツールの数）、結果（`hits`、`no_hits`、`connector_unavailable`、`error`） | `tool_search` がツールを見つけられているか、ホスト型コネクター側の検索がどれくらいの頻度で失敗するか。`tool_search` の呼び出し1回につき1行です。 |
| `hermes.tool_batch.count` | プロバイダー、モデル、件数の区分（論理的な呼び出しの数。`tool_call` の各項目を1回ずつ数えます）、コネクターの割合（`none`、`some`、`all`） | モデルが1回の返答にいくつの呼び出しを詰め込むか、そのうちどれくらいがコネクターの作業か。ツールを呼び出すモデルの返答1回につき1行です。 |
| `hermes.connector_chain.count` | 事前の検索回数の区分（直前のコネクター実行から数えた `tool_search` の呼び出し回数）、先に説明を見たか（`yes`/`no`: そのツールが会話の中で先に `tool_describe` で指定されていたか）、結果（ツールの結果） | モデルがコネクターのツールを実行する前に検索と説明の確認をしているか、それを飛ばすと失敗が増えるか。実行したコネクターの項目1つにつき1行です。 |

<!-- ---- v5 harness ---- -->
#### エージェントのハーネスの精度 {#agent-harness-accuracy}

これらは、エージェントのループそのものを調整するためのものです。Hermes 自身の背景レビューとキュレーターの
ループは数えません。委任したサブエージェントは数えます（そのツール呼び出し、ループ、返答も
モデルのふるまいだからです）。コマンドの本文、ファイルパス、ツールの引数、返答の本文が外に出ることはなく、
出るのは下の閉じた値だけです。

| メトリクス | 次元 | 答える問い |
|---|---|---|
| `hermes.file_edit.count` | ツール（`patch`、`write_file`）、モード（`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`） | どのあいまい一致の方法が役に立っているか、編集が外れたりあいまいになったりする頻度。編集ツールの呼び出し1回につき1行です。複数の hunk を持つ V4A のパッチは、いずれかの hunk が必要とした最もゆるい方法を報告します。 |
| `hermes.loop_guard.count` | プロバイダー、モデル、シグナル（`repeated_tool_call`、`loop_detected`、`iteration_cap`）、検出器（`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` は反復の上限を使い切ったターンです。シグナルと検出器の組み合わせごとに、1ターンにつき最大1回です。 |
| `hermes.tool_recovery.count` | プロバイダー、モデル、ツール（組み込みの名前、それ以外は `mcp` / `plugin`）、次のツール（`same`、`different`、`none`）、次の結果（`success`、`error`、`no_tool_call`、`gave_up`） | ツール呼び出しが失敗したあと、モデルが立て直せているか。失敗した呼び出し1回につき1行で、モデルの次の回で判定します。同じツールへの次の呼び出しがあればそれ、なければ最初の呼び出しです。代わりにテキストで答えた場合は `no_tool_call`、返答なしでターンが終わった場合（停止、上限の使い切り、中断、エラー）は `gave_up` です。 |
| `hermes.terminal.outcome.count` | バックエンド（ターミナルのバックエンド）、コマンドの種類（`git`、`package_manager`、`build`、`test_runner`、`python`、`node`、`shell_builtin`、`shell`、`file_ops`、`network`、`container`、`other`）、結果（`ok`、`nonzero`、`timeout`、`killed`） | バックエンドごとに、どの種類のコマンドが失敗したりタイムアウトしたりするか。種類は、（環境変数の代入と `sudo` のようなラッパーのあとの）最初のプログラム名を固定の表に当てて決めます。終了ステータスに達したフォアグラウンドのコマンド1つにつき1行です。`timeout` / `killed` は Hermes 自身の期限と中断のフラグから決まるので、コマンド自身の `exit 124` は `nonzero` です。`hermes.execution_backend.count` は同じ呼び出しを、バックエンドが処理できたかどうかで数えます。次元が重ならないので、結果を二重に数えているわけではありません。 |
| `hermes.model_reply_issue.count` | プロバイダー、モデル、問題（`none`、`empty`、`reasoning_only`、`refusal`、`truncated_length`） | どのモデルが使えない返答を返すか。主のモデルの応答1つにつき1行です（使える応答は `none` で、割合の分母になります）。`refusal` と `truncated_length` は、構造化された終了理由（`content_filter`、`length`）からだけ判定します。`empty` は、見えるテキスト、ツール呼び出し、推論のどれもない、形式上は正しい応答です。 |
<!-- ---- end v5 harness ---- -->

<!-- ---- v5 efficiency ---- -->
#### 効率: ターンのコスト、無駄、ツールの負担、プロンプトキャッシュの破棄 {#efficiency-turn-cost-waste-tool-overhead-and-prompt-cache-breaks}

プロバイダーとモデルは、上のモデル経路の規則に従います。「ユーザーのターン」は、ユーザーの1つの
メッセージから最終的な返答までです。Hermes 自身の作業（メモリ・スキルの背景レビュー、
キュレーター、委任したサブエージェント自身のターン）はユーザーのターンではありません。`cache_break` と
`tool_output_truncation` はモデルとツールのふるまいを表すので、委任した
サブエージェントもそこでは数えます。背景レビューとキュレーターはどちらでも数えません。

| メトリクス | 次元 | 答える問い |
|---|---|---|
| `hermes.task_cost.count` | プロバイダー、モデル、トークン数の区分（`lt_2k` … `gte_1m`、`unknown`）、ツール呼び出し数の区分、API 呼び出し数の区分（`0` … `51_to_100`、`gte_101`）、結果（`completed`、`interrupted`、`failed`） | モデルごとに、ユーザーの1ターンにどれだけかかるか。トークンは、そのターンの主の呼び出し全体でのプロンプト（キャッシュの読み書きを含む）と生成の合計です。プロバイダーが使用量を報告しなかった場合は `unknown` です。利用者が終わりを見た対話的なターン1つにつき1行です（セッションを閉じたことによる打ち切りはターンではありません）。 |
| `hermes.wasted_tokens.count` | プロバイダー、モデル、理由（`interrupt`、`retry`、`undo`）、トークン数の区分 | 利用者がどれだけのトークンを捨てているか。中断、`/retry`、`/undo` で捨てたターン1つにつき1行で（`/undo N` は N ターンと数えます）、そのターンを出力したモデルに結び付けます。中断してから元に戻したターンは1回だけ数えます。このプロセスがそのターンを見ていない場合（再起動、リモートのホスト）は `unknown` です。 |
| `hermes.tool_output_truncation.count` | ツール（同梱のツール名、それ以外は `mcp` / `plugin`）、切り詰めの有無（`yes`/`no`）、元のサイズの区分（文字数: `lt_1k` … `gte_500k`） | どのツールが、そのまま載せておけないほど大きな出力を出すか。ツール結果1つにつき1行です。ツールが自分で出力を切った場合（ターミナル、`execute_code`、MCP の先頭・末尾の切り詰め。このときサイズは元の大きさです）や、結果ごとの上限やターンごとの予算でディスクへ逃がした場合は `yes` です。 |
| `hermes.tool_overhead.count` | 有効なツール数の区分、ツールのスキーマのトークン数の区分（`0`、`lt_2k` … `gte_40k`）、実行面 | ツールの定義を持ち歩くことにどれだけかかるか。閉じた対話的な会話1つにつき1行で、その会話で有効だったツールと、その定義が各リクエストに加えるトークン数の Hermes 自身による見積もりです。 |
| `hermes.tool_enabled_unused.count` | ツールセット（Hermes が同梱しているツールセット。MCP サーバー、プラグイン、ユーザーのツールセットは `custom`）、使用の有無（`yes`/`no`） | 既定のツールセットのうち、負担だけかかって使われていないのはどれか。閉じた対話的な会話ごとに、有効なツールセット1つにつき1行です（同梱のツールセットの数が上限です）。 |
| `hermes.cache_break.count` | プロバイダー、モデル、原因（`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 が把握している原因なしに、主の呼び出しがキャッシュ済みトークンを0と読んだものです。`cache_expired` は、同じことが5分以上のアイドルのあとに起きたものです。把握している原因は、ミスとして重ねて数えません。 |
<!-- ---- end v5 efficiency ---- -->

<!-- ---- v5 engagement ---- -->
#### 利用の定着と、モデルへの暗黙の満足度 {#engagement-and-implicit-model-satisfaction}

| メトリクス | 次元 | 答える問い |
|---|---|---|
| `hermes.engagement.surface_day.count` | 実行面（`cli`、`tui`、`desktop`、`gateway`、`acp`）、アクティブ時間（分）の区分（`0`、`lt_5m`、`5m_to_30m`、`30m_to_2h`、`2h_to_6h`、`gte_6h`） | 各実行面が1日に実際どれだけ使われているか。締まった UTC の1日に使われた実行面1つにつき1行です。 |
| `hermes.engagement.day.count` | アクティブ時間（分）の区分、使った実行面の数（`0`–`3`、`gte_4`）、主なプロバイダー、主なモデル、アクティブなプロファイル数の区分 | 1週間のうちアクティブな日数、複数の実行面の利用、モデルごとの翌日・翌週の再訪。利用者が Hermes を使った、締まった UTC の1日につき1行です。ルート（既定）のプロファイルは、ほかのプロファイルだけがアクティブだった日にもホストの行を書きます。その行は `surfaces_used_count` が `0`、アクティブ時間が `0` で、アクティブなプロファイル数を持ちます。アクティブな日数を数えるときは `surfaces_used_count=0` の行を除いてください。 |
| `hermes.model_switch_after.count` | プロバイダー、モデル（切り替え元のモデル）、切り替えまでのターン数の区分（`1`、`2_to_3`、`4_to_10`、`11_to_30`、`gte_31`） | `/model` で離れるまで、利用者がそのモデルをどれだけ使い続けるか。その会話で古いモデルに送ったユーザーのターンを数えます（圧縮による区間を含みます。代替に切り替わったターンも、送った先のモデルの分として数えます。背景レビューの fork はターンではありません）。現在のモデルでまだ1ターンもないうちの切り替えは数えません。 |

アクティブ時間は、UTC の1日ごとにローカルで積み上げます。連続する操作の間隔の合計で
（対話的な実行面でユーザーのターンが始まる・終わる操作、またはゲートウェイのメッセージ。無人の cron 実行（`hermes.cron.run`
が数えます）、委任した子、背景レビュー、キュレーター、バッチ、API サーバー /
Python への組み込みは除きます）、各間隔の上限は5
分です。その日の行は、日が締まったとき、つまり後の日の最初の
操作のときに、1つのデータベーストランザクションで記録します。そのため、日付の切り替わりをいくつのプロセスが見ても、1日は
プロファイルにつき必ず1回だけ報告され、その行が表す日の日付が付きます。主なモデルは、その日の
ユーザーのターンを最も多く処理したモデルで（1つもなければ `none`）、モデル経路の
規則で名前を付けます。1週間のうちアクティブな日数やモデルごとの再訪は、これらの日ごとの行と
既存の `install_id` からサーバー側で導きます。そのために Hermes が1週間の範囲を持つことはなく、
`install_id` 以外の識別子も持ちません。

`active_profile_count_bucket` は、その UTC の1日に、ユーザー自身のターン（上の操作）があった
ホストのプロファイルの数を、重複なしで数えます。各プロファイルは自分のターンを、ルート（既定）のプロファイルの
データベースにあるホスト全体の累積に畳み込みます。そこには各プロファイルのホームを表す、中身の見えないローカルの
ハッシュとして入り、外には出ません。そのため、どのプロセスや多重化したランタイムが処理しても、プロファイルは1回だけ数えます。
数を持つのはルートのプロファイルの
日ごとの行だけです（ルート自身が使われていなかった日も、アクティブ時間と実行面を `0` として
その日を報告します）。ほかのプロファイルの行は
`0` です。ルートのプロファイルで収集がオフの場合、その
データベースには何も書き込まず、数も報告しません。
<!-- ---- end v5 engagement ---- -->

<!-- ---- v5 signals ---- -->
#### 導入時と機能のシグナル {#onboarding-and-feature-signals}

| メトリクス | 次元 | 答える問い |
|---|---|---|
| `hermes.tool_unavailable.count` | プロバイダー、モデル、ツール名（同梱の組み込みのみ） | どのツールセットを既定で有効にすべきか。モデルが、Hermes が同梱しているのにそのセッションでは有効でないツールを呼び出した場合です。それ以外の未知の名前（プラグイン、MCP、モデルの作り話）は、`model_tool_quality` の `unknown_tool` の問題としてだけ扱います。セッションで有効になっていても `tool_search` の後ろに遅延させた組み込み（`tool_call` から到達できるもの）は、使えないものとは扱いません。ツールセットを意図して絞っている背景レビュー、委任した子、cron ジョブは除きます。 |
| `hermes.provider_setup.count` | プロバイダー（カタログの名前。独自のエンドポイントは `custom`）、実行面（`cli_setup`、`cli_model`、`tui`、`desktop`、`dashboard`）、イベント（`started`、`completed`、`failed`、`abandoned`）、失敗の分類（`auth`、`network`、`no_models`、`other`。失敗していなければ `none`。`cancelled` はもう記録せず、取り消しは `abandoned` になります） | プロバイダーの接続がどこでつまずくか。`started` はプロバイダーを選んだ時点で1回数え、フローの終わりはそのフローを実行した実行面が記録します。利用者が途中でやめたフローは `abandoned` で、`failed` にはなりません。CLI の選択画面での Esc や Ctrl-C、Desktop やダッシュボードのサインインでのキャンセル・戻る、プロバイダーのページでの同意の拒否、期限切れまで放置したサインインのコード（どのプロバイダーでも）がこれに当たります。CLI で戻る（左矢印）を押してもフローは開いたままなので、同じプロバイダーをもう一度選ぶと続きになり（`started` は1回）、別のプロバイダーを選ぶかコマンドを抜けると `abandoned` として終わります。誰も完了させなかったフローはローカルに目印を残し、そのプロファイルで次にセットアップか Hermes を起動したときに `abandoned` として報告します（そのプロセスがもうないか、1時間以上保留になっている場合）。Desktop やダッシュボードのサインインがポーリングの途中で止まった場合は、ただの `other` ではなく、止めたエラーの分類をそのまま使います（接続が切れたなら `network`、拒否されたなら `auth`）。フォーム（TUI/Desktop/ダッシュボード）から保存した新しい、または変更したプロバイダーの API キーと、新しく追加した独自のエンドポイントは、1回の操作で開始と完了になります。キーの消去、同じキーの再保存、既存のエンドポイントの編集、エコシステムのトークン（`GITHUB_TOKEN`、`GH_TOKEN`、`HF_TOKEN`）、ツールの設定パネルでも求められるキー（たとえば `GEMINI_API_KEY`、`XAI_API_KEY`、`DEEPINFRA_API_KEY`）は、汎用のキーのフォームからは数えません（Desktop の導入画面とモデルの設定は、保存をプロバイダーの接続として印を付けるので、そちらは数えます）。キー、トークン、ベース URL、エラーの文言は含めません。プロバイダーを選ぶ前に選択画面を離れた場合は数えません。 |
| `hermes.feature_adoption.count` | 機能（`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`） | インストールから、主な機能がそれぞれ初めて本当に使われるまでどれだけかかるか。1インストールにつき機能ごとに1回で、ローカルデータベースにラッチします。上のカウンターから導くもの（フォアグラウンドでのメモリへの書き込み、ユーザーの求めで作ったスキル（Hermes の背景レビューが作ったものは除く）、成功した MCP/プラグイン/ブラウザー/TTS/kanban のツール呼び出し、Desktop/TUI/ゲートウェイのタスク、cron の実行、手動のキュレーター実行。定期のキュレーター実行は数えません）に加えて、Bot Mode のメッセージとプロジェクトの作成は初回利用を直接報告します。期間は、それを持つプロファイルのもの（そのプロファイルの最初のセッションから）です。 |
| `hermes.feature_disabled.count` | 種類（`toolset`、`skill`、`plugin`、`platform`、`setting`、`memory`、`curator`、`compression`）、名前、実行面（`cli_tools`、`cli_config`、`cli_slash`、`tui`、`desktop`、`dashboard`）、イベント（`disabled`、`re_enabled`） | 利用者が何をオフにしているか。設定の書き込みそのものの時点で差分を取ります。既定でオンのツールセットの削除、スキルやプラグインの無効リストへの追加、既定で `true` の設定の false への変更（と、それぞれを元に戻したもの）です。名前を公開するのは同梱のものだけで、ツールセットのキー、同梱・カタログのスキル、同梱・カタログのプラグイン（メッセージングプラットフォームのプラグインは `platform` として報告）、`DEFAULT_CONFIG` のキーのパス（値は含めない）です。それ以外は `custom` です。カタログのスキルのアンインストールは `disabled` として数えます。記録するのはユーザーが使う入口（`hermes tools` / `config` / `skills` / `plugins`、チャットのスラッシュコマンド、TUI/Desktop、ダッシュボード）だけで、セットアップや移行では、それらの入口の中で移行が動いた場合（`hermes config migrate`、ダッシュボードから作成したプロファイル）でも記録しません。値が `${VAR}` のテンプレートの設定は比較しません。差分の計算と記録は、書き込みのあと、どの設定のロックの外でもバックグラウンドのスレッドで行います。（種類、名前、イベント）の組み合わせごとに、1日最大1回です。 |
<!-- ---- end v5 signals ---- -->

<!-- ---- iuf c1 ---- -->
#### インストールと更新の失敗の理由 {#install-and-update-failure-reasons}

パッケージスキーマ v4 から（その場で拡張しました。追加だけなので、すでに送った形はどれも有効なままです）、既存の2つの指標が失敗の理由を示すようになりました。
変更前に記録した行は古いフィールドの組み合わせのままで、そのままパッケージにできます。

| 指標 | 追加した次元 | 答えたい問い |
|---|---|---|
| `hermes.extension.install.count` | 失敗の分類、レジストリ | スキル・プラグイン・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` が組み立てるアダプターの ID）のどれかです。どのアダプターも答えなかったときは `unresolved`、組み込みの復元とプラグイン/MCP のすべての行では `none`、それ以外は `other` です。`failure_class` がその種類の一覧にない行、失敗なのに `none` の行、成功なのに値が入っている行、プラグイン/MCP の行でレジストリの名前を持つ行は無効です。識別子、URL、エラーの文言、パスは含めません。`name` は、これまでどおりカタログの名前か `custom` の規則に従います。 |
| `hermes.update.run` | 失敗の分類 | `hermes update` がなぜ失敗するか。どこで失敗したか（`failed_stage`）と並べて見ます。結果が `failed`、`refused`、`partial` のいずれかのとき以外は `none` です。最終的な更新のレシートだけから導きます。`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`、errno が ENOSPC の `OSError` は `disk_full` になります）。更新のあと Windows でゲートウェイの再開に失敗した場合は `restart_failed` になります。実行を apply 段階の印より前で止める出口は、どれもその出口自身で決まった名前のトークンを1つ記録し、実行はそれを読みます（このうち `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`（コミットポイントを設定できなかった。何も動いていない）、`downgrade_refused`（更新先が、あるプロファイルの `state.db` にすでに履歴があるゲートウェイの実行環境より古い。何も動いていない）、`venv_foreign_owner`、`local_changes_blocked`（git が退避できなかったローカルの変更、または ZIP による代替手段が上書きしない変更済みのツリー）、`stash_restore_rejected`、`not_git_checkout`、`old_version_handoff`（古い更新処理からの引き継ぎを完了できなかった）、`branch_unsupported`、`download_failed`、`zip_failed`、`disk_full`、`lock_held`、`managed_install` です。`aborted_before_apply` は、こうした出口でトークンが記録されていなかった場合の代替としてだけ残ります。起動時の構文の事前チェックは、更新先の重要なファイルを git のオブジェクトストアから読みます。その読み込みが時間切れになったとき（blobless のインストールは blob を1つずつ遅延取得します）、事前チェックはそれらを存在しないファイルと同じように飛ばし、移動後の構文チェックに判断を任せるので、実行が `subprocess_failed` として失敗することはなくなりました。更新ロックによる拒否、Git 操作による拒否、管理されたインストールによる拒否は、実行のレシートが開く前に発火します。これらは引き続きレシートを書きません（レシートの保存先はロックを持っている更新のものだからです）。代わりにそのトークンで行を1つ記録し、更新マーカーがこの実行の Desktop 引き継ぎ（委任先か引き継ぎ相手）を指しているときは `kind` を `desktop` にします。別の更新がマーカーを持っているというだけではそうしません。チェックアウトの移動を終わらせた git のエラーが `git_index_locked` になるのは、git がインデックスのロックがすでに存在すると報告したときだけです。git がそれ（またはいずれかのファイル）への書き込み権限を拒否した場合は `permission_denied` になります。トークンを読むのは実行に apply の印がない間だけなので、理由がそれを記録した出口より長く残ることはありません。コミットしたあとに中断された実行や、退避したローカルの変更を再適用できなかった実行は、結果が `failed` ではなく `partial` になります（それぞれ `interrupted` / `local_changes_parked` で、後者の `failed_stage` は `apply`）。後続処理が残っているだけの実行は `success` のままです（contract C3）。更新前のインタープリターが完了させた実行は、トークン、終了コード、停止理由の先頭にある例外の型（または Hermes の決まった2つの文言のどちらか）、再起動とユーザー操作のフラグを持って保留されます（argv、パス、メッセージは持ちません）。そのため、同じ実行をプロセス内で完了させた場合とまったく同じように分類されます。すべての段階の印が通ったのに失敗した実行は、`other` ではなく、`failed_stage` が `verify`（再起動後の確認が `partial` を書いた）か `restart`（再起動を飛ばしたため、フリートに再起動が残っている）になりました。ビルドや再起動の印が失敗している `partial` の実行はその段階のままで、verify の段階の行が失敗になるのは、フリートに古い行か止まっている行があるときだけです。 |
<!-- ---- end iuf c1 ---- -->
<!-- ---- iuf c2 ---- -->
#### 新規インストール {#fresh-installs}

| 指標 | 次元 | 答えたい問い |
|---|---|---|
| `hermes.install.run` | インストーラー（`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` を1つだけ呼んだものは除くので、Desktop のブートストラップ用インストーラーが段階ごとに呼ぶものは数えません）は、`$HERMES_HOME/telemetry/shared_metrics/pending_installs/` に小さなローカルファイルを1つ残します。中身はこれらのトークン、ランダムな ID、開始と終了の時刻だけです（エラーの文言、パス、URL、ホスト名は含めません）。そのプロファイルで後に Hermes を起動したとき、その時点で収集がオンの場合に限り、1回だけ数えます。収集がオフの状態で起動した場合や、参加しないと答えた場合は、報告せずにファイルを消します。送信がオンの場合、この行を記録するのは、その UTC の日のパッケージが送信の同意ゲートを通れる日、つまりその日の 00:00 UTC 以前に送信の同意期間が始まっている日だけです。インストール中（setup の段階）に参加すると、同意期間は日の途中で始まります。そのため記録は、参加した日のパッケージ（これは送信されません）には入らず、翌日以降の最初の起動まで待ちます。収集がオンで送信がオフの場合は、最初の起動で行を記録し、この端末の中にとどめます。7日以内にどの起動にも数えられなかった記録は、報告せずに消します。古いインストールが後の Hermes のバージョンとして数えられることはありません。読めない記録も消します。Ctrl-C、SIGTERM、ターミナルを閉じたこと（SIGHUP）でインストールが止まった場合は、その段階の子プロセスがそれを受け取って独自のエラーで終了したときでも `interrupted` になります。段階が自分で処理して立て直した Ctrl-C（setup のプロバイダーの選択画面）では、インストールは止まりません。したがって失敗したインストールが数えられるのは、同じプロファイルで後のインストールか Hermes の起動が、収集がオンの状態で成功したときだけです。そのあとに何も続かない失敗したインストールは、数えられません。 |
<!-- ---- end iuf c2 ---- -->

ローカルの状態は次の場所に書き込まれます。

```text
$HERMES_HOME/telemetry/shared_metrics/metrics.sqlite3
$HERMES_HOME/telemetry/shared_metrics/outbox/*.json
```

データベースには、トランザクションで扱う集計と、パッケージの outbox の状態を保存します。
パッケージファイルは、閉じた JSON スキーマに適合する変更不可の差分文書で、アトミックな置き換えにより、詰めた形式の JSON として書き込みます（`jq .` で整形して表示できます）。
取り込み側がパッケージを受け付けるか拒否すると、データベースは送信の状態だけを残して本文の写しを消します。ローカルの履歴としての写しはファイルです。
各パッケージには、Hermes のバージョン、OS の種類、アーキテクチャ、インストール方法を、範囲を限ったクライアントのリソースとして記録します。
認識できないプラットフォームやインストールの値は `unknown` として出力し、生のプラットフォーム文字列、ホスト名、パスは含めません。
すべてパッケージ化済みの集計行と、出力に成功したパッケージの行とファイルは、ローカルに30日間保持します。
未処理のパッケージ行と、未出力の差分を持つカウンターは削除しません。
既存の outbox ファイルでは、パッケージスキーマ v1、v2、v3 はそのままです。
新しいパッケージは v4 を使います。
v4 は `hermes.compression.count` と `hermes.memory.op.count` に `failure_class` を加え、それらの v3 のフィールドの組み合わせも引き続き受け付けます（追加の値だけによるその場での拡張として、v4 が安定版のリリースに出たあとにも、
`hermes.extension.install.count` / `hermes.update.run` への `failure_class` と、前者への `registry` も加えており、それらの以前の v4 のフィールドの組み合わせも引き続き受け付けます）。
v3 が `hermes.model_route.count`、`hermes.tool_call.count`、タスクのカウンターについて v2 のフィールドの組み合わせを受け付けたのと同じで、アップグレード前に記録したカウンターを安全に出し切れます。
リポジトリ内のレジストリから導く語彙（ツール名、プラットフォーム、メモリプロバイダー、エラーの分類）は、JSON スキーマではパターンで範囲を限っています。
正式な許可リストは `shared_metrics_contract.py` です。

各パッケージには、ランダムな UUID として生成した `install_id` が含まれます。
スキーマ上のフィールド名とは違い、現在の範囲は1つの `HERMES_HOME` なので、正確には永続的な仮名のプロファイル識別子です。
ハードウェア、アカウント、ホスト、パス、認証情報のデータから導いたものではありません。
そのプロファイルのパッケージ間では一定なので、それらのローカルパッケージを結び付けることができます。
`$HERMES_HOME/telemetry/shared_metrics` を削除すると、すべての集計とパッケージファイルと一緒に識別子もリセットされます。

リモートへの送信は利用者が自分で有効にするもので、既定ではオフです。
永続的なローカル識別子をリモートで再利用するには、同意、識別子の範囲、リセット時の動き、保持期間、削除を扱う、製品とプライバシーについての別の判断が必要でした。
その判断はすでに下されています。

> それらの判断は
> [付録 A](#appendix-a-remote-exporter-decisions-phase-2) に記録しており、それを実装した
> エクスポーターも提供済みです。収集だけでは今も何も送信しません。送信器が動くのは
> `telemetry.shared_metrics.send` も true のときだけです。送信する各パッケージは、
> 固定の `install_id` をそのまま持ちます（製品判断、
> 2026-08-27。置き換えられた HMAC 仮名の設計を含む記録は A.2 を参照してください）。

インストール識別子の範囲は1つの `HERMES_HOME` です。
リセットするには、Hermes のプロセスを止めてから `$HERMES_HOME/telemetry/shared_metrics` を削除します。
これは意図して、古い識別子、集計データベース、キューにあるローカルパッケージをまとめて消します。
次に同意したセッションで新しい識別子が作られます。
共有メトリクスを無効にすると新しい収集は止まりますが、それまでに収集したローカルの状態を黙って削除することはありません。

## スモークテスト {#smoke-test}

決定的な応答を返すローカルのモデルサーバーに対して、実際の Hermes CLI のターンを実行します。

```bash
./.venv/bin/python scripts/smoke_nemo_relay_shared_metrics.py
```

このスクリプトは、既定ではインストール済みの `nemo-relay` 依存を使います。
ローカルでビルドした Relay バインディングを試すときだけ、`--relay-python ../nemo-relay/python` を渡してください。

スモークテストでは、ローカルモデルが最終回答の前に実際の `read_file` ツール呼び出しを要求します。
続いて、インストール済みの Relay バインディングを通して、作成、読み込み、再利用、パッチ、編集、古くなった状態、アーカイブ、復元、インストールの各スキル遷移を実行します。
SQLite のモデル・プロバイダー・タスク・ツール・スキルのカウンターを確認し、出力されたすべての差分パッケージを閉じたスキーマで検証し、仮名の client-active カウンターを確認し、プロンプト、レスポンス、ツール呼び出し ID、ツール結果、スキル名の目印がパッケージに含まれていないことを確かめます。

## 付録 A: リモートエクスポーターに関する判断（フェーズ2） {#appendix-a-remote-exporter-decisions-phase-2}

状態: **実装済み。**
この付録は、「現在の対象範囲」が将来のリモートエクスポーターに先送りしていた、製品とプライバシーについての問いに答えるものです。
何を決めたか、なぜそう決めたかを記録し、実装のあとも理由が残るようにしています。

送信は既定でオフで、`telemetry.shared_metrics.enabled` と `telemetry.shared_metrics.send` の両方が必要です。

エクスポーターは、`$HERMES_HOME/telemetry/shared_metrics/outbox/` にすでに書き込まれたパッケージファイルを、Hermes のテレメトリー取り込みサービスへ送ります。
このサービスが検証するのは外枠（`schema_version` と UUID の `package_id`）だけで、本文はそのまま S3 に保存します。

### A.1 同意 {#a1-consent}

送信は、収集とは **別に利用者が自分で有効にするもの** で、新しい設定キーで指定します。

```yaml
telemetry:
  shared_metrics:
    enabled: false   # collect locally
    send: false      # NEW: transmit to the Nous telemetry service
```

- `send` の既定値は **false** です。収集だけでは決して送信しません。
- `send` には `enabled` が必要です。ただし、送信を有効にすれば収集も有効になる、という **意味ではありません**。
  送信のフラグが、黙って収集を有効にしてはいけないからです。`send: true` で `enabled: false` の場合は、
  警告を出すだけで何もしません。
- `enabled` と同じく、`send` はプロファイルが持つ設定で、管理側スコープの設定で
  上書きされることはありません。

どちらのキーも、プロファイルごとに1回だけたずねます。どの画面でも答えは同じ3つです
（Send to Nous / Local only / No thanks）。

| 画面 | 提案が出る場所 |
| --- | --- |
| `hermes setup` | すべての流れ（Quick、Full、Blank Slate、Portal、`--quick`）の最後。 |
| `hermes` / `hermes --tui` | 対話のチャットを始める前に1回。`-q`、パイプや JSON での出力、起動されたアクション、Desktop の中で開いたペインでは出しません。 |
| Hermes Desktop | 初回起動の導入が終わったあと、入力欄の上に出る帯。入力欄をふさがず、フォーカスも奪いません。 |
| Web ダッシュボード | 管理しているプロファイルについて、すべてのページの上に出るバナー。 |

ターミナルでは「No thanks」が既定なので、Enter を押しただけで誰かが参加してしまうことはありません。
ターミナルで Esc を押すか、ダッシュボードのバナーの ✕ を押した場合は、質問は答えのないまま残り、
次の機会にまたたずねます。Desktop の帯は、初回のオンボーディングを終えるか飛ばしたあとにだけ表示されます。どの画面で答えても、両方のキーと `offer_version` がプロファイルの `config.yaml` に書き込まれます。
どちらかのキーがすでにあるプロファイルでは、二度とたずねません。管理されたインストールでは、提案そのものを出しません。

例外が1つあります。先行入力の修正より前は、`hermes chat` の起動中に押した Enter が、質問が出る前に「No thanks」を保存してしまうことがありました。
そのため、`offer_version` なしで記録された「No thanks」には、どの画面でももう1回だけ、理由を添えて提案を出します。
どんな反応でもそれで決着し、Esc やバナーの ✕ でも「No thanks」のまま確定します。参加すると答えたものは、二度とたずねません。
あとから答えを変えるには、`hermes setup telemetry`、`hermes tools`、または Desktop の Settings ›
Safety › Privacy & network を使います。

**パッケージを送信するのは、その期間全体が記録済みの同意期間に収まるときだけです。**
同意は、共有メトリクスの SQLite ストア（`send_consent_windows`）に明示的な区間として保存します。
区間は `send: true` を初めて観測したときに開き、その後の観測のたびに先へ確認が延び、`send: false` を観測したときに閉じます。
閉じる時点は壁時計の時刻ではなく、最後に *確認できた* 時点です。
1つの照合処理が、プロセスが起動するたびに設定からこの表を導きます。
そのため、セットアップウィザードでの変更、`config.yaml` の手編集、処理途中での取り消しがすべて同じ経路をたどり、どれを使っても状態の変化を取りこぼしません。

期間が最初の同意期間より前にあるパッケージ、同意期間の間に落ちるパッケージ、最新の確認時点を越えるパッケージは除外します。
このゲートは閉じる側に倒れます。
そのため、新しいパッケージは期間が終わってから、最大でプロセスの起動1回分待つと送信の対象になります。

ゲートの基準はパッケージの作成時刻ではなく **期間** です。
1つの期間は、別々の日に作られる複数のパッケージに分かれます。
その日の最初のパッケージはその日のうちに書き込まれ、同じ期間の残りのパッケージはたいてい翌日に続きます。
作成時刻を基準にすると、ある期間の後半だけを送って前半を落とし、**黙って少なく数えた** 1日を報告することになります。
期間を基準にすれば、同意は先に向かってだけ効き、送信する期間はどれも欠けがありません。

ローカルの履歴は最大30日前までさかのぼることがあり、そのデータは何もアップロードしないという約束のもとで集めたものです。
同意を先に向かってだけ尊重することで失うのは、そもそも送る許可を得ていなかった最大30日分の未送信分だけです。

### A.2 識別子の範囲: 固定の install_id をそのまま送信する {#a2-identity-scope-the-stable-installid-is-transmitted-as-is}

**判断の記録。**
このエクスポーターの当初の設計（およびこの付録の改訂1〜8）では、識別子ではなく鍵付きの仮名を送信していました。
`HMAC-SHA256(key = locally-held rotating salt, message = install_id)` で、salt は30日ごとに入れ替えていました。
**2026-08-27**、機能の提供前（同意した利用者も本番での送信もゼロ）に、製品の責任者は、分析に必要なのは **期間をまたいで一定の識別子** だと判断しました。
継続率の曲線や、インストールの長期的な動きを見るためのもので、入れ替えは設計上それを壊します。
仮名化の層は、その場で弱めるのではなく、丸ごと取り除きました。

現在送信しているものは次のとおりです。

- 各パッケージは `install_id` をそのまま持ちます。上で説明した、プロファイル単位の永続的な
  ランダム UUID です。
- ローカルで生成し（`uuid4`）、ハードウェア、アカウント、ユーザー、マシンから導いた情報は含まず、
  識別するのは人ではなく *プロファイル* です。
- 利用者が共有メトリクスのディレクトリを削除して作り直すまで一定です（A.4 を参照）。

結果として起きることを、取り繕わずに書いておきます。

- 1つのプロファイルのパッケージは、期間ごとではなく **無期限に** 結び付きます。
  1つのインストールの日々の外枠の並びを長期にわたって結び付けられるのは、残ってしまった性質ではなく、今は設計どおりの動きです。
- 旧設計についての A.3 の残存リスク分析（一定の `resource` の組み合わせと、
  連続した期間が入れ替えの境界をまたいでつながる問題）は意味を失いました。またぐべき期間の
  境界がもう無いからです。
- セットアップウィザードの同意の文言は、この識別子の方式を明示しています。
  導出を取り除いたのと同じ変更で文言も更新したので、提供したどのビルドでも、古い文言のもとで同意を集めたことはありません。

**バイト単位で同じ再送は引き続き保証します。**
送信する id は、パッケージを最初に準備したときに行（`sent_install_id`）へ記録し、送信する本文は常にその記録した値から組み立て直すので、再試行でも同じバイト列になります。
取り決めがこれを要求しています。
同じ `package_id` を異なる内容で再送したときの動きは未定義です。
（id が一定になったので、記録した写しが入れ替えへの対策として必須というわけではなくなりましたが、監査用の列として、また将来識別子の意味が変わったときの安価な保険として残しています。）

### A.3 入れ替え: 削除済み（判断の記録） {#a3-rotation-removed-decision-record}

salt の入れ替えは、導出と一緒に削除しました（製品判断、2026-08-27）。
この節は、以前の設計が何をしていたか、なぜ削除を受け入れたかの記録として残しています。

- 入れ替えは、長期的な結び付けを制限するためにありました。30日の期間ごとに1つの識別子で、
  期間をまたぐと無関係な識別子になります。
- 文書化していた残存リスク（分析の全文は git 履歴を参照）: 外枠の一定で情報量の少ない
  `resource` の組み合わせと、連続する日ごとの期間によって、珍しい構成ではいずれにせよ
  期間をまたいでつながりうるため、境界は壁ではなく、コストを上げるだけのものでした。
- 入れ替えをやめることになった製品上の必要: 期間をまたいだ連続性こそ、継続率の分析に
  必要なものです。誠実な分析をおもに妨げる一方で、本気で突き合わせようとする相手には
  コストを上げるだけの境界は、一定の識別子が要件になった時点で、割に合わない取引だと判断しました。

ストアに salt は無く、入れ替えの予定も無く、パイプラインのどこにも導出された識別子はありません。

### A.4 リセット時の動き {#a4-reset-behavior}

`$HERMES_HOME/telemetry/shared_metrics` を削除すると、上で説明したとおり、ローカルの識別子、集計、パッケージファイルが今もリセットされます。
ただし、正直に書いておくべき注意点が2つ加わりました。

- リセットで `install_id` が作り直されるので、その後のパッケージは **新しい**
  識別子で送信されます。ローカルのリセットで、リモート側でも新しい識別子になります。
- リセットで **送信を取り消すことはできません**。すでに送信したパッケージは、
  送ったときの識別子のまま取り込みサービスの保存先に残ります。v1 の取り決めには、
  読み戻しや削除の API はありません。

`send: false` にすると、送信はすぐに止まります。
同意はパッケージを送る前に毎回読み直すので、処理の途中であっても、まとめて全部を送り切るのではなく、いま送っているパッケージの後で止まります。
すでに送信したパッケージは削除せず、ローカルでの収集も止めません。

送信をオフにすると、**同意期間も閉じます**。
閉じる時点は壁時計の時刻ではなく、同意を実際に観測した最後の時点です。
期間がある同意期間と次の同意期間の間に落ちるパッケージは、後で送信を有効にし直しても送信しません。
これは、オンとオフを何回繰り返しても、プロセスが動いていない間の手編集でも、時計がどちらの方向に飛んでも成り立ちます
（同意期間の開始は、ストアにすでにあるどのタイムスタンプより後に切り上げます。観測の印は1回の呼び出しごとに限られた幅しか進まないので、1回だけおかしな未来の値が来ても確認の範囲が何年も先へ引っ張られることはありません。閉じる時点が、閉じる観測自身の時計より後になることもありません）。
以前の、1つの動く同意開始日の方式とは違い、閉じて開き直しても、前の同意期間の未配送分は捨てません。
それらのパッケージは自分の区間の中に収まったまま、送信の対象であり続けます。

意図したアップグレード時の結果が1つあります。
区間方式より前の同意モデル（`send_consent_windows` ができる前）で出力したパッケージは、記録済みの最初の同意期間より前にあるため、アップグレード後に送信されることはありません。
これは閉じる側に倒した結果です。
それらを送るために古い動く日付の印を持ち込み直すと、5回のレビューで不健全だとわかった意味合いを持ち込み直すことになります。
失うのはせいぜい未配送分だけで、収集したデータを失うことはありません。

### A.5 保持期間 {#a5-retention}

- **ローカル:** 変更なしです。出力に成功した履歴は30日間保持し、未処理の差分は
  出力するまで保持します。送信の状態でローカルの保持期間が **延びることはありません**。
  送信できなかったパッケージも30日で削除します。
  恒久的に到達できない送り先に向けてローカルが際限なく膨らむほうが、1か月壊れたままのインストールのメトリクスを失うより悪い失敗だからです。
- **リモート:** 生のパッケージは、本番では期限なく、ステージングでは
  30日間、S3 に保持します。

### A.6 削除 {#a6-deletion}

v1 の取り決めにはリモートでの削除の経路が無く、この付録もそれを新たに作ることはしません。
利用者ができることは次のとおりです。

| 操作 | 効果 |
|---|---|
| `send: false` | それ以上のパッケージはマシンの外へ出ません |
| `enabled: false` | 収集が止まります。既存のローカルの状態は残ります |
| `.../shared_metrics` を削除 | ローカルの識別子、集計、ファイルがリセットされます。以後の送信は新しい install_id を使います |
| 送信済みのデータを削除 | 利用者自身ではできません。運用者が S3 バケットを操作する必要があります |

要求に応じた削除の義務を負うことになった場合でも、今は探す経路が直接的です。
利用者の `install_id`（ローカルのストアから読めます）が、その利用者のデータが保存されているキーです。
サービス側の削除 API を作るかどうかは、実装の細部ではなく、新しい製品判断のままです。

### A.7 outbox ディレクトリとは何か {#a7-what-the-outbox-directory-is}

フェーズ2の計画中に一度誤読され、それが利用者のデータを削除しかねない形だったので、記録しておきます。

このディレクトリは **ローカルの履歴であって、送信キューではありません**。
`package_outbox` は SQLite のテーブルで、その `exported_at` 列の意味は「ディスクに書き込んだ」であり、「送信した」ではありません。
ファイルは変更不可で、**経過日数だけ** を基準に削除します。

取り込みの取り決めでは、送信側は `202` を受け取ったらパッケージを outbox から削除すべきとされています。
**エクスポーターはこれを行いません。**
受領確認で削除すると、利用者の30日分のローカル履歴を送信キューに流用することになり、保持すると約束した状態を壊してしまいます。
代わりに、送信の状態は `package_outbox` テーブルの新しい列に持たせており、ファイルは送信によって一切変更されません。

### A.8 範囲についての注記 {#a8-scope-note}

パッケージ本文の中の `install_id` フィールドは、生成した側が書いたとおりに送信します（同じ値を記録している行の固定済みの `sent_install_id` から書き直します）。
ほかのペイロードのフィールドは変わらず、何も追加されず、サービスは本文全体を中身を見ないデータとして扱います。
そのため、ペイロードのスキーマの変更は、これまでどおり送信側の関心事のままです。
