Automatischer Push: 46 Datei(en) geändert
- .claude/agents/planer.md - .claude/settings.json - .claude/workflows/executer-welle.js - .claude/workflows/plankritik.js - .claude/workflows/planung.js - .claude/workflows/quorum.js - .claude/workflows/verbrauch.js - .claude/workflows/welle-mit-pruefung.js - .vault-tools/shell-wache/ENTWURF.md - .vault-tools/shell-wache/docs/code/shell_wache.md - .vault-tools/shell-wache/docs/plans/2026-10-30-shell-wache/00-uebersicht.md - .vault-tools/shell-wache/docs/plans/2026-10-30-shell-wache/task-01-shell-wache.md - … und 34 weitere Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
f2ccec39d6
commit
13213ea0ac
46 files changed
+2380
-48
No files matched your search
@@ -0,0 +1,48 @@
|
||||
---
|
||||
type: resource
|
||||
status: active
|
||||
title: "Verbrauchsauswertung — Hintergrund"
|
||||
summary: "Warum analyse_transkripte.py je Nachricht den größten usage-Wert nimmt, über Dateien dedupliziert, Haiku 5.5 gestuft rechnet, nie in den Vault schreibt, und was die Zahlen nicht aussagen."
|
||||
tags: [vault-tools, codeworkflow]
|
||||
created: 2026-10-30
|
||||
updated: 2026-10-30
|
||||
---
|
||||
|
||||
# Verbrauchsauswertung — Hintergrund
|
||||
|
||||
Zur Anleitung: [LIESMICH](LIESMICH.md)
|
||||
|
||||
## Warum die drei Zählregeln
|
||||
|
||||
- **Größter Wert je Nachricht:** Claude Code schreibt je Antwort eine Zeile pro Inhaltsblock, alle mit derselben `message.id` und je einem `usage`-Block. In Subagenten-Transkripten bis Version 2.1.304 trägt die erste Zeile nur eine Teilzahl der `output_tokens`. Wer sie behält, unterzählt die Ausgabe; im Workflow-Review vom 2026-10-30 waren es 56.484 Nachrichten und rund 2.050 USD Listenpreis.
|
||||
- **Größter statt letzter Wert:** In den gesehenen Transkripten ist der größte zugleich der letzte. Der größte Wert hängt aber nicht von der Zeilenfolge ab; der Fall `teilausgabe-letzte-zeile` hält das fest.
|
||||
- **Deduplizierung über Dateien:** Eine fortgesetzte oder geklonte Sitzung kann alle Nachrichten ihrer Vorgängerin enthalten. Gezählt wird die Nachricht in der ersten Datei in Sortierreihenfolge; die Sitzung der Kopie verliert sie. Je Sitzung stimmen die Zahlen also nur für die Erstfassung.
|
||||
- **Haiku 5.5 über 100k Prompt:** Diese Stufe kostet das Fünffache. Genau 100.000 Prompt-Tokens liegen noch in der Grundstufe (Fall `haiku-grenze-100k`).
|
||||
|
||||
## Warum nie in den Vault
|
||||
|
||||
- **Sitzungstitel tragen Kunden- und Projektnamen.** Die Auswertung schreibt sie in `auswertung.md` und `auswertung.json`.
|
||||
- **`.vault-tools/` wird täglich veröffentlicht.** Eine Ausgabe neben dem Skript wäre beim nächsten Push um 04:00 draußen; andere Vault-Ordner liegen in eigenen Git-Repos. Das Skript lehnt deshalb jeden Ordner unter der Vault-Wurzel ab, die es aus dem eigenen Pfad bestimmt.
|
||||
|
||||
## Was die Zahlen nicht aussagen
|
||||
|
||||
- **Keine Planpunkte.** Die USD-Werte sind Listenpreis-Äquivalente nach der Tabelle `PREISE`. Wie der Max-Plan das 5-Stunden-Fenster und die Woche zählt, ist nicht veröffentlicht. Für Prozentpunkte des Plans gibt es nur die Ablesungen über `get_usage`, ausgewertet mit `.vault-tools/workflow-pruefung/verbrauch_reihe.py`.
|
||||
- **`output_tokens` als abgerechneter Wert ist angenommen.** Ebenso, dass Thinking darin steckt. Beides ist nicht belegt.
|
||||
- **`PREISE` ist abgeschrieben, nicht geprüft.** Quelle ist die Preistabelle des Workflow-Reviews vom 2026-10-30. Ein unbekanntes Modell rechnet das Skript mit Opus-5.5-Preisen und führt es als `unbekannt:<name>`.
|
||||
- **Cache-Schreiben ohne Aufteilung:** Ältere Transkripte ohne `ephemeral_5m`/`ephemeral_1h` zählen ganz als 5-min-Schreiben, also billiger als womöglich abgerechnet.
|
||||
- **Gelöschte Transkripte fehlen.** Am 2026-10-30 wurden Sitzungen mit letzter Aktivität vor dem 2026-09-30 gelöscht (Aufräumstufe C1 des Reviews). Zahlen vor diesem Tag sind seitdem unvollständig und mit dem Review-Bericht nicht vergleichbar.
|
||||
- **Dauer in `auswahl.md`** ist erste bis letzte Runde der Hauptsitzung, Pausen eingeschlossen. Für die Arbeitszeit taugt sie nur bei Sitzungen ohne lange Unterbrechung.
|
||||
- **Andere Konten, Rechner und Codex** sieht das Skript nicht.
|
||||
|
||||
## Was die Fälle belegen und was nicht
|
||||
|
||||
- **Belegt:** die drei Zählregeln je an zwei Positionen, die Haiku-Grenze, die Vault-Sperre, der fehlende projects-Ordner, Zeilen, die kein JSON-Objekt sind, und die Auswahl nach Titel.
|
||||
- **Rotnachweis:** Je Fall eine Mutante, die genau die geprüfte Stelle bricht; alle zehn werden gefangen. Die Kopie der Mutante bekommt den echten Vault-Pfad, sonst lehnte sie jede Ausgabe im Temp ab und jede Mutante wäre scheinbar gefangen.
|
||||
- **Ein echter Fund beim Ablegen:** Fassung 2 aus dem Review brach den ganzen Lauf mit `AttributeError` ab, sobald eine Zeile gültiges JSON, aber kein Objekt ist (etwa `[1]`; gegen die Fixture `kaputte-zeilen` nachgestellt am 2026-10-30). Jetzt wird die Zeile übersprungen, und eine Datei, die trotzdem scheitert, steht als Hinweis im Bericht, statt den Lauf zu beenden.
|
||||
- **Nicht belegt:** die Berichtstabellen über die abgefragten Zahlen hinaus, die Preise selbst, `claudedata_wurzel.py` und `scratchpads.py`. Ein Fangnetz, kein Nachweis.
|
||||
|
||||
## Quellen
|
||||
|
||||
- Harness-Erinnerung `transkript-usage-groesster-wert-je-nachricht` (Messfallen, Belege aus dem Review).
|
||||
- Dump des Workflow-Reviews: `AI/secondbrain/regelwerk-memory/2026-10-30-workflow-review/00-uebersicht.md`.
|
||||
- Regeln für Kopf und Fälle: [[AI/Regelwerk/pruefwerkzeuge|pruefwerkzeuge.md]], [[AI/Regelwerk/codedoku|codedoku.md]].
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
type: resource
|
||||
status: active
|
||||
title: "Verbrauchsauswertung: Tokens und Listenpreis aus den Claude-Code-Transkripten"
|
||||
summary: "analyse_transkripte.py zählt je Nachricht Tokens, Modell, Effort und Listenpreis über alle Transkripte eines projects-Ordners, mit Auswahl einzelner Sitzungen für Effort-Messungen; dazu zwei Hilfsskripte für die Ablageorte unter D:\\ClaudeData. Entstanden 2026-10-30 im Workflow-Review."
|
||||
tags: [vault-tools, codeworkflow]
|
||||
created: 2026-10-30
|
||||
updated: 2026-10-30
|
||||
---
|
||||
|
||||
# Verbrauchsauswertung — Anleitung
|
||||
|
||||
Zählt, wo Tokens und Listenpreis hingehen: je Modell, Effort, Woche, Sitzung, Subagent und Werkzeug. Warum so: [Hintergrund](LIESMICH-hintergrund.md)
|
||||
|
||||
**Voraussetzungen**
|
||||
|
||||
- Python ab 3.12 unter dem Befehl `python`.
|
||||
- Lesezugriff auf den projects-Ordner von Claude Code, auf dieser Maschine `D:\ClaudeData\config\projects`.
|
||||
- Ein Ausgabeordner **außerhalb des Vaults**, etwa das Scratchpad der Sitzung unter `D:\ClaudeData\claude\D--Claude\<sitzung>\scratchpad`.
|
||||
|
||||
**Schritte**
|
||||
|
||||
1. Fälle fahren, bevor du Zahlen verwendest:
|
||||
|
||||
```bash
|
||||
# Fixture und Mutanten, rund 5 Sekunden
|
||||
python -I -X utf8 .vault-tools/verbrauch-auswertung/faelle.py
|
||||
```
|
||||
|
||||
2. Auswertung starten:
|
||||
|
||||
```bash
|
||||
# Gesamtauswertung ab dem Stichtag, Ausgabe ins Scratchpad
|
||||
python -I -X utf8 .vault-tools/verbrauch-auswertung/analyse_transkripte.py D:/ClaudeData/config/projects --ausgabe "D:/ClaudeData/claude/D--Claude/<sitzung>/scratchpad/verbrauch" --seit 2026-10-01
|
||||
```
|
||||
|
||||
⚠ Ein Ausgabeordner im Vault bricht mit Exit 2 ab. Das ist Absicht.
|
||||
|
||||
3. Für eine Messung einzelne Sitzungen auswählen:
|
||||
|
||||
```bash
|
||||
# Sitzungen, deren Titel, Slug oder Kennung den Ausdruck trifft; schreibt zusätzlich auswahl.md
|
||||
python -I -X utf8 .vault-tools/verbrauch-auswertung/analyse_transkripte.py D:/ClaudeData/config/projects --ausgabe "<scratchpad>/verbrauch" --seit 2026-10-30 --auswahl "paket 1|paket 2"
|
||||
```
|
||||
|
||||
⚠ Die Kennung ist die der Claude-Code-Sitzung (Dateiname des Transkripts), nicht die `local_…`-Kennung der App. Sicher zuordnen kannst du nur über den Titel.
|
||||
|
||||
4. Ablageorte ansehen, wenn es ums Aufräumen geht:
|
||||
|
||||
```bash
|
||||
# Zufallsordner unter D:\ClaudeData nach Alter und Größe
|
||||
python -I -X utf8 .vault-tools/verbrauch-auswertung/claudedata_wurzel.py D:/ClaudeData
|
||||
# Scratchpads ab 50 MB, laufende Sitzungen als Kennungsanfänge angeben
|
||||
python -I -X utf8 .vault-tools/verbrauch-auswertung/scratchpads.py --laufend 370f5095,13a29464
|
||||
```
|
||||
|
||||
⚠ Beide Skripte löschen nichts und wissen nicht, welche Sitzung läuft. Das liest du in der App nach.
|
||||
|
||||
**Prüfen**
|
||||
|
||||
- `faelle.py` endet mit „11 Fälle grün; 10 von 10 Mutanten gefangen“ und Exit 0.
|
||||
- Die erste Zeile der Auswertung nennt Dateien, Zeilen, Sekunden, Kopien und Nachrichten mit Teilausgabe.
|
||||
- `auswertung.md` nennt unter dem Kopf die Teilausgaben nach Claude-Code-Version. Tauchen dort Versionen ab 2.1.308 auf, stimmt die Annahme zur Teilausgabe nicht mehr; dann erst nachsehen, dann Zahlen weitergeben.
|
||||
- In `auswertung.md` stehen Sitzungstitel. Kopierst du Tabellen in den Vault, prüfst du sie vorher auf Kundennamen.
|
||||
|
||||
## Ausgabe
|
||||
|
||||
| Datei | Inhalt |
|
||||
|---|---|
|
||||
| `auswertung.md` | Tabellen je Modell, Effort, Woche, teuerste Sitzungen, Kontextgröße, Subagenten, Werkzeuge, Lesezugriffe |
|
||||
| `auswertung.json` | dieselben Summen plus je Sitzung und Subagent ein Datensatz, zum Weiterrechnen |
|
||||
| `auswahl.md` | nur mit `--auswahl`: je getroffene Hauptsitzung Effort, Runden, Dauer, Ausgabe je Runde, USD; darunter ihre Subagenten nach Typ und Effort |
|
||||
|
||||
## Fassungen
|
||||
|
||||
| Fassung | Datum | Änderung |
|
||||
|---|---|---|
|
||||
| 2 | 2026-10-30 | Größter usage-Wert je Nachricht, Deduplizierung über Dateien, Haiku-5.5-Stufe über 100k. Als Werkzeug abgelegt mit `--ausgabe`, Vault-Sperre, `--seit`, `--auswahl`, Teilausgabe nach Version, Typprüfung je Zeile. |
|
||||
|
||||
## Dateien
|
||||
|
||||
| Datei | Zweck |
|
||||
|---|---|
|
||||
| `analyse_transkripte.py` | die Auswertung |
|
||||
| `faelle.py` | Regressionstreiber: Fälle unter `fixture/` und Mutanten |
|
||||
| `fixture/` | je Fall ein synthetischer projects-Ordner mit zwei bis drei JSONL-Zeilen |
|
||||
| `claudedata_wurzel.py` | Einträge unter `D:\ClaudeData` nach bekannt und zufällig benannt |
|
||||
| `scratchpads.py` | Sitzungs-Scratchpads mit Größe, Titel und Alter |
|
||||
@@ -0,0 +1,601 @@
|
||||
"""Zählt Tokenverbrauch und Listenpreis aus den Claude-Code-Transkripten eines projects-Ordners.
|
||||
|
||||
Ersteller: Pascal Michel
|
||||
Erstellt: 2026-10-30
|
||||
Doku: LIESMICH.md in diesem Ordner · Fassung und Änderungen: dort unter „Fassungen"
|
||||
|
||||
Was es misst: je Nachricht (message.id) die usage-Felder input_tokens, cache_creation_input_tokens
|
||||
(getrennt nach 5 min und 1 h, wo das Transkript das trägt), cache_read_input_tokens und
|
||||
output_tokens, dazu Modell, Effort, Werkzeugaufrufe, Größe der Werkzeugergebnisse und Sitzungstitel.
|
||||
Es gibt keine Inhalte aus, nur Zahlen, Namen von Modellen, Werkzeugen und Sitzungstitel.
|
||||
|
||||
Drei Zählregeln, ohne die die Zahlen falsch werden (Harness-Erinnerung
|
||||
transkript-usage-groesster-wert-je-nachricht):
|
||||
- Je message.id der größte Wert jedes usage-Felds. Bis Claude Code 2.1.304 trägt in
|
||||
Subagenten-Transkripten die erste Zeile einer Nachricht nur eine Teilzahl der output_tokens.
|
||||
- message.id über alle Dateien dedupliziert. Fortgesetzte Sitzungen tragen die Nachrichten ihrer
|
||||
Vorgängerin; gezählt wird die Nachricht in der ersten Datei in Sortierreihenfolge.
|
||||
- Haiku 5.5 über 100k Prompt mit der höheren Preisstufe.
|
||||
|
||||
Was es nicht sieht:
|
||||
- Wie der Max-Plan zählt. Die Gewichte der Planprozente sind nicht veröffentlicht; die USD-Zahlen
|
||||
sind Listenpreis-Äquivalente nach PREISE und taugen nur zum Vergleich, nicht als Planpunkte.
|
||||
- Ob output_tokens der abgerechnete Wert ist. Angenommen, nicht belegt; Thinking steckt nach
|
||||
derselben Annahme darin.
|
||||
- Gelöschte Transkripte, Verbrauch anderer Konten und Rechner, Codex.
|
||||
- PREISE ist abgeschrieben aus der Preistabelle des Workflow-Reviews vom 2026-10-30, nicht aus einer
|
||||
Quelle, die das Skript prüft. Ein unbekanntes Modell rechnet es mit Opus-5.5-Preisen und weist es
|
||||
als „unbekannt:<name>" aus.
|
||||
|
||||
Ein Messwerkzeug, kein Nachweis: Die Fixture in faelle.py belegt nur die drei Zählregeln.
|
||||
Aufruf: python -I -X utf8 analyse_transkripte.py <projects-Ordner> --ausgabe <Ordner außerhalb des Vaults>
|
||||
[--seit JJJJ-MM-TT] [--auswahl REGEX]
|
||||
Exit 0 mit Auswertung, 2 bei fehlendem projects-Ordner, Ausgabe im Vault oder ungültigem Argument.
|
||||
"""
|
||||
import argparse
|
||||
import collections
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import statistics
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# $/MTok: Eingabe, Ausgabe, Cache-Lesen, Cache-Schreiben 5 min, Cache-Schreiben 1 h
|
||||
PREISE = {
|
||||
"claude-fable-5-1": (10, 50, 0.25, 12.5, 20),
|
||||
"claude-fable-5": (10, 50, 1.0, 12.5, 20),
|
||||
"claude-opus-5-5": (4, 20, 0.20, 5, 8),
|
||||
"claude-opus-5": (5, 25, 0.5, 6.25, 10),
|
||||
"claude-opus-4-8": (5, 25, 0.5, 6.25, 10),
|
||||
"claude-opus-4-7": (5, 25, 0.5, 6.25, 10),
|
||||
"claude-opus-4-6": (5, 25, 0.5, 6.25, 10),
|
||||
"claude-sonnet-5-5": (2, 10, 0.10, 2.5, 4),
|
||||
"claude-sonnet-5": (2, 10, 0.20, 2.5, 4),
|
||||
"claude-sonnet-4-6": (3, 15, 0.3, 3.75, 6),
|
||||
"claude-haiku-5-5": (0.10, 0.50, 0.01, 0.125, 0.2),
|
||||
"claude-haiku-4-5": (1, 5, 0.1, 1.25, 2),
|
||||
}
|
||||
HAIKU55_UEBER_100K = (0.50, 2.50, 0.05, 0.625, 1.0)
|
||||
# Längster Präfix zuerst, sonst fängt claude-opus-5 auch claude-opus-5-5 ab.
|
||||
ORDNUNG = sorted(PREISE, key=len, reverse=True)
|
||||
USAGE_FELDER = ("input_tokens", "cache_creation_input_tokens", "cache_read_input_tokens", "output_tokens")
|
||||
STUFEN = [(0, 100000, "0-100k"), (100000, 200000, "100-200k"), (200000, 400000, "200-400k"), (400000, 700000, "400-700k"), (700000, 10**9, "700k+")]
|
||||
VAULT = Path(__file__).resolve().parents[2]
|
||||
|
||||
ROOT = OUT = SEIT = None
|
||||
AUSWAHL = None
|
||||
|
||||
|
||||
def norm(m):
|
||||
m = m or "?"
|
||||
for k in ORDNUNG:
|
||||
if m.startswith(k):
|
||||
return k
|
||||
return "unbekannt:" + m
|
||||
|
||||
|
||||
def kosten(mk, u, cc, ctx):
|
||||
p = PREISE.get(mk, PREISE["claude-opus-5-5"])
|
||||
if mk == "claude-haiku-5-5" and ctx > 100000:
|
||||
p = HAIKU55_UEBER_100K
|
||||
c5 = cc.get("ephemeral_5m_input_tokens")
|
||||
c1 = cc.get("ephemeral_1h_input_tokens")
|
||||
# Ältere Transkripte ohne Aufteilung: alles als 5-min-Schreiben, das ist die billigere Annahme.
|
||||
if c5 is None and c1 is None:
|
||||
c5 = u.get("cache_creation_input_tokens") or 0
|
||||
c1 = 0
|
||||
return ((u.get("input_tokens") or 0) * p[0] + (u.get("output_tokens") or 0) * p[1]
|
||||
+ (u.get("cache_read_input_tokens") or 0) * p[2] + (c5 or 0) * p[3] + (c1 or 0) * p[4]) / 1e6
|
||||
|
||||
|
||||
def Z():
|
||||
return collections.Counter()
|
||||
|
||||
|
||||
agg = {
|
||||
"modell": collections.defaultdict(Z), # (scope, art, modell)
|
||||
"effort": collections.defaultdict(Z), # (scope, modell, effort) nur Hauptsitzungen
|
||||
"tag": collections.defaultdict(Z), # (datum, art)
|
||||
"tool": collections.defaultdict(Z), # name
|
||||
"stop": collections.Counter(),
|
||||
"versionen": collections.Counter(),
|
||||
"teilausgabe_version": collections.Counter(),
|
||||
"agent_tool": collections.Counter(), # (subagent_type, model)
|
||||
"workflow_calls": 0,
|
||||
"dateien": 0,
|
||||
"zeilen": 0,
|
||||
"doppelt_dateiuebergreifend": 0,
|
||||
"nachrichten_mit_teilausgabe": 0,
|
||||
"ctx_stufe": collections.defaultdict(Z), # (art, stufe) seit SEIT: Kosten je Runde nach Kontextgroesse
|
||||
"read_pfad": collections.defaultdict(Z), # Pfad: Aufrufe, Zeichen (seit SEIT)
|
||||
}
|
||||
GESEHEN = set() # Nachrichten-IDs ueber alle Dateien
|
||||
BEFUNDE = []
|
||||
sessions = {}
|
||||
subagents = []
|
||||
meta_keys = collections.Counter()
|
||||
|
||||
|
||||
def stufe(ctx):
|
||||
for a, b, n in STUFEN:
|
||||
if a <= ctx < b:
|
||||
return n
|
||||
return "?"
|
||||
|
||||
|
||||
def kurzpfad(p):
|
||||
p = str(p).replace("\\", "/")
|
||||
teile = p.split("/")
|
||||
return "/".join(teile[-3:]) if len(teile) > 3 else p
|
||||
|
||||
|
||||
def dateien():
|
||||
for proj in sorted(os.listdir(ROOT)):
|
||||
pd = os.path.join(ROOT, proj)
|
||||
if not os.path.isdir(pd):
|
||||
continue
|
||||
for root, dirs, files in os.walk(pd):
|
||||
dirs.sort()
|
||||
for f in sorted(files):
|
||||
if not f.endswith(".jsonl"):
|
||||
continue
|
||||
p = os.path.join(root, f)
|
||||
rel = os.path.relpath(p, pd).replace("\\", "/")
|
||||
parts = rel.split("/")
|
||||
if len(parts) == 1:
|
||||
yield proj, parts[0][:-6], None, p
|
||||
elif "subagents" in parts:
|
||||
yield proj, parts[0], f[:-6], p
|
||||
else:
|
||||
yield proj, parts[0], "sonst:" + f[:-6], p
|
||||
|
||||
|
||||
def lese_meta(path):
|
||||
mp = path[:-6] + ".meta.json"
|
||||
out = {}
|
||||
if os.path.exists(mp):
|
||||
try:
|
||||
d = json.load(open(mp, encoding="utf-8"))
|
||||
except Exception:
|
||||
return out
|
||||
if isinstance(d, dict):
|
||||
for k, v in d.items():
|
||||
meta_keys[k] += 1
|
||||
if k.lower() in ("prompt", "message", "content", "messages", "systemprompt", "system_prompt"):
|
||||
continue
|
||||
if isinstance(v, str) and len(v) <= 80:
|
||||
out[k] = v
|
||||
elif isinstance(v, (int, float, bool)):
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
|
||||
def verarbeite(proj, sid, agent_id, path):
|
||||
art_datei = "haupt" if agent_id is None else "sub"
|
||||
s = {"proj": proj, "sid": sid, "agent": agent_id, "turns": 0, "in": 0, "cc": 0, "cr": 0, "out": 0,
|
||||
"usd": 0.0, "first": None, "last": None, "max_ctx": 0, "last_ctx": 0, "modelle": collections.Counter(),
|
||||
"efforts": collections.Counter(), "tools": collections.Counter(), "title": None, "slug": None,
|
||||
"cold": 0, "sidechain_usd": 0.0, "sidechain_turns": 0, "turns_seit": 0, "usd_seit": 0.0,
|
||||
"out_seit": 0, "cr_seit": 0, "in_seit": 0, "cc_seit": 0, "doppelt": 0}
|
||||
id2name = {}
|
||||
msgs = {}
|
||||
with open(path, encoding="utf-8", errors="replace") as fh:
|
||||
for line in fh:
|
||||
agg["zeilen"] += 1
|
||||
try:
|
||||
o = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
if not isinstance(o, dict):
|
||||
continue
|
||||
t = o.get("type")
|
||||
if t == "custom-title":
|
||||
s["title"] = o.get("customTitle")
|
||||
continue
|
||||
if t not in ("assistant", "user"):
|
||||
continue
|
||||
ts = o.get("timestamp") or ""
|
||||
if t == "assistant":
|
||||
m = o.get("message") or {}
|
||||
if not isinstance(m, dict):
|
||||
continue
|
||||
c = m.get("content")
|
||||
if isinstance(c, list):
|
||||
for b in c:
|
||||
if isinstance(b, dict) and b.get("type") == "tool_use":
|
||||
n = b.get("name") or "?"
|
||||
id2name[b.get("id")] = n
|
||||
s["tools"][n] += 1
|
||||
z = agg["tool"][n]
|
||||
z["aufrufe"] += 1
|
||||
if ts >= SEIT:
|
||||
z["aufrufe_seit"] += 1
|
||||
inp = b.get("input") or {}
|
||||
if n == "Read" and isinstance(inp, dict) and inp.get("file_path"):
|
||||
id2name[b.get("id")] = ("Read", kurzpfad(inp.get("file_path")))
|
||||
if n == "Agent" and isinstance(inp, dict) and ts >= SEIT:
|
||||
agg["agent_tool"][(str(inp.get("subagent_type") or "-"), str(inp.get("model") or "-"))] += 1
|
||||
if n == "Workflow" and ts >= SEIT:
|
||||
agg["workflow_calls"] += 1
|
||||
u = m.get("usage") or {}
|
||||
if not isinstance(u, dict) or not u:
|
||||
continue
|
||||
mid = m.get("id") or o.get("requestId")
|
||||
rec = msgs.get(mid)
|
||||
if rec is None:
|
||||
msgs[mid] = {"ts": ts, "mk": norm(m.get("model") or o.get("requestedModel")), "eff": str(o.get("effort") or "?"),
|
||||
"side": bool(o.get("isSidechain")), "stop": str(m.get("stop_reason")), "version": str(o.get("version")),
|
||||
"slug": o.get("slug"), "u": {k: (u.get(k) or 0) for k in USAGE_FELDER},
|
||||
"cc": dict(u.get("cache_creation") or {}), "erste_out": u.get("output_tokens") or 0}
|
||||
else:
|
||||
for k in USAGE_FELDER:
|
||||
rec["u"][k] = max(rec["u"][k], u.get(k) or 0)
|
||||
cc = u.get("cache_creation") or {}
|
||||
for k in ("ephemeral_5m_input_tokens", "ephemeral_1h_input_tokens"):
|
||||
if cc.get(k) is not None:
|
||||
rec["cc"][k] = max(rec["cc"].get(k) or 0, cc.get(k) or 0)
|
||||
else:
|
||||
m = o.get("message") or {}
|
||||
c = m.get("content") if isinstance(m, dict) else None
|
||||
if isinstance(c, list):
|
||||
for b in c:
|
||||
if isinstance(b, dict) and b.get("type") == "tool_result":
|
||||
n = id2name.get(b.get("tool_use_id"), "?")
|
||||
pfad = None
|
||||
if isinstance(n, tuple):
|
||||
n, pfad = n
|
||||
cont = b.get("content")
|
||||
if isinstance(cont, str):
|
||||
ch = len(cont); txt = cont[:300]
|
||||
elif isinstance(cont, list):
|
||||
ch = sum(len(x.get("text", "")) for x in cont if isinstance(x, dict))
|
||||
txt = "".join(x.get("text", "") for x in cont if isinstance(x, dict))[:300]
|
||||
else:
|
||||
ch = 0; txt = ""
|
||||
z = agg["tool"][n]
|
||||
z["ergebnisse"] += 1; z["zeichen"] += ch
|
||||
z["max"] = max(z["max"], ch)
|
||||
if ch > 50000:
|
||||
z["ueber50k"] += 1
|
||||
if "persisted-output" in txt:
|
||||
z["persistiert"] += 1
|
||||
if ts >= SEIT:
|
||||
z["zeichen_seit"] += ch; z["ergebnisse_seit"] += 1
|
||||
if ch > 50000:
|
||||
z["ueber50k_seit"] += 1
|
||||
if pfad:
|
||||
zp = agg["read_pfad"][pfad]
|
||||
zp["aufrufe"] += 1; zp["zeichen"] += ch
|
||||
# Nachrichten einmal zaehlen, mit dem groessten Wert je Feld; Kopien anderer Dateien ueberspringen
|
||||
for mid, rec in msgs.items():
|
||||
if mid in GESEHEN:
|
||||
s["doppelt"] += 1; agg["doppelt_dateiuebergreifend"] += 1
|
||||
continue
|
||||
GESEHEN.add(mid)
|
||||
u = rec["u"]; ts = rec["ts"]; mk = rec["mk"]; eff = rec["eff"]
|
||||
if u["output_tokens"] > rec["erste_out"]:
|
||||
agg["nachrichten_mit_teilausgabe"] += 1
|
||||
agg["teilausgabe_version"][rec["version"]] += 1
|
||||
ti, tc, tr, to = u["input_tokens"], u["cache_creation_input_tokens"], u["cache_read_input_tokens"], u["output_tokens"]
|
||||
ctx = ti + tc + tr
|
||||
usd = kosten(mk, u, rec["cc"], ctx)
|
||||
art = "sub" if (art_datei == "sub" or rec["side"]) else "haupt"
|
||||
if art_datei == "haupt" and rec["side"]:
|
||||
s["sidechain_usd"] += usd; s["sidechain_turns"] += 1
|
||||
scopes = ["gesamt"] + (["seit"] if ts >= SEIT else [])
|
||||
for scope in scopes:
|
||||
z = agg["modell"][(scope, art, mk)]
|
||||
z["turns"] += 1; z["in"] += ti; z["cc"] += tc; z["cr"] += tr; z["out"] += to; z["usd_x1000"] += int(usd * 1000)
|
||||
if tr == 0:
|
||||
z["kalt"] += 1
|
||||
if art == "haupt":
|
||||
z2 = agg["effort"][(scope, mk, eff)]
|
||||
z2["turns"] += 1; z2["out"] += to; z2["usd_x1000"] += int(usd * 1000); z2["ctx_sum"] += ctx
|
||||
tag = ts[:10]
|
||||
zt = agg["tag"][(tag, art)]
|
||||
zt["turns"] += 1; zt["out"] += to; zt["usd_x1000"] += int(usd * 1000); zt["cr"] += tr; zt["cc"] += tc; zt["in"] += ti
|
||||
if ts >= SEIT:
|
||||
zs = agg["ctx_stufe"][(art, stufe(ctx))]
|
||||
zs["turns"] += 1; zs["usd_x1000"] += int(usd * 1000); zs["cr"] += tr; zs["cc"] += tc; zs["out"] += to
|
||||
if s["turns"] == 0:
|
||||
s["first_cc"] = tc + ti
|
||||
s["turns"] += 1; s["in"] += ti; s["cc"] += tc; s["cr"] += tr; s["out"] += to; s["usd"] += usd
|
||||
if ts >= SEIT:
|
||||
s["turns_seit"] += 1; s["usd_seit"] += usd; s["out_seit"] += to; s["cr_seit"] += tr; s["in_seit"] += ti; s["cc_seit"] += tc
|
||||
s["first"] = ts if (s["first"] is None or ts < s["first"]) else s["first"]
|
||||
s["last"] = ts if (s["last"] is None or ts > s["last"]) else s["last"]
|
||||
s["max_ctx"] = max(s["max_ctx"], ctx)
|
||||
s["last_ctx"] = ctx
|
||||
s["modelle"][mk] += 1
|
||||
s["efforts"][eff] += 1
|
||||
if tr == 0:
|
||||
s["cold"] += 1
|
||||
if not s["slug"] and rec["slug"]:
|
||||
s["slug"] = rec["slug"]
|
||||
agg["stop"][rec["stop"]] += 1
|
||||
agg["versionen"][rec["version"]] += 1
|
||||
return s
|
||||
|
||||
|
||||
def mio(n):
|
||||
return f"{n/1e6:.2f}"
|
||||
|
||||
|
||||
def usd(c):
|
||||
return f"{c:.2f}"
|
||||
|
||||
|
||||
def dauer_h(s):
|
||||
try:
|
||||
a = datetime.datetime.fromisoformat(s["first"].replace("Z", "+00:00"))
|
||||
b = datetime.datetime.fromisoformat(s["last"].replace("Z", "+00:00"))
|
||||
except (AttributeError, ValueError):
|
||||
return "?"
|
||||
return f"{(b - a).total_seconds() / 3600:.1f}"
|
||||
|
||||
|
||||
def auswahl_bericht():
|
||||
"""Je Hauptsitzung, deren Titel, Slug oder Kennung AUSWAHL trifft: Runden, Ausgabe, Effort, Subagenten."""
|
||||
rx = re.compile(AUSWAHL, re.I)
|
||||
L = [f"# Auswahl `{AUSWAHL}` (seit {SEIT}, Fassung 2)\n",
|
||||
"Runden und Ausgabe ohne Subagenten; Dauer ist erste bis letzte Runde, Pausen eingeschlossen.\n",
|
||||
"| Beginn | Titel | Kennung | Effort (Runden) | Runden | Dauer h | Ausgabe | Ausgabe je Runde | USD | max. Kontext |",
|
||||
"|---|---|---|---|---|---|---|---|---|---|"]
|
||||
treffer = [s for s in sessions.values()
|
||||
if s["turns_seit"] > 0 and any(rx.search(str(x or "")) for x in (s["title"], s["slug"], s["sid"]))]
|
||||
for s in sorted(treffer, key=lambda s: s["first"] or ""):
|
||||
ef = ", ".join(f"{k} {v}" for k, v in s["efforts"].most_common())
|
||||
L.append(f"| {(s['first'] or '')[:16]} | {(s['title'] or s['slug'] or '-')[:50]} | {s['sid'][:8]} | {ef} | {s['turns_seit']} "
|
||||
f"| {dauer_h(s)} | {s['out_seit']} | {s['out_seit'] // max(s['turns_seit'], 1)} | {usd(s['usd_seit'])} | {s['max_ctx']} |")
|
||||
L.append("\n## Subagenten der ausgewählten Sitzungen\n")
|
||||
L.append("| Kennung | Typ (meta) | Modell | Effort | Anzahl | Runden | Ausgabe | USD |")
|
||||
L.append("|---|---|---|---|---|---|---|---|")
|
||||
schluessel = {(s["proj"], s["sid"]) for s in treffer}
|
||||
grp = collections.defaultdict(lambda: [0, 0, 0, 0.0])
|
||||
for sa in subagents:
|
||||
if (sa["proj"], sa["sid"]) not in schluessel or sa["turns"] == 0:
|
||||
continue
|
||||
meta = sa.get("meta") or {}
|
||||
mk = sa["modelle"].most_common(1)[0][0] if sa["modelle"] else "?"
|
||||
ef = sa["efforts"].most_common(1)[0][0] if sa["efforts"] else "?"
|
||||
g = grp[(sa["sid"][:8], str(meta.get("agentType") or "-"), mk, ef)]
|
||||
g[0] += 1; g[1] += sa["turns"]; g[2] += sa["out"]; g[3] += sa["usd"]
|
||||
for k, g in sorted(grp.items()):
|
||||
L.append(f"| {k[0]} | {k[1]} | {k[2]} | {k[3]} | {g[0]} | {g[1]} | {g[2]} | {usd(g[3])} |")
|
||||
L.append(f"\nTreffer: {len(treffer)} Hauptsitzungen.")
|
||||
return "\n".join(L)
|
||||
|
||||
|
||||
def bericht(t0):
|
||||
L = []
|
||||
L.append(f"# Transkript-Auswertung (Zeitraum seit {SEIT}, Stand {datetime.date.today().isoformat()}, Fassung 2)\n")
|
||||
L.append(f"Quelle: {ROOT} — {agg['dateien']} JSONL-Dateien, {agg['zeilen']} Zeilen, Lauf {round(time.time()-t0)} s. Kosten sind Listenpreis-Äquivalente (USD) nach der Preistabelle im Skript; der Max-Plan rechnet nicht in USD ab, die Zahl dient dem Vergleich der Hebel. Fassung 2 zählt je Nachrichten-ID den größten Wert jedes usage-Felds (vor Claude Code 2.1.308 trug die erste Zeile einer Subagenten-Nachricht nur eine Teilzahl der Ausgabe; betroffen: {agg['nachrichten_mit_teilausgabe']} Nachrichten), überspringt Nachrichten, die schon in einer anderen Datei standen ({agg['doppelt_dateiuebergreifend']} Kopien), und rechnet Haiku 5.5 über 100k Prompt mit dem Fünffachen.\n")
|
||||
L.append("Nachrichten mit Teilausgabe in der ersten Zeile nach Claude-Code-Version: "
|
||||
+ (", ".join(f"{k}: {v}" for k, v in sorted(agg["teilausgabe_version"].items())) or "keine") + "\n")
|
||||
if BEFUNDE:
|
||||
L.append("Hinweise: " + "; ".join(BEFUNDE[:20]) + (f" (und {len(BEFUNDE) - 20} weitere)" if len(BEFUNDE) > 20 else "") + "\n")
|
||||
|
||||
for scope in ("seit", "gesamt"):
|
||||
L.append(f"\n## Verbrauch nach Modell ({'seit ' + SEIT if scope == 'seit' else 'gesamt'})\n")
|
||||
L.append("| Art | Modell | Runden | Eingabe Mio | Cache-Schreiben Mio | Cache-Lesen Mio | Ausgabe Mio | USD | Anteil USD | Runden ohne Cache |")
|
||||
L.append("|---|---|---|---|---|---|---|---|---|---|")
|
||||
rows = [(k, v) for k, v in agg["modell"].items() if k[0] == scope]
|
||||
total = sum(v["usd_x1000"] for _, v in rows) / 1000 or 1
|
||||
for (sc, art, mk), v in sorted(rows, key=lambda kv: -kv[1]["usd_x1000"]):
|
||||
c = v["usd_x1000"] / 1000
|
||||
L.append(f"| {art} | {mk} | {v['turns']} | {mio(v['in'])} | {mio(v['cc'])} | {mio(v['cr'])} | {mio(v['out'])} | {usd(c)} | {100*c/total:.0f} % | {v['kalt']} |")
|
||||
L.append(f"| **Summe** | | | | | | | **{usd(total)}** | | |")
|
||||
|
||||
L.append(f"\n## Kostenzerlegung je Art und Modell (seit {SEIT}, Listenpreis)\n")
|
||||
L.append("| Art | Modell | Eingabe | Cache-Schreiben | Cache-Lesen | Ausgabe |")
|
||||
L.append("|---|---|---|---|---|---|")
|
||||
for (sc, art, mk), v in sorted(((k, v) for k, v in agg["modell"].items() if k[0] == "seit"), key=lambda kv: -kv[1]["usd_x1000"])[:6]:
|
||||
p = PREISE.get(mk, PREISE["claude-opus-5-5"])
|
||||
ein = v["in"] * p[0] / 1e6; les = v["cr"] * p[2] / 1e6; aus = v["out"] * p[1] / 1e6
|
||||
ges = v["usd_x1000"] / 1000 or 1
|
||||
schr = max(ges - ein - les - aus, 0)
|
||||
L.append(f"| {art} | {mk} | {100*ein/ges:.0f} % | {100*schr/ges:.0f} % | {100*les/ges:.0f} % | {100*aus/ges:.0f} % |")
|
||||
|
||||
L.append(f"\n## Hauptsitzungen nach Modell und Effort (seit {SEIT})\n")
|
||||
L.append("| Modell | Effort | Runden | Ausgabe Mio | Ausgabe je Runde | USD | mittlerer Kontext je Runde |")
|
||||
L.append("|---|---|---|---|---|---|---|")
|
||||
for (sc, mk, eff), v in sorted(((k, v) for k, v in agg["effort"].items() if k[0] == "seit"), key=lambda kv: -kv[1]["usd_x1000"]):
|
||||
L.append(f"| {mk} | {eff} | {v['turns']} | {mio(v['out'])} | {v['out']//max(v['turns'],1)} | {usd(v['usd_x1000']/1000)} | {v['ctx_sum']//max(v['turns'],1)} |")
|
||||
|
||||
L.append("\n## Je Kalenderwoche (UTC-Datum)\n")
|
||||
L.append("| Woche | Runden Haupt | USD Haupt | Runden Sub | USD Sub | Ausgabe Mio Haupt | Ausgabe Mio Sub | Cache-Lesen Mio | Cache-Schreiben Mio |")
|
||||
L.append("|---|---|---|---|---|---|---|---|---|")
|
||||
wochen = collections.defaultdict(Z)
|
||||
for (tag, art), v in agg["tag"].items():
|
||||
try:
|
||||
d = datetime.date.fromisoformat(tag)
|
||||
except Exception:
|
||||
continue
|
||||
w = f"{d.isocalendar()[0]}-W{d.isocalendar()[1]:02d}"
|
||||
z = wochen[w]
|
||||
z[f"turns_{art}"] += v["turns"]; z[f"usd_{art}"] += v["usd_x1000"]; z[f"out_{art}"] += v["out"]; z["cr"] += v["cr"]; z["cc"] += v["cc"]
|
||||
for w in sorted(wochen):
|
||||
z = wochen[w]
|
||||
L.append(f"| {w} | {z['turns_haupt']} | {usd(z['usd_haupt']/1000)} | {z['turns_sub']} | {usd(z['usd_sub']/1000)} | {mio(z['out_haupt'])} | {mio(z['out_sub'])} | {mio(z['cr'])} | {mio(z['cc'])} |")
|
||||
|
||||
L.append(f"\n## Teuerste Hauptsitzungen (seit {SEIT}, 25)\n")
|
||||
L.append("| Beginn | Titel | Modell / Effort | Runden | USD | max. Kontext | Cache-Lesen Anteil | Subagenten (n / USD) |")
|
||||
L.append("|---|---|---|---|---|---|---|---|")
|
||||
sub_by_parent = collections.defaultdict(lambda: [0, 0.0])
|
||||
for sa in subagents:
|
||||
if (sa["last"] or "") >= SEIT:
|
||||
sub_by_parent[(sa["proj"], sa["sid"])][0] += 1
|
||||
sub_by_parent[(sa["proj"], sa["sid"])][1] += sa["usd"]
|
||||
haupt = [s for s in sessions.values() if s["turns_seit"] > 0]
|
||||
for s in sorted(haupt, key=lambda s: -s["usd_seit"])[:25]:
|
||||
mk = ", ".join(f"{k}" for k, _ in s["modelle"].most_common(2))
|
||||
ef = ", ".join(f"{k}" for k, _ in s["efforts"].most_common(2))
|
||||
ges = s["in_seit"] + s["cc_seit"] + s["cr_seit"] or 1
|
||||
n, c = sub_by_parent.get((s["proj"], s["sid"]), [0, 0.0])
|
||||
L.append(f"| {(s['first'] or '')[:10]} | {(s['title'] or s['slug'] or s['sid'][:8])[:50]} | {mk} / {ef} | {s['turns_seit']} | {usd(s['usd_seit'])} | {s['max_ctx']} | {100*s['cr_seit']/ges:.0f} % | {n} / {usd(c)} |")
|
||||
L.append(f"\nHauptsitzungen mit Runden seit {SEIT}: {len(haupt)}; davon mit maximalem Kontext über 400k: {sum(1 for s in haupt if s['max_ctx'] > 400000)}, über 700k: {sum(1 for s in haupt if s['max_ctx'] > 700000)}.")
|
||||
L.append(f"Summe USD Hauptsitzungen seit {SEIT}: {usd(sum(s['usd_seit'] for s in haupt))}; Summe USD Subagenten seit {SEIT}: {usd(sum(c for n, c in sub_by_parent.values()))}.")
|
||||
|
||||
L.append(f"\n## Kontextgröße der Hauptsitzungen (max je Sitzung, seit {SEIT})\n")
|
||||
L.append("| Stufe | Sitzungen | USD | Anteil USD |")
|
||||
L.append("|---|---|---|---|")
|
||||
tot = sum(s["usd_seit"] for s in haupt) or 1
|
||||
for a, b, n in STUFEN:
|
||||
grp = [s for s in haupt if a <= s["max_ctx"] < b]
|
||||
L.append(f"| {n} | {len(grp)} | {usd(sum(s['usd_seit'] for s in grp))} | {100*sum(s['usd_seit'] for s in grp)/tot:.0f} % |")
|
||||
|
||||
L.append(f"\n## Subagenten (seit {SEIT}, aus Subagenten-Transkripten)\n")
|
||||
sa_seit = [sa for sa in subagents if (sa["last"] or "") >= SEIT and sa["turns"] > 0]
|
||||
L.append(f"Anzahl: {len(sa_seit)}; Meta-Schlüssel gesehen: {dict(meta_keys.most_common(12))}\n")
|
||||
L.append("| Typ (meta) | Modell | Effort | Anzahl | Median Ausgabe | Median USD | Summe USD | Median Runden | Median max. Kontext |")
|
||||
L.append("|---|---|---|---|---|---|---|---|---|")
|
||||
grp = collections.defaultdict(list)
|
||||
for sa in sa_seit:
|
||||
meta = sa.get("meta") or {}
|
||||
typ = str(meta.get("agentType") or meta.get("subagent_type") or meta.get("type") or meta.get("agent_type") or "-")
|
||||
mk = sa["modelle"].most_common(1)[0][0] if sa["modelle"] else "?"
|
||||
ef = sa["efforts"].most_common(1)[0][0] if sa["efforts"] else "?"
|
||||
grp[(typ, mk, ef)].append(sa)
|
||||
for k, lst in sorted(grp.items(), key=lambda kv: -sum(x["usd"] for x in kv[1])):
|
||||
L.append(f"| {k[0]} | {k[1]} | {k[2]} | {len(lst)} | {int(statistics.median(x['out'] for x in lst))} | {usd(statistics.median(x['usd'] for x in lst))} | {usd(sum(x['usd'] for x in lst))} | {int(statistics.median(x['turns'] for x in lst))} | {int(statistics.median(x['max_ctx'] for x in lst))} |")
|
||||
|
||||
L.append(f"\n## Subagenten nach Workflow-Phase (meta.workflowPhase, seit {SEIT})\n")
|
||||
L.append("| Phase | Anzahl | Summe USD | Mittel USD | Median Runden |")
|
||||
L.append("|---|---|---|---|---|")
|
||||
ph = collections.defaultdict(list)
|
||||
for sa in sa_seit:
|
||||
ph[str((sa.get("meta") or {}).get("workflowPhase") or "-")].append(sa)
|
||||
for k, lst in sorted(ph.items(), key=lambda kv: -sum(x["usd"] for x in kv[1]))[:20]:
|
||||
L.append(f"| {k} | {len(lst)} | {usd(sum(x['usd'] for x in lst))} | {usd(sum(x['usd'] for x in lst)/len(lst))} | {int(statistics.median(x['turns'] for x in lst))} |")
|
||||
|
||||
L.append(f"\n## Agent-Werkzeug: subagent_type und model (seit {SEIT})\n")
|
||||
L.append("| subagent_type | model | Aufrufe |")
|
||||
L.append("|---|---|---|")
|
||||
for (st, mo), n in agg["agent_tool"].most_common(25):
|
||||
L.append(f"| {st} | {mo} | {n} |")
|
||||
L.append(f"\nWorkflow-Aufrufe seit {SEIT}: {agg['workflow_calls']}")
|
||||
|
||||
L.append(f"\n## Werkzeuge nach Ergebnisvolumen (seit {SEIT}, Zeichen der Werkzeugergebnisse)\n")
|
||||
L.append("| Werkzeug | Aufrufe | Ergebnis-MB | Ø Zeichen | Max Zeichen | Ergebnisse > 50k | persistiert |")
|
||||
L.append("|---|---|---|---|---|---|---|")
|
||||
for n, z in sorted(agg["tool"].items(), key=lambda kv: -kv[1]["zeichen_seit"])[:30]:
|
||||
L.append(f"| {n} | {z['aufrufe_seit']} | {z['zeichen_seit']/1e6:.1f} | {z['zeichen_seit']//max(z['ergebnisse_seit'],1)} | {z['max']} | {z['ueber50k_seit']} | {z['persistiert']} |")
|
||||
|
||||
L.append(f"\n## Kosten je Runde nach Kontextgröße der Runde (seit {SEIT})\n")
|
||||
L.append("| Art | Kontext der Runde | Runden | USD gesamt | USD je Runde | Cache-Lesen je Runde | Cache-Schreiben je Runde | Ausgabe je Runde |")
|
||||
L.append("|---|---|---|---|---|---|---|---|")
|
||||
for art in ("haupt", "sub"):
|
||||
for a, b, n in STUFEN:
|
||||
z = agg["ctx_stufe"].get((art, n))
|
||||
if not z or not z["turns"]:
|
||||
continue
|
||||
t = z["turns"]
|
||||
L.append(f"| {art} | {n} | {t} | {usd(z['usd_x1000']/1000)} | {z['usd_x1000']/1000/t:.3f} | {z['cr']//t} | {z['cc']//t} | {z['out']//t} |")
|
||||
|
||||
L.append(f"\n## Fester Startaufwand je Subagent: Tokens der ersten Runde (Eingabe plus Cache-Schreiben), seit {SEIT}\n")
|
||||
L.append("| Typ (meta) | Modell | Anzahl | Median erste Runde | Median Runden | Anteil erste Runde am Gesamtkontext-Maximum |")
|
||||
L.append("|---|---|---|---|---|---|")
|
||||
grp2 = collections.defaultdict(list)
|
||||
for sa in sa_seit:
|
||||
meta = sa.get("meta") or {}
|
||||
typ = str(meta.get("agentType") or "-")
|
||||
mk = sa["modelle"].most_common(1)[0][0] if sa["modelle"] else "?"
|
||||
if sa.get("first_cc") is not None:
|
||||
grp2[(typ, mk)].append(sa)
|
||||
for k, lst in sorted(grp2.items(), key=lambda kv: -len(kv[1]))[:20]:
|
||||
f = statistics.median(x["first_cc"] for x in lst)
|
||||
m = statistics.median(x["max_ctx"] for x in lst) or 1
|
||||
L.append(f"| {k[0]} | {k[1]} | {len(lst)} | {int(f)} | {int(statistics.median(x['turns'] for x in lst))} | {100*f/m:.0f} % |")
|
||||
|
||||
for titel, schl, n in (("nach Zeichen", "zeichen", 40), ("nach Aufrufen", "aufrufe", 25)):
|
||||
L.append(f"\n## Meistgelesene Dateien (Read, seit {SEIT}, {titel})\n")
|
||||
L.append("| Pfad (letzte drei Glieder) | Aufrufe | Zeichen gesamt Mio | Ø Zeichen |")
|
||||
L.append("|---|---|---|---|")
|
||||
for p, z in sorted(agg["read_pfad"].items(), key=lambda kv: -kv[1][schl])[:n]:
|
||||
L.append(f"| {p} | {z['aufrufe']} | {z['zeichen']/1e6:.2f} | {z['zeichen']//max(z['aufrufe'],1)} |")
|
||||
|
||||
L.append("\n## Stop-Gründe (gesamt)\n")
|
||||
L.append(", ".join(f"{k}: {v}" for k, v in agg["stop"].most_common()))
|
||||
L.append("\n## Claude-Code-Versionen (Nachrichten, gesamt)\n")
|
||||
L.append(", ".join(f"{k}: {v}" for k, v in agg["versionen"].most_common(12)))
|
||||
return "\n".join(L)
|
||||
|
||||
|
||||
def im_vault(pfad):
|
||||
p = Path(pfad).resolve()
|
||||
return p == VAULT or VAULT in p.parents
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
global ROOT, OUT, SEIT, AUSWAHL
|
||||
t0 = time.time()
|
||||
ap = argparse.ArgumentParser(description="Tokenverbrauch aus Claude-Code-Transkripten")
|
||||
ap.add_argument("projects", help="projects-Ordner von Claude Code, etwa D:\\ClaudeData\\config\\projects")
|
||||
ap.add_argument("--ausgabe", required=True, help="Ordner für auswertung.md und auswertung.json, nie im Vault")
|
||||
ap.add_argument("--seit", default="2026-09-15", help="Beginn des Zeitraums JJJJ-MM-TT (UTC), Vorgabe 2026-09-15")
|
||||
ap.add_argument("--auswahl", default=None, help="regulärer Ausdruck auf Titel, Slug oder Kennung der Hauptsitzung; schreibt auswahl.md")
|
||||
a = ap.parse_args(argv)
|
||||
ROOT, OUT, SEIT, AUSWAHL = a.projects, a.ausgabe, a.seit, a.auswahl
|
||||
try:
|
||||
datetime.date.fromisoformat(SEIT)
|
||||
except ValueError:
|
||||
print(f"--seit {SEIT!r} ist kein Datum JJJJ-MM-TT")
|
||||
return 2
|
||||
if AUSWAHL is not None:
|
||||
try:
|
||||
re.compile(AUSWAHL)
|
||||
except re.error as e:
|
||||
print(f"--auswahl ist kein regulärer Ausdruck: {e}")
|
||||
return 2
|
||||
if not os.path.isdir(ROOT):
|
||||
print(f"projects-Ordner fehlt: {ROOT}")
|
||||
return 2
|
||||
# Sitzungstitel können Kundennamen tragen, und der Vault wird teilweise veröffentlicht.
|
||||
if im_vault(OUT):
|
||||
print(f"Ausgabe im Vault abgelehnt: {OUT} liegt unter {VAULT}")
|
||||
return 2
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
for proj, sid, agent_id, path in dateien():
|
||||
agg["dateien"] += 1
|
||||
try:
|
||||
s = verarbeite(proj, sid, agent_id, path)
|
||||
except Exception as e: # Eine kaputte Datei ist ein Hinweis im Bericht, kein Abbruch der ganzen Zählung.
|
||||
BEFUNDE.append(f"{proj}/{os.path.basename(path)}: {type(e).__name__}")
|
||||
continue
|
||||
if agent_id is None:
|
||||
sessions[(proj, sid)] = s
|
||||
else:
|
||||
s["meta"] = lese_meta(path)
|
||||
subagents.append(s)
|
||||
|
||||
print("Dateien", agg["dateien"], "Zeilen", agg["zeilen"], "Sekunden", round(time.time() - t0),
|
||||
"doppelt (andere Datei)", agg["doppelt_dateiuebergreifend"], "Nachrichten mit Teilausgabe in erster Zeile", agg["nachrichten_mit_teilausgabe"])
|
||||
md = bericht(t0)
|
||||
with open(os.path.join(OUT, "auswertung.md"), "w", encoding="utf-8") as f:
|
||||
f.write(md)
|
||||
with open(os.path.join(OUT, "auswertung.json"), "w", encoding="utf-8") as f:
|
||||
json.dump({
|
||||
"kennzahlen": {k: agg[k] for k in ("dateien", "zeilen", "doppelt_dateiuebergreifend", "nachrichten_mit_teilausgabe", "workflow_calls")},
|
||||
"teilausgabe_version": dict(agg["teilausgabe_version"]),
|
||||
"befunde": BEFUNDE,
|
||||
"modell": {"|".join(k): dict(v) for k, v in agg["modell"].items()},
|
||||
"effort": {"|".join(k): dict(v) for k, v in agg["effort"].items()},
|
||||
"tag": {"|".join(k): dict(v) for k, v in agg["tag"].items()},
|
||||
"tool": {k: dict(v) for k, v in agg["tool"].items()},
|
||||
"ctx_stufe": {"|".join(k): dict(v) for k, v in agg["ctx_stufe"].items()},
|
||||
"sessions": [{k: (dict(v) if isinstance(v, collections.Counter) else v) for k, v in s.items()} for s in sessions.values()],
|
||||
"subagents": [{k: (dict(v) if isinstance(v, collections.Counter) else v) for k, v in s.items()} for s in subagents],
|
||||
}, f, ensure_ascii=False, indent=0)
|
||||
if AUSWAHL is not None:
|
||||
with open(os.path.join(OUT, "auswahl.md"), "w", encoding="utf-8") as f:
|
||||
f.write(auswahl_bericht())
|
||||
print(f"Geschrieben: {os.path.join(OUT, 'auswertung.md')}" + (" und auswahl.md" if AUSWAHL is not None else ""))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Ordnet die Einträge direkt unter dem Datenordner von Claude Code ein: bekannte Ordner gegen zufällig benannte Temp-Ordner.
|
||||
|
||||
Ersteller: Pascal Michel
|
||||
Erstellt: 2026-10-30
|
||||
Doku: LIESMICH.md in diesem Ordner
|
||||
|
||||
Nur lesend. Gibt Anzahl, Größe, Alter und Endungsmuster aus, keine Dateiinhalte; die Strukturbeispiele
|
||||
am Schluss nennen bis zu fünf Dateinamen je Ordner.
|
||||
Was es nicht sieht: Ob ein Zufallsordner noch von einer laufenden Sitzung benutzt wird. Das Alter kommt
|
||||
aus mtime, und die App fasst beim Neustart viele Dateien an (Harness-Erinnerung
|
||||
transkript-usage-groesster-wert-je-nachricht). Vor jedem Löschen also laufende Sitzungen über die App
|
||||
prüfen, nicht über dieses Skript.
|
||||
Zufällig benannt heißt: 21 Zeichen aus [A-Za-z0-9_-] (nanoid-artig) und nicht in BEKANNT.
|
||||
Aufruf: python -I -X utf8 claudedata_wurzel.py [Wurzel, Vorgabe D:\\ClaudeData]
|
||||
"""
|
||||
import collections
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
BEKANNT = {"claude", "config", "tmp", "umzug", "codex-review"}
|
||||
MUSTER = re.compile(r"^[A-Za-z0-9_-]{21}$")
|
||||
|
||||
|
||||
def groesse(p):
|
||||
total = 0; anzahl = 0; namen = collections.Counter(); neueste = 0
|
||||
for root, dirs, files in os.walk(p):
|
||||
for f in files:
|
||||
fp = os.path.join(root, f)
|
||||
try:
|
||||
st = os.stat(fp)
|
||||
except OSError:
|
||||
continue
|
||||
total += st.st_size; anzahl += 1; neueste = max(neueste, st.st_mtime)
|
||||
ext = os.path.splitext(f)[1].lower() or f[:20]
|
||||
namen[ext] += 1
|
||||
return total, anzahl, namen, neueste
|
||||
|
||||
|
||||
def main(argv):
|
||||
wurzel = argv[1] if len(argv) > 1 else r"D:\ClaudeData"
|
||||
if not os.path.isdir(wurzel):
|
||||
print(f"Wurzel fehlt: {wurzel}")
|
||||
return 2
|
||||
jetzt = time.time()
|
||||
with os.scandir(wurzel) as it:
|
||||
eintraege = list(it)
|
||||
zufall = [e for e in eintraege if e.is_dir() and MUSTER.match(e.name) and e.name not in BEKANNT]
|
||||
andere = [e for e in eintraege if e not in zufall]
|
||||
print(f"Einträge gesamt: {len(eintraege)}; zufällig benannte Ordner: {len(zufall)}; andere: {len(andere)}")
|
||||
print("\nAndere Einträge:")
|
||||
for e in sorted(andere, key=lambda x: x.name):
|
||||
try:
|
||||
st = e.stat()
|
||||
print(f" {'D' if e.is_dir() else 'F'} {e.name} mtime {datetime.datetime.fromtimestamp(st.st_mtime):%Y-%m-%d}")
|
||||
except OSError:
|
||||
print(" ?", e.name)
|
||||
|
||||
gesamt = 0; dateien = 0; alter = collections.Counter(); muster = collections.Counter(); tage_liste = []
|
||||
topnamen = collections.Counter(); beispiele = {}
|
||||
for e in zufall:
|
||||
t, n, namen, neueste = groesse(e.path)
|
||||
gesamt += t; dateien += n
|
||||
try:
|
||||
mt = e.stat().st_mtime
|
||||
except OSError:
|
||||
mt = neueste
|
||||
tage = (jetzt - max(mt, neueste)) / 86400
|
||||
tage_liste.append(tage)
|
||||
stufe = "<1 Tag" if tage < 1 else "1-7 Tage" if tage < 7 else "7-30 Tage" if tage < 30 else ">30 Tage"
|
||||
alter[stufe] += 1
|
||||
sig = ",".join(f"{k}:{v}" for k, v in sorted(namen.items())[:4])
|
||||
muster[sig] += 1
|
||||
for k in namen:
|
||||
topnamen[k] += 1
|
||||
beispiele.setdefault(sig, e.name)
|
||||
print(f"\nZufallsordner: {len(zufall)}, Dateien {dateien}, Größe {gesamt/1e6:.1f} MB")
|
||||
print("Alter (nach jüngster Änderung):", dict(alter))
|
||||
if tage_liste:
|
||||
print(f"ältester {max(tage_liste):.0f} Tage, jüngster {min(tage_liste):.2f} Tage")
|
||||
print("\nHäufigste Inhaltsmuster (Endung:Anzahl je Ordner):")
|
||||
for sig, n in muster.most_common(12):
|
||||
print(f" {n:5d} x {sig or '(leer)'} Beispiel: {beispiele[sig]}")
|
||||
print("\nHäufigste Dateiendungen über alle Zufallsordner:", dict(topnamen.most_common(12)))
|
||||
for sig, n in muster.most_common(3):
|
||||
p = os.path.join(wurzel, beispiele[sig])
|
||||
print(f"\nStruktur von {beispiele[sig]}:")
|
||||
for root, dirs, files in os.walk(p):
|
||||
tiefe = root[len(p):].count(os.sep)
|
||||
if tiefe > 2:
|
||||
continue
|
||||
print(" " * tiefe + os.path.basename(root) + "/", f"({len(dirs)} Ordner, {len(files)} Dateien)", sorted(files)[:5])
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
@@ -0,0 +1,247 @@
|
||||
"""Regressionstreiber für analyse_transkripte.py: fährt die Fälle unter fixture/ und die Mutanten, die jeder Fall fangen soll.
|
||||
|
||||
Ersteller: Pascal Michel
|
||||
Erstellt: 2026-10-30
|
||||
Doku: LIESMICH.md in diesem Ordner
|
||||
|
||||
Was er belegt: die drei Zählregeln (größter Wert je message.id, Deduplizierung über Dateigrenzen,
|
||||
Haiku-5.5-Stufe über 100k Prompt), die Ablehnung einer Ausgabe im Vault, den fehlenden
|
||||
projects-Ordner, kaputte Zeilen ohne Absturz und die Auswahl nach Titel. Jeder Fall läuft einzeln
|
||||
gegen einen eigenen Fixture-Ordner und einen frischen Ausgabeordner im System-Temp; zuerst die
|
||||
Grundlinie, scheitert sie, messen die übrigen Fälle nichts.
|
||||
Rotnachweis: Je Mutante wird eine Kopie des Skripts mit genau einer Ersetzung gebaut; die Mutante
|
||||
gilt als gefangen, wenn der ihr zugeordnete Fall rot wird. Eine Ersetzung, die im Skript nicht
|
||||
(genau einmal) vorkommt, bricht mit Exit 2 ab, statt still nichts zu prüfen.
|
||||
Was er nicht sieht: echte Transkripte, Preise (PREISE wird nicht gegen eine Quelle geprüft), die
|
||||
Berichtstabellen außer den hier abgefragten Zahlen, claudedata_wurzel.py und scratchpads.py.
|
||||
Ein Fangnetz, kein Nachweis.
|
||||
Aufruf: python -I -X utf8 faelle.py [--ohne-mutanten]
|
||||
Exit 0 alle Fälle grün und alle Mutanten gefangen, 1 sonst, 2 bei veraltetem Mutantenmuster.
|
||||
"""
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
HIER = Path(__file__).resolve().parent
|
||||
SKRIPT = HIER / "analyse_transkripte.py"
|
||||
FIXTURE = HIER / "fixture"
|
||||
VAULT = HIER.parents[1]
|
||||
|
||||
|
||||
def lauf(skript, fixture, ausgabe, *extra):
|
||||
r = subprocess.run([sys.executable, "-I", "-X", "utf8", str(skript), str(fixture), "--ausgabe", str(ausgabe), *extra],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=120)
|
||||
daten = None
|
||||
j = Path(ausgabe) / "auswertung.json"
|
||||
if r.returncode == 0 and j.exists():
|
||||
daten = json.loads(j.read_text(encoding="utf-8"))
|
||||
return r, daten
|
||||
|
||||
|
||||
def sitzung(d, sid):
|
||||
return next((s for s in d["sessions"] if s["sid"] == sid), None)
|
||||
|
||||
|
||||
def nahe(a, b):
|
||||
return abs(a - b) < 1e-9
|
||||
|
||||
|
||||
def f_grundlinie(skript, tmp):
|
||||
r, d = lauf(skript, FIXTURE / "grundlinie", tmp)
|
||||
if d is None:
|
||||
return f"Exit {r.returncode}: {r.stdout[-200:]}{r.stderr[-200:]}"
|
||||
s = sitzung(d, "sess-g")
|
||||
if not s or s["turns"] != 1 or s["out"] != 100 or not nahe(s["usd"], 0.006):
|
||||
return f"erwartet 1 Runde, 100 Ausgabe, 0.006 USD; gesehen {s and (s['turns'], s['out'], s['usd'])}"
|
||||
if d["kennzahlen"]["doppelt_dateiuebergreifend"] or d["kennzahlen"]["nachrichten_mit_teilausgabe"] or d["befunde"]:
|
||||
return f"Grundlinie meldet Kopie, Teilausgabe oder Befund: {d['kennzahlen']} {d['befunde']}"
|
||||
return None
|
||||
|
||||
|
||||
def teilausgabe(ordner, agent, erwartet_teil):
|
||||
def fall(skript, tmp):
|
||||
r, d = lauf(skript, FIXTURE / ordner, tmp)
|
||||
if d is None:
|
||||
return f"Exit {r.returncode}"
|
||||
sa = next((x for x in d["subagents"] if x["agent"] == agent), None)
|
||||
if not sa or sa["turns"] != 1 or sa["out"] != 900:
|
||||
return f"Subagent: erwartet 1 Runde mit 900 Ausgabe, gesehen {sa and (sa['turns'], sa['out'])}"
|
||||
teil = d["kennzahlen"]["nachrichten_mit_teilausgabe"]
|
||||
if teil != erwartet_teil:
|
||||
return f"Teilausgabe gezählt {teil}, erwartet {erwartet_teil}"
|
||||
if erwartet_teil and d["teilausgabe_version"] != {"2.1.300": 1}:
|
||||
return f"Verteilung nach Version falsch: {d['teilausgabe_version']}"
|
||||
return None
|
||||
return fall
|
||||
|
||||
|
||||
def kopie(ordner):
|
||||
def fall(skript, tmp):
|
||||
r, d = lauf(skript, FIXTURE / ordner, tmp)
|
||||
if d is None:
|
||||
return f"Exit {r.returncode}"
|
||||
runden = sum(s["turns"] for s in d["sessions"])
|
||||
out = sum(s["out"] for s in d["sessions"])
|
||||
if runden != 2 or out != 300 or d["kennzahlen"]["doppelt_dateiuebergreifend"] != 1:
|
||||
return f"erwartet 2 Runden, 300 Ausgabe, 1 Kopie; gesehen {runden}, {out}, {d['kennzahlen']['doppelt_dateiuebergreifend']}"
|
||||
return None
|
||||
return fall
|
||||
|
||||
|
||||
def f_haiku_ueber(skript, tmp):
|
||||
r, d = lauf(skript, FIXTURE / "haiku-ueber-100k", tmp)
|
||||
if d is None:
|
||||
return f"Exit {r.returncode}"
|
||||
s = sitzung(d, "sess-h")
|
||||
# 150000 x 0.50 + 1000 x 2.50 (Stufe über 100k) plus 50000 x 0.10 + 1000 x 0.50, je /1e6
|
||||
if not s or not nahe(s["usd"], 0.0775 + 0.0055):
|
||||
return f"erwartet 0.083 USD, gesehen {s and s['usd']}"
|
||||
return None
|
||||
|
||||
|
||||
def f_haiku_grenze(skript, tmp):
|
||||
r, d = lauf(skript, FIXTURE / "haiku-grenze-100k", tmp)
|
||||
if d is None:
|
||||
return f"Exit {r.returncode}"
|
||||
s = sitzung(d, "sess-k")
|
||||
# Genau 100000 Prompt-Tokens liegen noch in der Grundstufe: 100000 x 0.10 / 1e6
|
||||
if not s or not nahe(s["usd"], 0.01):
|
||||
return f"erwartet 0.01 USD, gesehen {s and s['usd']}"
|
||||
return None
|
||||
|
||||
|
||||
def f_kaputte_zeilen(skript, tmp):
|
||||
r, d = lauf(skript, FIXTURE / "kaputte-zeilen", tmp)
|
||||
if d is None:
|
||||
return f"Exit {r.returncode}: {r.stderr[-200:]}"
|
||||
s = sitzung(d, "sess-z")
|
||||
if not s or s["turns"] != 1 or d["befunde"]:
|
||||
return f"erwartet 1 Runde ohne Befund, gesehen {s and s['turns']} und {d['befunde']}"
|
||||
return None
|
||||
|
||||
|
||||
def f_auswahl(skript, tmp):
|
||||
r, d = lauf(skript, FIXTURE / "auswahl", tmp, "--auswahl", "paket probe")
|
||||
a = Path(tmp) / "auswahl.md"
|
||||
if d is None or not a.exists():
|
||||
return f"Exit {r.returncode}, auswahl.md fehlt"
|
||||
text = a.read_text(encoding="utf-8")
|
||||
if "Paket Probe eins" not in text or "Anderes Thema" in text or "Treffer: 1 Hauptsitzungen" not in text:
|
||||
return "auswahl.md trifft nicht genau die Sitzung mit passendem Titel"
|
||||
return None
|
||||
|
||||
|
||||
def f_ausgabe_im_vault(skript, tmp):
|
||||
ziel = VAULT / "_verbrauch_auswertung_probe"
|
||||
if ziel.exists():
|
||||
return f"Vorbedingung: {ziel} existiert schon"
|
||||
r, d = lauf(skript, FIXTURE / "grundlinie", ziel)
|
||||
angelegt = ziel.exists()
|
||||
if angelegt:
|
||||
shutil.rmtree(ziel)
|
||||
if r.returncode != 2 or angelegt:
|
||||
return f"erwartet Exit 2 ohne angelegten Ordner, gesehen Exit {r.returncode}, angelegt {angelegt}"
|
||||
return None
|
||||
|
||||
|
||||
def f_projects_fehlt(skript, tmp):
|
||||
r, d = lauf(skript, Path(tmp) / "gibt-es-nicht", Path(tmp) / "aus")
|
||||
if r.returncode != 2:
|
||||
return f"erwartet Exit 2, gesehen {r.returncode}"
|
||||
return None
|
||||
|
||||
|
||||
FAELLE = [
|
||||
("grundlinie", f_grundlinie),
|
||||
("teilausgabe-erste-zeile", teilausgabe("teilausgabe-erste-zeile", "agent-t1", 1)),
|
||||
("teilausgabe-letzte-zeile", teilausgabe("teilausgabe-letzte-zeile", "agent-l1", 0)),
|
||||
("kopie-vorn", kopie("kopie-vorn")),
|
||||
("kopie-hinten", kopie("kopie-hinten")),
|
||||
("haiku-ueber-100k", f_haiku_ueber),
|
||||
("haiku-grenze-100k", f_haiku_grenze),
|
||||
("kaputte-zeilen", f_kaputte_zeilen),
|
||||
("auswahl", f_auswahl),
|
||||
("ausgabe-im-vault", f_ausgabe_im_vault),
|
||||
("projects-fehlt", f_projects_fehlt),
|
||||
]
|
||||
|
||||
# (Name, alter Text, neuer Text, Fall, der rot werden muss)
|
||||
MUTANTEN = [
|
||||
("erste Zeile behalten", 'rec["u"][k] = max(rec["u"][k], u.get(k) or 0)', "pass", "teilausgabe-erste-zeile"),
|
||||
("letzte Zeile statt größter Wert", 'rec["u"][k] = max(rec["u"][k], u.get(k) or 0)', 'rec["u"][k] = u.get(k) or 0', "teilausgabe-letzte-zeile"),
|
||||
("keine Deduplizierung über Dateien", " if mid in GESEHEN:", " if False:", "kopie-vorn"),
|
||||
("keine Haiku-Stufe", 'if mk == "claude-haiku-5-5" and ctx > 100000:', 'if mk == "claude-haiku-5-5" and ctx > 10**12:', "haiku-ueber-100k"),
|
||||
("Haiku-Grenze einschließlich", 'if mk == "claude-haiku-5-5" and ctx > 100000:', 'if mk == "claude-haiku-5-5" and ctx >= 100000:', "haiku-grenze-100k"),
|
||||
("Teilausgabe nicht nach Version", ' agg["teilausgabe_version"][rec["version"]] += 1\n', "", "teilausgabe-erste-zeile"),
|
||||
("Zeile ohne Typprüfung", " if not isinstance(o, dict):\n continue\n", "", "kaputte-zeilen"),
|
||||
("Vault-Sperre fehlt", " if im_vault(OUT):", " if False:", "ausgabe-im-vault"),
|
||||
("fehlender Ordner nicht gemeldet", " if not os.path.isdir(ROOT):", " if False:", "projects-fehlt"),
|
||||
("Auswahl ohne Filter", "if s[\"turns_seit\"] > 0 and any(rx.search", "if s[\"turns_seit\"] > 0 and True or any(rx.search", "auswahl"),
|
||||
]
|
||||
|
||||
|
||||
def fahre(skript, nur=None):
|
||||
ergebnisse = {}
|
||||
for name, fall in FAELLE:
|
||||
if nur and name not in nur:
|
||||
continue
|
||||
tmp = tempfile.mkdtemp(prefix="va-fall-")
|
||||
try:
|
||||
ergebnisse[name] = fall(skript, tmp)
|
||||
except Exception as e: # Ein Absturz des Falls ist ein roter Fall, kein Abbruch des Treibers.
|
||||
ergebnisse[name] = f"Absturz im Fall: {type(e).__name__}: {e}"
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
if name == "grundlinie" and ergebnisse[name]:
|
||||
break
|
||||
return ergebnisse
|
||||
|
||||
|
||||
def main(argv):
|
||||
rot = 0
|
||||
erg = fahre(SKRIPT)
|
||||
for name, _ in FAELLE:
|
||||
if name not in erg:
|
||||
print(f"NICHT GEFAHREN {name} (Grundlinie rot)")
|
||||
rot += 1
|
||||
continue
|
||||
print(("ROT " if erg[name] else "gruen ") + name + (f": {erg[name]}" if erg[name] else ""))
|
||||
rot += bool(erg[name])
|
||||
if rot or "--ohne-mutanten" in argv:
|
||||
print(f"{len(FAELLE) - rot} von {len(FAELLE)} Fällen grün")
|
||||
return 1 if rot else 0
|
||||
quelle = SKRIPT.read_text(encoding="utf-8")
|
||||
# Die Kopie liegt im Temp; ohne festgenagelten VAULT hielte sie das Temp für den Vault und lehnte jede Ausgabe ab.
|
||||
vault_zeile = "VAULT = Path(__file__).resolve().parents[2]"
|
||||
if quelle.count(vault_zeile) != 1:
|
||||
print("Mutantenmuster veraltet: VAULT-Zeile")
|
||||
return 2
|
||||
quelle = quelle.replace(vault_zeile, f"VAULT = Path({str(VAULT)!r})")
|
||||
entkommen = 0
|
||||
for name, alt, neu, ziel in MUTANTEN:
|
||||
if quelle.count(alt) != 1:
|
||||
print(f"Mutantenmuster veraltet ({quelle.count(alt)} Treffer): {name}")
|
||||
return 2
|
||||
tmp = Path(tempfile.mkdtemp(prefix="va-mut-"))
|
||||
try:
|
||||
m = tmp / "analyse_transkripte.py"
|
||||
m.write_text(quelle.replace(alt, neu), encoding="utf-8")
|
||||
e = fahre(m, nur={"grundlinie", ziel})
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
if e.get("grundlinie"):
|
||||
print(f"UNGUELTIG {name}: Grundlinie der Mutante rot ({e['grundlinie']})")
|
||||
entkommen += 1
|
||||
continue
|
||||
gefangen = bool(e.get(ziel))
|
||||
entkommen += not gefangen
|
||||
print(("gefangen " if gefangen else "ENTKOMMEN ") + f"{name} -> {ziel}" + (f" ({e.get(ziel)})" if gefangen else ""))
|
||||
print(f"{len(FAELLE)} Fälle grün; {len(MUTANTEN) - entkommen} von {len(MUTANTEN)} Mutanten gefangen")
|
||||
return 1 if entkommen else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
@@ -0,0 +1,2 @@
|
||||
{"type": "custom-title", "customTitle": "Paket Probe eins"}
|
||||
{"type": "assistant", "timestamp": "2026-10-20T10:00:00.000Z", "version": "2.1.300", "effort": "high", "isSidechain": false, "message": {"id": "msg_s1", "model": "claude-opus-5-5", "stop_reason": "end_turn", "usage": {"input_tokens": 1000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 100}, "content": [{"type": "text", "text": "synthetisch"}]}}
|
||||
@@ -0,0 +1,2 @@
|
||||
{"type": "custom-title", "customTitle": "Anderes Thema"}
|
||||
{"type": "assistant", "timestamp": "2026-10-20T10:00:00.000Z", "version": "2.1.300", "effort": "high", "isSidechain": false, "message": {"id": "msg_s2", "model": "claude-opus-5-5", "stop_reason": "end_turn", "usage": {"input_tokens": 1000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 100}, "content": [{"type": "text", "text": "synthetisch"}]}}
|
||||
@@ -0,0 +1,2 @@
|
||||
{"type": "user", "timestamp": "2026-10-20T09:59:00.000Z", "message": {"role": "user", "content": "synthetisch"}}
|
||||
{"type": "assistant", "timestamp": "2026-10-20T10:00:00.000Z", "version": "2.1.300", "effort": "high", "isSidechain": false, "message": {"id": "msg_g1", "model": "claude-opus-5-5", "stop_reason": "end_turn", "usage": {"input_tokens": 1000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 100}, "content": [{"type": "text", "text": "synthetisch"}]}}
|
||||
@@ -0,0 +1,2 @@
|
||||
{"type": "user", "timestamp": "2026-10-20T09:59:00.000Z", "message": {"role": "user", "content": "synthetisch"}}
|
||||
{"type": "assistant", "timestamp": "2026-10-20T10:00:00.000Z", "version": "2.1.300", "effort": "high", "isSidechain": false, "message": {"id": "msg_k1", "model": "claude-haiku-5-5", "stop_reason": "end_turn", "usage": {"input_tokens": 100000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 0}, "content": [{"type": "text", "text": "synthetisch"}]}}
|
||||
@@ -0,0 +1,2 @@
|
||||
{"type": "assistant", "timestamp": "2026-10-20T10:00:00.000Z", "version": "2.1.300", "effort": "high", "isSidechain": false, "message": {"id": "msg_h1", "model": "claude-haiku-5-5", "stop_reason": "end_turn", "usage": {"input_tokens": 150000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 1000}, "content": [{"type": "text", "text": "synthetisch"}]}}
|
||||
{"type": "assistant", "timestamp": "2026-10-20T10:01:00.000Z", "version": "2.1.300", "effort": "high", "isSidechain": false, "message": {"id": "msg_h2", "model": "claude-haiku-5-5", "stop_reason": "end_turn", "usage": {"input_tokens": 50000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 1000}, "content": [{"type": "text", "text": "synthetisch"}]}}
|
||||
@@ -0,0 +1,3 @@
|
||||
{kein json
|
||||
[1]
|
||||
{"type": "assistant", "timestamp": "2026-10-20T10:00:00.000Z", "version": "2.1.300", "effort": "high", "isSidechain": false, "message": {"id": "msg_z1", "model": "claude-opus-5-5", "stop_reason": "end_turn", "usage": {"input_tokens": 1000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 100}, "content": [{"type": "text", "text": "synthetisch"}]}}
|
||||
@@ -0,0 +1,2 @@
|
||||
{"type": "user", "timestamp": "2026-10-20T09:59:00.000Z", "message": {"role": "user", "content": "synthetisch"}}
|
||||
{"type": "assistant", "timestamp": "2026-10-20T10:00:00.000Z", "version": "2.1.300", "effort": "high", "isSidechain": false, "message": {"id": "msg_x", "model": "claude-opus-5-5", "stop_reason": "end_turn", "usage": {"input_tokens": 1000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 100}, "content": [{"type": "text", "text": "synthetisch"}]}}
|
||||
@@ -0,0 +1,2 @@
|
||||
{"type": "assistant", "timestamp": "2026-10-20T11:00:00.000Z", "version": "2.1.300", "effort": "high", "isSidechain": false, "message": {"id": "msg_y", "model": "claude-opus-5-5", "stop_reason": "end_turn", "usage": {"input_tokens": 2000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 200}, "content": [{"type": "text", "text": "synthetisch"}]}}
|
||||
{"type": "assistant", "timestamp": "2026-10-20T10:00:00.000Z", "version": "2.1.300", "effort": "high", "isSidechain": false, "message": {"id": "msg_x", "model": "claude-opus-5-5", "stop_reason": "end_turn", "usage": {"input_tokens": 1000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 100}, "content": [{"type": "text", "text": "synthetisch"}]}}
|
||||
@@ -0,0 +1,2 @@
|
||||
{"type": "user", "timestamp": "2026-10-20T09:59:00.000Z", "message": {"role": "user", "content": "synthetisch"}}
|
||||
{"type": "assistant", "timestamp": "2026-10-20T10:00:00.000Z", "version": "2.1.300", "effort": "high", "isSidechain": false, "message": {"id": "msg_x", "model": "claude-opus-5-5", "stop_reason": "end_turn", "usage": {"input_tokens": 1000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 100}, "content": [{"type": "text", "text": "synthetisch"}]}}
|
||||
@@ -0,0 +1,2 @@
|
||||
{"type": "assistant", "timestamp": "2026-10-20T10:00:00.000Z", "version": "2.1.300", "effort": "high", "isSidechain": false, "message": {"id": "msg_x", "model": "claude-opus-5-5", "stop_reason": "end_turn", "usage": {"input_tokens": 1000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 100}, "content": [{"type": "text", "text": "synthetisch"}]}}
|
||||
{"type": "assistant", "timestamp": "2026-10-20T11:00:00.000Z", "version": "2.1.300", "effort": "high", "isSidechain": false, "message": {"id": "msg_y", "model": "claude-opus-5-5", "stop_reason": "end_turn", "usage": {"input_tokens": 2000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 200}, "content": [{"type": "text", "text": "synthetisch"}]}}
|
||||
@@ -0,0 +1,2 @@
|
||||
{"type": "user", "timestamp": "2026-10-20T09:59:00.000Z", "message": {"role": "user", "content": "synthetisch"}}
|
||||
{"type": "assistant", "timestamp": "2026-10-20T10:00:00.000Z", "version": "2.1.300", "effort": "high", "isSidechain": false, "message": {"id": "msg_t0", "model": "claude-opus-5-5", "stop_reason": "end_turn", "usage": {"input_tokens": 1000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 100}, "content": [{"type": "text", "text": "synthetisch"}]}}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
{"type": "assistant", "timestamp": "2026-10-20T10:00:00.000Z", "version": "2.1.300", "effort": "high", "isSidechain": true, "message": {"id": "msg_t1", "model": "claude-opus-5-5", "stop_reason": "end_turn", "usage": {"input_tokens": 1000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 7}, "content": [{"type": "text", "text": "synthetisch"}]}}
|
||||
{"type": "assistant", "timestamp": "2026-10-20T10:00:05.000Z", "version": "2.1.300", "effort": "high", "isSidechain": true, "message": {"id": "msg_t1", "model": "claude-opus-5-5", "stop_reason": "end_turn", "usage": {"input_tokens": 1000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 900}, "content": [{"type": "text", "text": "synthetisch"}]}}
|
||||
@@ -0,0 +1,2 @@
|
||||
{"type": "user", "timestamp": "2026-10-20T09:59:00.000Z", "message": {"role": "user", "content": "synthetisch"}}
|
||||
{"type": "assistant", "timestamp": "2026-10-20T10:00:00.000Z", "version": "2.1.300", "effort": "high", "isSidechain": false, "message": {"id": "msg_l0", "model": "claude-opus-5-5", "stop_reason": "end_turn", "usage": {"input_tokens": 1000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 100}, "content": [{"type": "text", "text": "synthetisch"}]}}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
{"type": "assistant", "timestamp": "2026-10-20T10:00:00.000Z", "version": "2.1.300", "effort": "high", "isSidechain": true, "message": {"id": "msg_l1", "model": "claude-opus-5-5", "stop_reason": "end_turn", "usage": {"input_tokens": 1000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 900}, "content": [{"type": "text", "text": "synthetisch"}]}}
|
||||
{"type": "assistant", "timestamp": "2026-10-20T10:00:05.000Z", "version": "2.1.300", "effort": "high", "isSidechain": true, "message": {"id": "msg_l1", "model": "claude-opus-5-5", "stop_reason": "end_turn", "usage": {"input_tokens": 1000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 7}, "content": [{"type": "text", "text": "synthetisch"}]}}
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Listet die Sitzungs-Scratchpads eines Projekts mit Größe, Titel und letzter Aktivität des Transkripts.
|
||||
|
||||
Ersteller: Pascal Michel
|
||||
Erstellt: 2026-10-30
|
||||
Doku: LIESMICH.md in diesem Ordner
|
||||
|
||||
Nur lesend. Gibt Sitzungskennung (acht Zeichen), Titel, Größe und Alter aus, keine Dateiinhalte.
|
||||
Was es nicht sieht: welche Sitzungen gerade laufen. Die nennst du mit --laufend (Kennungsanfänge der
|
||||
Claude-Code-Sitzung, nicht die local_-Kennung der App; Zuordnung nur über den Titel). Das Alter kommt
|
||||
aus der mtime des Transkripts, die nach einem App-Neustart für viele Sitzungen frisch ist
|
||||
(Harness-Erinnerung transkript-usage-groesster-wert-je-nachricht).
|
||||
Aufruf: python -I -X utf8 scratchpads.py [--scratch <Ordner>] [--projekt <Ordner>] [--laufend id8,id8] [--ab-mb 50]
|
||||
"""
|
||||
import argparse
|
||||
import collections
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
def groesse_kb(p):
|
||||
total = 0
|
||||
for root, dirs, files in os.walk(p):
|
||||
# Junctions nicht betreten, sonst zählt ein verlinkter Ordner doppelt (os.path.isjunction ab Python 3.12).
|
||||
dirs[:] = [d for d in dirs if not getattr(os.path, "isjunction", lambda _: False)(os.path.join(root, d))]
|
||||
for f in files:
|
||||
try:
|
||||
total += os.lstat(os.path.join(root, f)).st_size
|
||||
except OSError:
|
||||
pass
|
||||
return total // 1024
|
||||
|
||||
|
||||
def titel_aus(ct):
|
||||
try:
|
||||
with open(ct, encoding="utf-8") as f:
|
||||
d = json.load(f)
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
return (d.get("customTitle") or d.get("title")) if isinstance(d, dict) else None
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description="Sitzungs-Scratchpads mit Größe und letzter Aktivität")
|
||||
ap.add_argument("--scratch", default=r"D:\ClaudeData\claude\D--Claude")
|
||||
ap.add_argument("--projekt", default=r"D:\ClaudeData\config\projects\D--Claude")
|
||||
ap.add_argument("--laufend", default="", help="kommagetrennte Kennungsanfänge laufender Sitzungen")
|
||||
ap.add_argument("--ab-mb", type=float, default=50, help="nur Scratchpads ab dieser Größe einzeln auflisten")
|
||||
a = ap.parse_args(argv)
|
||||
if not os.path.isdir(a.scratch) or not os.path.isdir(a.projekt):
|
||||
print(f"Ordner fehlt: {a.scratch if not os.path.isdir(a.scratch) else a.projekt}")
|
||||
return 2
|
||||
laufend = {x.strip()[:8] for x in a.laufend.split(",") if x.strip()}
|
||||
jetzt = time.time()
|
||||
zeilen = []
|
||||
for name in os.listdir(a.scratch):
|
||||
d = os.path.join(a.scratch, name)
|
||||
if not os.path.isdir(d):
|
||||
continue
|
||||
tr = os.path.join(a.projekt, name + ".jsonl")
|
||||
letzte = os.path.getmtime(tr) if os.path.exists(tr) else None
|
||||
titel = titel_aus(os.path.join(a.projekt, name, "custom-title.json"))
|
||||
tage = (jetzt - letzte) / 86400 if letzte else None
|
||||
zeilen.append((groesse_kb(d), name, titel, tage, letzte is not None))
|
||||
|
||||
zeilen.sort(reverse=True)
|
||||
print("| Sitzung | Titel | Scratchpad MB | letzte Transkript-Aktivität | Klasse |")
|
||||
print("|---|---|---|---|---|")
|
||||
summen = collections.Counter(); anzahl = collections.Counter()
|
||||
for kb, name, titel, tage, hat_tr in zeilen:
|
||||
if name[:8] in laufend:
|
||||
klasse = "läuft"
|
||||
elif not hat_tr:
|
||||
klasse = "kein Transkript"
|
||||
elif tage <= 7:
|
||||
klasse = "aktiv ≤ 7 Tage"
|
||||
elif tage <= 14:
|
||||
klasse = "ruhend 7-14 Tage"
|
||||
else:
|
||||
klasse = "ruhend > 14 Tage"
|
||||
summen[klasse] += kb; anzahl[klasse] += 1
|
||||
if kb > a.ab_mb * 1024:
|
||||
print(f"| {name[:8]} | {(titel or '-')[:45]} | {kb/1024:.0f} | {('vor %.1f Tagen' % tage) if tage is not None else '-'} | {klasse} |")
|
||||
print()
|
||||
print("| Klasse | Sitzungen | GB |")
|
||||
print("|---|---|---|")
|
||||
for k in summen:
|
||||
print(f"| {k} | {anzahl[k]} | {summen[k]/1024/1024:.1f} |")
|
||||
print(f"| Summe | {sum(anzahl.values())} | {sum(summen.values())/1024/1024:.1f} |")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user