# 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, ``-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 ExportAsync(DateOnly from, DateOnly to, CancellationToken)` + `Task 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.