claude-usage : constats non bloquants des revues de la pile #52/#51/#53 #54

Closed
opened 2026-10-04 19:30:24 +00:00 by maximus · 3 comments
Owner

Contexte

Constats non bloquants des revues de la pile #52 -> #51 -> #53 (milestone planned-2026-10-04-suivi-claude, mergee le 2026-10-04), repris pour ne pas mourir avec les PRs fermees. Analyse du 2026-10-07 sur main @ 719d651 : les six premisses sont confirmees dans le code. Arbitrages de Max consignes plus bas.

Une seule issue, une seule PR, deux commits (serveur, agent) : les deux cotes se livrent separement (redeploy Coolify pour le serveur, copie de fichiers sur le ThinkPad pour l'agent, voir agent/README.md).

Partie serveur (index.js, claude-usage.js)

S1. Nettoyage du temporaire jamais exerce

__tests__/claude-usage.test.js:739 (« 500 when the snapshot cannot be written, and no temp file is left ») echoue des le mkdirSync, avant que le temporaire existe. Le unlinkSync(tmpPath) de writeClaudeUsage() (index.js:529) n'est donc jamais execute : le retirer laisse tout vert.

  • Ajouter un cas ou le renameSync echoue apres l'ecriture du temporaire : CLAUDE_USAGE_PATH pointe sur un dossier non vide. Exiger 500, et aucun .tmp dans le dossier.
  • Meme trou cote writeHostSnapshot() (index.js:431) : hosts.test.js n'a aucun cas d'echec du rename. Ajouter le cas symetrique (<HOSTS_DIR>/thinkpad.json cree comme dossier non vide).

S2. receivedAt relu accepte par new Date()

readClaudeUsage() (index.js:557) et readHostSnapshot() (index.js:503) acceptent tout ce que new Date() sait lire, "1" compris.

  • Exiger une date ISO complete avec decalage, en reutilisant ISO_TIMESTAMP_RE / cleanTimestamp() de claude-usage.js (a exporter), sans dupliquer la regex.
  • Tests : receivedAt: "1", "2026-10-07" et "2026-10-07T12:00:00" (sans decalage) degradent en no_data cote /claude-usage, et en neverSeen cote /hosts.

Partie agent (agent/claude-usage/)

A1. Double comptage d'une requete a cheval sur minuit (decision : corriger)

Un jour ne se fige qu'a J+2 (nextState(), state.js:96). Le passage qui le fige voit donc toutes les lignes de la requete, et la deduplication l'attribue au jour de sa meilleure ligne. En cas d'egalite d'output (aggregate.js:378, > strict), c'est la ligne la plus ancienne qui gagne. Si cette ligne tombe sur la veille, le jour est fige avec la requete. Aux passages suivants, le filtre « jour fige » (aggregate.js:360) ecarte cette ligne avant la deduplication. La ligne d'apres minuit reste seule et la requete est comptee une seconde fois sur le jour ouvert.

  • Implementation retenue : deplacer le filtre « jour fige » apres la deduplication. On deduplique toutes les lignes de la fenetre, puis on ecarte les requetes dont le jour retenu est fige. On reproduit ainsi la decision du passage qui a fige le jour, sans changer le schema de state.json. Variante ecartee : memoriser les cles (message.id, requestId) dans state.json, plus lourde pour le meme resultat.
  • Test : une requete dont les deux lignes ont le meme output (23:59 le jour D-1, 00:01 le jour D) et D-1 fige. La requete ne doit pas apparaitre sur D. Second cas : meilleure ligne sur D, et D-1 fige sans la requete, qui reste comptee une fois sur D.

A2. Erreur de lecture : ne rien figer (decision)

readLines() (aggregate.js:254) avale une erreur de flux. listTranscripts() saute aussi en silence un readdir en echec, donc un dossier de projet entier. La boucle principale saute un stat en echec. Le jour est ensuite fige incomplet pour 35 jours.

  • aggregate() rend un drapeau (ex. readErrors: number), compte sur les trois chemins : readdir, stat et flux. Un ENOENT sur la racine elle-meme reste un scan vide, pas une erreur.
  • push-claude-usage.js : si readErrors > 0, nextState() ne fige aucun nouveau jour lors de ce passage (les jours deja figes et lastGoodLimits suivent la regle actuelle). Le snapshot part quand meme.
  • Une ligne logger sans chemin ni message d'erreur, avec un compteur seulement (ex. read_errors=N), selon la regle de sortie de l'agent.
  • Tests : openFile qui emet error sur un fichier, readdir qui echoue sur un sous-dossier. Aucun jour nouvellement fige, snapshot envoye. Le passage suivant, sans erreur, fige normalement.

A3. MAX_RESPONSE_BYTES verifie trop tard (decision : lecture bornee)

readBody() (limits.js:177) lit tout par res.text(), puis compare text.length (des caracteres) a 256 Kio.

  • Lire res.body morceau par morceau en comptant les octets, et annuler le flux (cancel) des que le cumul depasse MAX_RESPONSE_BYTES. Le resultat reste dans l'enum detail existant, sans nouveau texte libre.
  • Tests : double de fetch dont le corps est un flux. Corps de 256 Kio + 1 octet en caracteres multi-octets, sous 256 Ki caracteres : refuse. Flux infini : refuse et annule sans tout lire. Corps de 256 Kio pile : accepte.

A4. chmodSync de saveState non couvert

state.js:141 : le cas existant (agent-claude-usage.test.js:1281) cree un temporaire neuf, et mode: 0o600 suffit alors.

  • Test : creer d'abord ${file}.${process.pid}.tmp en mode 644 (reste d'un passage tue), puis saveState : le fichier final est en mode 600.

