199 lines
11 KiB
Markdown
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.
|