Agréger l'usage Claude depuis les transcripts locaux #49

Closed
opened 2026-10-04 12:59:46 +00:00 by maximus · 0 comments
Owner

Refs : spec-decisions-suivi-claude.md + spec-plan-suivi-claude.md (dépôt la-compagnie-maximus). Ce corps est autosuffisant : tout ce qu'il faut pour implémenter est ci-dessous.

Module pur de l'agent claude-usage : lit les transcripts de Claude Code (~/.claude/projects/**/*.jsonl) et produit la série de 30 jours par modèle et le top projets du contrat. Aucune dépendance, aucun réseau.

Contrat du snapshot (schemaVersion: 1, figé)

Corps de POST /claude-usage, partagé entre vps-health-api (agent + serveur) et la-compagnie-maximus (dashboard) :

{
  "schemaVersion": 1,
  "generatedAt": "2026-09-27T19:00:00.000Z",     // horloge du poste, informatif
  "claudeCodeVersion": "2.1.283",                 // champ `version` du transcript le plus récent
  "timeZone": "America/Toronto",
  "limits": {
    "status": "ok",          // ok | token_expired | http_error | unavailable | disabled
    "detail": "http_429",    // optionnel : no_credentials | parse_error | no_version | timeout | network | bad_response | http_<code>
    "fetchedAt": "2026-09-27T19:00:00.000Z",     // date des items (dernières valeurs valides), ou null
    "items": [
      { "kind": "session", "group": "session", "percent": 1,
        "severity": "normal", "resetsAt": "2026-09-27T21:30:00Z", "model": null },
      { "kind": "weekly_scoped", "group": "weekly", "percent": 63,
        "severity": "normal", "resetsAt": "2026-10-01T07:00:00Z", "model": "Fable" }
    ]
  },
  "days": [                  // exactement 30 jours Toronto, du plus ancien à aujourd'hui, jours vides inclus
    { "date": "2026-09-27",
      "models": { "claude-opus-5-5": { "input": 2, "output": 1950, "cacheCreation": 43176,
                                        "cacheRead": 33814, "requests": 1 } } }
  ],
  "projects": [              // top 8 sur 30 j (tokens de travail = input + output + cacheCreation), puis "(autres)"
    { "name": "simpl-resultat", "work": 123456, "cacheRead": 7890123, "requests": 812 }
  ]
}

Bornes serveur (sanitizeClaudeUsage, liste blanche, tout champ inconnu jeté) : corps ≤ 64 Ko ; days ≤ 31, date ^\d{4}-\d{2}-\d{2}$ ; models ≤ 12 clés ^[a-z0-9][a-z0-9.\-]{0,63}$ ; limits.items ≤ 12 ; projects ≤ 9 ; nombres Number.isSafeInteger ≥ 0, percent 0-100 ; status dans l'enum ; detail ^[a-z0-9_]{1,32}$ sinon jeté ; autres chaînes ≤ 128 car. ; dates ISO validées. Le serveur ajoute receivedAt.

Réponse de GET /claude-usage : { staleAfterSeconds, receivedAt, ageSeconds, stale, snapshot }, ou { "status": "no_data" } si le fichier est absent ou illisible.

