yoi/tickets/session-metrics.md
2026-05-03 15:10:43 +09:00

80 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# セッションメトリクス: Extension 経由の汎用計測レーン
## 背景
セッション中の挙動を後から定量評価したい需要が増えている。直近の動機は Prune projection の効果測定どこでどれくらいの頻度で発火したか、KV キャッシュ無効化のコストに対して回収トークンがどれだけあったかだが、Compact 起動条件、Hook の実行時間、ツール呼び出しのリトライ回数など、同種の「セッション中に積み上げて後で引きたい」値は今後も発生する。
現状の session-log は `UserInput` / `AssistantItems` / `LlmUsage` といった状態遷移を典型化した variant のみで、ad hoc な計測値の置き場所がない。一方 `LogEntry::Extension { domain, payload }` という汎用エスケープハッチが既に用意されており、ここに乗せれば hash chain・replay 経路を流用できる。
## 方針
- `LogEntry::Extension``domain = "metrics"` 名前空間として実装する。session-store の型は触らない
- メトリクス型は `name + dimensions(sparse map) + value(Option) + correlation_id(Option)` 程度の最小スキーマ。値が測れない次元は `None` で明示し、Prometheus 的な厳格な label set は持たない
- 「後から埋まる値」(例: prune 発火直後の LLM 呼び出しで観測される `cache_read_tokens`)は前 entry に書き戻さず、`correlation_id` を共有する別 metric として流す。集計は読み手側で join
- 集計 / 可視化 API は本チケットでは作らない。session-log を読めば取り出せる、までを到達点とする
## 要件
### メトリクス型
- 専用 crateまたは既存の適切な配置に定義。`serde` で JSON ラウンドトリップ可能
- 必須: `name``namespace.metric` 形式の文字列、例: `prune.fire`)、`ts`u64 epoch ms
- 任意: `dimensions: BTreeMap<String, String>`、`value: Option<f64>`、`correlation_id: Option<String>`
- 「unknown」は対応フィールドを `None` にすることで表現。schema レベルで dimension の網羅性は要求しない
### 書き込み経路
- `Pod` から呼べる薄いヘルパー(例: `Pod::record_metric(&self, metric)`) を session-store に追加し、`LogEntry::Extension { domain: "metrics", payload: serde_json::to_value(metric) }` として append する
- 既存の `append_entry` フローを踏襲し、hash chain に乗る
- 書き込み失敗store IO エラー)はメトリクス側で握りつぶす。本体処理を阻害しない
### 読み出し経路
- replay 時、`RestoredState.extensions` に `("metrics", payload)` として既に積まれる(既存挙動)
- メトリクスドメイン側で payload を `Vec<Metric>` に fold するヘルパーを提供
- session-store のテストハーネスから「特定セッションの metric 列を取り出す」サンプルが書ける状態にする
### 最初の利用者: Prune projection
本チケットの完了は、最低 1 つの実利用者が乗っていることを条件とする。Prune を最初の利用者として組み込む:
- `pod::compact::prune``attach_prune` 経路で、projection 評価のたびに以下を発行
- 発火時: `name = "prune.fire"`, `dimensions = { border_turn, candidate_count }`, `value = estimated_savings`, `correlation_id = <次の LLM 呼び出しと紐付ける ID>`
- スキップ時: `name = "prune.skip"`, `dimensions = { reason }``below_min_savings` / `no_candidates` 等)
- 直後の LLM リクエストで `LlmUsage` が記録される際、同じ `correlation_id` を持つ補助 metric `prune.post_request` を併発し、`cache_read_tokens` / `cache_write_tokens` を value/dimension として記録
- `correlation_id` の生成・伝搬経路は実装側で決定。既存の request-id 系があれば再利用
### Resume 互換
- 旧セッションmetric entry を持たないログ)の replay は何も変えない。`extensions` に metrics domain が無いだけ
- payload schema が将来変わった場合、deserialize 失敗した metric は無視してよいfold ヘルパー側で `serde_json::from_value` の Err を skip
## 完了条件
- メトリクス型と書き込み / 読み出しヘルパーが定義され、unit test がある
- Prune projection から `prune.fire` / `prune.skip` / `prune.post_request` が session-log に乗る
- 既存セッションログの replay が壊れない(後方互換)
- セッションログから prune metric 列を取り出すテストが通る
- correlation_id で prune 発火と直後の LLM 呼び出しの cache 値が join できることを test で示す
## 範囲外
- 集計 / 可視化ツールCLI / TUI。後続で別途
- ワークスペースまたぎのメトリクス集約(複数セッション横断分析)
- リアルタイム購読 APIWatcher 経由の stream 配信)
- session-log 以外の sinkjsonl 別系統、外部時系列 DB 等)
- Prune 以外のメトリクス利用者の追加Compact / Hook 等は別チケット)
- メトリクス保存量の自動圧縮 / 退避
## 参照
- 設計指針: `CLAUDE.md`(最小の構造化 / 概念の追加は不在が問題になってから)
- `crates/session-store/src/session_log.rs``LogEntry::Extension` と `RestoredState.extensions` の既存仕様)
- `crates/llm-worker/src/usage_record.rs`、`crates/llm-worker/src/llm_client/event.rs`cache_read / cache_write の取得経路)
- `crates/pod/src/compact/prune.rs`、`crates/llm-worker/src/prune.rs`(最初の利用者の挿入点)
## Review
- 状態: Approve with follow-up
- レビュー詳細: [./session-metrics.review.md](./session-metrics.review.md)
- 日付: 2026-05-03