feat(agent): claude-usage agent — plan limits, local cache, push (#50) #53
No reviewers
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/vps-health-api#53
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-50-claude-usage-agent"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Resume
Agent
claude-usagedu ThinkPad (issue #50), lance toutes les 15 min par le crontab : lit les limites du plan Claude, maintient le cache des jours figes, assemble le snapshotschemaVersion: 1et le pousse versPOST /claude-usage.agent/claude-usage/limits.js: credentials lus en lecture seule (fs.readFileSync), jamais rafraichis ni ecrits ; URL Anthropic en constante (https://api.anthropic.com/api/oauth/usage, aucune variable d'env),redirect: "error", timeout 10 s, un appel, aucun retry.limits[]-> items (resets_at->resetsAt,scope.model.display_name->model), replifive_hour/seven_day, sinonunavailable/bad_response.detailsort uniquement de l'enum (no_credentials,parse_error,no_version,timeout,network,bad_response,http_<code>).agent/claude-usage/state.js:~/.local/state/maximus-claude-usage/state.json, mode 600, ecriture atomique (tmp suffixe pid + rename). Fige tout jour strictement anterieur a hier (Toronto), vide compris ; elague au-dela de 35 jours ;lastGoodLimitsmis a jour surokseulement et servi avec sonfetchedAtsinon ; fichier illisible -> etat vide (scan complet).agent/claude-usage/push-claude-usage.js: agrege, lit les limites, ecrit le cache avant le push, mesure le corps (> 65 536 octets ->payload_too_large, code 2, rien d'envoye), pousse une fois (https:exige, sinon code 1).--dry-runimprime le snapshot sans envoyer.agent/claude-usage/run-claude-usage.sh(executable) :flock -nsur~/.local/state/maximus-claude-usage/run.lock(sortie 0 si un passage tourne deja), source~/.config/maximus-claude-usage.env, meme style queagent/run-push.sh, sansset -x.agent/README.md: section dediee (copie figee, fichier env 600, ligne crontab, raison de la presence dans ce depot).CLAUDE.md: compte de tests et une ligne de Gotchas.__tests__/fixtures/claude-usage/oauth-usage.json: reponse de l'endpoint anonymisee.Aucun fichier serveur modifie (
index.js,claude-usage.js, Dockerfile intacts).Decisions (autopilot)
percentarrondi puis borne a 0-100 (MEDIUM : unutilizationau-dela de 100 reste affiche « sature » plutot que d'etre jete).--dry-runn'ecrit passtate.json(MEDIUM : apercu sans effet de bord ; il appelle encore Anthropic saufCLAUDE_USAGE_LIMITS=off, documente).accessToken->no_credentials; erreur de lecture autre qu'ENOENT ->parse_error;expiresAtabsent -> appel tente (le serveur tranche par 401).lastGoodLimitsservi pour tout statut nonok,disabledcompris (texte de l'issue).claudeCodeVersionomis (pasnull) quand aucun transcript n'en porte : le sanitizer jetteraitnull.E.../UND_ERR_...en majuscules ou a un nom de classe*Error, sinonUNKNOWN.Tests
__tests__/agent-claude-usage.test.js: 106 cas (38 de #49 + 68 nouveaux). Couvre tous les criteres d'acceptation, dont :fetch(double) jamais appele sur jeton expire /off/ credentials tronques / absents ; 429 ->http_error/http_429avec les dernieres limites ; corps hostiles ->detaildans l'enum ; bearer OAuth vers la seule URL epinglee ; espions sur toutes les ecritures denode:fs; aucun fragment de 5 caracteres des deux jetons (snapshot, etat, sortie, vrai processus) ; deux passages autour d'un jour vide ;state.jsoncorrompu ; snapshot inchange parsanitizeClaudeUsage;http:-> 1 ; > 64 Kio -> 2 ; wrapper lance deux fois en parallele (fauxnode, HOME temporaire).err.messagedans la sortie (3 rouges), sansflock(2 rouges).npm test: 606 cas verts.bash test-curl.sh: 45/45.sh scripts/boot-from-copy.sh: PASS.node agent/claude-usage/push-claude-usage.js --dry-runsur fixtures (HOME temporaire, limitesoff) : 30 jours, snapshot identique apressanitizeClaudeUsage. Aucun appel reel a api.anthropic.com ni a health.lacompagniemaximus.com.Actions requises de Max
Apres merge de la pile et deploiement du serveur (#48 :
CLAUDE_USAGE_INGEST_TOKENcote Coolify, montage/data/claude-usage) :main) :~/.config/maximus-claude-usage.env(ecrit avecprintf),chmod 600:CLAUDE_USAGE_API_URL=https://health.lacompagniemaximus.com,CLAUDE_USAGE_INGEST_TOKEN=<token>,NODE_BINsi besoin.~/.local/share/maximus-claude-usage/run-claude-usage.sh), puis la ligne crontab :Pile
#52 (#48, routes serveur) -> #51 (#49, agregation) -> cette PR (#50). Base :
issue-49-claude-usage-aggregate. A merger dans cet ordre.Resolves #50
Generated autonomously by /autopilot run of 2026-10-04
🤖 Generated with Claude Code
Auto-revue (autopilot) —
git diff origin/issue-49-claude-usage-aggregate...HEADVerdict : pret pour revue humaine, aucun point bloquant trouve. Critere par critere :
weekly_scoped/Fable) ; replifive_hour/seven_day; toutnull->unavailablelimits — the endpoint's answeroff, credentials tronques, absents :fetchjamais appelelimits — cases that never call the endpoint(8 cas, plus «offne lit meme pas les credentials »)http_error/http_429, items = dernieres limites valides avec leurfetchedAtmain()detaildans l'enumhttps://api.anthropic.comseulement ; aucune env ne change l'URLfetch+ epingle sur le source delimits.jsnode:fs(Sync, callback, promises), contenu et mtime inchangesstate.jsoncorrompu ; elagage 35 jsanitizeClaudeUsagetoEqualstrict, sur 4 etats de limiteshttp:-> 1 ; > 64 Kio -> 2 sans envoirun-claude-usage.shen parallele : second sort 0 sans travaillernode, HOME temporaire ; verrou relache ensuiteMutations verifiees (le test rougit) : jours vides non figes,
err.messageimprime,flockretire.Points d'attention pour Max (non bloquants) :
percentest arrondi et borne a 100 : un depassement s'affiche 100 %.--dry-runappelle encore Anthropic siCLAUDE_USAGE_LIMITSn'est pas aoff(documente dans le README).CLAUDE.mdcompte aussi les 38 cas de #49, que sa PR n'avait pas documentes : conflit trivial possible si #51 touche cette ligne avant le merge.Generated autonomously by /autopilot run of 2026-10-04
Revue adversariale — PR #53 (issue #50)
Verdict : APPROVE
Resume : l'agent tient ses invariants (credentials en lecture seule, bearer OAuth vers la seule URL constante avec
redirect: "error",detailen enum, cache ecrit avant le push, aucune donnee de token dans la sortie) et chaque propriete testee resiste a une mutation du relecteur. Contrat aligne avecsanitizeClaudeUsage(meme regex ISO,percententier borne, 12 items max).Verifications executees
9279b15extraite hors depot (git archive) :npx vitest run-> 606/606 verts. CI de la tete :success.__tests__/agent-claude-usage.test.jsseul :redirect: "error"retire de l'appel Anthropicredirect: "error"retire du pushexpiresAt <= nowneutraliseday >= today)lastGoodLimitslastGoodLimitsmis a jour quel que soit le statutsaveStateretirehttps:neutraliseTOKEN_REneutraliseexport ... CLAUDE_USAGE_LIMITSretire du wrapperpercentnon borneMAX_ITEMSretirefs.chmodSync(tmp, 0o600)retire desaveStatefeat(agent),docs(agent)). Timeline de #50 :pull_refclosesdepuis #53, conforme auResolves #50.Suggestions (non bloquantes)
agent/claude-usage/aggregate.js:242+state.js:104— un jour se fige sur ce que le passage a pu lire :readLinesetreaddiravalent une erreur de lecture (EACCES, fichier en cours de rotation), et le jour fige avec un total incomplet n'est plus jamais recalcule (35 jours de retention). Le choix « une erreur ne coule pas le snapshot » est juste pour les jours ouverts ; pour les jours qu'on fige, il vaudrait mieux ne pas geler lors d'un passage qui a rencontre une erreur de lecture, ou au minimum documenter dansagent/README.mdquerm state.jsonforce un recalcul complet.agent/claude-usage/limits.js:177-180—MAX_RESPONSE_BYTESest verifie apresres.text(), donc apres la mise en memoire du corps entier, et surtext.length(unites UTF-16, pas octets). Le commentaire « past it the body is not parsed » est exact, mais ce n'est pas une borne memoire. Risque faible (URL constante, timeout 10 s) : soit lire le flux avec un plafond, soit reformuler le commentaire.agent/claude-usage/state.js:141— lechmodSyncdu fichier temporaire (cas du.tmplaisse par un passage tue) n'est epingle par aucun test : sa suppression laisse 106/106 verts. Le fichier ne contient pas de secret, donc epingle a ajouter en suivi plutot qu'un tour de plus.Actions faites
status:review->status:approved.Rebasee sur le correctif de #51 (
c888f7c). Conflit d ajout en fin de__tests__/agent-claude-usage.test.jsresolu par union. Commit ajoutedocs(claude-md): compteurs 106 -> 111 et 606 -> 611. Tip de pile : vitest 611/611, test-curl 45/45, boot-from-copy PASS. Le verdict APPROVE precedent porte sur un code inchange (seuls les tests et CLAUDE.md ont bouge dans ce maillon).9279b15b7fto719d65129aMergee en fast-forward par l API (tete
719d651). Le commit719d651(docs : compteurs de tests dans CLAUDE.md apres le rebase sur le correctif de #51) a ete ajoute apres l APPROVE, non relu ; il ne touche que CLAUDE.md. Tip valide avant merge : npm ci propre, vitest 611/611 (sonde rouge -> rc=1), test-curl 45/45, boot-from-copy PASS, CI verte.