Files
ClaudeDo/docs/superpowers/specs/2026-08-24-tokentracker-usage-analytics-design.md
T

199 lines
11 KiB
Markdown

# TokenTracker als Auswertungs-Backend für die Token-Analytics
**Datum:** 2026-08-24
**Status:** freigegeben, Implementierung offen
## Problem
ClaudeDo pflegt zwei eigene Token-Datenpfade:
1. `ClaudeOAuthUsageClient` (170 Zeilen) — pollt `https://api.anthropic.com/api/oauth/usage`
für die 5h-/7d-Limits. Speist Pill, Gauges, `UsageGate` und `UsageThrottle`.
2. `TranscriptUsageReader` (161 Zeilen) — parst `~/.claude/projects/**/*.jsonl` und aggregiert
nach Datum/Modell/Scope für die Analytics-Ansicht im Usage-Monitor.
Pfad 2 ist der teure und undankbare Teil: ein Voll-Scan über die Transkript-Historie (gemessen
2026-08-06: 501 Dateien / 230 MB, ein 7-Tage-Range berührt ~190 Dateien / ~106 MB), eigener
mtime/Length-Cache, `requestId`-Dedupe, `<synthetic>`-Filter — und trotzdem ohne Kosten in USD,
weil wir keine Preistabelle pflegen.
[TokenTracker](https://github.com/xiufengsun/TokenTracker) (MIT, Node 20+) macht genau diese
Auswertung als Kernprodukt, inklusive gepflegter Preistabelle. Ziel: die **Auswertung** abgeben,
die **Darstellung** in ClaudeDo behalten.
## Nicht-Ziel: Limits, Gate, Throttle
Die 5h-/7d-Limits bleiben bei unserem eigenen OAuth-Poll. Begründung (gegen den installierten
TokenTracker verifiziert, 2026-08-24):
- `~/.tokentracker/tracker/claude-usage-limits-cache.json` hat **exakt die Shape unserer
OAuth-Antwort** (`five_hour` / `seven_day` / `seven_day_opus` / `weekly_scoped` /
`extra_usage`) — dieselbe Quelle, kein inhaltlicher Gewinn.
- Der Cache war zum Prüfzeitpunkt **13 Tage alt** (`cached_at: 2026-08-11`), während die
Token-Queue desselben Tools von heute war. Ein Gate, das über das Starten von Tasks
entscheidet, darf nicht an einem Cache hängen, der still veraltet.
`UsageState`, `UsageMonitorService`, `UsageSnapshotBuilder`, `UsageGate`, `UsageThrottle`,
`UsagePillViewModel` und die Gauges bleiben unangetastet.
## Datenquelle
`tokentracker sessions --from YYYY-MM-DD --to YYYY-MM-DD --no-git --format json`
Dokumentiert in `tokentracker --help`. Liefert pro Session:
```json
{
"session_hash": "51d4de2afadecd97f9a600ad",
"source": "claude",
"project_key": "ClaudeDo",
"model": "claude-opus-5",
"started_at": "2026-08-24T07:38:03.653Z",
"ended_at": "2026-08-24T07:48:04.728Z",
"turns": 1, "edit_turns": 0, "retry_turns": 0,
"tokens": { "input_tokens": 32, "cached_input_tokens": 1063433,
"cache_creation_input_tokens": 67524, "output_tokens": 14048,
"reasoning_output_tokens": 0, "total_tokens": 1145037 },
"total_tokens": 1145037,
"cost_usd": 1.3051015,
"productive": false, "first_pass": false, "one_shot": false,
"provenance": { "source": "local-session-log", "content_retained": false },
"version": 11
}
```
Dazu fertige Aggregate `totals` und `by_model` (die wir nicht verwenden, siehe *Caching*).
### Verifizierte Eigenschaften
| Behauptung | Befund |
|---|---|
| Rein lokal | `buildSessionAnalytics` liest `~/.claude/projects/**` direkt (`lib/session-analytics.js:976-979`). Kein Account, kein Netzwerk, keine Hooks nötig. |
| Kein `init` erforderlich | Der Export funktioniert mit dem bloßen npm-Paket. |
| Join auf unsere Runs möglich | `session_hash = sha256("claude\0" + sessionId).hex[0..24]` (`lib/session-analytics.js:88`), deterministisch in .NET nachrechenbar. |
| Worktree-Sessions enthalten | Drei ClaudeDo-Worktree-Sessions aus `~/.claude/projects/…claudedo-worktrees-…` per Hash im Export gefunden (2026-08-24). Der Kommentar auf `:149` über „throwaway per-agent checkouts" betrifft nur die Projekt-Attribution, nicht das Verwerfen von Sessions. |
| `project_key` ist für uns unbrauchbar | Bei Worktrees fällt er teils auf die nackte Worktree-GUID zurück (`11da6ebb-…` u. a. im Export). Deshalb joinen wir über `session_hash`, nie über `project_key`. |
| Laufzeit | **11,5 s** für einen 1-Tages-Range (gecacht). `buildSessionAnalytics` läuft immer über die gesamte Historie; `--from`/`--to` filtern erst in `summarizeSessions`. Also kein Performance-Gewinn gegenüber unserem Reader (1,7 s kalt) — der Gewinn liegt bei Kosten, Effizienzmetriken und entfallender Pflege. |
### Warum kein `tokentracker init`
`init` schreibt `Stop`- und `SessionEnd`-Hooks in die **globale** `~/.claude/settings.json` und
aktiviert Cloud-Sync (`cloud-sync-pref.enabled: true`) zu `srctyff5.us-east.insforge.app`. Der
Upload enthält u. a. `project_ref` — auf dieser Maschine `https://git.kuns.dev/releases/ClaudeDo`,
also einen internen Hostnamen. Das ist eine zu weitgehende Nebenwirkung, um sie hinter einen
Button in ClaudeDo zu legen. ClaudeDo ruft ausschließlich `sessions` auf und nie `init`.
Wer Dashboard und Cloud-Sync will, richtet das selbst ein.
## Architektur
Neuer Ordner `src/ClaudeDo.Worker/Usage/TokenTracker/`:
| Typ | Rolle |
|---|---|
| `ITokenTrackerClient` (in `Interfaces/`) | `Task<TokenTrackerExport?> ExportAsync(DateOnly from, DateOnly to, CancellationToken)` + `Task<TokenTrackerProbe> ProbeAsync(CancellationToken)` |
| `TokenTrackerClient` | Startet den Prozess, liest stdout, parst JSON. Timeout 120 s, danach Prozessbaum killen. Auflösung der Binary über `ExecutableResolver` (`ClaudeDo.Data`) wie beim `claude`-CLI, inklusive `.cmd`-Shim-Start über `cmd.exe /c`. **Nie `npx`** — das würde still nachladen. |
| `TokenTrackerProbe` | `(bool Installed, string? Version, bool NodeOk, string? NodeVersion, string? Error)`. `tokentracker -v` und `node --version`; Ergebnis gecacht bis zu einem expliziten Re-Probe. |
| `TokenTrackerState` | Threadsafe Singleton nach Vorbild von `UsageState`: letztes gutes `TokenTrackerExport`, `FetchedAtUtc`, `LastError`. **Ein Fehler überschreibt nie ein gutes Snapshot.** |
| `TokenTrackerModels` | `TokenTrackerExport`, `TokenTrackerSession`, `TokenTrackerProbe`. |
| `TokenTrackerAggregator` | Pure statisch: filtert `sessions[]` auf einen Range und aggregiert nach Modell bzw. nach Task. Keine I/O, voll testbar. |
| `TokenTrackerInstaller` | Führt `npm i -g tokentracker-cli` aus und streamt stdout/stderr zeilenweise an den Aufrufer; danach Re-Probe. |
### Caching und Refresh
- Ein Fetch holt ein **90-Tage-Fenster** und wird gecacht. Alle Ansichten (7d/30d/custom) werden
lokal aus `sessions[]` gefiltert und aggregiert — daher verwenden wir `totals`/`by_model` aus dem
Export nicht, sondern rechnen selbst aus den Session-Zeilen. Range-Wechsel im Modal sind damit
instant, die 11,5 s fallen nur beim Refresh an.
- Refresh-Auslöser: Modal-Open, wenn das Snapshot älter als 15 min ist (im Hintergrund, nie
UI-blockierend), plus ein manueller Refresh-Button. Die UI zeigt „Stand: hh:mm".
- Ist gar kein Snapshot da, zeigt der Analytics-Block einen Ladezustand, nie leere Nullwerte.
### Versionsschutz
`sessions` ist kein zugesagter API-Vertrag. Wir prüfen das `version`-Feld der Session-Zeilen
(aktuell `11`). Bei unbekannter Version zeigt der Analytics-Block einen Klartext-Hinweis
(„Auswertung nicht verfügbar: unerwartetes TokenTracker-Format vX") statt Zahlen zu erfinden.
Fehlende Felder werden defensiv als `null`/`0` behandelt, ein Parse-Fehler landet in
`LastError` und lässt das letzte gute Snapshot stehen.
## Hub-Oberfläche
- **`GetModelUsage(from, to)`** behält Namen und Aufrufer, speist sich aber aus
`TokenTrackerState`. `ModelUsageRowDto` bekommt ein zusätzliches `double? CostUsd`.
Der Scope-Split `claudedo` / `other` kommt jetzt aus der **`session_hash`-Mitgliedschaft in
`task_runs`** statt aus der Pfad-Heuristik des alten Readers — das ist genauer, weil
Worktree-Pfade und die GUID-Fallbacks von `project_key` keine Rolle mehr spielen.
- **`GetTaskUsage(from, to)`** bleibt `task_runs`-basiert (sofort verfügbar, funktioniert ohne
TokenTracker). `TaskUsageRowDto` wird um `double? CostUsd`, `int? Retries`, `bool? Productive`,
`bool? OneShot` erweitert. Gefüllt werden diese Felder über die `SessionId` der Runs des Tasks →
Hash → passende TokenTracker-Session; ohne Snapshot bleiben sie `null`.
- **Neu `GetTokenTrackerStatus()`** → `TokenTrackerStatusDto(bool Installed, string? Version,
bool NodeOk, string? NodeVersion, DateTime? LastFetchedUtc, string? LastError,
int? FormatVersion)`.
- **Neu `RefreshTokenTrackerAsync()`** → erzwingt einen Fetch (für den Refresh-Button).
- **Neu `InstallTokenTrackerAsync()`** → startet `npm i -g tokentracker-cli`; Fortschritt läuft
über den bestehenden `OperationProgress`-Kanal, Ausgabezeilen über `WorkerLog`.
`IWorkerClient` und `WorkerClient` in `ClaudeDo.Ui` spiegeln alles.
## UI
Nur der Analytics-Teil des Usage-Monitors ändert sich; Gauges, Legenden, Drag-Marker und die
Pill bleiben wie sie sind.
**Mit TokenTracker:**
- Kopfzeile des Analytics-Blocks: Total Tokens **und Total Cost (USD)** für den Range,
plus „Stand: hh:mm" und Refresh-Button.
- Breakdown pro Modell: Tokens (In/Out/Cache) + USD, weiterhin mit ClaudeDo-/Other-Split.
- Breakdown pro Task: bestehende Spalten + USD + Retries + Effizienz-Marker
(`productive` / `one_shot`).
**Ohne TokenTracker** steht an derselben Stelle eine Hinweis-Karte:
> Die Token-Auswertung übernimmt TokenTracker (MIT, läuft rein lokal, liest keine Prompts).
> ClaudeDo ruft nur den lokalen Export auf und aktiviert **kein** `init`, keine Hooks und keinen
> Cloud-Sync.
mit Button **TokenTracker installieren** → `npm i -g tokentracker-cli`. Die Ausgabezeilen laufen
über `WorkerLog` in die Footer-Leiste, die Karte selbst zeigt die jeweils letzte Zeile als
Statuszeile; danach automatischer Re-Probe. Fehlt Node 20+, zeigt die Karte statt des Buttons den
Node-Hinweis samt Versionsangabe. Die Task-Tabelle bleibt in diesem Zustand funktionsfähig, nur
ohne die USD-/Effizienz-Spalten. Fehler beim Installieren gehen über `FlashFooterError`, nicht in
ein `catch {}`.
## Was entfällt
- `TranscriptUsageReader.ReadAsync` samt Datums-/Modell-/Scope-Aggregation, mtime/Length-Cache,
`requestId`-Dedupe und dem 106-MB-Scan (~120 der 161 Zeilen).
- `ITranscriptUsageReader` schrumpft auf `ReadSessionTotalsAsync`.
- `UsageAggregateRow` / `UsageScope`, falls nach dem Umbau ohne Konsumenten.
`ReadSessionTotalsAsync` **bleibt**: das Per-Run-Accounting in `TaskRunner.ApplyUsageAsync`
schreibt `task_runs.tokens_*` direkt am Ende eines Runs und darf nicht auf einen 11 s
dauernden Batch-Export warten.
## Tests
- Ein anonymisiertes, eingechecktes Export-Fixture (JSON) für Parser und `TokenTrackerAggregator`:
Range-Filter, Modell-Aggregation, Task-Aggregation, Kostensummen.
- `session_hash`-Test: bekannte Session-ID → erwarteter Hash (Gegenprobe zu
`sha256("claude\0" + id)[0..24]`).
- Join-Test: `task_runs`-Zeilen mit Session-IDs + Fixture → erwartete USD/Effizienz pro Task.
- Fail-open-Tests: Binary fehlt, Exit-Code ≠ 0, Timeout, kaputtes JSON, unbekannte
`version` — jeweils letztes gutes Snapshot bleibt stehen, `LastError` gesetzt.
- **Kein Test ruft das echte CLI auf** (analog zur Regel für den `claude`-CLI). `ITokenTrackerClient`
wird gefakt.
- Fakes in `Worker.Tests` und `Ui.Tests` nachziehen: `IWorkerClient`, `WorkerHub` und die
erweiterten DTOs brechen die handgeschriebenen Fakes in beiden Projekten.
## Offene Verifikationspunkte
- Visueller Pass für den umgebauten Analytics-Block und die Hinweis-Karte (beide Zustände:
installiert / nicht installiert) — muss der Nutzer prüfen.
- Einmal echt gegen ein System ohne TokenTracker: Probe, Hinweis-Karte, Install-Button,
Re-Probe, erste Auswertung.
- Nach dem Umbau `docs/explore-notes/usage-monitoring.md` fortschreiben (Abschnitte
*Per-run token accounting*, *UI surfaces*, *Hub surface*) und den „verified against"-Commit
anheben.