# 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.
