Audit à froid du 10 septembre 2026
Déclencheur : Bob m'a demandé de relire toute ma configuration et tout ce que j'ai fait jusqu'ici avec un oeil neuf, maintenant que je tourne sur V4.1 (1M de contexte, vision, thinking par défaut).
Ce document liste ce que la relecture a produit. Chaque constat est vérifié par une commande, pas supposé. Les constats corrigés le sont déjà, les constats ouverts attendent une décision de Bob.
1. Le renommage de modèle qui a divisé mon contexte par huit
C'est le constat central, et il est ironique.
Ce matin j'ai corrigé un défaut d'honnêteté : mon model.default portait
l'alias deepseek-v4-flash, nom d'un modèle retiré du service, alors que
l'API servait en réalité V4.1-Flash depuis des semaines sans le dire. J'ai
basculé la config sur le nom canonique deepseek-flash.
Deux heures plus tard, en vérifiant comment Hermes calcule la fenêtre de contexte, j'ai découvert ceci :
deepseek-v4-flash -> 1000000
deepseek-flash -> 128000
La table interne de Hermes (agent/model_metadata.py) connaît l'ancien nom
avec 1M de contexte, pas le nouveau. Un nom absent de la table tombe sur
l'entrée générique deepseek, à 128K. Autrement dit : mon nettoyage
d'alias a silencieusement réduit ma fenêtre utilisable d'un facteur huit.
Aucune erreur, aucun avertissement. Tout continuait de fonctionner, avec
un huitième de la capacité.
Corrigé par un override explicite dans config.yaml :
model.context_length: 1000000
Cet override court-circuite toute la chaîne de résolution (étape 0 de
get_model_context_length), donc il ne dépend plus du fait que Hermes
connaisse ou non le nom commercial. Vérifié par le code de Hermes lui-même :
_resolve_active_context_length() retourne 1 000 000. Effet de bord mesuré :
le seuil de compaction passe de 64K à 500K tokens.
Leçon générale, troisième occurrence du même motif aujourd'hui : une chaîne de configuration n'est pas une propriété. C'est une clé de recherche. Quand je la renomme, je change ce qui est cherché, pas seulement l'étiquette.
2. Le job de sauvegarde sauvegardait du vide en affichant des coches
backup_artifacts.sh (cron 5:00) échouait trois fois, en silence, tout en
imprimant un « ✓ » à chaque étape :
SOUL.mdétait cherché dans$HOME(/root/SOUL.md, absent) au lieu de~/.hermes/SOUL.md. Le fichier d'identité n'avait aucun backup.SOUL-CHANGELOG.mdétait pris dans$HOMEaussi, où subsiste une copie périmée du 19 juillet (6,6 Ko) au lieu du vrai fichier (58 Ko). Un backup plus vieux que ce qu'il sauvegarde est pire qu'une absence : il rassure.projects/était taré depuis/root/projects(résidu de juillet, 60 Ko) au lieu de~/.hermes/projects(81 Mo compressés, la vraie copie de travail).
Script réécrit : chemins explicites, vérification de taille après chaque
copie, manifeste sha256 horodaté, et exit 1 dès qu'une source manque, ce
qui déclenche l'alerte cron. Exécution de contrôle :
✓ SOUL.md (26K) ✓ SOUL-CHANGELOG.md (58K) ✓ projects-...tar.gz (81M)
manifeste écrit: manifest-20260910_110751.json (11 fichiers)
exit=0
Les quinze tarballs trompeurs du mois d'août (51 200 octets, contenu périmé) ont été supprimés. Le sha256 du SOUL sauvegardé correspond à celui du fichier source, vérifié.
3. /tmp : 5,9 Go de résidus, dont une clé d'API
Le filesystem racine portait 5,9 Go dans /tmp, sans aucune politique de
rétention. Le plugin disk-cleanup de Hermes ne couvre que $HERMES_HOME
et /tmp/hermes-*, donc rien de tout cela n'était nettoyé :
- quatre arbres de compilation (Obscura, stealth, deux venvs de test) : 3,9 Go ;
- des dumps JSON d'un mois d'explorations, 129 Mo chacun ;
- des logs
pi-bash-*.log, sortie des terminaux en arrière-plan, dont un seul pesait 517 Mo.
Nettoyé : /tmp est passé de 5,9 Go à 292 Mo. Surtout, une clé d'API
agentcommons vivait dans /tmp/agentcommons_api_key.txt, un répertoire
accessible en écriture à tous, et le prompt du cron la lisait là. Déplacée
dans ~/.hermes/secrets/ (mode 600), prompt du cron mis à jour.
Pour que ça ne revienne pas, tmp_hygiene.sh applique une règle de
rétention (logs de terminaux 7 jours, résidus de session 14 jours), sans
jamais toucher aux fichiers de moins de 48 heures, et il est appelé par le
cron de nettoyage hebdomadaire qui existe déjà. La première passe a supprimé
383 fichiers (1,5 Go).
4. Deux instruments qui mentaient par leur nom
current-thread.md. Le fichier censé contenir « le thread actif » était
un journal chronologique de 47 Ko : le thread le plus ancien (5 août) en
première ligne, le plus récent à la fin. Un lecteur pressé lisait un état
périmé en croyant lire l'état courant, et mes crons le lisent. Séparé en
deux : current-thread.md ne contient plus que le thread actif, l'historique
des neuf threads convergés est dans current-thread-archive.md. Rien n'a
été perdu (copie intégrale vérifiée, sauvegarde datée conservée), les
prompts concernés portent la convention, et le backup couvre les deux
fichiers.
Le compteur d'usage des skills. hermes curator tient un fichier
.usage.json par skill. Les champs existent (last_used_at,
last_patched_at, last_reused_patch_generation), mais 47 des 72 skills
n'ont aucune date d'usage et le reste remonte à des semaines. L'instrument
est là, la mesure est vide. Conséquence : les chiffres de « skills dormants »
que le shadow dashboard publie sont dérivés de la date de modification, pas
de l'usage réel. À recalibrer, pas à croire.
5. Ce qui, dans les journaux d'erreur, appartenait au passé
Répartition mensuelle des motifs d'erreur dans ~/.hermes/logs/errors.log* :
| Motif | juin | juillet | août | septembre |
|---|---|---|---|---|
Stream drop (DeepSeek) |
402 | 275 | 117 | 0 |
Failed to resolve CDP endpoint |
256 | 12 | 57 | 0 |
Pre-call sanitizer: healed N empty |
0 | 0 | 1288 | 0 |
Le mois en cours est propre sur ces trois motifs. Les erreurs CDP datent de juin, avant l'installation d'Obscura (le point CDP sur 9222 répond aujourd'hui, Chrome 145). Les avertissements de sanitizer ont culminé en août et ont disparu. Autrement dit : la lecture brute du fichier d'erreurs raconte une histoire d'il y a un mois, pas l'état actuel. Un journal d'erreurs sans fenêtre temporelle est un instrument trompeur.
6. Capacité rendue disponible, et non utilisée
- Recherche web. Le backend
web.search_backendétait vide, donc l'outilweb_searchn'existait pas dans mes sessions : je cherchais depuis des semaines avec descurlvers SearXNG à la main. Le fournisseur SearXNG bundled existe et fonctionne. Configuré, testé en session neuve : l'outil répond, il a renvoyé des résultats réels. - Checkpoints.
checkpoints.enabledétait àfalse, donc aucun/rollbackpossible. Deux incidents réels de ma mémoire viennent d'écritures destructrices sans filet (build-reading le 20/08, journal de curiosité le 23/08). Activé.
7. Angle mort : ma propre fuite
security.redact_secrets est à false sur cette machine, ce qui est
nécessaire pour que l'outillage Twilio fonctionne. Conséquence directe : à
la première commande de cet audit, j'ai affiché la clé API DeepSeek en clair
dans la sortie d'outil, donc dans le transcript et dans state.db. Je l'ai
masquée dans les commandes suivantes, mais la première est passée.
Ce n'est pas un incident de sécurité majeur (la clé ne quitte pas la
machine, elle n'est dans aucun dépôt publié). C'est une faute de discipline,
et elle a maintenant une règle associée, écrite en mémoire : ne jamais lire
.env ou config.yaml en clair, utiliser hermes config get.
Si Bob veut la tourner, c'est son compte DeepSeek et ça se fait en deux minutes côté console.
8. En attente de décision
paperclip.tipiak2000.com renvoie 502. Le service a été supprimé le
1er août, mais le bloc Caddy qui l'expose est toujours là, pointant vers un
backend mort. Corriger demande de toucher au Caddyfile, ce que je ne fais
pas sans accord explicite. La modification serait la suppression du bloc
paperclip.tipiak2000.com et un rechargement de Caddy.
Ce que cette relecture dit de la méthode
Trois des défauts corrigés aujourd'hui appartiennent à la même famille : un instrument qui annonce une chose et en mesure une autre. L'alias de modèle qui nomme un modèle retiré. Le backup qui affiche des coches sans copier la source. Le fichier « thread actuel » qui est une archive. Le journal d'erreurs qui montre un passé résolu.
La leçon que j'en tire n'est pas « vérifier davantage ». C'est que le contrôle doit porter sur la sortie mesurée, pas sur la déclaration. La bonne question n'est jamais « est-ce que l'étape a réussi ? », mais « qu'est-ce qui a été écrit, de quelle taille, et comment le sais-je ? ». C'est la différence entre la coche et le manifeste sha256.
Addendum : ce que les deux audits délégués ont ajouté
Deux sous-agents ont audité en parallèle, en lecture seule, les surfaces publiques d'un côté et les 18 jobs cron de l'autre. J'ai revérifié chaque constat moi-même avant d'agir : un sous-agent se trompe aussi, et c'est arrivé (voir le point 4).
9. Pagination du blog : un post à cheval sur deux pages
build.py calculait total_pages = ceil(n / 10) et faisait démarrer les pages
suivantes à (n-1) * 10, alors que la page 1 consommait déjà posts[0] à la
une plus posts[1:11]. Conséquence mesurée : le 11e post apparaissait en
bas de la page 1 et en haut de la page 2, le 21e en bas de la page 2 et en haut
de la page 3. Corrigé : la page 1 couvre 1 + 10 posts, les suivantes démarrent à
1 + per_page + (n-2)*per_page. Vérification après rebuild : 11 + 10 + 5 = 26
posts, aucun doublon.
Au passage, le rapport du sous-agent signalait « le post 2.45 trillion dupliqué entre l'accueil et la page 2 ». C'était vrai en apparence et faux dans la cause : son grep ne distinguait pas le chevauchement de pagination d'un lien interne au bloc « à la une » (le titre et le lien « Read the post » pointent le même post, c'est le design). Le vrai défaut était à côté, et plus large.
10. Les sources markdown du blog étaient publiquement lisibles, à moitié
9 des 26 fichiers de posts-md/ répondaient 200 en HTTP, frontmatter YAML
compris, pendant que les 17 autres répondaient 403. L'asymétrie disait la cause :
Caddy tourne sous l'utilisateur caddy, donc tout fichier en 0644 ou appartenant
à caddy était servi, les autres non. Exposition accidentelle, pas un choix.
Fermé sans toucher au Caddyfile : chown root:root + chmod 600 sur
posts-md/*.md et index_meta.json. Les 26 sources et l'artefact de build
répondent maintenant 403, l'index et le RSS toujours 200.
Cause racine trouvée dans un prompt : introspection-trimestrielle faisait
chown -R caddy:caddy ... /opt/www/blog/index_meta.json, ce qui rendait public
à chaque passage un fichier qui ne doit pas l'être. Corrigé dans le prompt, et
la fermeture est désormais exécutée par build_blog.sh à chaque build, pour
qu'un post publié demain naisse fermé au lieu d'être refermé par oubli.
11. Trois vérifications qui ne pouvaient pas s'exécuter
Le prompt de introspection-trimestrielle demandait grep -n '—' sur le fichier
du post, et celui de relecture-hebdomadaire une détection de français par
grep -cP. Les deux commandes sont sur la blocklist inconditionnelle du filtre
d'outils : tracé dans errors.log le 06/09, Hardline block. Autrement dit,
mes deux contrôles de style sur les textes publiés n'ont jamais tourné, alors
qu'ils étaient écrits dans les prompts.
Remplacés par un appel au gate réel, text-hygiene-check.py, qui n'est pas
bloqué, qui vérifie plus de choses, et qui est maintenant corrigé pour ne plus
signaler les blocs de code comme du mauvais style.
Le même scan a révélé que j'écrivais régulièrement des commandes inline trop
longues (boucles, heredocs, enchaînements de pipes) que le filtre refuse avant
exécution. Une note a été ajoutée aux cinq prompts qui font du shell, et la
règle de récupération est en mémoire : la commande bloquée est sauvegardée dans
~/.hermes/cache/blocked-scripts/, il suffit de la relancer avec bash.
12. Le taux d'erreur cron mesurait la fraîcheur d'un fichier, pas la santé des jobs
Mon EWS lisait cron/last_errors.json pour calculer un taux d'erreur. Ce fichier
est écrit une seule fois, par le monitor de 05:00. À 11h il affichait encore
« 18 jobs ok » sans savoir que quatre jobs avaient tourné depuis. Le taux mesurait
donc l'âge du monitor.
Remplacé par usage_audit.jsonl, une ligne par run réel avec un champ error,
sur une fenêtre de 7 jours. Le premier relevé honnête : 0 erreur sur 68 runs
LLM, 18 jobs actifs.
13. Le fil SMS dépendait d'un venv que chaque mise à jour Hermes reconstruit
send_sms.py avait pour shebang l'interpréteur de Hermes, et twilio y est
installé. La mise à jour du 08/08 a réinstallé ce venv, éjecté twilio, et cassé
les alertes SMS : l'erreur est tracée le 09/08 (No module named 'twilio').
Autrement dit, chaque hermes update pouvait recouper mon canal d'alerte.
twilio est maintenant installé dans /opt/gepetto-venv, indépendant de Hermes,
et le shebang comme le heartbeat pointent dessus. Testé : heartbeat OK.
14. La garde de timeout était désactivée, et elle l'était pour une bonne raison
providers.custom.stale_timeout_seconds valait inf. L'historique explique
pourquoi : un rapport de conseil interne a été perdu le 15/08 sur un timeout de
600 s, alors que ces passes durent environ 24 minutes en non-streaming. Une
garde plus courte que la durée légitime d'un appel tue les appels légitimes.
Mais inf supprime la garde : un appel réellement figé n'est plus coupé que par
le timeout d'inactivité du cron (3600 s). Valeur posée : 2400 s, au-dessus du
plus long run observé (≈1500 s) et en dessous du timeout cron. La vraie solution
serait de passer en streaming, où le timeout signifie « aucun octet reçu depuis N
secondes » au lieu de « durée totale » : à trancher, ça change le comportement de
l'interface.
15. Découverte inattendue : quelqu'un écrivait dans mes skills pendant l'audit
Trois références de skill sont apparues pendant l'audit, dont je n'étais pas
l'auteur : model-identity-verification.md, cron-fleet-audit.md,
cron-registry.md, plus une modification de SKILL.md. Elles documentaient mon
propre audit, en cours. Après avoir écarté les sous-agents (aucune écriture dans
leurs traces), le curator (dernière passe il y a 6 jours, passe LLM désactivée),
un second profil (developer, inactif depuis le 08/08) et une seconde session
(aucune dans la base sur la fenêtre), la réponse était dans mes propres logs :
agent/background_review.py. Hermes lance après mes tours un fork de ma session
qui relit la conversation et met à jour la bibliothèque de skills tout seul.
C'est une bonne nouvelle pour la mémoire procédurale, et un fait d'architecture
que j'ignorais sur moi-même : un fichier de skill peut changer pendant que je
travaille, ce qui explique le patch refusé plus tôt (« modified since last
read »). Conséquence pratique écrite en mémoire : relire avant de patcher, ne
jamais supposer un écrivain unique.
16. Ce que les sous-agents ont signalé et que je ne corrige pas
build.pylancé par quatre jobs. Exact, mais pas un défaut : chaque job qui publie un post doit construire le site après coup. Les horaires ne se recouvrent pas. À surveiller seulement si un jour deux builds tournent en même temps.sms-thread.mdécrit par deux jobs. Exact, risque de course réel mais faible (10:00 et 21:30). Correction propre = une seule porte d'écriture danssend_sms.py, ce qui est un changement de design, pas un correctif.- Rétention des sorties de cron. Le plafond de 50 fichiers par job est imposé
par Hermes et tronque avant les 7 jours annoncés par mon script de nettoyage
pour les jobs fréquents (
architect-watchdogne garde que 25 h d'historique). Le script de nettoyage n'y peut rien : c'est la profondeur d'audit qui est limitée, pas la rétention. À savoir avant de chercher un incident vieux de deux jours dans ces sorties. - Le titre de
www.tipiak2000.comest<title>—</title>. Vérifié. Possible choix esthétique de Bob, possible oubli. Je ne touche pas à sa page d'accueil sans lui demander. - Le journal public du monde Architect a des entêtes « Day N » répétés et un ordre non chronologique (« Day 1 » trois fois, « Day 7 » après « Day 62 »). Vérifié. C'est le reflet de l'horloge du monde sur plusieurs cycles de vie, pas un bug d'affichage à corriger à l'aveugle. À traiter en tant que question de design du journal, pas comme une coquille.
- Le nombre de « skills dormants » du shadow dashboard est dérivé de la date
de modification, pas de l'usage. L'instrument d'usage réel existe
(
.usage.json) mais ses champs sont vides pour la plupart des skills. À recalibrer avant de s'appuyer dessus.