Le fork est parti de la v0.9.4 ; l'amont en est à la v0.10.7. Reprise de ce
qui manque VRAIMENT ici, en laissant de côté ce que loki a déjà résolu à sa
façon (contexte MTP via --parallel 1, jauge de contexte, chrono de tour,
vignettes d'images, ligne d'état de génération).
Rendu du chat cadencé puis lissé (amont v0.9.5 issue #24, v0.10.5). Chaque
token re-parsait le Markdown du bloc ENTIER : du O(n²) qui faisait ramer
l'interface sur un long raisonnement — le moteur débitait toujours autant, mais
les tokens semblaient arriver au ralenti et un simple rafraîchissement
« réparait » tout. Le texte s'accumule désormais et n'est re-rendu qu'à
intervalle adaptatif (16 ms sur un petit bloc, jusqu'à 500 ms sur un énorme),
soldé à chaque frontière (outil, bascule de rôle, fin de tour, erreur, rejeu).
Par-dessus, un lissage d'apparition découple l'arrivée de l'affichage : le
décodage spéculatif rend les tokens par rafales, le texte sautait par paquets ;
il s'écoule maintenant à cadence régulière. Rejeu exclu — relire un fil ne doit
pas être une lente réécriture.
Échantillonnage réglable par preset (amont v0.9.5/v0.9.6) : TEMP, TOP_P, TOP_K,
MIN_P, PRESENCE_PENALTY, REPEAT_PENALTY, injectés dans chaque requête (donc sans
redémarrage du moteur), vide = défaut du serveur. Sans ça seule la température
voyageait et le reste retombait sur les défauts de llama.cpp, rarement ceux que
recommande le modèle. Différence avec l'amont : REASONING_EFFORT n'est PAS
traité là — loki lui réserve un chemin plus riche, et l'écrire ici écraserait
`chat_template_kwargs`, donc la consigne « aucune ».
Un seul message système, en tête, à l'envoi (amont v0.9.8, issue #26).
steerSystem ne couvrait que les consignes de loki ; un historique venu
d'ailleurs peut encore en porter deux, et Qwen3.x en --jinja répond alors
« System message must be at the beginning ». Copie normalisée : l'historique
affiché et persisté garde sa forme.
Détection Vulkan multi-distro (amont issues #28, #29) : le chemin Debian codé en
dur est invisible sur Fedora/RHEL/Atomic, où le plan de build retombait sur le
CPU. ldconfig d'abord, puis les chemins connus.
Dossier de travail (amont v0.10.2) : la consigne dit maintenant ce que le
dossier EST — l'endroit par défaut de tout ce que le modèle produit — et nomme
les dossiers système à ne pas toucher, au lieu d'interdire vaguement d'en sortir.
Non repris : les tâches planifiées (~1200 lignes + interface, à décider), et le
quoting cmd.exe par .bat temporaire (loki tourne en conteneur Linux).
Qwen3.8-27B valide `reasoning_effort` au lieu de l'ignorer : il connaît
xhigh/medium/low, pas « high » — le niveau que l'interface enregistre par
défaut. Chaque message partait donc en 500, avec une trace jinja affichée
en guise d'erreur, et le 500 tombait dans la branche « prompt trop long »
de runChat : loki compactait l'historique pour rien avant d'abandonner.
Le refus dit lui-même ce que le gabarit accepte. On le lit (llm_effort.go),
on traduit le niveau demandé vers le plus proche sur l'échelle
none/minimal/low/medium/high/xhigh — à égalité, le plus fort, dégrader en
silence étant pire que générer un peu plus longtemps — et on rejoue le tour,
historique intact. Sans liste annoncée, le champ est simplement retiré.
« aucune » n'est jamais traduite : c'est une coupure, portée par
`enable_thinking` que tous les gabarits comprennent.
La traduction est retenue par modèle : les messages suivants ne repaient pas
l'aller-retour. Le repli est tracé sur stderr, sinon l'intensité choisie dans
l'interface n'est pas celle qui part au moteur sans que rien ne le dise.
« maximale » (xhigh) rejoint la liste des niveaux proposés : aucun gabarit ne
les connaît toutes, et sans elle le maximum d'un Qwen3.8 restait hors
d'atteinte. Le repli couvre les gabarits qui la refusent.
La capture n'apparaissait dans le fil QUE si le modèle recopiait la ligne
markdown rendue par l'outil. Un petit modèle l'oublie, et l'utilisateur
ne voyait jamais l'image qu'il avait demandée — la capture existait
pourtant bien sur le disque.
L'événement d'outil porte désormais le chemin de l'image et l'interface
la rend directement dans la bulle web_screenshot (cliquable pour la
loupe, comme les autres images du fil). Ce que le modèle en écrit
ensuite reste possible, mais n'est plus la condition de l'affichage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le modèle cherchait à vérifier visuellement son travail, ne trouvait pas
web_screenshot (offert seulement avec l'accès internet), en concluait
« pas de Chromium local » et partait installer puppeteer — alors qu'un
Chromium headless est dans l'image depuis toujours.
L'outil est donc offert aussi au mode code sans internet, mais borné aux
adresses locales (localhost, 127.0.0.1, ::1, *.localhost) : photographier
son propre serveur de dev est une vérification locale, pas une sortie sur
le web — l'interrupteur d'accès internet garde tout son sens. Le prompt
du builder dit explicitement que le navigateur est déjà là et qu'il ne
faut installer aucun paquet de navigateur.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- gofmt sur trois fichiers oubliés par le dernier commit (ci/test rouge).
- gopls@latest exige Go ≥ 1.26 alors que l'image de build est en 1.25 avec
GOTOOLCHAIN=local : l'étape Docker échouait net. GOTOOLCHAIN=auto télécharge
le toolchain requis, borné à l'étape de build (build-push GHCR rouge).
- Serveurs MCP : npx/uvx téléchargent leur paquet au premier lancement, et le
délai de connexion de 20 s tombait dessus (« enregistré mais connexion
échouée : context deadline exceeded » depuis le catalogue). Délai à froid
de 3 min pour ces deux lanceurs, et erreur de délai réécrite pour dire quoi
faire (réessayer : le paquet reste en cache).
- README : note sur ce premier lancement, ligne mode Code dans le tableau
des différences.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sélecteur Chat|Code par discussion dans le pied du composeur ; en mode
chat, une détection serveur suggère la bascule (puce ignorable, jamais
automatique). Conception reprise d'OpenFox (MIT), réécrite en Go — voir
NOTICE.md.
- Outils fichiers : read (lignes numérotées, borné), grep, glob, et le
tracker « lu avant d'écrire » qui refuse write/edit sur un fichier non
lu ou modifié depuis la lecture ; edit préserve les fins de ligne CRLF.
- Politique d'exécution : commandes catastrophiques refusées (rm -rf /,
mkfs, reboot…), chemins bornés au dossier de la discussion en mode
code, mutex par fichier.
- Critères d'acceptation : contrat posé via l'outil criteria (éditable
dans l'UI), passe de vérification indépendante sur contexte isolé —
seule habilitée à marquer « passed » — puis corrections plafonnées.
- Rôles embarqués (agents/*.md) : builder, planner, verifier, explorer,
code-reviewer ; badge de rôle dans le fil.
- LSP : gopls / typescript-language-server / pyright (inclus dans
l'image), diagnostics injectés dans le retour de write/edit.
- Git natif : git_status, git_diff, git_clone (borné à la discussion).
- Jobs d'arrière-plan bash_bg/bash_tail (serveur de dev, build long).
- Auto-retry : un appel d'outil écrit en texte (default_api:…,
<tool_call>…) relance le tour une fois avec consigne corrective.
- Outil ask : question à choix rendue en carte à boutons.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le sélecteur envoyait seulement `reasoning_effort` dans le corps de la
requête. Ce champ n'agit que sur les gabarits qui le LISENT (gpt-oss et
apparentés). Un modèle hybride à la Qwen3 ne connaît que `enable_thinking` :
il recevait `reasoning_effort: none`, son gabarit l'ignorait, et il
continuait de réfléchir pendant que l'interface affichait « aucune ». Les
llama-server récents traduisent eux-mêmes `none` en `enable_thinking=false`,
mais les binaires plus anciens laissent simplement tomber le champ.
On joint donc `chat_template_kwargs` à la requête — même procédé que la
compaction, qui coupe déjà la réflexion ainsi :
aucune (ou REASONING=off explicite) → {"enable_thinking": false}
basse/moyenne/haute → {"reasoning_effort": "<niveau>"}
auto → rien, le gabarit garde son défaut
Une clé REASONING absente n'est PAS une interdiction : sans consigne on ne
touche à rien. Une clé inconnue d'un gabarit est ignorée sans erreur.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RodUJoPJDBvVKhA6S5msvb
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.