TestTuneRecoverNeverReapsOwnLock échouait au hasard sous Linux (2 fois sur
5 déjà en v0.15.0, CI rouge). Le ménage (tuneReapStale) déplaçait un verrou
vivant, le relisait puis le remettait en place ; si release() passait entre
les deux, il ne trouvait plus le fichier et rendait l'inscription. Le verrou
remis en place devenait orphelin, et le ménage suivant le prenait pour une
optimisation interrompue : moteur relancé, preset « rétabli » à tort.
Un mutex de processus (tuneFileMu) sérialise désormais les opérations sur
le fichier — prise, réécriture, rendu, déplacement-relecture du ménage.
L'arrêt d'un essai orphelin et le retour du preset restent hors du mutex.
Vérifié sous Linux : 40 passages sans échec, suite complète verte.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vécu : 62 Go de RAM dont 18 déjà pris par le reste du serveur. Le mode
« carte d'appoint hors RAM » avait été choisi sur la RAM TOTALE, et le
premier lancement, chargé d'écrire experts.bin, a monté le moteur à ~41 Go
de mémoire anonyme : le noyau l'a tué (OOM).
- Placement décidé AU LANCEMENT sur la RAM disponible (MemAvailable), plus
sur la RAM totale ; le mode retenu est écrit dans le journal du moteur.
- Nouveau mode « disque » : --mmap-experts, les experts sont lus dans les
fichiers du modèle par le cache système, rien n'est chargé au démarrage.
Choisi d'office quand la RAM disponible ne suffit pas.
- Plus de premier lancement qui écrit experts.bin : les modes « hors RAM »
et « experts.bin mappé » ne servent que si le fichier existe déjà.
- Réglage « Experts » dans la fenêtre de Strata : auto, en RAM, ou depuis le
disque ; la configuration montre le mode du dernier lancement et la RAM
disponible de la machine.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Les barres de progression qui se redessinent avec \r (téléchargement du
modèle par l'installeur de Strata) n'apparaissaient jamais : le journal ne
découpait que sur \n et restait figé pendant tout le téléchargement. Une
version toutes les 10 s est montrée ; \r\n reste une fin de ligne.
- Le suivi du job relançait une requête chaque seconde sans attendre la
précédente : quand le serveur était occupé, deux réponses ajoutaient les
mêmes lignes au journal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- web_images (amont v0.17.7) : recherche d'images DuckDuckGo, pendant de
web_search ; les images du web s'affichent dans le fil avec la loupe, sans
Referer, et disparaissent si le site les refuse. Schéma minimal pour tenir
le budget du préambule.
- Aperçu en direct du navigateur piloté (cu_autoshot.go) : une capture après
chaque action browser_* qui change la page, gardée en RAM, jamais envoyée au
modèle, retirée du journal en fin de tour ; une carte dans le fil, en fondu.
- Vignettes calculées par le serveur (?thumb=, amont v0.17.9) : ~560 px au
lieu de l'original (7 Ko au lieu de 550 Ko sur une capture), chargées deux
par deux avec un nouvel essai ; la loupe ouvre l'original.
- Un lien vers une image ([graphique](graph.png)) l'affiche au lieu d'un
bouton de téléchargement.
- Chrome du computer use tué avec Loki sous Linux (Pdeathsig), même en cas
d'arrêt brutal : plus de navigateur orphelin dans le conteneur.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN et adapté à Loki :
- liste réordonnable par glisser-déposer (SortableJS 1.15.6, MIT), ordre
gardé côté serveur (/api/presets/order) et suivi par le sélecteur de
l'en-tête ; le clic de fin de glissement ne bascule pas de preset
- pastille de contexte (32K, 128K, 40K…) et pastille « capacités » : œil
quand le preset lit les images (MMPROJ, API externe déclarée multimodale,
Strata), ampoule + niveau quand il raisonne
- éditeur : prompt système DE CE preset (rangé en base, sysprompt:<id>),
projecteur vision sur le CPU (--no-mmproj-offload, visible seulement avec
un mmproj)
- les projecteurs mmproj ne sont plus proposés comme modèle (éditeur et
sélecteur de l'en-tête)
- l'aide « ? » reste survolable sur un réglage grisé
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Désactivation du chiffrement (amont cac0cda, 19918c6) : une valeur
illisible ne bloque plus tout (quarantaine datée + copie du keyvault), et la
reprise au démarrage déchiffre aussi les conversations au lieu de retirer la
clé après les seules pages. Propre à Loki : les images de conversation
(chatimg/) sont maintenant déchiffrées elles aussi — elles restaient
chiffrées sans clé. Les .bak chiffrés devenus illisibles sont retirés.
- Preset externe (amont #95) : `loki serve` sort sans erreur (plus de boucle
systemd « MODEL non défini »), le pré-vol l'accepte, le superviseur du
conteneur ne le prend plus pour un plantage.
- Unité systemd du moteur : TimeoutStopSec=5 (bascule de preset bloquée 90 s
en pleine génération).
- Heartbeat SSE (amont #105) : plus d'écriture après le retour du handler.
- Paramètres → Configuration : commande exacte du moteur copiable (clé API
masquée, Strata compris) et nombre de couches du modèle à côté de NGL, lu
dans l'en-tête GGUF (amont #108, #43) ; aussi dans l'éditeur de preset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Strata (github.com/Niko1221/Strata, MIT) fait tourner Qwen3.8 Flash Next
(MoE 125 B) sur une carte de joueur en répartissant les experts entre VRAM,
RAM et disque. Intégration reprise d'AJEAN (backend_moe.go, « AJEAN MoE
1.0 ») : paquet figé de la release moe-v1.0, vérifié par SHA-256.
- backend_strata.go : détection de la machine, quant conseillé, installation
en tâche (même suivi que llama.cpp), preset ENGINE=strata, lancement du
serveur de Strata par `loki serve`, réglages d'un modèle installé
- pré-vol, vision, liste des presets, journal de chargement : Strata reconnu
- proxy OpenAI et relais : Host local pour Strata (il refuse un Host inconnu)
- benchmark et optimiseur refusés sur Strata (protocole propre à llama-server)
- UI : ligne Strata dans Paramètres → Moteur (visible aussi avec le moteur de
l'image), fenêtre d'installation et de réglages, moteur actif affiché
- image : python3 + python3-venv ; compose : memlock illimité
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Version consacrée à la vitesse sans dénaturer le modèle : les lots 1, 2 et 3
de performance. Ce qui est sans perte est actif d'office ; tout ce qui change
ce que voit le modèle, le placement ou les slots attend une clé.
- Version passée à 0.15.0 (run.go, versioninfo.json, ressources Windows
régénérées par goversioninfo seul — icônes inchangées).
- Notes de release réécrites : actif d'office, opt-in et leurs clés, ordre de
test conseillé, ce qui a été écarté (cache-reuse, context-shift, KV
quantifié par défaut) et pourquoi.
- README : une section « Performance » rassemble toutes les clés (défaut,
effet, prix ou zone grise), les outils sans clé, l'ordre de test et les
pistes écartées ; le détail de chaque clé y est déplacé depuis
« Fonctionnalités », qui n'y renvoie plus que par un lien.
- NOTICE inchangé : rien de ces lots ne reprend OpenFox ni AJEAN.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La vérification du rendu du gabarit après une mise à jour attend jusqu'à
vingt minutes que le nouveau moteur réponde. Un retour à la version
précédente, une autre version installée ou une seconde mise à jour pendant
cette attente la laissaient courir : elle comparait alors l'ancien moteur à
celui d'une autre bascule, ou écrasait le relevé de la suivante.
- Chaque bascule ouvre une génération ; une vérification périmée s'arrête
sans rien ranger, et son relevé n'écrase plus le suivant. Retour et
« utiliser » effacent le relevé, qui parlerait d'un moteur arrêté.
- /api/engine/plan (registre) et /api/engine/rollback (configuration et
redémarrage) refusent tout autre verbe que POST — l'interface n'utilise
que POST.
- Tests : vérification annulée en attente, relevé périmé ignoré, GET refusé.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Depuis que le processus web refait la reprise des optimisations
interrompues toutes les 30 s, elle tourne pendant que ce même processus
prend et rend ses propres verrous (bouton « Optimiser… », application d'un
résultat). Trois fenêtres lui faisaient écarter un verrou bien vivant : le
fichier créé avant l'inscription du verrou comme nôtre, l'inscription
retirée avant le fichier (sous Windows, un fichier qu'un autre lit ne se
supprime pas), et le renommage d'un verrou neuf pris entre la lecture et le
ménage. En phase « application », le preset tout juste vérifié était alors
défait, et le moteur relancé.
- Inscription avant la création du fichier, retrait après lui ; un
identifiant de prise (nonce) distingue deux verrous du même processus dans
la même seconde.
- Au moment de le rendre, le verrou est réécrit neutre (rien à défaire,
rien à relancer, aucun essai), puis retiré avec quelques essais.
- Le ménage relit ce qu'il vient de déplacer : un autre verrou, ou un verrou
vivant, est remis en place par un lien (jamais par-dessus un verrou neuf).
- La relance d'après reprise passe par serviceAction : une optimisation
neuve qui a pris le verrou entre-temps garde le moteur arrêté.
- Test : 60 prises et rendus en phase « application » sous une reprise en
boucle, aucune relance ; échoue sur l'ancien code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le lot 1 savait dire qu'un moteur officiel antérieur à b10864 évince trop tôt
les points de reprise des hybrides, mais laissait l'utilisateur chercher seul
quelle version prendre. Le panneau Moteur propose maintenant la version
recommandée, sur clic seulement, et garde un chemin de retour sans réseau.
La mise à jour elle-même n'est pas neutre pour le prompt : depuis b10763,
llama-server active preserve_reasoning par défaut, et un gabarit qui retirait
la réflexion des tours passés (Qwen3.6, clear_thinking) la rend alors — vide,
puisque Loki ne la renvoie pas sans REASONING_ECHO. Avant la bascule, Loki lit
le gabarit chargé et le verdict de la sonde ; si le rendu va changer, la
confirmation le dit et propose REASONING_PRESERVE=off (case cochée seulement
quand c'est établi : sur Qwen3.8, off changerait à son tour le rendu).
L'utilisateur choisit. Après la bascule, le rendu de conversations
synthétiques est comparé via /apply-template entre l'ancien et le nouveau
moteur, et le premier écart est signalé.
- encart sans réseau (build de confiance < b10864) : gains cités (points de
reprise, MTP rapide b11009, MTP qwen4exp b11331), SPEC reste off
- POST /api/engine/plan, sur clic : dernier build publié, vérifié ≥ b10864 et
tag figé existant ; sans réseau, une erreur et rien d'installé
- POST /api/engine/rollback + fichier engine/previous-bin : la version quittée
(gardée par le ménage du lot 1) ; le retour la rend « précédente » à son tour
- REASONING_PRESERVE=off posée dans le preset actif, seulement si le nouveau
moteur connaît --no-reasoning-preserve et que la clé est vide
- comparaison du rendu en tâche de fond, après chargement et changement de
build_info (un ancien moteur survivant ne fausse pas le verdict)
- aucune clé nouvelle ; rien au démarrage ; aucune mise à jour sans clic
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
L'avis rendu quand le refus attend les cartes du moteur (VRAM NVIDIA seule)
annonçait « refus seulement si les cartes listées par le moteur le
confirment » même avec LOAD_GUARD=off, où aucun refus n'est possible. Il
retombe alors sur l'avis ordinaire.
- Test : VRAM NVIDIA seule avec LOAD_GUARD=off, et aucun avis qui évoque un
refus quand la clé d'échappement est posée.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
À l'ouverture, l'éditeur demande en parallèle la taille du cache de prompts
(et celle du second slot) et les avis MoE. Chaque aperçu lançait son
nvidia-smi pour lire les mémoires totales — trois processus pour une réponse
identique, la mémoire d'une carte ne bougeant pas.
- La lecture brute des mémoires totales est partagée dix secondes ; le verrou
tenu pendant la lecture fait attendre les demandes simultanées sur la
même. La sélection des cartes (CUDA_VISIBLE_DEVICES) s'applique ensuite,
comme avant.
- Un échec est gardé le même délai : une salve ne relance pas trois lectures
vouées à échouer. « loki serve » ne lit qu'une fois : rien n'y change.
- Test : trois demandes simultanées, une lecture, chaque sélection juste ;
échec non relancé dans la salve.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La sonde de gabarit gardait le verdict de la DERNIÈRE forme sondée (outils,
chat_template_kwargs, reasoning_effort), quelle qu'elle soit. NUDGE_IN_TOOL
pouvait donc glisser le rappel de budget dans un résultat d'outil sur la foi
d'un « préfixe instable » conclu pour une autre forme — un autre jeu
d'outils, la réflexion coupée, un autre niveau — dont le rendu n'a rien à
voir.
- Les verdicts sont aussi rangés par forme (empreinte tplShape, au plus 16,
la plus ancienne cède) ; tplCapsFor rend celui de la forme demandée.
- NUDGE_IN_TOOL calcule la forme de la requête courante (mêmes outils,
kwargs et niveau que ceux envoyés) et ne place le rappel dans le résultat
que si CETTE forme est « instable » pour ce modèle ; forme jamais sondée ou
inconnue : message à part, comme sans la clé. Une relance outils coupés ou
tool_choice « none » (forme que la sonde ne voit jamais) se replie aussi.
- L'affichage (/api/perf/summary) et REASONING_ECHO gardent le dernier
verdict, comme avant. Sans la clé, rien ne change.
- Tests : verdict par forme dans la sonde, autre forme / autres kwargs /
autre niveau refusés, bout à bout avec le verdict d'une autre forme
(échouait sur l'ancien code).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
En ligne de commande, « loki tune » et le processus web avec des LOKI_HOME
différents (sudo qui retire un LOKI_HOME exporté, variable posée dans un seul
shell) ne se voient pas : le verrou tombait dans un dossier que l'interface ne
lit pas, l'arrêt du « vrai » moteur visait celui d'une autre configuration, et
l'essai chargeait à côté d'un moteur bien vivant — VRAM saturée, mesures
fausses, relance possible par l'interface en pleine mesure.
- Avant de commencer, la CLI regarde ce qui répond sur le port de son
LOKI_HOME : un llama-server (/health 200, 503 ou 401) alors que ce dossier
dit son moteur arrêté, un 401 sur /props avec sa clé d'API, ou un
model_path qui n'est pas son MODEL (même fichier vérifié par os.SameFile)
— refus, avec LOKI_HOME en clair et la marche à suivre (relancer avec le
bon LOKI_HOME, ou le bouton « Optimiser… »).
- Tout ce qui ne conclut pas laisse passer : rien n'écoute, autre service
(404), /props absent d'un moteur ancien, chemin illisible d'ici. Le
processus web n'est pas concerné : il fait foi pour son moteur.
- Test : faux moteurs httptest pour chaque cas, refus et passages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le verrou périmé n'était repris qu'au démarrage du processus web. Un
« loki tune » en ligne de commande tué sans ses defers (kill -9, terminal
perdu) laissait donc le vrai moteur arrêté — et peut-être l'essai en VRAM —
jusqu'au prochain redémarrage de l'interface.
- Le processus web refait la reprise toutes les 30 s, avec la même logique
qu'au démarrage : essai orphelin arrêté (identité complète vérifiée),
application interrompue défaite, moteur relancé seulement s'il tournait
avant, ni sur un preset externe ni s'il est déjà reparti.
- Coût : un stat par tic tant qu'aucun verrou n'existe ; pendant une
optimisation vivante, la lecture du verrou et de l'identité de son
propriétaire. Une seule goroutine, endormie sur le tic.
- Test : relance au tic qui suit la mort du propriétaire, une fois ; rien
sans verrou, rien pendant une optimisation vivante, rien pour un moteur qui
ne tournait pas.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La borne basse des poids en RAM se calculait avec la VRAM de nvidia-smi. Un
moteur Vulkan sur une machine NVIDIA + AMD, sans --device, place aussi des
poids sur la carte AMD : la borne était fausse et le lancement refusé à tort.
Un moteur ROCm à côté d'une carte NVIDIA n'utilise pas du tout celle-ci.
- Le refus se décide désormais sur les cartes que le moteur liste lui-même
(--list-devices, dans l'environnement du lancement, sélection GPU
comprise), toutes additionnées : rien au-delà ne peut recevoir de poids, la
borne est sûre. Une carte listée sans servir (iGPU) ne fait que rendre le
refus plus rare.
- Lu seulement quand un refus se profile sur la VRAM NVIDIA : aucun lancement
de plus dans le cas ordinaire. Les cartes de SPLIT_MODE sont reprises si
elles sont déjà lues.
- Liste illisible, vide ou carte à 0 Mio (déjà pleine) : avertissement,
jamais de refus. Même chose dans l'aperçu de l'éditeur, qui reprend la
dernière liste complète du moteur (devices.json) sans le lancer.
- Le reste ne bouge pas : VRAM inconnue, --device, --rpc, mémoire unifiée.
- Tests : faux refus Vulkan évité, nvidia-smi seul n'est qu'un avis, lecture
déclenchée seulement avec un refus en vue, somme des cartes (JSON relu).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le refus d'un chargement résident trop gros pour la RAM faisait sortir
« loki serve » en erreur (code 1) ; l'unité loki-engine (Restart=on-failure,
RestartSec=3) le relançait toutes les trois secondes, sans fin, pour un échec
qui ne dépend que du preset et de la machine.
- Le refus sort avec un code dédié, 78 (EX_CONFIG) ; mustExit respecte le
code porté par l'erreur, toutes les autres gardent 1.
- L'unité écrite par « loki install » porte RestartPreventExitStatus=78 ;
les autres échecs restent relancés.
- Unité d'une version précédente (« loki update » ne réécrit pas les unités,
il tourne sans droits root) : reconnue à coup sûr (parent systemd,
INVOCATION_ID, unité et compléments lus sans la directive), le refus y sort
sans erreur pour ne pas boucler, avec la marche à suivre au journal.
Même chose sous launchd, qui relance toute sortie non nulle.
- Le moteur d'essai de l'optimiseur garde toujours le vrai code. Le
conteneur ne relance jamais le moteur de lui-même : rien à y changer, la
raison reste au journal et l'interface la montre (modelLoadError).
- Tests : directive reconnue (et seulement elle), code de sortie du refus,
enveloppé ou non, unité générée (Linux).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
L'éditeur de preset offrait deux fois les n-grammes du contexte : l'option
« n-grammes du contexte » (SPEC=ngram, bornes explicites et garde-fous de
Loki) et l'ancienne « n-grammes (mod) », qui écrivait --spec-type ngram-mod
tel quel dans EXTRA_ARGS. Le sélecteur n'écrit plus que SPEC.
- Option brute ngram-mod retirée de la liste.
- Un preset qui porte encore --spec-type ngram-mod l'affiche sous son nom
(« réglé à la main dans EXTRA_ARGS »), et une ligne propose de passer à
SPEC=ngram en disant ce qu'elle retirerait (--spec-type, --spec-ngram-*,
--spec-draft-* : laissés, ils feraient ignorer SPEC ou refuser les
n-grammes au lancement).
- Rien n'est réécrit à l'ouverture ni sans clic, et le moteur lit toujours le
drapeau brut : les presets existants tournent comme avant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Après une compaction en cours de tour, generate rangeait la vue publiée en
retirant TOUS les messages système de tête. Ceux que la vue d'envoi avait
posés (préambule, prompt du preset, contexte du projet ou bloc figé) n'ont
rien à faire dans l'historique ; mais le rappel « [MEMORY PAGES REMINDER] »,
posé par une compaction de début de tour, en fait partie — il partait avec
eux, et le modèle perdait la liste des pages à relire.
- historyFromView : un système de tête n'est gardé que s'il figurait en tête
de l'historique d'où la vue est partie (la compaction garde la tête telle
quelle) ; un système fusionné par InjectSkills devant le préambule est rendu
tel qu'il était.
- Même correction pour la reprise du builder (mode Code) et le terminal, où
le /sys de l'utilisateur disparaissait de la même façon.
- Test : la compaction réactive en plein tour garde le rappel, avec et sans
PROJ_SNAPSHOT, et ne range aucun système injecté ; échoue sur l'ancien code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le système portait deux valeurs qui bougent : la date du jour — à minuit,
toute discussion en cours recalculait son prompt entier — et le dossier de
travail, propre à chaque discussion, si bien que deux discussions ne
partageaient même pas leur système. Avec PROJ_SNAPSHOT, ces valeurs
rejoignent le bloc projet figé, en tête du premier message ; le système ne
garde que les consignes (« ton dossier de travail est dans ton contexte »).
- Bloc « Environment (from Loki) » : date (avec l'année, et l'année périmée
que la consigne web nommait), dossier de la discussion, ou poste distant
ciblé (nom, système, dossier, hors ligne).
- Contenu toujours vivant : relu à chaque tour ; un changement de jour part en
une ligne de <context_update>, tout autre écart (poste hors ligne, autre
cible) renvoie le message entier. L'avertissement « hors ligne » ne fige
donc plus le système.
- Hôte, compte et dossier des scripts, les mêmes pour toutes les discussions,
restent dans le système.
- Seul l'assemblage d'un tour de discussion avec la clé pose caps.envInCtx :
tâches planifiées, sous-agents, vérification et terminal gardent leur
système, date et dossier compris.
- Sans la clé : système identique à l'octet (gabarits relevés sur le code
d'avant, test). Format d'instantané passé à 2 : les instantanés persistés
sont repris neufs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La ligne de l'optimiseur s'affichait dans l'éditeur de TOUT preset local, y
compris un preset neuf ou un autre que celui en service, alors que la mesure
porte toujours sur le preset actif (le README le dit) : un clic depuis
l'éditeur d'un autre preset aurait optimisé et proposé d'écrire celui en
service.
- La dernière liste des presets est gardée ; la ligne n'apparaît que si le
preset ouvert est l'actif, et local.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Avec REASONING_ECHO, tout 400/500 dont le corps contenait « thinking » passait
pour un refus du raisonnement renvoyé, et ce test passe avant le filet des
outils. Une erreur d'analyse d'appel d'outil cite le texte du modèle : s'il y
écrivait « thinking », le tour était rejoué sans raisonnement et, si la
relance passait par chance, la clé était coupée pour ce modèle jusqu'au
redémarrage.
- « Failed to parse input … » (appel d'outil mal formé) n'est jamais un refus
du gabarit : le filet des outils s'en charge, comme sans la clé.
- « thinking » ne compte que dans une erreur du gabarit (jinja, template) ;
raise_exception et les messages invalides restent reconnus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Les blocs <context_update> de PROJ_SNAPSHOT étaient reconnus à leur seul
préfixe de texte, et le nettoyage tournait AUSSI sans la clé (requête,
historique persisté, titre, tâche du vérificateur, export, résumé). Un message
de l'utilisateur qui commençait par cette balise — un journal de requête collé
par qui travaille sur Loki — perdait son début en silence : le défaut n'était
plus identique à l'octet.
- Message.CtxUpd marque le message où Loki a posé un bloc (prependCtxUpdate),
comme ImgRelay pour les relais d'image ; seuls les messages marqués sont
nettoyés, et la marque tombe avec le bloc.
- La marque ne quitte jamais Loki : retirée à l'envoi avec celle des relais.
- Sans la clé, aucun message n'est marqué : rien n'est retouché.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le README promet que les images gardées partent d'abord et que le résumé
(avec perte) ne suit que s'il reste nécessaire. C'était vrai en début de tour
et entre deux étapes, pas ailleurs : la compaction de fin de tour, le filet
réactif (prompt refusé) et la fenêtre pleine en génération retiraient les
images ET résumaient d'un même geste, alors que le retrait seul suffisait.
- Fin de tour : même retrait que le début de tour, puis le seuil est relu.
- Prompt refusé pour débordement : images retirées, requête rejouée sans
elles, avant la compaction de secours.
- finish=length : images retirées d'abord ; l'étape est rejouée sans résumé
si le contexte libéré suffit.
Sans la clé, aucune image gardée : chemins d'avant à l'identique.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La garde du mode de chargement est le seul refus actif par défaut du lot 2 :
il ne doit tomber que sur un échec certain, et se dire là où l'on regarde.
- Serveurs --rpc (ou LLAMA_ARG_RPC) : une part des poids part ailleurs, la
VRAM locale ne borne plus rien — avertissement, jamais de refus.
- Mac Intel : la mémoire n'est unifiée que sur Apple Silicon ; ailleurs la
VRAM est inconnue, donc jamais de refus (l'agrandissement du cache RAM
reste coupé sur tout macOS, comme avant).
- LLAMA_ARG_NO_MMAP / LLAMA_ARG_MLOCK restées dans l'environnement ne
comptent plus sur un moteur à --load-mode, qui les ignore : un lancement en
mmap n'est plus refusé pour elles.
- Le refus sortait de « loki serve » avant tout chargement : l'interface
affichait un chargement sans fin. modelLoadError le reconnaît et dit quoi
faire (--load-mode mmap ou LOAD_GUARD=off).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Les proxys de Loki (/v1 et le relais) comptaient TOUTE requête d'un client
comme une requête qui passe par le slot : un GET /v1/models ou une sonde
/health (Open WebUI en envoie régulièrement) annulait le préchauffage en vol
et retirait le tampon de la discussion. Avec PREWARM, COMPACT_CONTINUATION ou
SLOT_PERSIST, la clé devenait inopérante dès qu'un client sondait le moteur.
- engineProxyBegin : une lecture (GET, HEAD) reste comptée en vol, comme
avant, mais n'avance pas le numéro des requêtes et garde le préchauffage ;
une complétion de client garde le comportement d'avant.
- README : avec PREWARM, SLOT_PERSIST ne garde presque rien (le préchauffage
passe par le slot après chaque tour) — dit plutôt que découvert.
Sans aucune de ces clés, rien ne change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le verrou de l'optimiseur ne doit jamais bloquer quand aucune optimisation
ne tourne, et ne jamais passer pour périmé quand elle tourne. Plusieurs
identités de processus ne tenaient pas ces deux promesses hors Windows.
- Linux : l'instant de démarrage est relatif au boot. Après une coupure, un
service relancé au même moment du démarrage retrouvait PID et top : le
verrou de l'ancien passait pour vivant et tout était refusé jusqu'à son
retrait à la main. L'identifiant du boot le préfixe ; un verrou qui porte
notre PID sans être l'un des nôtres est périmé.
- Linux : un binaire remplacé par une mise à jour (« (deleted) ») faisait
passer le propriétaire vivant pour mort. La mise à jour est de toute façon
refusée pendant une optimisation (interface et loki update).
- macOS : ps rend lstart dans la langue et le fuseau de l'appelant ; le web
(launchd) et un terminal en français ne lisaient pas la même identité. LC_ALL=C
et TZ=UTC.
- Un verrou illisible tout juste créé (vide entre création et écriture) n'est
plus retiré : seulement après 10 s.
- loki tune : terminal fermé ou kill (SIGTERM, SIGHUP) annulent proprement,
comme Ctrl-C ; Ctrl-C retrouve son sens aux questions de fin.
- Application : la sauvegarde est notée dans le verrou ; un Loki tué avant la
vérification remet l'ancienne version au redémarrage. Deux clics rapprochés
ne lancent plus deux applications. Un échec de relance du vrai moteur se
voit dans le résultat, plus seulement au journal ; un essai qui survit à son
arrêt reste noté dans le verrou.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Avec SIDE_SLOT, les tours en profondeur d'un essai restent sur le second
slot : la reprise du cache ne dépend plus du slot choisi par le moteur
- Après application, un moteur qui meurt au chargement (OOM) ramène l'ancienne
version en quelques secondes au lieu d'attendre le délai entier
- La réécriture du verrou (essai en cours) réessaie quand Windows refuse de
remplacer un fichier lu au même instant
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chercher à la main le bon micro-lot, les threads ou la marge --fit d'un preset
demandait de dupliquer, basculer et comparer des bench complets un par un.
L'optimiseur le fait sur un moteur d'essai, sans jamais toucher à config.env ni
à ce que calcule le modèle, et n'écrit rien sans un clic.
- Isolation : moteur principal arrêté puis relancé ; chaque essai est une copie
de la configuration (LOKI_HOME/tune/run), lancée par « loki serve » sur
127.0.0.1 et un port libre, groupe de processus tué en sortie
- Verrou exclusif inter-processus (LOKI_HOME/tune.lock, PID + démarrage +
binaire) vérifié par serviceAction start/restart et par le chat, les tâches,
bash_bg, /v1, le bench, la bascule et l'enregistrement de preset, GPU, clé
d'API, moteur ; essai orphelin arrêté avant tout démarrage du moteur
- Essais : lots, threads, délestage, marges --fit, files CUDA ; placement --fit
et SPEC/CUDA_GRAPH_OPT/--backend-sampling seulement sur demande
- EXTRA_ARGS jeton par jeton, liste blanche seulement ; ligne de commande de
chaque essai composée à blanc et comparée à la référence (« dénature »),
contexte, slots et cache KV recontrôlés une fois chargé
- Mesure : bench complet du lot 1 à profondeur fixe ; score = durée d'un tour
type (médianes de la télémétrie) ; gain > max(3 %, écart entre passages)
- Application sur clic : copie du preset, ou preset actuel sauvegardé, réécrit
clé par clé, vérifié par une sonde et rétabli en cas d'échec
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Un MoE aux experts sur CPU se règle aujourd'hui à la main (-ot, --n-cpu-moe
40, UBATCH 512) sans que Loki dise ce que --fit ferait mieux, ni qu'un
--load-mode none/mlock trop gros pour la RAM mène au swap ou à l'OOM
(llama.cpp #26110). Rien n'est réécrit en douce : des avis, une copie sur
demande, un refus seulement quand l'échec est certain.
- moeNotes (pure, comme nglArgs) : experts placés à la main → --fit ne les
place pas, et ne pas monter UBATCH seul (tampon plus gros sur chaque carte) ;
placement auto, MoE plus gros que la VRAM, UBATCH ≤ 512 → « UBATCH 2048+ » ;
moteur sans --fit → « garde --n-cpu-moe » ; --fit coupé par un autre -ot.
Le détecteur cpuExperts ne compte un -ot que s'il vise les experts (exps)
vers le CPU : celui de per_layer_token_embd n'en est pas.
- « Dupliquer en placement auto… » (éditeur, /api/preset/autoplace) : COPIE du
preset affiché, sans -ot des experts, --n-cpu-moe, --cpu-moe, -ngl,
--tensor-split, --fit off, -b/-ub ni drapeaux de chargement ; NGL retiré,
UBATCH 2048, BATCH 4096, --load-mode mmap ; CTX, cache KV, échantillonnage
intacts. Refusée si --fit resterait inactif ou si CTX vaut 0. L'original
n'est pas touché, la copie n'est pas activée.
- loadModeRisk : none, mlock, dio, mmap+mlock (et --no-mmap, --mlock,
LLAMA_ARG_*) face à la RAM effective (limite cgroup comprise), en comptant
modèle moins VRAM (tout le modèle sur macOS) : avertissement au-delà de
80 %, refus au lancement au-delà de 90 % de la borne basse seulement ;
LOAD_GUARD=off le lève. VRAM inconnue : jamais de refus.
- Éditeur : avis sous « Experts MoE sur CPU » et sous « Chargement ».
Ligne de commande et environnement du moteur inchangés (test de
non-régression) ; presets externes ignorés.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deux cartes inégales (5060 Ti + 3060) se placent par deux règles du moteur
qui peuvent s'opposer : --fit remplit d'abord la DERNIÈRE carte, qui porte
la couche de sortie, et les experts MoE en RAM sont recopiés au prefill vers
la PREMIÈRE. Pour choisir l'ordre il faut voir la liaison de chaque carte ;
l'éditeur n'en montrait rien.
- detectGPUs lit aussi la liaison PCIe (génération et largeur, actuelles et
maximales), le bus et l'horloge mémoire max, dans la même invocation de
nvidia-smi. « [N/A] » vaut zéro ; un pilote qui refuse un champ fait
retomber sur la requête historique (jamais de GPU perdu), un délai dépassé
n'est pas relancé. Borné à 10 s. La largeur du bus mémoire n'existe pas
dans nvidia-smi : absente.
- /api/backends/devices : une seule lecture nvidia-smi par énumération
complète mémoire manquante ET liaison (annotateDevices, pure), par nom de
carte, CUDA seulement, rien pour deux cartes homonymes. La lecture des
jauges (gpuStatsCached) n'est pas touchée.
- Éditeur : liaison max par carte (l'actuelle et l'horloge en info-bulle),
guide de placement dans l'ordre du moteur (--device compris), marges
FIT_TARGET carte par carte, signalement d'un --tensor-split ou d'un
CUDA_VISIBLE_DEVICES propre au preset, lien « inverser l'ordre » ; l'ordre
choisi survit aux cases cochées.
- Pas de bouton de mesure : comparer deux ordres demande deux rechargements
et un ordre inversé peut manquer de VRAM — le guide explique la marche à
suivre (copie du preset, bench complet sur chacun).
- loki gpu affiche la liaison.
Rien ne change dans la ligne de commande du moteur.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TestToolResultsSaveLoadDelete échouait de temps en temps en suite
complète (1 fois sur ~300 en isolé). Vraie course dans le code, pas dans
le test : deleteToolResultsFor retirait les résultats EN ATTENTE de
l'écrivain différé, mais pas ceux qu'il avait déjà pris dans son lot.
Si la transaction de suppression passait avant celle de l'écrivain, le
résultat était écrit juste après avoir été effacé, et renaissait.
En production, la suppression d'une discussion passe d'abord par
forget + flush, ce qui masquait le problème ; mais forget non plus ne
pouvait rien contre un lot déjà parti.
- l'écrivain note les résultats du lot en vol ; dropToolRes (donc
forget et deleteToolResultsFor) les marque annulés
- l'écrivain vérifie l'annulation DANS sa transaction ; la suppression
annule AVANT d'ouvrir la sienne : bbolt sérialisant les deux, soit le
résultat est sauté, soit il est écrit puis effacé
- annulations oubliées à la fin du lot : un nouveau résultat s'écrit
- test déterministe (écrivain retenu en plein lot) qui échouait avant
la correction ; suite complète passée 3 fois, TestToolResults ×400
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
En découpe par couches (défaut), chaque carte traverse ses couches à son
tour. En mode tensor, chaque couche est coupée entre les cartes, qui
additionnent ensuite leurs morceaux : décodage plus rapide d'après
l'amont, prefill nettement plus lent. Sur deux GeForce en PCIe et une
boucle d'agent qui relit beaucoup, rien n'est acquis : expérimental,
off par défaut, et une longue liste de refus propres plutôt qu'un
moteur qui meurt en boucle. Sans la clé, ligne et environnement
identiques à l'octet près (test).
- détection : la liste littérale {none,layer,row,tensor} dans l'aide du
moteur — « tensor » seul y est partout (--tensor-split) ; absente =
aucun compagnon posé, note au journal
- GGML_CUDA_ALLREDUCE=internal sauf choix de l'environnement : NCCL
compresse en BF16 les réductions du prefill (avec perte) ; nccl
gardé s'il est posé, avec un avertissement ; note NCCL indisponible /
P2P à poser soi-même
- -ngl all, -ts au prorata de la VRAM totale (--list-devices, ordre du
moteur, --device compris), -fa on : chacun seulement si EXTRA_ARGS ou
une LLAMA_ARG_* ne l'a pas déjà ; CTX explicite exigé
- refus : KV autre que f16/bf16/f32 (KV_TYPE*, -ctk/-ctv, LLAMA_ARG_*,
jamais réécrit), --backend-sampling, -fa off, -sm à la main, poids ou
KV sur CPU, NGL partiel, SPEC, SIDE_SLOT, une seule carte ou non CUDA,
architectures exclues par llama.cpp et qwen4exp, preset externe
- pas de --fit en mode tensor : estimation poids + cache de CTX jetons
contre 90 % de la VRAM, refus au-delà, CTX jamais réduit
- files CUDA, CUDA_GRAPH_OPT et FIT_TARGET voient le -sm tensor ;
SLOT_PERSIST refusé avec la clé ; diagnostic du journal pour les
échecs propres au mode tensor
- README et configTemplate
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
En mode Code, le modèle recopie sans cesse ce qui est déjà dans le
contexte : chemins, diffs, arguments JSON, fichiers réécrits. ngram-mod
de llama.cpp y cherche la suite des 24 derniers jetons et propose d'un
coup les 48 à 64 suivants, sans modèle brouillon. La clé SPEC (off par
défaut, inchangé) gagne deux valeurs : ngram, et mtp+ngram qui y ajoute
la tête MTP. Même vérification exacte que MTP : distribution inchangée,
mais pas le même texte au bit près (vérification par lots), donc on
compare des sessions rejouées, pas des diffs.
- forme explicite seulement : --spec-type ngram-mod
--spec-ngram-mod-n-match 24 --spec-ngram-mod-n-min 48
--spec-ngram-mod-n-max 64 ; jamais --spec-default (TODO amont, contenu
susceptible de changer avec le moteur)
- garde-fou d'aide sur « --spec-ngram-mod-n-match » (pas le simple mot
ngram-mod, encore listé par les moteurs d'avant le renommage) ; absent =
SPEC ignoré, note au journal
- mtp+ngram : --spec-type draft-mtp,ngram-mod seulement avec draft-mtp
dans l'aide ET une tête MTP réelle (tenseur nextn ou MODEL_DRAFT) ;
sinon n-grammes seuls, avec la raison — jamais « failed to create MTP
context » en boucle
- EXTRA_ARGS/LLAMA_ARG_* : mêmes exclusions qu'avant, plus --spec_type et
tout --spec-ngram-* (--spec-type s'additionne sans prévenir) ; pour
ngram seul, un --spec-draft-* fait aussi taire Loki
- poids sur CPU (experts MoE, -ot, NGL partiel) : refusé tant que
GGML_OP_OFFLOAD_MIN_BATCH (env ou OP_OFFLOAD_MIN_BATCH) ne dépasse pas
le lot de vérification de 65 jetons, avec la suggestion
OP_OFFLOAD_MIN_BATCH=128 ; avertissements pour un modèle plus gros que
la RAM, les points de reprise d'un hybride, NGL imposé (logits)
- avec ngram, une tête MTP ou MODEL_DRAFT inutilisés sont signalés
- --spec-synth-len/--spec-synth-rates (et LLAMA_ARG_SPEC_SYNTH_LEN) :
acceptation au hasard, avertissement au lancement ; Loki ne les pose
jamais
- télémétrie : /api/perf/summary donne draft_n, draft_accepted et
draft_rate par nature de requête
- UI : deux options du sélecteur de décodage spéculatif ; README et
configTemplate
- tests : forme explicite, garde d'aide, repli de mtp+ngram, exclusions,
seuil d'offload, ligne inchangée sans SPEC, acceptation par nature
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Passer de A à B puis revenir à A relance llama-server deux fois, et la
conversation de A est recalculée en entier au retour. Avec SLOT_PERSIST=on
(off par défaut), Loki demande au moteur d'écrire l'état du slot 0 juste
avant la bascule, et le recharge au retour avant le premier message de la
discussion — seulement si tout concorde, sinon calcul normal, en silence.
llama.cpp ne vérifie à la relecture que types et tailles : rien ne dit quel
build ni quels réglages ont calculé ce cache. La clé de validité est donc
l'empreinte de tout ce qui touche au calcul, prise par « loki serve » au
lancement : ligne de commande complète (modèle, CTX, types KV, NGL, lots,
gabarit, SPEC, EXTRA_ARGS…), LLAMA_ARG_*/GGML_*/CUDA_*, taille et date du
modèle et de ses tranches, du projecteur, du brouillon, du binaire et de
ses bibliothèques. Pas de cas « mise à jour du moteur » : elle change la clé.
- --slot-save-path posé aussi pour SLOT_PERSIST accepté, sous les mêmes
conditions de sécurité que l'isolation du lot 1 (boucle locale ou clé
d'API) ; refusé avec le décodage spéculatif (le save ne garde pas le
brouillon ; /slots speculative revérifié à chaque fois), des poids sur
CPU, un --slot-save-path à la main ; la ligne reste alors celle d'avant
- sauvegarde depuis l'interface ou une tâche, avant l'arrêt : le slot doit
porter un tour de la discussion (tampon d'engineMarkMain), ≥ 4096 jetons,
≤ 8 Gio estimés, double de place libre ; nom provisoire, renommé
seulement si aucune requête n'est partie pendant l'écriture
- rechargement avant la première requête de la discussion (ou son
préchauffage) : même clé de moteur, même discussion, slot 0 vierge (/slots
sans id_task) ; une seule tentative, fichier retiré ; toute erreur =
calcul normal
- deux fichiers au plus ; clé retirée = tout effacé au lancement suivant ;
SLOT_PERSIST préservé à la bascule comme HOST (sinon B effaçait l'état de A)
- README : SIDE_SLOT, le cache KV plein ne déclenche jamais de compaction
- tests : plan et refus, ligne inchangée, empreinte (clé d'API exclue,
fichiers et réglages inclus), rotation, séquence save puis restore sur un
faux moteur, refus à la sauvegarde et au rechargement, autre clé, défaut
sans aucun appel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
En --parallel 1, une vérification, un sous-agent, une tâche ou un bench
prennent le slot de la discussion : son état part dans le cache RAM et en
revient, ou se recalcule s'il n'y tient plus. Avec SIDE_SLOT=on (off par
défaut), le moteur ouvre deux slots et la discussion garde le sien.
Dimensionnement choisi pour qu'un débordement concurrent soit impossible par
construction : cache KV NON unifié (--no-kv-unified) et -c 2×CTX, soit deux
flux séparés de CTX jetons. Sous cache unifié, les deux slots partagent le
pool et, plein, llama.cpp renvoie « Context size has been exceeded. » aux
deux — que Loki prenait pour un débordement de la conversation. Chaque slot
garde la fenêtre entière : ctxWindow et la compaction restent sur CTX, rien
n'est raccourci. --cache-idle-slots ne vide les slots au repos que sous cache
unifié : rien à régler.
- lancement : refus (un slot, ligne d'avant, note au journal) si modèle + deux
états dépassent 90 % de la VRAM, VRAM ou GGUF inconnus, poids sur CPU,
moteur sans --no-kv-unified, PARALLEL≠2, ou -c/-np/-kvu/--no-kv-unified/
--kv-unified-per-slot/--cache-idle-slots dans EXTRA_ARGS (ou leurs
LLAMA_ARG_*) ; note de VRAM en plus, avertissement si NGL imposé
- routage id_slot seulement si /props annonce 2 slots d'au moins CTX jetons :
0 pour le tour, ses étapes, le préchauffage et le résumé en continuation ;
1 pour vérification, sous-agents, tâches, bench, résumé sur transcription
- clients /v1 (proxy et relais) : corps réécrit en id_slot 1, quel qu'il soit
- cohérence lot 1 : l'effacement de slot s'abstient (2 slots) ; une requête
du slot 1 n'avance pas le numéro d'engineSlotHolds, la continuation de
compaction reste possible après une vérification
- « Context size has been exceeded. » sans nombre de jetons, deux slots en
service : requête rejouée telle quelle, jamais compaction ni réduction
- préchauffage permis sur les deux slots de SIDE_SLOT, vers le slot 0
- éditeur de preset : VRAM du second slot ou raison du refus
- tests : ligne identique sans la clé, chaque conflit refuse sans changer la
ligne, routage par nature, aucun id_slot sur un slot / preset externe /
fenêtre partagée, rejeu sans compaction, proxy forcé, préchauffage
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le relais rangeait l'image sur disque avant de regarder si le budget des
images gardées était déjà plein : un fichier écrit pour rien, élagué 24 h
plus tard.
- budget vérifié d'abord ; plein, l'image reste éphémère sans être rangée
- README : sur un modèle hybride, le gain dépend des points de reprise du
moteur après une image, à vérifier dans la télémétrie avant d'adopter la clé
- test : budget plein, rien d'écrit dans chatimg
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Une capture ou une image vue par see_image ne vivait que le temps du tour :
au tour suivant elle manquait à l'historique, le préfixe en cache divergeait
à elle et toute la boucle d'outils qui suivait était recalculée. Sur Qwen3.5,
le rappel de budget en message user faisait de même pour le tour en cours :
le nouveau message devient la « dernière question » et tout le tour est rendu
autrement. Deux clés, off par défaut et étiquetées zone grise ; sans elles,
requêtes identiques à l'octet près (testé).
- KEEP_TURN_IMAGES : image gardée par référence (chatimg, chiffrée si la
mémoire l'est), relue à l'identique avant d'être gardée, vision active
revérifiée à chaque tour ; preset externe seulement s'il déclare la vision
- coût mesuré par le moteur (écart de prompt_tokens moins le texte ajouté) ;
image non mesurable = éphémère comme avant ; total sous 10 % de la fenêtre,
la première qui dépasse et les suivantes du tour restent éphémères
- compaction, réduction forcée et début ou milieu de tour au seuil retirent
ces images d'abord (légende et imageLostMarker gardés), le résumé ne suit
que s'il reste nécessaire ; clé retirée ou vision perdue : retirées au tour
- relais marqué ImgRelay (persisté, retiré à l'envoi) : ni demande réinjectée
ni preuve de demande servie, fil rouvert compris ; une image non gardée
n'entre plus dans l'historique par une compaction en cours de tour
- discussion seulement (ni tâche, ni sous-agent, ni vérification) ; fichier
d'image rafraîchi à chaque usage, l'élagage de 24 h ne le prend pas
- NUDGE_IN_TOOL : rappel de budget au bout du dernier résultat d'outil, même
message persisté ; moteur local, sonde de gabarit « préfixe instable » pour
ce modèle ; sinon, ou forme inattendue, message à part comme avant
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le refus mémorisé ne regardait que la taille du contexte. Il n'est évalué
qu'au-dessus du seuil : une discussion vidée, éditée ou régénérée qui
remontait dans la même plage de jetons voyait sa compaction sautée sur la foi
d'un refus qui concernait un autre fil.
- le refus garde l'empreinte de l'historique (celle de perfPrefix) ; seul un
historique qui prolonge celui d'alors en profite, tout autre retente
- testé : historique modifié ou raccourci, refus noté par une vraie
compaction, filet réactif jamais bloqué
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le résumé d'une compaction part aujourd'hui dans une requête à part — prompt
du résumeur et transcription des anciens tours — que le moteur local calcule à
froid : des dizaines de secondes sur un 27B, des minutes sur un MoE, à chaque
compaction. Nouvelle clé COMPACT_CONTINUATION (off par défaut) : quand le slot
porte encore le prompt de la discussion, la requête de résumé est celle du tour
telle qu'elle est partie, suivie d'une seule demande de résumé ; le moteur ne
calcule qu'elle. Sans la clé, requêtes de compaction identiques à l'octet près
(prompt du résumeur comparé à une copie figée, testé).
- vue MODÈLE pour tout ce qui est rangé : bornes, archives recall, demande
réinjectée, garantie de réduction et sortie ne changent pas ; la vue
d'envoi (turnViewDry, wireMessages, buildChatPayload) ne sert qu'à la
requête, rien d'injecté n'entre dans l'historique (testé)
- frontière recalée sur la vue envoyée (messages non système un pour un,
vérifiés rôle par rôle), désignée par le nombre de messages gardés et le
début du premier ; tout résumer avant, dater l'avancement à la frontière
- mêmes règles de résumé (+ mode Code), même budget, température 0.2 sans
l'échantillonnage du preset, enable_thinking=false ajouté aux arguments du
tour, reasoning_effort du tour, tool_choice « none », sans flux
- messages utilisateur de fin hors de la vue : pas en cache, et pas deux
`user` d'affilée pour les gabarits stricts
- slot vérifié : tampon posé par une étape de tour acceptée (llm_slots.go,
sous le verrou du compteur en vol), perdu dès qu'une autre requête part —
vérification, sous-agent, tâche, préchauffage, résumé, bench, /v1 — ou que
le modèle ou la fenêtre changent ; relu à l'envoi
- marge en jetons réels (dernier compte + non vu + demande + budget + 5 %)
- repli sur la transcription au moindre écart : refus du moteur, réseau,
appel d'outil émis (tool_calls décodés) ou écrit en texte, raisonnement
seul, résumé vide ; refus du gabarit ou appel d'outil = suspendue pour le
modèle jusqu'au redémarrage, deux échecs de suite aussi
- avec la clé : pas de résumé quand même un vide ne réduirait pas de 20 %
(borne exacte, avant tout archivage) ; après un refus faute de réduction,
pas de nouvel essai avant +10 % de contexte, jamais à 90 % de la fenêtre,
oublié quand le contexte baisse
- réactif, fenêtre pleine, bouton manuel, tâches, sous-agents, terminal,
preset externe : chemin d'avant ; télémétrie kind=compact avec la discussion
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le différé du préchauffage s'était glissé entre le commentaire du projet forcé
et setProjectOverride : il passe au-dessus, avec le sien.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Une compaction, un dernier message rendu autrement au tour suivant ou une
tâche planifiée qui a pris le slot laissent au message suivant un recalcul
inévitable : des secondes sur un 27B, des dizaines sur un MoE aux experts en
RAM, avant le premier mot. Nouvelle clé PREWARM (off par défaut) : dès que le
moteur local est libre, Loki lui envoie la requête du prochain tour suivie
d'un message utilisateur « . », max_tokens 1, sans flux. Le moteur calcule le
préfixe et pose un point de reprise au début de ce message ; le vrai tour ne
calcule plus que le sien. Sans la clé, aucune requête de plus (testé).
- assemblage partagé, contenu vivant : turnViewDry (même code que turnView,
sans rien toucher à la discussion, décision PROJ_SNAPSHOT via
projSnapDecide), wireMessages, turnReasoning et buildChatPayload, extraits
de runChat ; corps identique à l'octet près à celui d'avant (matrice
d'options et de modes, testée contre une copie de l'ancien code)
- jamais devant un vrai travail : toute requête de Loki l'annule au départ
(compteur de llm_slots.go, sous son verrou), sauf un tour de chat dont les
messages sérialisés, outils et arguments du gabarit prolongent exactement
le préfixe préchauffé
- moteur local, un seul slot (/props total_slots), pas pendant un tour, une
tâche, un bench, un travail annexe ni une requête en vol ; un seul à la
fois ; pas si le prochain tour compactera, ni à moins de 10 min de minuit
- requête brute : ni compaction, ni relance, ni effort appris, ni CtxUsed,
ni stats ; erreurs lâchées (une ligne de journal par statut) ; rien persisté
- déclencheurs : fin de tour sans file d'attente, fin de tâche (projet forcé
levé, slot effacé) ; full = aussi changement de discussion après 3 s
- capacités du dernier tour gardées en mémoire par discussion ; mode Code =
rôle d'un message ordinaire, une demande de plan diverge et annule
- télémétrie kind=prewarm ; la vérification du cache RAM après une tâche se
fait sur lui
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le contexte du projet (description, index mémoire, trackers, AGENTS.md) part
en tête du premier message utilisateur, reconstruit à chaque tour : une page
créée ou une valeur de tracker notée faisait recalculer toute la conversation
derrière lui, 10 à 45k tokens. Nouvelle clé PROJ_SNAPSHOT (off par défaut) :
avec on, chaque discussion garde une copie datée du bloc, renvoyée à l'octet
près, et les changements arrivent en tête du message suivant dans un bloc
<context_update from="loki">, rangé dans l'historique. Sans la clé, la requête
est identique à l'octet près (testé contre l'ancien assemblage).
- en-têtes figés « as of <date> » pour l'index mémoire et les trackers, sans
« answer straight from this » ; ligne fixe du système, seulement avec la clé
- mises à jour par type : +/~/- par page (clé « ](fichier) »), une ligne
complète par tracker (clé = slug), texte COMPLET pour la description,
AGENTS.md, ou un index dont la prose a changé — jamais de diff de prose
- état annoncé structuré et persisté (ProjSnap, omitempty), instantané pris
sur marqueur explicite : la première page d'un projet vide arrive en mise
à jour, le premier message ne bouge pas
- rafraîchi (blocs retirés de tout l'historique) quand le prompt change de
toute façon : compaction début/fin/manuelle, compaction ou réduction en
cours de tour (bloc vivant aussitôt, rangé comme instantané : pas de
recalcul de plus), système ou outils modifiés, redémarrage de Loki, réglage
moteur ou modèle, projet, nom, mode mémoire, mode Code, dépôt, textes des
en-têtes ; et au-delà d'un seuil de mises à jour accumulées
- mise à jour posée sous c.mu avec l'epoch, après la compaction de début de
tour ; texte ou multimodal ; retirée du titre, de l'export JSON, de l'entrée
du résumeur et de la tâche du vérificateur
- loadFrom, Reset et le chargement remettent l'instantané à zéro ; clé retirée
= nettoyage ; agent off = rien du projet ; tâches et presets externes
inchangés ; un sous-agent ne voit pas le bloc du tour
- ordre des trackers déjà déterministe (lot 1, test existant)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Les gabarits Qwen3.5/3.6 rendent <think>…</think> pour chaque message
assistant après la dernière question : c'est leur format entraîné. Loki ne
gardait que le texte et les appels, donc à chaque étape d'une boucle d'outils
le modèle relisait ses étapes précédentes avec des blocs vides, et le moteur
recalculait le dernier message. Nouvelle clé REASONING_ECHO (off par défaut) :
avec on, le raisonnement séparé par le serveur est gardé avec chaque message
et renvoyé au même modèle. Sans la clé, rien n'est capturé ni envoyé : la
requête est identique à l'octet près (testé).
- Message.ReasoningContent (reasoning_content) + ReasoningModel, persistés ;
seulement depuis reasoning_content du flux, jamais depuis le découpage
« </think> » fait chez nous (rendu en double)
- un seul point de sortie (echoMessages) : étiquette toujours retirée,
raisonnement retiré pour une API externe, un autre modèle, un message sans
texte ni appel, ou si la sonde dit que le gabarit ne le rend jamais
- comptage : estimateTokens/compactBounds comptent ce qui part et que le
gabarit rend (passé seulement si la sonde dit qu'il le garde, ou l'ignore) ;
ctxAfter ne retire plus un raisonnement renvoyé
- débordement : relance d'abord sans raisonnement, avant toute compaction ;
shrinkToFit retire le raisonnement le plus ancien avant tout le reste ;
le torse compacté le perd comme le résumé
- erreur de gabarit (raise_exception, thinking, Failed to parse messages) :
relance une fois sans raisonnement avant tool_choice none / outils coupés ;
retenu pour le modèle si la relance passe
- runBuilderTurn applique enfin NewHistory comme generate (compaction perdue
pendant une passe de correction) ; forwardStream affiche la bannière
- sonde de gabarit : nouveau verdict renders_reasoning
- REASONING_PRESERVE (lot 1) inchangée, son interaction documentée
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Les pistes de réutilisation du cache dépendent de deux faits que rien ne
mesurait : le gabarit du modèle rend-il encore le raisonnement d'un tour
passé, et le rendu d'un tour d'outil reste-t-il un préfixe exact quand un
message utilisateur s'y ajoute ? La sonde pose ces deux questions au moteur
local par POST /apply-template — un rendu sans état, qui ne prend ni slot ni
place dans la file — et n'en tire que des verdicts oui/non/inconnu.
Elle ne touche à aucune construction de prompt : Loki ne renvoie toujours
aucun reasoning_content au modèle. Toute évolution qui voudrait s'en servir
passera sa propre revue, et lira « unknown » comme « ne rien changer ».
- moteur local seulement ; preset externe : aucune requête, rien d'affiché
- jamais de complétion de repli : avec un slot, elle évincerait le cache
- en tâche de fond après une complétion complète du fil principal, /health
à 200, au plus une vérification par minute ; authHeader ; 3 s par requête
- mêmes outils, chat_template_kwargs et reasoning_effort que la complétion
- add_generation_prompt=false demandé au moteur ; un moteur qui l'ignore
donne « inconnu » plutôt qu'un faux « instable »
- tour d'outil bien formé (identifiant de 9 alphanumériques, résultat lié)
- 404, 401, exception du gabarit, délai : inconnu ; 503 relancé
- cache par build_info + modèle + empreinte du gabarit + forme de requête
- verdict dans /api/perf/summary (champ template) et une ligne [tplprobe]
au journal par verdict nouveau
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La relecture du bench en tâche de fond a trouvé quatre défauts qui le
faisaient échouer ou s'interrompre là où il n'aurait pas dû, et une
course entre la fin annoncée et le verrou rendu.
- Niveau de raisonnement refusé par le gabarit (Qwen3.8 et « high ») :
le bench rejoue une fois avec le niveau accepté, comme runChatTools,
et retient la traduction. Il échouait sur ce 500 dans un processus
neuf. L'erreur HTTP garde le corps entier : les niveaux acceptés sont
cités au-delà des 300 caractères affichés.
- Préchauffage borné au quart du contexte : -ub 4096 sur 8k de contexte
envoyait un prompt plus grand que la fenêtre.
- Changer de discussion n'annule plus le bench, et ne lui reprend pas
le verrou. La libération suit le drapeau benching au lieu de l'epoch,
qu'une bascule de fil bumpe. Reset arrête toujours le bench et rend
le verrou.
- Le verrou du chat est rendu avant que le statut ne dise « terminé » :
un message envoyé aussitôt n'est plus refusé (et le test n'est plus
instable).
- Indication « possible thrash » mesurée sur les tours seulement, pas
sur le prefill à froid qui lit légitimement les pages du modèle.
- Interface : agrégats absents (omitempty) affichés à 0 au lieu de
casser le rendu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
L'ancien bench ne lisait pas le statut HTTP, inventait un partage 15/85
du temps quand les timings manquaient (et l'enregistrait comme mesuré),
tuilait un petit corpus qu'un brouillon recopiait, et ne disait rien du
prefill à 30k ni du chemin où le cache est repris. Synchrone, il était
coupé par les proxys (60-100 s) pendant que le moteur continuait, et un
simple GET suffisait à le lancer.
- Tâche de fond : POST /api/bench (202, 405 sur GET, 409 si occupé),
/api/bench/status pour la progression, /api/bench/cancel ; chaque
requête au moteur porte le contexte annulable.
- Verrou de génération tenu pendant toute la mesure : chat, tâches et
compaction reçoivent un refus clair ; les clients /v1 un 503 avec
Retry-After ; /slots occupé (client externe, CLI) refuse aussi.
- Mode rapide par défaut : préchauffage jeté + la ligne courte, sur le
chat avec gabarit, raisonnement et échantillonnage du preset, seed fixe.
- Mode complet (bouton « bench complet », loki bench --full) : prefill à
froid à D = min(CTX/2, 32k, ce qui laisse tenir les tours), 16k si des
poids tournent sur CPU, puis 3 tours user → assistant → user de code
jamais vu. Reprise du cache lue dans cache_n, jamais supposée ; note
pour les hybrides (points de contrôle). Pas d'ignore_eos.
- Rien n'est enregistré sans réponses 200 et timings réels, ni pour un
résultat partiel (budget de 6 min par requête, contexte plein).
- Empreinte enregistrée (configuration + CUDA_VISIBLE_DEVICES + protocole) ;
les anciennes mesures retombent sur le nom du modèle. Types de cache KV
et build du moteur affichés, KV quantifié signalé (zone grise).
- Linux : indication « possible thrash » (read_bytes, VmRSS), jamais
enregistrée.
- Effacement du slot en fin de bench inchangé (engineSideJob, différé :
il tourne aussi sur erreur et annulation).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
« Exclusive access » n'est pas un réglage qu'on passe à Yes sur la page du
partage : c'est un état, que Unraid 6.12 accorde quand « Permit exclusive
shares » est activé et que le partage vit tout entier sur un pool, sans
stockage secondaire (/mnt/user/<partage> devient un lien vers le pool). La
consigne précédente envoyait chercher une option introuvable.
- conseil, compose et README : le vrai chemin (Global Share Settings,
mover vers le pool puis Secondary = None, « Exclusive access : Yes »),
et redémarrer le conteneur, Docker ne résolvant le lien qu'au démarrage ;
- un seul chemin par montage fuse.shfs : /data/models répétait /data ;
- l'encart de l'UI, permanent tant que la cause dure, devient masquable
(localStorage, pour ce texte seulement — un autre conseil réapparaît).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sur Unraid, /mnt/user/… n'est pas un disque mais shfs, un démon FUSE :
chaque fsync de loki.db et chaque page d'un gros MoE mmappé relue depuis
le disque (modèle plus gros que la RAM) le traverse, à chaque token. Rien
n'est perdu ni altéré, mais le décodage ralentit — sans que rien ne le
dise.
Loki ne déplace rien : changer le chemin hôte d'une installation donnerait
un /data vide. Il le voit et le dit, sur un ton d'information :
- sys_storagefuse.go lit /proc/self/mountinfo (type lu après le séparateur
« - », montage le plus long qui contient le chemin) pour LOKI_HOME et
chaque dossier de modèles ; Linux en conteneur seulement, mis en cache
une minute puisque /api/status est interrogé en boucle ;
- conseil en jaune au démarrage et champ « hint » de /api/status, affiché
en encart discret, distinct du bandeau rouge de perte de données ;
- compose Unraid et README : « Exclusive access » recommandé (même chemin,
FUSE court-circuité), /mnt/<pool> en option avancée avec migration
manuelle et mise en garde sur un pool inexistant (RAM).
La détection de thrash du cache de pages est laissée de côté : elle exige
un signal majflt calibré par modèle sur le serveur.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Relecture de l'écrivain différé (chat_persist.go) : l'ordre, la suppression
et les bascules tiennent ; trois écarts de comportement corrigés.
- saveToolResult et la télémétrie de generate lisaient la clé brute de la
discussion active ; passés à convEnsureActive, ils pouvaient en CRÉER une
(tâche de fond sur base neuve → discussion vide dans la liste). Nouveau
convActiveID : cache chaud, sinon la clé en base, jamais de création.
- Un résultat d'outil confié après la suppression de sa discussion restait en
mémoire pour toujours (l'écrivain l'écarte, la copie ne partait plus) : il
n'est plus retenu.
- Vidage de l'écrivain avant « verrouiller la mémoire » (sinon l'envoi tout
juste fait ne touchait le disque qu'au déverrouillage) et avant le restart
systemd après MAJ (SIGTERM ne laisse rien finir).
Tests : résultat d'une discussion supprimée ni écrit ni retenu ; base neuve,
saveToolResult reste sous « nosession » sans créer de discussion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
StartTurn persistait AVANT de lancer la génération : un marshal, puis deux
commits bbolt (contenu, puis index), soit deux fsync et plusieurs ouvertures
de base — 26-30 ms mesurés sur un NVMe Windows, bien plus sur un /mnt/user
d'Unraid ou un disque de parité, payés à chaque message avant que la requête
ne parte vers llama-server. Même facture au milieu d'un tour (vérification du
mode Code) et pour chaque résultat d'outil coupé (« voir plus »).
- chat_persist.go : un écrivain unique et ordonné. L'instantané reste pris
sous c.mu (images → références, marshal, identifiant de la discussion) ;
seule l'E/S part en différé. Le dernier instantané de chaque discussion
l'emporte (numéro pris sous c.mu), les lots sont fusionnés en UNE
transaction : contenu + index ensemble, résultats d'outils compris.
- Seuls StartTurn, la persistance en cours de tour et les résultats d'outils
sont asynchrones. Fin de tour, reset, bascules, compactage manuel restent
synchrones et attendent tout ce qui précède ; la suppression écarte puis
attend les écritures qui la visent (plus de discussion ressuscitée).
- Discussion active gardée en RAM, créée sous verrou (plus de double
identifiant sur base neuve) et changée sous c.mu avec le contenu : un
instantané ne peut plus écrire l'ancien fil sous le nouvel identifiant.
- « Voir plus » servi depuis la mémoire tant que le résultat n'est pas écrit ;
verrouillage vérifié avant de rendre un id.
- En plein tour, la base reçoit le journal sous sa forme de fin de tour
(compactLog, version pure de compactLogLocked) ; la mémoire n'est pas touchée.
- Vidage de l'écrivain avant redémarrage, « Quitter », exec et (dé)chiffrement.
Le contexte vu par le modèle ne change pas : il lit c.Messages en mémoire, les
résultats complets d'outils et la compaction du journal ne servent qu'à l'UI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gpuStatsCached tient son verrou pendant la requête : c'est ce qui fait
partager une seule lecture aux onglets. Mais la requête n'était pas bornée,
et le code le dit ailleurs (nvidiaGPUCount) : un pilote coincé peut faire
pendre nvidia-smi. Avant le cache, chaque nouvelle requête relançait son
propre nvidia-smi et les jauges revenaient avec le pilote ; avec le verrou,
un seul processus pendu bloquait /api/vram pour tous les onglets jusqu'au
redémarrage de Loki.
- nvidiaSmiQuery : CommandContext borné à 10 s (large pour une première
lecture lente sans mode persistant), WaitDelay d'une seconde. Au-delà,
une erreur ordinaire, retentée au délai normal du cache.
- Le déchargement profite de la même borne : une lecture pendue devient une
mesure ratée, que gpuSettle sait déjà sauter.
- Test : la vraie requête, binaire absent du PATH, rend bien
exec.ErrNotFound — la seule erreur que le cache garde une minute.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chaque onglet ouvert (et chaque téléphone) interrogeait /api/vram toutes les
3 s, et chaque requête lançait son propre nvidia-smi — y compris depuis un
onglet en arrière-plan que personne ne regarde, pendant que llama-server
génère. Rien de tout cela ne touche au modèle : seules les jauges sont
concernées.
- gpuStatsCached : lecture partagée de 2 s, verrou tenu pendant la requête
pour que des appels simultanés attendent la même lecture. nvidia-smi
introuvable (Mac, CPU seul) mémorisé une minute ; une autre erreur se
retente au délai normal.
- Seul handleVram passe par le cache. Le déchargement (lecture « avant » et
gpuSettle) garde des lectures fraîches : deux valeurs égales servies par
le cache feraient conclure gpuSettle à tort. Le cache est vidé après un
déchargement.
- UI : statut, VRAM et RAM ne sont plus interrogés dans un onglet caché ;
lecture immédiate au retour.
- Tests : cache (réutilisation, erreur, binaire absent, appels simultanés)
et échantillonneur du déchargement qui contourne le cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chaque événement de frappe (un par ligne) appelait bodyLineCount sur le corps
ENTIER pour remplir body_lines : un reste de coût quadratique, léger mais
inutile, puisque argPreview tient déjà le compte des sauts de ligne.
- argPreview.lineCount : même règle que bodyLineCount (saut de ligne final
sans ligne de plus), en temps constant.
- Le test de décodage par morceaux vérifie l'égalité avec bodyLineCount à
chaque préfixe (fuzz compris).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pendant qu'un modèle tapait un write de 120 Ko, chaque morceau faisait relire
et redécoder tout le JSON des arguments, puis republiait le corps ENTIER à
chaque ligne : 250 Mo de flux SSE, autant de tas vivant dans le journal, et
près de 8 s de CPU volées au décodage quand des experts tournent sur le
processeur. Même travers pour la détection des appels d'outils écrits en
texte, qui repassait toutes les regex sur toute la réponse à chaque jeton.
Rien de ce que voit le modèle ne change : les arguments exécutés restent
entiers, la passe complète de fin de tour aussi.
- argPreview : lecture incrémentale de l'argument affiché et du corps, même
règle que previewArgDone (barre oblique coupée gardée en attente) ;
arguments accumulés dans un strings.Builder, cur.Function.Arguments recalé
sur lui à chaque morceau.
- Événement de frappe : seules les 40 dernières lignes (4 Kio au plus) du
corps, avec body_lines (vrai « +N ») et body_tail ; l'UI affiche « … » et
garde le repli sur le décompte local.
- textualToolCallFrom : ne relit que la fin, depuis un vrai début de ligne
avant la fin du balayage précédent, en reculant sur les suites que les
motifs embarqués peuvent traverser. Surcharge retry_patterns : balayage
complet, comme avant.
- Direct SSE : les événements trouvés à un réveil partent en une écriture
(lots de 128 Kio / 256 événements au plus), un sceau par événement en E2E.
- Tests : fuzz d'argPreview contre previewArgDone, équivalence fenêtre /
balayage complet au morceau près, corps borné de bout en bout, ordre des
lots ; bancs d'essai.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mcpPromptLine relisait le préfixe « [MCP: <serveur>] » des descriptions en le
réécrivant à la main : changer le format dans mcpTools aurait fait disparaître
la ligne en silence. Un nom de serveur contenant « ] » y était aussi coupé.
- mcpTagDescription pose le préfixe, mcpToolServer le relit : un seul endroit.
- mcpToolServer retient la coupure dont la forme assainie est celle du nom
d'outil — « a] b » reste « a] b ».
- Les tests qui appellent EnabledTools isolent $LOKI_HOME : la configuration
MCP de la machine de test (et ses connexions) ne fausse plus le cas « sans
outil MCP » ni le budget du préambule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le préambule système était bâti (InjectSkills → baseSystemPrompt) AVANT que
runChat n'appelle EnabledTools, donc avant que mcpTools n'ouvre les connexions.
mcpPromptLine lisait le pool encore vide : le premier tour après un démarrage
partait sans la ligne MCP, le second avec — tout le prompt à recalculer pour
une ligne. Elle comptait en plus les outils masqués par l'utilisateur, et
s'affichait pour le planner, qui ne reçoit aucun outil MCP.
- prepareTurn calcule les outils UNE fois par tour, puis le préambule à partir
d'eux ; runChatTools les reprend tels quels (pas de second passage par
mcpEnsureAll, qui retentait deux fois un serveur en panne).
- mcpPromptLine se déduit de cette tranche : seuls les outils réellement
envoyés sont comptés, et rien n'est annoncé sans outil MCP.
- trackerList départage les égalités d'horodatage par nom puis slug : la liste
part dans le contexte, un ordre tiré au sort changeait le prompt.
- Le commentaire de tasks_run.go ne prétend plus que le préfixe d'une tâche
égale celui du chat (dossier de travail et taskCaps diffèrent).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Persister les rappels ouvrait trois façons de coller deux `user` dans
l'historique, celles-là même qu'appendNudge voulait éviter : sur un gabarit à
alternance stricte, chaque tour suivant aurait échoué.
- Fin de tour : un rappel resté sans réponse (stop, erreur) ou suivi d'un
ajout en cours de réponse est retiré avant persistance (dropStrayNudges),
comme avant sa persistance.
- Compaction : la queue ne commence plus sur un rappel ; la vraie demande
réinjectée devant elle lui était collée. Elle recule d'un groupe d'outils.
- isLokiInjected ne se contente plus du préfixe « [system] » : un message de
l'utilisateur qui commence ainsi n'est plus pris pour un rappel (sa demande
était sautée par la compaction, le vérificateur et le titre).
- Relance tool_choice « none » : pas de repli après un stop, qui partait sur
un contexte annulé et affichait une erreur.
- Tests : appel par le protocole et refus 4xx sous « none » (et rien
d'exécuté), frontière de compaction, nettoyage de fin de tour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le rappel de budget d'outils et la relance « pensé sans agir » partaient au
moteur sans entrer dans l'historique : au tour suivant, le fil divergeait
juste avant eux et toute la boucle d'outils qui suivait était recalculée.
Le modèle relisait en prime une histoire qu'il n'avait jamais vue. La
relance après un 500 (appel d'outil mal parsé) retirait les outils et
modifiait le message système : tout le prompt repartait de zéro, deux fois.
- Rappels persistés tels qu'envoyés (appendNudge), sauf derrière un autre
message user : éphémères là, pour ne pas casser à chaque tour les gabarits
à alternance stricte.
- Ils restent reconnaissables (isLokiInjected) : la compaction ne les
réinjecte pas comme demande en cours et ne les prend pas pour la preuve
qu'une demande a été servie ; la réduction forcée coupe d'abord aux vraies
demandes ; le vérificateur, l'export, le titre et le résumeur les sautent
ou les présentent comme note du système.
- Relance après un 500 sur le llama-server local : mêmes outils, même
message 0, tool_choice « none » et la consigne au bout du dernier message,
jamais persistée. Nouveau refus, appel tenté malgré tout ou réponse vide :
repli une fois sur le chemin historique. Preset externe inchangé.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cleanSummary coupait toujours au DERNIER </think>. Un résumé de session
Code qui cite la balise (un parseur, un gabarit — sessions sur Loki même)
perdait donc tout ce qui la précédait, sans erreur ni trace : exactement la
perte que le commit précédent voulait supprimer.
- Le raisonnement ne se retire qu'en tête : un bloc <think>…</think> qui
ouvre la réponse (plusieurs à la suite au besoin), ou un </think> sans
<think> avant lui (gabarit qui ouvre le bloc dans le prompt).
- Un bloc cité en milieu de texte reste tel quel.
- La coupe par le filet (API qui ignore max_tokens) est tracée elle aussi.
- Tests : CTX fixé (le filet en dépend), réponse du faux moteur derrière un
mutex, cas de balises citées et de blocs multiples.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le résumé de compaction était coupé net à 2200 caractères (≈ 550 tokens)
alors que max_tokens l'autorise jusqu'à ~1600 tokens. Le prompt du résumeur
place l'ÉTAT D'AVANCEMENT en dernier : c'était donc exactement ce dont
l'agent a besoin pour reprendre qui tombait, et les tokens décodés au-delà
étaient jetés.
- La borne passe à compactSummaryBudget()×6 caractères : un simple filet
pour une API qui ignorerait max_tokens, qui borne déjà la sortie.
- finish_reason=length : le texte est gardé, marqué « […] » et tracé
dans le journal ([compact] résumé coupé par max_tokens…).
- Un <think> ouvert en tête et jamais refermé, ou un contenu vide à côté
d'un reasoning_content, est une erreur : l'appelant retombe sur le torse
dégraissé au lieu d'installer du raisonnement brut comme mémoire.
- Tests : table de cleanSummary, et un résumé de 7000 caractères qui
revient intact du moteur.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La garde de marge de génération réclamait 1,2 × la plus longue génération
récente, sans plafond. Une étape de 40 k sur une fenêtre de 65 k (ou de 20 k
sur 32 k, sans budget de raisonnement) demandait plus de place que
l'historique fraîchement compacté n'en laisse : la garde repartait à chaque
étape, et chaque fois résumait le résumé précédent.
- Garde : jamais avant la moitié de la fenêtre. Au-delà, le rejeu sur
« length » reste le filet.
- Preset externe : le complément « messages arrivés depuis la mesure » ne
s'applique plus. Son CtxUsed garde le raisonnement, qui couvrait déjà ce
complément ; l'ajouter le faisait compacter plus tôt qu'avant.
- Journal [ctx] : l'estimation d'avant compaction (début de tour, en tour)
n'est plus comparée au compte d'une requête compactée, ce qui donnait de
faux écarts.
- Tests : génération énorme juste après compaction, seuil de 50 % pour toutes
les fenêtres, comptage externe contre local.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CtxUsed valait usage.prompt_tokens + généré, raisonnement compris. Or Loki
ne renvoie jamais ce raisonnement au modèle (Message n'a pas de champ pour
lui) : la requête suivante pesait 1 à 8 k jetons de moins que le compte, et
la compaction, qui perd de l'information, partait vers 44-47 k au lieu de
49 k sur une fenêtre de 65 k. Rien ne change dans les requêtes : seul le
compte qui décide de compacter.
- Comptage : seulement les morceaux reasoning_content séparés par le
llama-server local, remis à zéro à chaque tentative. Le <think> découpé
chez nous reste dans le message renvoyé pendant le tour, il n'est pas
retiré ; l'API externe garde l'ancien calcul. Un morceau vaut au plus un
jeton : on ne peut que sous-estimer, le sens sans danger.
- Début de tour : le message utilisateur (et la file) arrivé depuis la
dernière mesure s'ajoute en estimation, comme en cours de tour.
- Vérificateur : sa trace isolée écrasait CtxUsed avec sa petite taille, et
la fin de tour décidait sur ce chiffre-là. Plus maintenant ; la jauge de
l'UI ne bouge pas non plus et suit le même calcul que le serveur.
- Marge de génération : le compte gonflé offrait une marge par accident.
Elle est remplacée par une vraie garde — compacter si 1,2 × la plus
longue génération récente ne tient plus dans la fenêtre. Plancher et
marge seuls ne devancent jamais le seuil de 75 %.
- Fenêtre pleine en plein raisonnement (finish « length » au-delà du
seuil) : compaction puis étape rejouée, au lieu du nudge « arrête de
raisonner ».
- Journal [ctx] : estimation contre compte réel au-delà de 2 % d'écart,
chaque « length » et chaque nudge, pour vérifier qu'ils n'augmentent pas.
- Le seuil reste à 75 % : la réserve fixe proposée (toolResultMax/3 + 6 k)
laissait trop peu de place sans budget de raisonnement ni max_tokens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La relecture du relevé par complétion a trouvé quatre écarts, tous
d'affichage ou de robustesse ; rien ne touche aux requêtes ni au contexte.
- Compaction : la réponse lue d'un bloc passait de Decoder à Unmarshal,
plus strict (un octet parasite après le JSON faisait échouer une
compaction qui passait). Retour au Decoder, et perfWire fait de même.
- API externe : son cache suit ses propres règles, sans slot ni points de
reprise à surveiller ici. Plus de ligne ambre « recalculés » pour elle ;
le relevé reste dans l'anneau.
- Seuil d'alerte : même priorité qu'au lancement — -cms d'EXTRA_ARGS, puis
LLAMA_ARG_CHECKPOINT_MIN_SPACING_NT, puis CKPT_MIN_STEP ; -ub d'EXTRA_ARGS
avant UBATCH.
- UI : « recalculés après sous-agent, client /v1 » au lieu des identifiants
internes (subagent, foreign).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jusqu'ici, impossible de savoir si une ligne volatile s'était glissée dans
le prompt, si une mise à jour du moteur avait cassé les points de reprise
ou si l'acceptation du MTP s'était effondrée : le chunk final de llama.cpp
porte ces chiffres (cache_n, draft_n, cached_tokens), on les jetait.
Lecture seule : aucun champ ajouté aux requêtes, et le comptage du
contexte (usage.prompt_tokens → CtxUsed → compaction) ne change pas.
- perf_log.go : décodage à part et tolérant (perfWire) — un compteur mal
typé ne jette plus le chunk final ni ne fait échouer une compaction ;
cached_tokens, strictement entier dans streamChunk, y déménage aussi.
- Inconnu n'est pas 0 : pointeurs, et l'UI garde « prefill N tok » quand
le moteur ne dit rien de son cache.
- Nature (main, subagent, verify, task, compact, bench, foreign) portée
par le contexte, pas par Caps ; TTFT au premier delta de tout type.
- Perte de cache = total précédent − cache_n, seulement quand les messages
prolongent strictement ceux de la complétion précédente de la même
discussion et de la même nature (empreintes cumulées) ; jamais sur un
flux coupé. Ce qui s'est intercalé (sous-agent, client /v1, bench) est
nommé.
- Anneau de 5000 entrées en mémoire, sans texte ; GET /api/perf/summary
(derrière la clé) : taux de cache, recalcul par tour et par nature,
médianes pp/tg par profondeur, acceptation du brouillon.
- Ligne [perf] sur stderr seulement avec LOKI_PERF_LOG.
- UI : « prefill X nouveaux / Y en cache », ambre au-delà d'un seuil
relevé sur un modèle hybride (espacement des points de reprise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La relecture de 3a6ad6f a trouvé quatre trous, aucun ne touche la sortie du
modèle ; tous font que le garde-fou se trompe de verdict.
- Couches en RAM : lastOffload repartait de la dernière ligne contenant
« load_model » — or llama-server écrit « load_model: initializing… » APRÈS
le chargement, et « loading model tensors » aussi pour le brouillon. Le
contrôle ne trouvait jamais rien, ou ne regardait que le brouillon. Repère
désormais la ligne « loading model '<chemin>' » du serveur, et la pire des
lignes offloaded compte (modèle ou brouillon).
- Jeton de tentative : il était consommé (et l'échec inscrit) AVANT le test du
port. Un second « loki serve » refusé pendant que le premier chargeait
marquait la configuration comme ratée. Lecture d'abord (specAutoPeek), puis
rangement une fois le port libre (specAutoSettle).
- Process web : il effaçait le jeton sans le relire sous verrou, donc parfois
celui d'un lancement plus récent. specAttemptClear ne touche qu'au sien.
- MODEL_DRAFT vers une tête EAGLE-3 ou dFlash : Loki imposait draft-simple, que
le moteur ne peut pas charger. La note renvoie maintenant à -md et
--spec-type dans EXTRA_ARGS.
- Une tête MTP choisie comme MODEL_DRAFT compte comme modèle utilisé (« utilisé
par », garde à la suppression).
- Éditeur : choisir un brouillon sur un ancien preset dont le MTP est en
--spec-type dans EXTRA_ARGS fait passer le preset à SPEC=mtp. Sans ça, le
brouillon était ignoré.
- Tests : un journal réel (initializing après chargement, brouillon chargé à
part), un second lancement qui ne fait que lire, le jeton d'un autre
lancement gardé, les têtes eagle3/dflash, et MODEL_DRAFT dans les références.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Une tête MTP (Qwen3.6/3.8, intégrée ou publiée à part en mtp-*.gguf) accélère
le décodage sans rien changer à la sortie : chaque jeton émis est tiré par
l'échantillonneur du modèle cible, un jeton du brouillon n'est gardé que s'il
coïncide. Le prix est ailleurs — 1 à 2 Go de VRAM, un prefill parfois plus
lent, une fonction très récente du moteur — d'où l'opt-in : rien ne change
pour un preset existant.
- SPEC=off (défaut, clé absente comprise) / auto / mtp. La tête se reconnaît
au TENSEUR blk.{N-1}.nextn.eh_proj (backend_gguf.go), comme llama.cpp : la
clé nextn_predict_layers seule ne prouve rien.
- MODEL_DRAFT : résolu comme MMPROJ, mais introuvable ne bloque pas le
lancement. Tête à part → -md + --spec-type draft-mtp explicite (llama.cpp ne
devine que sur la première tranche) ; autre brouillon → draft-simple, sinon
chargé en VRAM pour rien.
- auto seulement si --fit place tout (ni couches chiffrées, -ts, -ot, experts
sur CPU, -sm row, --fit off, -dev), contexte chiffré (fit pourrait sinon le
réduire), pas de vision, build officiel ≥ 11009, jamais qwen4exp. mtp impose,
avec un avertissement.
- Aide du moteur lue à chaque fois : draft-mtp et --spec-draft-n-max requis ;
--draft/--draft-max jamais émis. EXTRA_ARGS (--spec-type, -md, -hfd,
--spec-default…) et LLAMA_ARG_SPEC_* gardent la main.
- Tirage greedy fixé (exact pour toute chaîne). probabilistic seulement avec
SPEC=mtp, refusé avec mirostat ou adaptive-p. SPEC_N_MAX → --spec-draft-n-max.
- Jeton de tentative : « loki serve » le pose avant de lancer, le process web
l'efface dès que le moteur répond (sans page ouverte). Resté au lancement
suivant, même preset, moteur et build → auto coupé et dit. Couches poussées
en RAM après chargement : même verdict. Les erreurs MTP/brouillon du journal
ont leur message.
- Éditeur : « auto » et « MTP » écrivent SPEC, « non » écrit SPEC=off ; champ
Brouillon ; la recherche HF propose la tête MTP du dépôt, décochée.
- Tests : table de specArgs (gardes, sauts, brouillons, tirage), place avant
EXTRA_ARGS, cycle du jeton sur une vraie base, lecture du journal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La liste des points de reprise de llama-server est PAR SLOT : la garde de RAM
de l'espacement d'office ne comptait qu'une liste, et sous-estimait d'autant
le pire cas avec PARALLEL > 1. Et « loki serve » lit désormais le build du
moteur avant de le lancer (hybrides seulement) : sans délai, un binaire coincé
à l'initialisation de ses backends bloquait le lancement, là où binHelp, lui,
était déjà borné.
- ckptSlots : --parallel d'EXTRA_ARGS, sinon PARALLEL, sinon 1 ; « auto » ou
illisible compté pour 4. Le pire cas devient points × slots × poids.
- engineBuildOf borné à 15 s (WaitDelay pour un descendant qui garde la
sortie) : passé le délai, build inconnu, ni avis ni drapeau. Profite aussi
au panneau Moteur.
- Tests : PARALLEL=2 garde l'espacement, PARALLEL=4 et --parallel 4 le
retirent ; table de ckptSlots.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Avant b10864 (PR #28302), llama-server effaçait un point de reprise trop
proche du précédent même quand la liste avait de la place : sur un hybride
(Qwen3.5/3.6, Qwen3-Next), une reprise retombait jusqu'à ~8 k jetons en
arrière, recalculés à chaque tour d'outil. Un point de reprise est une copie
exacte de l'état récurrent : rien de ce que voit le modèle ne change, seul le
temps de prefill et la RAM hôte. Aucune mise à jour du moteur ici.
- Build cru seulement pour un moteur officiel (image, téléchargé, précompilé)
et au-dessus de 5000 : un compilé en clone superficiel (build 1) ou un fork
n'a ni avis ni drapeau. --version gardé par binaire, taille et date.
- Avis « moteur trop ancien » dans le panneau Moteur et au lancement d'un
hybride.
- Opt-in CTX_CHECKPOINTS (--ctx-checkpoints) et CKPT_MIN_STEP
(--checkpoint-min-step, > 0 : 0 laisserait l'éviction FIFO jeter le point du
début de la conversation). Gardes séparées : -ctxcp/--ctx-checkpoints/
--swa-checkpoints et -cms/--checkpoint-min-step, plus LLAMA_ARG_*.
- D'office : --checkpoint-min-step 2048 seulement sur un hybride lu dans le
GGUF, build officiel < 10864, aide qui connaît le drapeau, aucun réglage de
l'utilisateur, points de reprise actifs, modèle sous 70 % de la RAM et 32
points sous le quart de la RAM libre. Sinon l'avis seul ; sur un MoE mmappé
trop gros, conseil CTX_CHECKPOINTS=8 à 16.
- REASONING_PRESERVE=on|off → --reasoning-preserve / --no-reasoning-preserve,
seulement si la clé est posée et que l'aide le connaît. Rien par défaut :
Loki ne renvoie pas la réflexion passée, et aucun choix global ne garde le
rendu de tous les gabarits (Qwen3.6 contre Qwen3.8).
- Mise à jour du moteur : la version téléchargée qui tournait avant n'est plus
supprimée ; le panneau propose d'y revenir sans réseau.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Un sous-agent, une vérification, une tâche ou un bench qui échoue avant que le
moteur ne le serve (erreur avant l'envoi, refus 4xx, arrêt immédiat) laissait
le slot 0 tel quel : il portait encore la conversation, que l'effacement jetait
alors sans qu'elle soit dans le cache RAM — recalcul complet garanti, soit
exactement ce que l'isolation devait éviter.
- engineServed() : runChat (réponse 200 du moteur local) et le bench le
notent ; un travail annexe n'efface que si le moteur a servi au moins une
requête de Loki depuis son début (finishSideJob, testable sans moteur).
- Les prompts d'un travail annexe ne sont plus retenus comme « la conversation »
par noteEnginePrompt : deux vérifications de suite comparaient sinon la
conversation au prompt du vérificateur, et le conseil pouvait se tromper.
- CACHE_RAM et CACHE_ISOLATE figurent dans le modèle de configuration
commenté (loki config).
- Éditeur : une valeur fixée par EXTRA_ARGS ou LLAMA_ARG_CACHE_RAM (-1, 0)
ne s'affiche plus comme « défaut du moteur ».
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
llama-server garde en RAM hôte une copie exacte des états de slot qu'il quitte
(KV, état récurrent, points de reprise, brouillon MTP) et la recharge octet pour
octet. Mais son défaut de 8 Gio ne tient pas une conversation de 30 à 65 k
jetons à côté de l'état d'un vérificateur, d'un sous-agent, d'une tâche ou d'un
bench : à la requête suivante il évince la conversation pour sauver l'annexe, et
tout est recalculé (30 à 80 s sur un 27B). Rien de ce que voit le modèle ne
change ici : seul le bruit de découpage des lots, comme le cache_prompt actuel.
- CACHE_RAM (Mio ; vide/auto, nombre passé tel quel, -1, 0) → --cache-ram,
seulement si l'aide du moteur le connaît. Loki se tait devant -cram
d'EXTRA_ARGS et LLAMA_ARG_CACHE_RAM. L'auto ne descend JAMAIS sous le défaut :
il agrandit seulement un modèle tout-GPU (VRAM NVIDIA connue, aucun poids ni
KV sur CPU, NGL complet), d'après l'état mesuré dans le GGUF (2,5 × KV à CTX
+ état récurrent et points de reprise des hybrides), plafonné à 30 % de la RAM
(limite cgroup comprise) et à la moitié de la RAM libre.
- --slot-save-path LOKI_HOME/slots (0700, purgé au lancement) uniquement si le
moteur est en boucle locale ou protégé par clé, et si le dossier existe. Les
proxys /v1 et du relais refusent désormais toute action /slots (405).
- engineSideJob efface le slot 0 APRÈS le sous-agent, la passe de vérification,
la tâche planifiée et le bench (pas après la compaction : rien à y gagner).
Synchrone, borné à 2 s, et seulement si : moteur local, build ≥ 8660 lu dans
/props, total_slots == 1, slot 0 au repos, aucune requête de Loki en vol
(compteur tenu par le chat, les résumés, le bench et les proxys).
CACHE_ISOLATE=off le coupe.
- usage.prompt_tokens_details.cached_tokens : si la conversation revient avec
moins de la moitié en cache, conseil (une fois) de relever CACHE_RAM.
- Éditeur de preset : champ « Cache de prompts » à côté d'UBATCH, avec la
valeur auto calculée par le serveur (/api/preset/cacheram).
- Lecteur GGUF : embedding_length, head_count et dimensions ssm.*.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La relecture n'a trouvé aucun défaut dans le lecteur lui-même : bornes,
versions, tranches, cache et vérification du début des données tiennent. Trois
cas réels restaient pourtant sans test.
- GGUF v2 : même disposition que v3, désormais vérifié avec v1 et gros-boutiste.
- gguf-split --no-tensor-first-split : la première tranche ne porte que les
clés, aucun tenseur ; la tête MTP est trouvée dans la suivante.
- Un octet corrompu à chaque position de l'en-tête (v1 et v3, valeurs 0xFF,
0x7F, 0x00) : le lecteur rend la main sans jamais tomber dans le filet du
panic rattrapé, qui ne doit rester qu'un filet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plusieurs réglages du moteur dépendent de ce que le fichier contient vraiment
(tête MTP, têtes KV par couche, couches récurrentes d'un hybride) et Loki ne
le devinait qu'au nom du fichier. ggufMeta lit l'en-tête, les clés et la
table des tenseurs, jamais les poids. Aucun appelant pour l'instant : rien ne
change au lancement.
- Champs retenus : architecture, block_count, nextn_predict_layers,
head_count_kv (valeur unique ou tableau par couche), key/value_length,
expert_count, context_length, full_attention_interval, hybride (une clé
<arch>.ssm.*).
- Tête MTP reconnue comme llama.cpp la reconnaît : le tenseur
blk.{block_count-1}.nextn.eh_proj.weight, cherché dans toutes les
tranches. La clé nextn seule ne prouve rien (tête publiée à part).
- Robuste : chaque longueur bornée par ce qui reste du fichier, compteurs et
imbrication plafonnés, GGUF v1/v2/v3 et gros-boutiste, panic rattrapé,
fichier coupé (en-tête ou début du dernier tenseur) refusé, tranche
manquante refusée.
- Cache par chemin, taille et date de chaque tranche ; les erreurs ne sont
pas gardées.
- Tests sur des GGUF synthétiques écrits par le test : hybride avec MTP, tête
absente ou mal placée, tranches, v1 et gros-boutiste, toutes les
coupures, fichiers aberrants, cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Les garde-fous ne lisaient qu'EXTRA_ARGS et KV_TYPE. Or llama.cpp applique ses
variables LLAMA_ARG_* avant la ligne de commande : un conteneur lancé avec
LLAMA_ARG_CACHE_TYPE_K=q8_0 ou LLAMA_ARG_CACHE_REUSE=256 changeait les calculs
sans un mot. Et un llama-server d'avant --context-shift glisse le contexte PAR
DÉFAUT : il jetait des jetons alors que rien n'était écrit nulle part.
- Type de cache effectif : variables, puis KV_TYPE*, puis EXTRA_ARGS, la
source la plus forte est nommée dans la note ; warnSlowKV suit.
- --context-shift et --cache-reuse lus aussi dans LLAMA_ARG_CONTEXT_SHIFT et
LLAMA_ARG_CACHE_REUSE ; moteur ancien (aide sans --context-shift) : on dit
qu'il glisse et que --no-context-shift l'en empêche. Aucun drapeau ajouté.
- Vision reconnue aussi par --mmproj d'EXTRA_ARGS et LLAMA_ARG_MMPROJ.
- Placement figé lu par tensorOverride : --n-cpu-moe 0 ne compte plus,
--n-cpu-ffn et LLAMA_ARG_N_CPU_MOE si. Même règle dans l'interface.
- Test du paquet : les étiquettes json:"cache_prompt" des structures sont
aussi refusées hors du benchmark.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le cache KV en q8_0 d'un preset MoE vivait dans EXTRA_ARGS : ni l'avertissement
de lenteur ni la liste de l'interface ne le voyaient, qui affichait « f16 (max
qualité) » pendant que le moteur tournait en q8_0. Rien n'empêchait non plus un
--context-shift ou un --cache-reuse de modifier en douce ce que voit le modèle.
Aucun drapeau n'est ajouté ni retiré : on DIT ce que la ligne finale change.
- Type de cache effectif (effectiveKVTypes) : KV_TYPE*, puis -ctk/-ctv et
--cache-type-k/v d'EXTRA_ARGS par-dessus, la dernière occurrence gagne comme
dans llama-server. warnSlowKV le reçoit désormais.
- Note au lancement quand ce cache n'est pas f16 : q8_0 modifie légèrement les
sorties, q4_0 perte mesurable, bf16 numérique différente. Avec -ot ou
--n-cpu-moe, --fit ne tourne pas : repasser en f16 demandera sans doute de
relever --n-cpu-moe. Pas d'estimation de VRAM sans les métadonnées du GGUF.
- Avertissement pour --context-shift (jamais --no-context-shift) et
--cache-reuse N>0 ; --swa-full, sans perte, n'en déclenche aucun. Vision
chargée : llama.cpp les ignore, on le précise.
- Interface : options du cache KV étiquetées, sous-titre qui montre le type
effectif (« défini par EXTRA_ARGS ») et les drapeaux de cache approché.
- Tests : notes et type effectif en table, aucun -ctk/-ctv sans KV_TYPE, corps
réels du chat et de la compaction sans cache_prompt:false ni n_cache_reuse,
et ces clés réservées au benchmark dans tout le paquet (arbre syntaxique).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
common/fit.cpp abandonne sur LLAMA_SPLIT_MODE_TENSOR comme sur ROW
(« not implemented, abort ») : avec -sm tensor, la marge partait quand même
et la note annonçait un --fit-target qui ne servait à rien — de quoi mesurer
deux fois le même placement. À l'inverse, -ngl -1 est la valeur par défaut
du moteur (celle qu'écrit « auto ») : fit tourne, et la clé était refusée à
tort.
- fitBlocker : row ou tensor, en drapeau ou en LLAMA_ARG_SPLIT_MODE ;
-1 traité comme auto.
- tests : -sm tensor, LLAMA_ARG_SPLIT_MODE=tensor, -sm layer, -ngl -1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sur deux cartes inégales (5060 Ti + 3060) ou un MoE aux experts sur CPU, il
reste quelques leviers de placement que llama.cpp expose mais que Loki ne
savait pas poser. Aucun ne touche au modèle (poids, contexte, cache,
échantillonnage) ; tous restent éteints tant que le preset ne les demande pas,
et le gain se décide à la mesure.
- FIT_TARGET=1024,3072 → --fit-target : marge libre par carte pour --fit.
Validée (entiers, 8 valeurs au plus, jamais transmise brute : stoull ferait
mourir le moteur en boucle), remontée à 1024 Mio au minimum. Seulement si
l'aide la liste, sans -fitt ni LLAMA_ARG_FIT_TARGET déjà posés, avec un
contexte chiffré (CTX=0 laisserait fit réduire le contexte) et si --fit
tournera vraiment : NGL chiffré, --tensor-split, -ot, --n-cpu-moe, -cmoe,
--fit off ou -sm row le coupent, et on le dit au lieu de ne rien faire.
- OP_OFFLOAD_MIN_BATCH=N → GGML_OP_OFFLOAD_MIN_BATCH : les lots de moins de N
jetons restent sur CPU pour les poids en RAM. Utile pour des brouillons
ngram de 32 jetons ou plus ; MTP vérifie déjà sous 32.
- CUDA_GRAPH_OPT=on → GGML_CUDA_GRAPH_OPT=1 : expérimental, sortie identique en
amont, gain de 0 à quelques % ; refusé si -sm row/tensor ou un -ot sur
l'attention pouvait séparer Q, K et V d'une couche. Le lancement rappelle
« off puis redémarrer » en cas de GGML_ASSERT.
- une variable déjà dans l'environnement n'est jamais touchée ; elles ne vont
qu'à llama-server via « loki serve ».
- tensorOverride, extrait de pipelineBlocker, sert aussi à la garde de --fit.
- --backend-sampling écarté : pas identique au bit près, et inactif sur les
requêtes à outils (grammaire) qui font l'essentiel du trafic.
- clés documentées dans le squelette de « loki edit » ; tests de table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La garde « pipeline possible » ne suivait pas tout à fait la façon dont
llama.cpp (common/arg.cpp) lit ses réglages. Faux positifs : 4x posé sans
pipeline possible, donc exposé au seul cas de panne connu sans aucun gain.
Faux négatifs : gain refusé à tort.
- surcharges de tenseurs cumulées : variable et drapeaux s'additionnent.
Un --n-cpu-moe 0 n'annule donc ni un LLAMA_ARG_N_CPU_MOE=30 ni un
--n-cpu-moe 20 placé avant lui.
- --n-cpu-ffn/-ncffn (FFN dense sur CPU) et LLAMA_ARG_OVERRIDE_TENSOR
comptent comme des surcharges.
- cache KV : le dernier -kvo/-nkvo l'emporte. Sinon, la seule présence de
LLAMA_ARG_NO_KV_OFFLOAD (même à 0) vaut « non », puis LLAMA_ARG_KV_OFFLOAD
est lu comme un booléen.
- LLAMA_ARG_CPU_MOE n'est actif que pour on/enabled/true/1.
- NGL absent : Loki passe -ngl lui-même, donc LLAMA_ARG_N_GPU_LAYERS ne
compte plus. NGL=Auto se lit sans tenir compte de la casse, comme nglArgs.
- buildServeArgs : le commentaire des threads retrouve son code.
- 12 cas de table en plus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sur plusieurs cartes, llama.cpp fait travailler les GPU en pipeline quand le
modèle y tient en entier ; encore faut-il que le CPU empile assez de
lancements CUDA d'avance. CUDA_SCALE_LAUNCH_QUEUES=4x agrandit cette file du
pilote : mêmes noyaux, même ordre, sortie identique — seul le prompt peut
gagner (+10-25 % en amont sur un 70B, non mesuré ici), le décodage ne bouge pas.
llama.cpp l'a retiré de ses défauts après des blocages sur Jetson : Loki ne le
pose donc que là où le pipeline peut réellement exister.
- launchQueuesEnv (pure) : 4x seulement si ≥ 2 GPU CUDA servis (--device,
sinon CUDA_VISIBLE_DEVICES, sinon nvidia-smi borné à 3 s) et aucun bloqueur :
-ot, --cpu-moe, --n-cpu-moe ≠ 0, -nkvo, -sm autre que layer, NGL chiffré
hors 999 — drapeaux d'EXTRA_ARGS ou LLAMA_ARG_* équivalents
- une variable déjà dans l'environnement n'est jamais touchée ;
CUDA_LAUNCH_QUEUES=off la coupe, 0.25x/0.5x/2x/4x l'imposent, toute autre
valeur est ignorée en le disant
- la valeur posée s'affiche sur la ligne « [loki serve] » ; la ligne de
commande ne change pas, BATCH/UBATCH non plus
- CUDA_LAUNCH_QUEUES survit aux bascules de preset (réglage machine qu'un
preset peut imposer)
- éditeur : « Experts MoE sur CPU » rappelle que ça coupe le pipeline sur 2 GPU
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sans -tb, llama.cpp ne recopie pas seulement le nombre de -t : il remplace
TOUT le réglage CPU du batch par celui de -t (postprocess_cpu_params,
« cpuparams = *role_model »). Un -Cb, -Crb, --cpu-strict-batch, --prio-batch
ou --poll-batch écrit dans EXTRA_ARGS était donc effacé sans un mot depuis que
Loki ne pose plus « -tb 0 » — un réglage utilisateur cassé.
- Dans ce cas seulement, Loki pose -tb : la valeur de -t quand elle est connue
(THREADS, sonde du conteneur ou -t / --threads d'EXTRA_ARGS), sinon 0,
l'ancien comportement, et le dit sur stderr.
- Un -tb déjà dans EXTRA_ARGS reste maître, comme avant.
- flagValue lit la dernière occurrence d'un drapeau (« -t 6 » ou « -t=6 »).
- Tests : quatre lignes de construction d'arguments et une table flagValue.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Loki passait toujours « -t <THREADS|0> -tb <THREADS_BATCH|0> ». Pour
llama.cpp, 0 veut dire hardware_concurrency() : tous les threads logiques,
frères SMT compris. Avec l'attente active par défaut (--poll 50), deux
threads sur un même cœur se gênent, et c'est le décodage des experts MoE sur
CPU qui paie. Le vrai auto du moteur, c'est l'absence du drapeau : il prend
alors ses cœurs physiques (cœurs P seulement sur Intel hybride sous Linux).
- THREADS vide ou 0 : plus de -t ; THREADS_BATCH vide ou 0 : plus de -tb,
le moteur recopie -t (prefill ET vérification spéculative MTP).
- Valeur illisible ou négative : ignorée et dite sur stderr, au lieu de
faire boucler le moteur sur son analyse d'arguments.
- -t / -tb déjà dans EXTRA_ARGS : Loki ne double plus le drapeau.
- Linux, conteneur à l'étroit (cpuset restreint, quota CFS) : le moteur se
rabattrait sur tous les cœurs de l'hôte. Une sonde compte les cœurs permis
comme llama.cpp (thread_siblings, cœurs E écartés), plafonne au quota et
ne pose -t que s'il est plus petit ; lecture ratée = rien. Muette si
LLAMA_ARG_THREADS est posé ou si EXTRA_ARGS fixe l'affinité (-C, -Cr…).
- Libellés de l'interface et du gabarit de config corrigés.
Le calcul du modèle ne change pas. Gain non mesuré : comparer tg du preset
MoE à physiques, physiques-1 et logiques avant de conclure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
buildServeArgs se disait pure, mais la traduction des drapeaux de chargement
qu'elle appelle écrivait encore sur stderr quand un --load-mode dio tombait sur
un moteur ancien. La note est désormais rendue comme les autres et affichée
par cmdServe, au même rang qu'avant (avant la note NGL).
- normalizeLoadFlags / downgradeLoadMode rendent la note au lieu de l'écrire.
- Table de tests : cas dio sur moteur ancien (drapeau retiré, une note), et
TestNormalizeLoadFlags vérifie que seul ce cas produit une note.
- Aucun changement de ligne de commande.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cmdServe mêlait deux métiers : sonder le monde (chemins du modèle et du
projecteur, aide du moteur, clé d'API, port) et composer la ligne de
commande. Tout réglage de lancement à venir devait donc se vérifier en
relançant un moteur. La composition passe dans buildServeArgs, pure, qui
reçoit ce que cmdServe a sondé et rend arguments, variables d'environnement
et notes ; cmdServe garde seul les effets (Setenv, chdir, port, exec).
- serveSysInfo porte l'aide du moteur, les chemins résolus et la clé.
- Les tests de capacité lisent le texte de l'aide (helpSupports*), plus le
binaire : une aide fabriquée suffit à les exercer.
- La sélection GPU reste posée avant la lecture de l'aide, comme avant.
- Aucun changement de comportement : même ordre, mêmes valeurs. Une table
de tests fige la ligne pour les presets courants (dense, NGL forcé,
MoE avec -ot/--n-cpu-moe/-ctk, raisonnement on/off, vision, clé d'API,
drapeaux de chargement, CUDA_VISIBLE_DEVICES).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Depuis la protection anti-site-tiers (0.14.0), un accès par nom de domaine
derrière un reverse proxy, sans clé de pilotage, reçoit un 403 sur toute
l'API. L'interface restait vide : chaque appel échouait en silence dans la
console.
- Au premier 403, un bandeau fixe affiche la raison donnée par le serveur
et le correctif : LOKI_TRUSTED_HOSTS=<le nom affiché> dans les variables
du conteneur, ou une clé de pilotage.
- docker-compose.yml et docker-compose.unraid.yml exposent la variable
LOKI_TRUSTED_HOSTS, commentée.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rattrapage d'AJEAN jusqu'à la 0.17.6 (contexte, reprise réseau, presets
externes, tâches ponctuelles, file d'attente) et reprises d'OpenFox pour le
mode Code (vérification quand le builder a fini, pré-vol des écritures,
relance des appels textuels, alias d'outils, consignes du dépôt, terminal).
Les 19 commits locaux de la synchro 0.13.6 → 0.16.3, restés hors de main,
y sont rejoués.
Bump des trois porteurs de version (constante Go, versioninfo.json, .syso
Windows régénérés), notes de release réécrites, NOTICE à jour des idées
reprises d'OpenFox.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris du pré-vol d'OpenFox (tool-preflight, 2.0.154).
Un write sur un fichier existant jamais lu était refusé par le tracker…
une fois tout le fichier généré. builder.md demande des fichiers écrits en
entier : un refus pouvait coûter des minutes de décodage pour rien.
- Les schémas write, edit, mem_add et mem_edit annoncent le chemin AVANT
le contenu. Une map Go sort ses clés par ordre alphabétique : le modèle,
qui suit l'ordre du schéma, déroulait tout le contenu avant le chemin.
L'interface nomme aussi le fichier dès le début de la frappe.
- Dès que le chemin d'un write/edit est complet dans le flux, les gardes du
mode Code (fichier lu, chemin permis) sont vérifiées. Refus : la
génération est coupée, l'appel réduit à son chemin entre dans
l'historique avec le refus pour résultat, et le modèle repart (lire le
fichier d'abord). Rien n'est écrit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris du COMPACTION_PROMPT d'OpenFox.
Après un compactage, le builder réécrivait des fichiers déjà faits et
retombait dans des erreurs déjà résolues : le résumé, pensé pour le chat,
ne gardait ni l'un ni l'autre. Et les critères, qui ne vivent pas dans le
fil (seuls les appels à l'outil criteria y passent), disparaissaient avec
le torse résumé.
- En mode Code, le résumé garde aussi chaque fichier créé ou modifié, les
erreurs rencontrées et leur résolution, et les commandes de build/test.
- L'état des critères est rendu au builder dans le message de résumé — un
message de toute façon neuf, donc sans cache invalidé en plus.
(Au passage : gofmt sur code_git.go, mal aligné au commit précédent.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris de context/instructions.ts d'OpenFox.
Un dépôt qui documente ses commandes de build et de test, ses conventions
et ses pièges le fait dans AGENTS.md ou CLAUDE.md. Le modèle les
redécouvrait à coups de read et de bash — quand il les redécouvrait.
En mode Code, le premier de ces fichiers trouvé (dépôt cloné détecté, puis
racine de la discussion) part avec le contexte du projet, tronqué à 4 000
caractères. Comme le reste du contexte projet, il est déplacé dans le
premier message utilisateur : le préfixe système, et le cache de prompt,
restent intacts.
Les tours de correction et de relance du builder reçoivent désormais le
même contexte projet que le tour de build : sans lui, le début du premier
message utilisateur changeait et tout le prompt était recalculé.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Trois trous relevés en comparant avec OpenFox.
- git_status / git_diff tournaient à la racine de la discussion, qui
n'est souvent pas un dépôt : git_clone range le projet dans un
sous-dossier, et le vérificateur — dont la consigne commence par
git_diff — tombait sur « not a git repository ». Ils visent maintenant
le sous-dossier demandé (dir), la racine si c'est un dépôt, sinon
l'unique dépôt présent ; plusieurs : on demande de choisir.
- Budget d'outils triplé en mode Code : lire, éditer, compiler, relancer
les tests dépasse vite 24 appels sans tourner en rond, et le troisième
rappel (« n'appelle plus d'outil ») coupait le build au milieu.
- Sous-agents : température 0.6 au lieu de 0 (en glouton, Qwen avec
réflexion boucle vite ; le TEMP du preset l'emporte toujours), et seul
le texte écrit après le dernier outil revient au builder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris de shell.ts / shell-tail.ts et diagnostics.ts d'OpenFox (2.0.160).
- « cmd | tail -N » : le code de sortie devenait celui de tail (0) et un
build en échec passait pour réussi. Le tail est retiré de la commande et
Loki garde lui-même les N dernières lignes : même sortie, vrai code.
- Délai dépassé : la sortie déjà produite est rendue (où en était le build,
quel test bloquait) au lieu d'un « [timeout] » sec, avec le conseil de
passer par bash_bg pour un serveur.
- Couleurs et séquences ANSI retirées : du bruit en tokens.
- La durée suit le code de sortie (« exit: 0 · 1.2s »).
- Mode Code : « cmd & » refusé, avec renvoi vers bash_bg — le process
orphelin n'avait ni sortie lisible ni moyen d'être arrêté.
- Diagnostics LSP : erreurs d'abord, avec le compte total ; au-delà de la
limite, des indices de style passaient devant l'erreur de compilation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Généralisation du transformSubAgentAliases d'OpenFox. Les petits modèles
ont appris d'autres agents : read_file, str_replace, run_command, ou path /
old_string au lieu de file / old. L'appel tombait sur « outil inconnu »
ou sur un argument manquant, et le tour se perdait en allers-retours.
Le nom et les arguments sont traduits vers l'outil réel quand la cible est
disponible dans ce tour, avant que l'appel soit rangé dans l'historique
(le modèle y relit l'appel tel qu'exécuté). Un sous-agent appelé comme un
outil (« explorer », « code_reviewer ») devient subagent{role, task}. Un
appel déjà correct, ou dont la cible n'est pas offerte, reste intact.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'OpenFox (stream-pure, agent-loop, 2.0.15x).
- Repéré PENDANT le flux (hors réflexion en ligne) : la génération est
coupée tout de suite au lieu de laisser le modèle dérouler un faux appel
— parfois un fichier entier — qui ne serait jamais exécuté.
- La consigne corrective cite l'extrait fautif : le modèle voit ce qu'il a
mal écrit.
- Jusqu'à 3 relances consécutives au lieu d'une par tour ; le compteur
repart à zéro après chaque appel émis par le protocole. Un long tour
d'agent pouvait rater la syntaxe deux fois et finir sur un pseudo-appel.
- Nouveaux motifs : le format XML de Qwen3-Coder (<function=…>,
<parameter=…>) quand le gabarit du moteur ne l'a pas converti.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris du buildAgentNudge d'OpenFox (2.0.133).
La passe de vérification partait à chaque fin de tour de build, y compris
quand Qwen s'arrêtait sur « ensuite je vais… » ou venait de poser une
question avec ask. Chaque passe inutile coûte un prefill complet, évince
le cache KV du fil, et produit des « failed » qui ne disent que « pas
encore fait » — en courant par-dessus la question restée sans réponse.
- Nouveau statut « completed » : le builder marque un critère fait une
fois vérifié par lui-même. Seule la vérification marque passed/failed.
- Fin de tour avec des critères encore ouverts : le builder est relancé
sur ces critères (deux fois au plus) avant toute vérification.
- Fin de tour sur une question (ask) : pas de vérification, ni de
correction, avant la réponse de l'utilisateur ; idem si le builder pose
une question pendant une correction.
- Un id de critère écrit entre guillemets (« "2" », « "#2" ») vise bien
le bon critère au lieu de #0.
- Le panneau des critères montre « completed » (◐).
- Test de non-régression de la passe de vérification (code_verify_test.go).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Activer le chiffrement de la mémoire chiffrait TOUTES les valeurs du
bucket des discussions, y compris des clés que le code lit et écrit en
clair (getStr/putStr) : le pointeur de discussion active, le mode
Chat|Code de chaque discussion, la puce « passer en mode Code ». Relues
comme du charabia, la discussion active devenait introuvable et le mode
Code disparaissait. Les critères, lus en clair eux aussi, s'effaçaient.
- Ces clés-pointeurs restent en clair (plainStoreKey) : ni rechiffrées, ni
comptées comme « chiffrement incomplet ».
- Celles qu'une base existante a déjà chiffrées sont remises en clair dès
le déverrouillage, avant le rechargement de la discussion active.
- Les critères passent par putStoreBytes/getStoreBytes : chiffrés comme le
reste du fil, et lisibles.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>