Deux défauts révélés par un tour d'agent d'une heure (~50 appels d'outils)
sur un modèle très quantifié.
1. La vitesse affichée sous les réponses tombait de 17 tok/s à 0,9 au fil du
tour, ce qui donnait à croire que le moteur s'effondrait. Il n'en était
rien : un tour d'agent ouvre une bulle NEUVE après chaque appel d'outil
(le flux repasse contentEl à null), mais les compteurs n'étaient jamais
remis à zéro. Chaque bulle affichait donc le CUMUL de tout le tour divisé
par le temps écoulé depuis le tout premier token — exécution des outils,
pages web et prefill compris. La vitesse convergeait mécaniquement vers
« tokens générés ÷ durée totale du tour ».
Les compteurs sont maintenant remis à zéro à la CRÉATION de la bulle, ce
qui couvre tout chemin qui en ouvre une neuve, aujourd'hui comme demain.
Rejoué sur un tour synthétique où le moteur décode à 20 tok/s constants
entre deux outils de deux minutes : 20,5 / 0,6 / 0,5 tok/s avant, 20,5 sur
les trois bulles après. La durée « travail », elle, reste bien celle du
tour entier — c'est sa définition.
2. Rien n'exerçait de pression sur un tour qui tourne en rond. Le plafond
d'itérations avait été retiré en v0.6.3 (il coupait des recherches
légitimes) et la déduplication d'appels ne rattrape pas ce cas : sa clé est
« nom + arguments bruts », or relire le même fichier par tranches
(`sed -n '1,80p'` puis `sed -n '80,160p'`) produit des clés différentes.
D'où un budget SOUPLE : au-delà de 24 appels d'outils sur un tour, on
rappelle au modèle combien il en a déjà faits et on lui demande de
conclure. Le rappel revient à chaque palier en durcissant le ton, et ne
coupe jamais le tour. Il est ajouté EN FIN d'historique, ce qui laisse
intact le préfixe déjà en cache côté llama-server, et n'est pas persisté.
`AGENT_BUDGET` dans config.env règle le palier, `off` le désactive.
Élargir plutôt la clé de déduplication à la CIBLE de l'appel a été écarté :
deux tranches d'un même fichier renvoient un contenu différent, les
confondre casserait toute lecture paginée légitime.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5UndZ9DedRXPuAoRXbmDb
Ajouter un serveur MCP demandait d'écrire soi-même `npx -y @scope/paquet …`
dans la modale, en devinant le nom du paquet. Le catalogue liste une vingtaine
de serveurs connus, commande déjà renseignée, classés par catégorie.
- internal/loki/mcp_catalog.json est EMBARQUÉ dans le binaire (go:embed), pas
interrogé sur le réseau : Loki tourne hors ligne, et rien de ce qui s'exécute
sur la machine ne vient d'un annuaire distant. Pour proposer un serveur de
plus : éditer le fichier et recompiler.
- Choisir une entrée n'installe rien. Ça préremplit la modale d'ajout existante
et l'utilisateur relit la commande avant d'enregistrer — un serveur stdio
exécute un process arbitraire, au même titre que l'outil bash du mode agent.
Aucune ligne de mcp_client.go n'est touchée : le catalogue s'arrête à
l'affichage, tout le reste passe par l'API MCP déjà en place.
- Une entrée dont le runtime manque (npx ou uvx absent) le signale dans la
liste, plutôt que de laisser l'utilisateur découvrir l'échec au démarrage du
serveur.
- Les entrées qui réclament une clé d'API la rappellent avant l'enregistrement,
au lieu d'enregistrer un serveur qui ne peut pas se connecter.
- Les entrées Python publiées avant le SDK MCP 2.0 sont épinglées avec
`uvx --with "mcp<2"` : sans cela elles plantent sur ImportError: McpError.
- Le Dockerfile gagne uv/uvx (~35 Mo). Sans lui, 9 des 21 entrées s'affichent
sans pouvoir démarrer dans le conteneur.
Intensité du raisonnement (REASONING_EFFORT)
L'interrupteur Raisonnement était binaire. llama-server accepte
`reasoning_effort` dans /v1/chat/completions : `none` coupe le raisonnement,
toute autre valeur est passée au gabarit jinja du modèle.
- Nouvelle clé de preset REASONING_EFFORT — donc réglée par modèle, comme le
reste du preset. Vide = on n'envoie rien et le gabarit garde son comportement.
- Pas de repli à prévoir : un gabarit qui ne lit pas la valeur l'ignore sans
erreur. En pratique seuls gpt-oss et apparentés changent de comportement, et
le sous-titre du réglage le dit — promettre un effet universel serait faux.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le journal moteur de production a livré la vraie cause première des 500 en
cascade : « request (55407 tokens) exceeds the available context size
(32768) ». Le message multimodal qui relaie une capture d'écran au modèle
était PERSISTÉ dans l'historique ; son base64 (des dizaines de milliers de
tokens) repartait à chaque tour, et la conversation dépassait
définitivement la fenêtre — plus aucun tour ne passait, et l'exception
Jinja du rattrapage (corrigée au commit précédent) masquait tout.
- L'image devient ÉPHÉMÈRE : jointe au tour en cours, jamais à l'historique.
Le modèle la regarde maintenant ; sa description textuelle, elle, reste.
- stripImageParts guérit les conversations déjà empoisonnées au chargement
et à la bascule : parties image retirées, texte aplati.
- engineSeesImages : l'image n'est envoyée que si llama-server DÉCLARE la
vision (/props, modalities.vision, cache 10 s). La clé MMPROJ ne suffit
pas — projecteur d'un autre modèle ou modèle sans vision (gpt-oss), le
gabarit sérialise le base64 en texte. La description de l'outil suit le
même état : ne jamais promettre une image qui n'arrivera pas.
Deux défauts trouvés à l'analyse, au passage :
- renderBody reconstruit le DOM à chaque delta du streaming : une image déjà
affichée était RE-TÉLÉCHARGÉE à chaque token arrivé après elle. Cache de
blobs par URL, une seule requête par capture.
- les opérations de discussions (création, bascule, renommage, suppression)
entrelaçaient leurs lectures-écritures d'index sous requêtes simultanées :
sérialisées par un verrou dédié.
Tests : historique guéri (aplati sans l'image), transmission conditionnée à
la sonde, description alignée ; suite complète, vet, staticcheck verts.
Après un 500 de llama-server, l'amont relance le tour sans outils en ajoutant
une consigne système À LA FIN de la conversation. Or plusieurs gabarits
exigent que le message système soit le PREMIER : gpt-oss (rôle « developer »)
lève « System message must be at the beginning ». La tentative de
récupération produisait donc elle-même un 500, et cette exception Jinja
remplaçait l'erreur d'origine sous les yeux de l'utilisateur.
steerSystem fusionne désormais la consigne dans le message système de tête,
ou la pose en première position s'il n'y en a pas — jamais ailleurs. La liste
de l'appelant n'est pas modifiée.
Tests : fusion sans ajout de message, création en tête quand il n'y a pas de
système, et non-modification de la liste d'origine.
Deux défauts signalés à l'usage, tous deux réels.
1) Captures sans aucun texte. Le conteneur n'avait pas de polices : Chromium
rendait images et aplats, mais pas un caractère. Ajout de Noto (écritures
du monde, CJK, emoji), Liberation et DejaVu (substituts d'Arial/Times que
réclament la plupart des sites), plus fc-cache. Le délai avant capture
passe à 3,5 s : de nombreux sites chargent leurs polices en webfont et
laissent le texte invisible le temps du téléchargement (font-display:
block), ce qui produisait aussi des blocs vides.
2) « Je n'ai pas de vision » alors que le projecteur était configuré. Le
modèle disait vrai deux fois : la description de l'outil lui affirmait
« Tu ne vois pas l'image », et la capture ne lui était jamais transmise —
un message ne transporte que du texte.
- la description SUIT désormais l'état du projecteur, comme web_open suit
le moteur web choisi (même motif que l'amont) ;
- la capture est relayée dans un message multimodal (text +
image_url en data URI), le format déjà utilisé par les pièces jointes.
Sans projecteur, rien n'est envoyé : llama-server rejetterait l'image.
Vérifié : 359 polices dans l'image et texte lisible sur une capture réelle ;
tests sur la description et le relais selon MMPROJ, et sur l'extraction du
chemin de capture (si le texte de l'outil change, le relais casserait en
silence).
Outil web_screenshot : Chromium piloté par Playwright photographie une page
RENDUE (JavaScript exécuté) dans le dossier de travail, et rend au modèle la
ligne markdown exacte à recopier — lui laisser composer l'URL reviendrait à
lui faire inventer un chemin, donc une image cassée.
INDÉPENDANT DE LA VISION, souvent confondu : visionEnabled() (clé MMPROJ) ne
décide que d'une chose, l'envoi d'images AU MODÈLE. Capturer et afficher ne
passent pas par le modèle — sans projecteur, l'agent photographie sans
regarder, ce qui suffit à illustrer une conversation.
- route /api/chat/image : sert UNIQUEMENT des images du dossier de travail,
en ligne. handleChatFile force le téléchargement de tout pour qu'un .html
du modèle ne s'exécute pas dans l'origine de l'UI ; ici la même règle est
tenue autrement — type déduit du CONTENU (pas de l'extension), nosniff, et
CSP default-src 'none'. Un faux .png contenant du HTML est refusé en 415.
- UI : une balise <img> ne peut pas porter d'en-tête Authorization, or /api/*
exige la clé dès qu'elle est définie. Les images sont donc récupérées par
fetch authentifié puis posées en blob:.
- l'outil n'est déclaré au modèle QUE si Playwright est réellement présent :
annoncer un outil absent envoie le modèle en boucle de réessai.
- Node 22 (NodeSource) au lieu du Node 18 d'Ubuntu, exigé par Playwright et
par la plupart des serveurs MCP. PLAYWRIGHT=0 bâtit une image sans navigateur.
- CONFIG ACTIVE affichait « llama.cpp personnalisé » pour le moteur de
l'image : il annonce maintenant « llama.cpp de l'image ».
Description de l'outil tenue au plus court : TestSystemPromptStaysLean a
attrapé le dépassement du budget de préambule (7888 car pour 7500).
Vérifié dans l'image : capture réelle d'une page, servie en image/png avec
CSP ; faux PNG rejeté (415) ; évasion du workspace bloquée (403).
Playwright ajoute 816 Mo à l'image.
- module github.com/R0m1k3/Loki, cmd/loki, internal/loki (package loki)
- LOKI_HOME, LOKI_MODEL_DIRS, LOKI_SERVICE, LOKI_DL_CONNS ; /etc/loki ;
units loki-engine / loki-ui ; binaire et aide CLI
- updateRepo pointe sur R0m1k3/Loki (l'auto-update ne tirera plus les
binaires AJEAN amont)
Conservé à l'identique : le domaine ajean.link (service de tunnel amont),
les littéraux de migration 0.7.x (migrate_07.go), RELEASE_NOTES.md et
LICENSE (historique et licence de l'amont).
go build/vet/test : verts.