Faits mesurés (2026-09-27, ThinkPad, Claude Code 2.1.283)

  • 438 fichiers dont 129 de sous-agents (<session>/subagents/*.jsonl), ~210 Mo.
  • 16 845 lignes avec message.usage sur 30 j pour 7 118 paires (message.id, requestId) uniques (×2,4). Dans un groupe, entrée et cache sont identiques mais output_tokens croît (ex. 8 puis 1 024).
  • cwd, message.id, requestId, timestamp, version présents sur 100 % des lignes utiles. Modèle <synthetic> présent (à ignorer).
  • ~9 % des requêtes ont un cwd /tmp/spike-defenseur-auto-* (runs defenseur-auto).

Tâches

  • agent/claude-usage/aggregate.js, pur : entrées = racine des transcripts, frozenDays (objet date → {models, projects}), date du jour Toronto, homedir ; sortie = { openDays, days, projects, claudeCodeVersion } où openDays sont les jours recalculés (non figés) et days la série fusionnée de 30 jours.
  • Parcours récursif **/*.jsonl (sous-agents compris). Seuil de scan = lendemain du plus récent jour figé (aucun seuil s'il n'y en a pas) : ne lire que les fichiers dont le jour Toronto du mtime est ≥ seuil. Lecture en flux (readline), ligne JSON invalide sautée.
  • Garder type === "assistant" avec message.usage ; ignorer le modèle <synthetic> ; ignorer les lignes dont le jour du timestamp est figé.
  • Déduplication par (message.id, requestId) sur tous les fichiers, en gardant l'entrée au plus grand output_tokens.
  • Compteurs : input = input_tokens, output = output_tokens, cacheCreation = cache_creation_input_tokens, cacheRead = cache_read_input_tokens, requests = nombre de paires.
  • Dates en chaînes uniquement : jour = Intl.DateTimeFormat('en-CA', { timeZone: 'America/Toronto' }) ; décalages via Date.UTC(y, m - 1, d - n) sur les parties. Jamais « maintenant − 86 400 000 ».
  • Mapping projet depuis cwd, dans l'ordre : (1) <home>/claude-code/<a>/... → <a>, et si <a> vaut .spikes ou .archive → <a>/<b> ; (2) <home>/claude-code exactement → (monorepo) ; (3) /tmp/spike-defenseur-auto-* → defenseurs ; (4) /tmp/claude-<uid>/-home-max-claude-code-<p>/... → <p> ; (5) reste → (autre). <home> = os.homedir() (injecté pour les tests).
  • claudeCodeVersion = champ version de la ligne au timestamp le plus récent lue ; null si aucune.
  • Série de 30 jours exactement (aujourd'hui inclus), jours vides inclus ; top 8 projets par work sur la fenêtre puis (autres) (agrégé) s'il reste du volume ; somme = total.
  • Bornes agent avant envoi : au plus 12 modèles par jour (au-delà, les moins consommateurs fusionnés dans une clé other).

Fichiers concernés

  • agent/claude-usage/aggregate.js (nouveau)
  • __tests__/agent-claude-usage.test.js (nouveau, partagé avec l'issue de l'agent)
  • __tests__/fixtures/claude-usage/ (nouveau, JSONL synthétiques uniquement, aucun vrai transcript)

Critères d'acceptation

  • Doublons répartis entre un fichier de session et un fichier subagents/ → un seul compte, au plus grand output_tokens
  • <synthetic>, lignes user, lignes JSON tronquées ignorées sans exception
  • Une ligne à 23 h 59 et une à 0 h 01 heure de Toronto tombent sur deux jours différents, y compris autour du changement d'heure du 2026-11-01
  • Chaque règle de mapping projet a son cas de test
  • La série compte toujours 30 entrées, jours vides compris, la plus récente = aujourd'hui Toronto
  • Top 8 + (autres) : la somme des work égale le total de la fenêtre
  • Avec un jour figé fourni, aucun fichier dont le jour du mtime est antérieur au seuil n'est ouvert (espion sur l'ouverture de fichiers)

Review caveats

  • 🔴 (résolu) Le seuil de scan dépend des jours figés ; l'issue de l'agent fige aussi les jours vides, sinon le seuil reste ~30 j en arrière et chaque passage relit ~210 Mo.
  • 🟡 (résolu) Arithmétique de dates en chaînes : les jours de 23 h / 25 h cassent les calculs en millisecondes.
  • 🟡 (résolu) Runs defenseur-auto en /tmp : sans règle dédiée, 9 % du volume tombe en (autre).

Décisions prises en planification

  • Métrique principale = tokens de travail (input + output + cacheCreation) ; lectures de cache comptées à part.
  • Tout compter : sous-agents et runs claude -p.
  • Top 8 projets (même N que la carte).

Spec source

la-compagnie-maximus : spec-plan-suivi-claude.md, Issue 2

Refs : [spec-decisions-suivi-claude.md](https://git.lacompagniemaximus.com/maximus/la-compagnie-maximus/src/branch/main/spec-decisions-suivi-claude.md) + [spec-plan-suivi-claude.md](https://git.lacompagniemaximus.com/maximus/la-compagnie-maximus/src/branch/main/spec-plan-suivi-claude.md) (dépôt la-compagnie-maximus). Ce corps est autosuffisant : tout ce qu'il faut pour implémenter est ci-dessous. Module pur de l'agent `claude-usage` : lit les transcripts de Claude Code (`~/.claude/projects/**/*.jsonl`) et produit la série de 30 jours par modèle et le top projets du contrat. Aucune dépendance, aucun réseau. ## Contrat du snapshot (`schemaVersion: 1`, figé) Corps de `POST /claude-usage`, partagé entre `vps-health-api` (agent + serveur) et `la-compagnie-maximus` (dashboard) : ```jsonc { "schemaVersion": 1, "generatedAt": "2026-09-27T19:00:00.000Z", // horloge du poste, informatif "claudeCodeVersion": "2.1.283", // champ `version` du transcript le plus récent "timeZone": "America/Toronto", "limits": { "status": "ok", // ok | token_expired | http_error | unavailable | disabled "detail": "http_429", // optionnel : no_credentials | parse_error | no_version | timeout | network | bad_response | http_<code> "fetchedAt": "2026-09-27T19:00:00.000Z", // date des items (dernières valeurs valides), ou null "items": [ { "kind": "session", "group": "session", "percent": 1, "severity": "normal", "resetsAt": "2026-09-27T21:30:00Z", "model": null }, { "kind": "weekly_scoped", "group": "weekly", "percent": 63, "severity": "normal", "resetsAt": "2026-10-01T07:00:00Z", "model": "Fable" } ] }, "days": [ // exactement 30 jours Toronto, du plus ancien à aujourd'hui, jours vides inclus { "date": "2026-09-27", "models": { "claude-opus-5-5": { "input": 2, "output": 1950, "cacheCreation": 43176, "cacheRead": 33814, "requests": 1 } } } ], "projects": [ // top 8 sur 30 j (tokens de travail = input + output + cacheCreation), puis "(autres)" { "name": "simpl-resultat", "work": 123456, "cacheRead": 7890123, "requests": 812 } ] } ``` Bornes serveur (`sanitizeClaudeUsage`, liste blanche, tout champ inconnu jeté) : corps ≤ 64 Ko ; `days` ≤ 31, `date` `^\d{4}-\d{2}-\d{2}$` ; `models` ≤ 12 clés `^[a-z0-9][a-z0-9.\-]{0,63}$` ; `limits.items` ≤ 12 ; `projects` ≤ 9 ; nombres `Number.isSafeInteger` ≥ 0, `percent` 0-100 ; `status` dans l'enum ; `detail` `^[a-z0-9_]{1,32}$` sinon jeté ; autres chaînes ≤ 128 car. ; dates ISO validées. Le serveur ajoute `receivedAt`. Réponse de `GET /claude-usage` : `{ staleAfterSeconds, receivedAt, ageSeconds, stale, snapshot }`, ou `{ "status": "no_data" }` si le fichier est absent ou illisible. ## Faits mesurés (2026-09-27, ThinkPad, Claude Code 2.1.283) - 438 fichiers dont 129 de sous-agents (`<session>/subagents/*.jsonl`), ~210 Mo. - 16 845 lignes avec `message.usage` sur 30 j pour 7 118 paires (`message.id`, `requestId`) uniques (×2,4). Dans un groupe, entrée et cache sont identiques mais `output_tokens` croît (ex. 8 puis 1 024). - `cwd`, `message.id`, `requestId`, `timestamp`, `version` présents sur 100 % des lignes utiles. Modèle `<synthetic>` présent (à ignorer). - ~9 % des requêtes ont un `cwd` `/tmp/spike-defenseur-auto-*` (runs `defenseur-auto`). ## Tâches - [ ] `agent/claude-usage/aggregate.js`, pur : entrées = racine des transcripts, `frozenDays` (objet date → `{models, projects}`), date du jour Toronto, `homedir` ; sortie = `{ openDays, days, projects, claudeCodeVersion }` où `openDays` sont les jours recalculés (non figés) et `days` la série fusionnée de 30 jours. - [ ] Parcours récursif `**/*.jsonl` (sous-agents compris). Seuil de scan = lendemain du plus récent jour figé (aucun seuil s'il n'y en a pas) : ne lire que les fichiers dont le jour Toronto du `mtime` est ≥ seuil. Lecture en flux (`readline`), ligne JSON invalide sautée. - [ ] Garder `type === "assistant"` avec `message.usage` ; ignorer le modèle `<synthetic>` ; ignorer les lignes dont le jour du `timestamp` est figé. - [ ] Déduplication par (`message.id`, `requestId`) sur tous les fichiers, en gardant l'entrée au plus grand `output_tokens`. - [ ] Compteurs : `input` = `input_tokens`, `output` = `output_tokens`, `cacheCreation` = `cache_creation_input_tokens`, `cacheRead` = `cache_read_input_tokens`, `requests` = nombre de paires. - [ ] Dates en chaînes uniquement : jour = `Intl.DateTimeFormat('en-CA', { timeZone: 'America/Toronto' })` ; décalages via `Date.UTC(y, m - 1, d - n)` sur les parties. Jamais « maintenant − 86 400 000 ». - [ ] Mapping projet depuis `cwd`, dans l'ordre : (1) `<home>/claude-code/<a>/...` → `<a>`, et si `<a>` vaut `.spikes` ou `.archive` → `<a>/<b>` ; (2) `<home>/claude-code` exactement → `(monorepo)` ; (3) `/tmp/spike-defenseur-auto-*` → `defenseurs` ; (4) `/tmp/claude-<uid>/-home-max-claude-code-<p>/...` → `<p>` ; (5) reste → `(autre)`. `<home>` = `os.homedir()` (injecté pour les tests). - [ ] `claudeCodeVersion` = champ `version` de la ligne au `timestamp` le plus récent lue ; `null` si aucune. - [ ] Série de 30 jours exactement (aujourd'hui inclus), jours vides inclus ; top 8 projets par `work` sur la fenêtre puis `(autres)` (agrégé) s'il reste du volume ; somme = total. - [ ] Bornes agent avant envoi : au plus 12 modèles par jour (au-delà, les moins consommateurs fusionnés dans une clé `other`). ## Fichiers concernés - `agent/claude-usage/aggregate.js` (nouveau) - `__tests__/agent-claude-usage.test.js` (nouveau, partagé avec l'issue de l'agent) - `__tests__/fixtures/claude-usage/` (nouveau, JSONL synthétiques uniquement, aucun vrai transcript) ## Critères d'acceptation - [ ] Doublons répartis entre un fichier de session et un fichier `subagents/` → un seul compte, au plus grand `output_tokens` - [ ] `<synthetic>`, lignes `user`, lignes JSON tronquées ignorées sans exception - [ ] Une ligne à 23 h 59 et une à 0 h 01 heure de Toronto tombent sur deux jours différents, y compris autour du changement d'heure du 2026-11-01 - [ ] Chaque règle de mapping projet a son cas de test - [ ] La série compte toujours 30 entrées, jours vides compris, la plus récente = aujourd'hui Toronto - [ ] Top 8 + `(autres)` : la somme des `work` égale le total de la fenêtre - [ ] Avec un jour figé fourni, aucun fichier dont le jour du mtime est antérieur au seuil n'est ouvert (espion sur l'ouverture de fichiers) ## Review caveats - 🔴 (résolu) Le seuil de scan dépend des jours figés ; l'issue de l'agent fige aussi les jours vides, sinon le seuil reste ~30 j en arrière et chaque passage relit ~210 Mo. - 🟡 (résolu) Arithmétique de dates en chaînes : les jours de 23 h / 25 h cassent les calculs en millisecondes. - 🟡 (résolu) Runs `defenseur-auto` en `/tmp` : sans règle dédiée, 9 % du volume tombe en `(autre)`. ## Décisions prises en planification - Métrique principale = tokens de travail (input + output + cacheCreation) ; lectures de cache comptées à part. - Tout compter : sous-agents et runs `claude -p`. - Top 8 projets (même N que la carte). ## Spec source la-compagnie-maximus : `spec-plan-suivi-claude.md`, Issue 2
maximus added this to the planned-2026-10-04-suivi-claude milestone 2026-10-04 12:59:46 +00:00
maximus added the
status:ready
type:feature
source:human
labels 2026-10-04 12:59:46 +00:00
maximus added
status:in-progress
and removed
status:ready
labels 2026-10-04 13:05:56 +00:00
maximus added
status:review
and removed
status:in-progress
labels 2026-10-04 13:11:20 +00:00
maximus added
status:needs-fix
and removed
status:review
labels 2026-10-04 14:47:25 +00:00
maximus added
status:review
and removed
status:needs-fix
labels 2026-10-04 19:26:39 +00:00
maximus added
status:approved
and removed
status:review
labels 2026-10-04 19:28:13 +00:00
maximus removed the
status:approved
label 2026-10-04 19:30:05 +00:00
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/vps-health-api#49
No description provided.