Fichiers concernes

  • index.js : readClaudeUsage(), readHostSnapshot()
  • claude-usage.js : export du validateur de date ISO
  • agent/claude-usage/aggregate.js : ordre filtre/deduplication, compteur d'erreurs de lecture
  • agent/claude-usage/push-claude-usage.js, agent/claude-usage/state.js (nextState, option pour ne pas figer)
  • agent/claude-usage/limits.js : readBody()
  • __tests__/claude-usage.test.js, __tests__/hosts.test.js, __tests__/agent-claude-usage.test.js
  • CLAUDE.md (compteurs de cas), agent/README.md (comportement en cas d'erreur de lecture)

Criteres d'acceptation

  • Chaque correction a un test qui rougit si on la retire (verification par mutation, a la main)
  • npm test, bash test-curl.sh, sh scripts/boot-from-copy.sh verts, CI verte
  • writes.test.js reste vert (aucune nouvelle surface d'ecriture)
  • Aucun secret ni chemin de transcript dans les sorties de l'agent
  • Compteurs de cas a jour dans CLAUDE.md
  • Apres le merge : redeploy Coolify (serveur), puis recopie de agent/claude-usage/ sur le ThinkPad (a confirmer selon agent/README.md)

Complexite

Medium : six petits volets independants, A1 et A2 touchent le coeur de l'agregation.

## Contexte Constats non bloquants des revues de la pile #52 -> #51 -> #53 (milestone `planned-2026-10-04-suivi-claude`, mergee le 2026-10-04), repris pour ne pas mourir avec les PRs fermees. Analyse du 2026-10-07 sur `main` @ `719d651` : les six premisses sont confirmees dans le code. Arbitrages de Max consignes plus bas. Une seule issue, une seule PR, deux commits (serveur, agent) : les deux cotes se livrent separement (redeploy Coolify pour le serveur, copie de fichiers sur le ThinkPad pour l'agent, voir `agent/README.md`). ## Partie serveur (`index.js`, `claude-usage.js`) ### S1. Nettoyage du temporaire jamais exerce `__tests__/claude-usage.test.js:739` (« 500 when the snapshot cannot be written, and no temp file is left ») echoue des le `mkdirSync`, avant que le temporaire existe. Le `unlinkSync(tmpPath)` de `writeClaudeUsage()` (`index.js:529`) n'est donc jamais execute : le retirer laisse tout vert. - [ ] Ajouter un cas ou le `renameSync` echoue apres l'ecriture du temporaire : `CLAUDE_USAGE_PATH` pointe sur un dossier non vide. Exiger 500, et aucun `.tmp` dans le dossier. - [ ] Meme trou cote `writeHostSnapshot()` (`index.js:431`) : `hosts.test.js` n'a aucun cas d'echec du `rename`. Ajouter le cas symetrique (`<HOSTS_DIR>/thinkpad.json` cree comme dossier non vide). ### S2. `receivedAt` relu accepte par `new Date()` `readClaudeUsage()` (`index.js:557`) et `readHostSnapshot()` (`index.js:503`) acceptent tout ce que `new Date()` sait lire, `"1"` compris. - [ ] Exiger une date ISO complete avec decalage, en reutilisant `ISO_TIMESTAMP_RE` / `cleanTimestamp()` de `claude-usage.js` (a exporter), sans dupliquer la regex. - [ ] Tests : `receivedAt: "1"`, `"2026-10-07"` et `"2026-10-07T12:00:00"` (sans decalage) degradent en `no_data` cote `/claude-usage`, et en `neverSeen` cote `/hosts`. ## Partie agent (`agent/claude-usage/`) ### A1. Double comptage d'une requete a cheval sur minuit (decision : corriger) Un jour ne se fige qu'a J+2 (`nextState()`, `state.js:96`). Le passage qui le fige voit donc toutes les lignes de la requete, et la deduplication l'attribue au jour de sa meilleure ligne. En cas d'egalite d'`output` (`aggregate.js:378`, `>` strict), c'est la ligne la plus ancienne qui gagne. Si cette ligne tombe sur la veille, le jour est fige avec la requete. Aux passages suivants, le filtre « jour fige » (`aggregate.js:360`) ecarte cette ligne **avant** la deduplication. La ligne d'apres minuit reste seule et la requete est comptee une seconde fois sur le jour ouvert. - [ ] Implementation retenue : deplacer le filtre « jour fige » **apres** la deduplication. On deduplique toutes les lignes de la fenetre, puis on ecarte les requetes dont le jour retenu est fige. On reproduit ainsi la decision du passage qui a fige le jour, sans changer le schema de `state.json`. Variante ecartee : memoriser les cles `(message.id, requestId)` dans `state.json`, plus lourde pour le meme resultat. - [ ] Test : une requete dont les deux lignes ont le meme `output` (23:59 le jour D-1, 00:01 le jour D) et D-1 fige. La requete ne doit pas apparaitre sur D. Second cas : meilleure ligne sur D, et D-1 fige sans la requete, qui reste comptee une fois sur D. ### A2. Erreur de lecture : ne rien figer (decision) `readLines()` (`aggregate.js:254`) avale une erreur de flux. `listTranscripts()` saute aussi en silence un `readdir` en echec, donc un dossier de projet entier. La boucle principale saute un `stat` en echec. Le jour est ensuite fige incomplet pour 35 jours. - [ ] `aggregate()` rend un drapeau (ex. `readErrors: number`), compte sur les trois chemins : `readdir`, `stat` et flux. Un `ENOENT` sur la racine elle-meme reste un scan vide, pas une erreur. - [ ] `push-claude-usage.js` : si `readErrors > 0`, `nextState()` ne fige aucun nouveau jour lors de ce passage (les jours deja figes et `lastGoodLimits` suivent la regle actuelle). Le snapshot part quand meme. - [ ] Une ligne `logger` sans chemin ni message d'erreur, avec un compteur seulement (ex. `read_errors=N`), selon la regle de sortie de l'agent. - [ ] Tests : `openFile` qui emet `error` sur un fichier, `readdir` qui echoue sur un sous-dossier. Aucun jour nouvellement fige, snapshot envoye. Le passage suivant, sans erreur, fige normalement. ### A3. `MAX_RESPONSE_BYTES` verifie trop tard (decision : lecture bornee) `readBody()` (`limits.js:177`) lit tout par `res.text()`, puis compare `text.length` (des caracteres) a 256 Kio. - [ ] Lire `res.body` morceau par morceau en comptant les octets, et annuler le flux (`cancel`) des que le cumul depasse `MAX_RESPONSE_BYTES`. Le resultat reste dans l'enum `detail` existant, sans nouveau texte libre. - [ ] Tests : double de `fetch` dont le corps est un flux. Corps de 256 Kio + 1 octet en caracteres multi-octets, sous 256 Ki caracteres : refuse. Flux infini : refuse et annule sans tout lire. Corps de 256 Kio pile : accepte. ### A4. `chmodSync` de `saveState` non couvert `state.js:141` : le cas existant (`agent-claude-usage.test.js:1281`) cree un temporaire neuf, et `mode: 0o600` suffit alors. - [ ] Test : creer d'abord `${file}.${process.pid}.tmp` en mode 644 (reste d'un passage tue), puis `saveState` : le fichier final est en mode 600. ## Fichiers concernes - `index.js` : `readClaudeUsage()`, `readHostSnapshot()` - `claude-usage.js` : export du validateur de date ISO - `agent/claude-usage/aggregate.js` : ordre filtre/deduplication, compteur d'erreurs de lecture - `agent/claude-usage/push-claude-usage.js`, `agent/claude-usage/state.js` (`nextState`, option pour ne pas figer) - `agent/claude-usage/limits.js` : `readBody()` - `__tests__/claude-usage.test.js`, `__tests__/hosts.test.js`, `__tests__/agent-claude-usage.test.js` - `CLAUDE.md` (compteurs de cas), `agent/README.md` (comportement en cas d'erreur de lecture) ## Criteres d'acceptation - [ ] Chaque correction a un test qui rougit si on la retire (verification par mutation, a la main) - [ ] `npm test`, `bash test-curl.sh`, `sh scripts/boot-from-copy.sh` verts, CI verte - [ ] `writes.test.js` reste vert (aucune nouvelle surface d'ecriture) - [ ] Aucun secret ni chemin de transcript dans les sorties de l'agent - [ ] Compteurs de cas a jour dans `CLAUDE.md` - [ ] Apres le merge : redeploy Coolify (serveur), puis recopie de `agent/claude-usage/` sur le ThinkPad (a confirmer selon `agent/README.md`) ## Complexite Medium : six petits volets independants, A1 et A2 touchent le coeur de l'agregation.
maximus added the
status:triage
source:human
type:bug
labels 2026-10-04 19:30:29 +00:00
maximus added
status:ready
and removed
status:triage
labels 2026-10-08 00:15:20 +00:00
maximus added
status:review
and removed
status:ready
labels 2026-10-08 00:30:41 +00:00
maximus added
status:approved
and removed
status:review
labels 2026-10-08 00:39:37 +00:00
maximus added
status:blocked
and removed
status:approved
labels 2026-10-08 00:54:39 +00:00
Author
Owner

PR #56 merged on main (f694677). Waiting on:

  1. Coolify redeploy, then GET /claude-usage and GET /hosts checked for content (not only the code).
  2. Copy of agent/claude-usage/* to ~/.local/share/maximus-claude-usage/ on the ThinkPad, then journalctl -t claude-usage on the next beat.
    The issue is closed by hand once both are done.
PR #56 merged on main (`f694677`). Waiting on: 1. Coolify redeploy, then GET /claude-usage and GET /hosts checked for content (not only the code). 2. Copy of `agent/claude-usage/*` to `~/.local/share/maximus-claude-usage/` on the ThinkPad, then `journalctl -t claude-usage` on the next beat. The issue is closed by hand once both are done.
Author
Owner

Step 1 done (2026-10-07): Coolify deployment finished on f694677. Checked from inside the container (token never leaves it):

  • mounts: /data/defenseurs and /data/defenseurs/reports RW=false, /data/hosts and /data/claude-usage RW=true;
  • GET /claude-usage: content, 30 days, limits ok, stale: false (the stored receivedAt passes the new ISO check);
  • GET /hosts: thinkpad online: true, not neverSeen.

Remaining: step 2, the copy of agent/claude-usage/* on the ThinkPad.

Step 1 done (2026-10-07): Coolify deployment `finished` on `f694677`. Checked from inside the container (token never leaves it): - mounts: `/data/defenseurs` and `/data/defenseurs/reports` RW=false, `/data/hosts` and `/data/claude-usage` RW=true; - `GET /claude-usage`: content, 30 days, limits `ok`, `stale: false` (the stored `receivedAt` passes the new ISO check); - `GET /hosts`: thinkpad `online: true`, not `neverSeen`. Remaining: step 2, the copy of `agent/claude-usage/*` on the ThinkPad.
Author
Owner

Step 2 done (2026-10-07): the five files of agent/claude-usage/ copied from main (f694677) to ~/.local/share/maximus-claude-usage/ on the ThinkPad at 21:45:43, byte-identical to the repo, node --check clean.

First beat on the new code, 22:00:03: claude-usage: ok status=204 limits=ok bytes=8741, no read_errors line. state.json rewritten at 22:00:03, 31 frozen days (2026-09-05 to 2026-10-05; yesterday 2026-10-06 and today stay open, as expected).

Both post-merge steps are done: closing. Reopen if a read_errors=N line persists over several beats in journalctl -t claude-usage, or if a day's count doubles around midnight.

Step 2 done (2026-10-07): the five files of `agent/claude-usage/` copied from `main` (`f694677`) to `~/.local/share/maximus-claude-usage/` on the ThinkPad at 21:45:43, byte-identical to the repo, `node --check` clean. First beat on the new code, 22:00:03: `claude-usage: ok status=204 limits=ok bytes=8741`, no `read_errors` line. `state.json` rewritten at 22:00:03, 31 frozen days (2026-09-05 to 2026-10-05; yesterday 2026-10-06 and today stay open, as expected). Both post-merge steps are done: closing. Reopen if a `read_errors=N` line persists over several beats in `journalctl -t claude-usage`, or if a day's count doubles around midnight.
maximus removed the
status:blocked
label 2026-10-08 02:00:31 +00:00
Sign in to join this conversation.
No milestone
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#54
No description provided.