diff --git a/docs/superpowers/specs/2026-08-24-tokentracker-usage-analytics-design.md b/docs/superpowers/specs/2026-08-24-tokentracker-usage-analytics-design.md new file mode 100644 index 00000000..5908c8ae --- /dev/null +++ b/docs/superpowers/specs/2026-08-24-tokentracker-usage-analytics-design.md @@ -0,0 +1,198 @@ +# 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.