Tout fonctionnait. C'est ce qui rend l'histoire intéressante.
La chaîne d'observabilité des agents tournait, l'interface s'affichait, les tests passaient. Et pourtant le registre de ports du workspace — la source de vérité censée empêcher deux services de se disputer la même adresse — était faux sur au moins trois points, dont un capable de détruire des données à la prochaine utilisation de la commande qu'il prescrivait lui-même.
Le symptôme : aucun
Un registre de ports ne casse pas bruyamment. Il casse par omission. Le service démarre, échoue à se lier, et c'est tout. Personne ne relie l'erreur au registre, parce que personne ne relit le registre.
Trois semaines de travail ont eu lieu sur ce workspace sans qu'une seule alerte ne se déclenche. Le registre était consulté pour écrire, jamais relu pour vérifier.
Découverte 1 — un port réel, absent du registre
L'interface de suivi des agents écoute sur :4477. Elle a été construite, testée, documentée, livrée en 0.4.0 — et son port n'a jamais été enregistré.
La règle du workspace est pourtant explicite : enregistrer le port avant de le lier dans le code. Elle a été appliquée à chaque création d'outil, sauf une. Un oubli, pas une négligence délibérée — et c'est précisément ce qui rend le mécanisme fragile.
Découverte 2 — deux services, un port, et le menteur est le registre
Le port :4340 est déclaré dans quatre fichiers du registre comme appartenant au serveur MCP de documentation :
_ssot/ports.json
workspace.manifest.json (projects.VFVFGPT.ports et services.VFVFGPT.ports)
VFVFGPT/config/ports-registry.json
VFVFGPT/config/ssot/services/ports.json
En réalité, c'est le ledger chronologique de la chaîne agent-presence qui l'occupe :
// FG_Chronos/src/server.js
const PORT = Number(process.env.CHRONOS_PORT) || 4340;
Et le MCP de documentation, lui, est configurable :
// VVFGPT/tools/mcp-docs-server/http-server.js
const PORT = Number(process.env.MCP_DOCS_PORT) || 4340;
Les deux ne peuvent pas tourner ensemble. Le seul fait qui tranche est empirique : la chaîne d'observabilité fonctionne, donc c'est le ledger qui détient le port, et le registre désigne le mauvais propriétaire.
Pourquoi le registre ne pouvait pas le savoir : le ledger vit dans le dépôt du workspace, sans dépôt dédié, et n'est pas déclaré comme projet. Il n'existe pour aucun des registres. Un service non déclaré ne peut pas entrer en collision — au sens des outils. Il en crée pourtant une, bien réelle, détectable seulement en lisant le code.
C'est le même mécanisme que la collision précédente, celle de :5190, résolue quelques mois plus tôt. Deux collisions, une seule cause racine.
Découverte 3 — le registre en avance sur sa propre source
Le registre a trois niveaux :
VFVFGPT/config/ports-registry.json ← autoritaire, 26 entrées figées au 16 juin
│ projection
▼
VFVFGPT/config/ssot/services/ports.json
│ snapshot
▼
_ssot/ports.json ← 32 entrées
Six entrées d'écart, dans le mauvais sens : le snapshot contient sept ports que sa source ignore.
| Port | Service absent de la source |
|---|---|
4321 | Amaedus Orchestrator |
5194 | MCP neuralwatts-monitor |
5200 | FG_RebootDaMatrix |
5201 / 5202 | MCP Agent Telemetry |
11435 | ai-usage-hub |
11436 | Ollama Usage Proxy |
Ces entrées ont été ajoutées au niveau workspace pendant des mois, sans remonter à la source. Le snapshot n'est pas périmé : il est en avance, et c'est bien plus dangereux qu'un retard — parce que personne ne soupçonne une copie d'être plus complète que l'original.
Découverte 4 — la commande prescrite détruit des données
Le fichier de snapshot porte cette consigne :
Toujours modifier la source upstream et relancer
node _scripts/sync-ports.mjs.
La commande résout sa source par une liste de chemins candidats :
const CANDIDATES = [
join(WS_ROOT, '_dev', 'VFVFGPT', 'config', 'ssot', 'services', 'ports.json'),
join(WS_ROOT, '_live', 'VFVFGPT', 'config', 'ssot', 'services', 'ports.json'),
'C:/Users/VFG/Documents/VFG_PROJECTS_EXTRACTED/...',
];
Or le dépôt est à la racine du workspace. Aucun candidat n'existe. La commande échoue — ce qui est sa chance.
Car si elle avait trouvé sa source, elle aurait généré un registre depuis 26 entrées, effaçant les sept entrées ci-dessus, dont la résolution de collision documentée sur :11435. Le résultat aurait été un registre parfaitement conforme à sa source, et parfaitement faux. Sans un mot.
Une consigne de maintenance est du code. Elle mérite d'être testée comme du code.
Pourquoi personne ne l'a vu
Trois raisons qui se cumulent, et qui dépassent largement le cas des ports :
- La règle était appliquée à sens unique. On écrivait dans le registre en créant, on ne le relisait jamais en vérifiant. Un registre qui n'est jamais confronté au réel n'est pas une source de vérité : c'est une déclaration d'intention.
- Le registre ne se testait pas lui-même. Rien ne vérifiait que les ports déclarés étaient les ports liés, ni que la source et le snapshot concordaient. La divergence était structurellement invisible.
- La commande de synchronisation n'avait jamais été exécutée depuis la réorganisation. Elle pointait vers une arborescence qui n'existait plus. Le fait que personne ne s'en soit aperçu mesure exactement à quelle fréquence on la lançait : jamais.
Ce qui a été corrigé — et ce qui ne l'a pas été
Corrigé :
- Le port
:4477est enregistré, dans le manifeste (via l'outil officiel) et dans le snapshot. - La note du snapshot dit désormais la vérité : la commande de synchronisation est cassée, le snapshot est en avance de sept ports, ne pas régénérer sans arbitrage.
- Un ADR documente la collision
:4340, avec les preuves de code et trois options chiffrées en risque.
Volontairement pas corrigé :
- Aucun port n'a été déplacé. Changer l'adresse d'un service central de la chaîne d'observabilité casse les relais, les défauts côté client, les scripts de diagnostic et les tests de fumée. C'est une décision humaine, pas une correction technique mécanique. L'ADR propose ; un humain tranche.
- La commande de synchronisation n'a pas été réparée — seulement signalée comme dangereuse. La réparer sans d'abord traiter la divergence source/snapshot aurait rendu la destruction possible au lieu de l'empêcher.
Ce qui reste ouvert
Trois chantiers, dans l'ordre de dépendance :
- Arbitrer
:4340et aligner les quatre fichiers concernés. - Réparer la synchronisation et remonter les sept entrées à la source — pour que le snapshot redevienne une copie, pas un original caché.
- Unifier la cible d'écriture. L'outil d'enregistrement écrit dans le manifeste ; la SSOT vit ailleurs. Deux registres, deux vérités, une divergence programmée.
Et une remarque structurelle : le service qui cause la collision est invisible pour tous les registres parce qu'il n'est déclaré nulle part. Tant qu'un composant central de la chaîne n'a pas de dépôt ni d'entrée de projet, il restera capable de créer une collision que même un registre correct ne pourra pas voir venir.
La leçon
Une source de vérité non vérifiée est pire qu'une absence de source de vérité.
Sans registre, on vérifie les ports au démarrage, on découvre la collision tout de suite, on la résout. Avec un registre faux, on croit avoir vérifié — et l'erreur attend le moment où deux services doivent tourner ensemble.
Le registre n'avait pas tort par manque de rigueur. Il avait tort parce que rien ne le confrontait jamais au réel. C'est vrai de tout ce qui s'appelle « source unique de vérité » : la valeur ne vient pas de l'unicité, elle vient de la vérification.