bail_travail_annexe_et_trace attendait prompt_n = 2036 : la longueur du
dernier prompt suit le corpus de code du bench, c'est-à-dire l'UI embarquée
(internal/loki/ui/index.html). Toute retouche de l'interface cassait donc
ce test (ci / rust rouge après l'ajout de la pause des téléchargements et du
choix de la seconde carte Strata). Le test vérifie désormais la forme de la
trace relayée (prompt_ms = prompt_n / 2, débits du faux moteur) pour un
prompt non vide. Le journal de parité du bench, lui, se régénère avec
LOKI_PARITY_UPDATE=1 comme les autres.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 9c7423bf (v0.18.1) : POST /api/models/download/pause
{"filename", "pause"} met en pause ou reprend un téléchargement en cours.
La pause coupe les connexions (rien ne reste ouvert vers le CDN), garde le
fichier partiel, et chaque morceau attend la reprise puis repart de l'octet
où il s'était arrêté par une requête de plage ; une pause ne consomme pas
d'essai. /status expose « paused », la vitesse publiée tombe à zéro.
Façade Rust (downloads.rs) : même route, même porte (watch), mêmes
réponses ; la route est ajoutée à celles que le Go remet à la façade quand
LOKI_MODELS=rust. Tests portés de part et d'autre (pause puis reprise de
bout en bout, fichier identique à la source).
UI : bouton « Mettre en pause » / « Reprendre » à côté de « Annuler le
téléchargement », « en pause » dans la ligne de progression.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 38919238 (#121 amont) et de la partie moteur de 6db3f3d0 :
STRATA_SPLIT=layers dans le preset fait lister les deux cartes dans la clé
« gpu » de la config (principale d'abord, numéros nvidia-smi) ; le serveur de
Strata en déduit --layer-split. Rien du mode d'aide ne subsiste alors : ni
--expert-cache-device1 / --remote-expert-opt, ni source d'aide à la lecture
des prompts, ni encodeur d'images déplacé ; la réserve de VRAM de
l'installeur reste. Sans la clé, ou la seconde carte coupée, rien ne change.
Propre à Loki : le placement des experts selon la RAM disponible ne choisit
jamais le mode hors RAM en partage des couches (la seconde carte n'y garde
pas d'experts, la marge calculée pour elle serait fausse).
Réglages : « split » (helper ou layers ; absent = inchangé pour un ancien
client), détail split_layers. Fenêtre Strata : la case « Carte d'appoint »
devient un choix « Seconde carte » (non utilisée, aide aux experts, partage
des couches) ; la carte de configuration du moteur actif l'affiche.
Façade Rust alignée (strata/mod.rs, strata/api.rs, tests portés) ; journal
de parité régénéré sous Linux avec les réglages et l'affichage du partage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 9c7423bf (v0.19.0, renumérotée v0.18.1 par e7b024ad) :
- release moe-v1.1 d'AJEAN, empreintes SHA-256 reprises telles qu'AJEAN les
publie dans son backend_moe.go ;
- avec une carte d'aide, la lecture d'un prompt passe par elle
(STRATA_PREFILL_HELPER_SOURCE=1) et le chemin direct ne garde que les
ajouts sous 48 jetons (--short-read 48 au lieu de 150) ;
- mode hors RAM : réserve de VRAM de la carte d'aide à 640 Mio, contexte
plafonné à 131 072 jetons, marge de RAM portée de 7 à 8 Go (le moteur 1.1
prend ~1 Go de plus) ;
- strataMigrate (moeMigrate d'AJEAN) : au premier lancement d'un modèle
installé avec la 1.0, `loki serve` pose les sources 1.1, reprend
l'environnement Python par un lien quand les dépendances n'ont pas changé,
installe le moteur 1.1, recopie la config de l'installeur en réécrivant ses
chemins, puis repointe les presets et la config vivante ensemble
(l'empreinte du preset actif suit). En cas d'échec, rien n'est réécrit et
la 1.0 continue de tourner.
Les correctifs propres à Loki restent : placement des experts selon la RAM
disponible et mode « depuis le disque » (6aaf9360).
Façade Rust : mêmes constantes et même configuration de lancement
(strata/mod.rs) ; la migration reste au Go, seul à servir un preset Strata.
Journal de parité régénéré sous Linux : noms du paquet tirés des constantes,
étapes du seuil du mode hors RAM recalées sur la nouvelle marge, étape du
contexte plafonné ajoutée.
La consolidation de Jean la nuit et le niveau de réflexion du moteur MoE
(même commit amont) ne sont pas repris : Loki n'a ni Jean ni ce raccourci.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Même règle que le Go (mcpCallTimeoutFor) : 120 s par défaut, remplacés par
callTimeoutSec du serveur, allongés par l'argument timeout de l'outil (+30 s,
millisecondes au-delà de 3600), plafond 30 min. Un délai dépassé rend la même
erreur que le Go sans reconnecter ni relancer l'outil. callTimeoutSec est lu
et réécrit tel quel dans la configuration des serveurs : un enregistrement
par la façade ne le perd plus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Un motif vide (ou blanc) dans retry_patterns donnait (?mi), qui correspond à
tout : chaque tour était relancé une fois (OpenFox 8e9b4869). Sans un seul
motif utilisable, la façade retombe sur les motifs embarqués, comme
compileRetryPatterns côté Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- LSP (lsp.go) : un signal resté dans le tampon par une republication
tardive faisait rendre « aucun diagnostic » à l'édition suivante, avant même
que le serveur ait vu la nouvelle version. Le canal est vidé, une
publication d'une version antérieure est ignorée, et check attend 350 ms de
calme après la dernière publication (liste vide puis analyse : c'est
l'analyse qui compte), toujours borné par lspDiagWait. Au passage :
interblocage latent levé (writeMsg → markDead reprenait mu, déjà tenu par
check) — dead devient atomique.
- run_shell : ErrWaitDelay (commande finie, process laissé en arrière-plan
sur les tubes) rend la sortie au lieu de « WaitDelay expired » qui
l'effaçait (AJEAN 7403a8a0).
- Clé de pilotage web : lue depuis un cache à chaque requête au lieu d'ouvrir
la base sous dbMu (AJEAN #120). Lectures réussies seulement, invalidé par
toute écriture du bucket d'état, par la date/taille du fichier (autre
process) et au bout d'une seconde.
- MCP : l'appel suit le contexte du tour (le bouton stop l'annule), délai par
serveur (callTimeoutSec) et allongé par l'argument timeout de l'outil,
plafonné à 30 min ; un délai dépassé n'est plus relancé (OpenFox 2ef584b8).
- Recall : les blocs d'une compaction s'archivent en une seule transaction
au lieu d'une par bloc (AJEAN #130).
- Compactage en cours de tour : après un essai qui n'a rien réduit, pas de
nouvel essai avant +5 % de la fenêtre (sauf à 90 %), pour les tâches et
sous-agents aussi ; la cause d'un résumé raté est journalisée (AJEAN
68659e73).
- Motifs de relance : un motif vide (qui correspondait à tout) est écarté,
une surcharge sans motif utilisable retombe sur l'embarqué, et un
changement de retry_patterns agit sans redémarrer (OpenFox 8e9b4869,
c52c5bf7).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
plan_par_machine (lot 41) a vu « cpu » au lieu de « cuda » en CI : le faux
nvidia-smi, écrit par le process des tests puis lancé aussitôt, peut être
encore ouvert en écriture dans le fils qu'un test voisin vient de forker ;
son exec échoue alors sur ETXTBSY et l'outil paraît absent. Les scripts des
tests de la compilation et le faux Chrome (lot 44, lancé à côté des tests
parallèles de tests/lot6.rs) sont désormais écrits par un sh à part, comme
les autres faux binaires du dépôt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Le briefing machine des vues des rôles nomme l'hôte et le compte : masqués
dans le journal ({{HOTE}}, {{COMPTE}}), qui différait sur le poste de la CI.
Il nomme aussi le système et l'architecture : journal tiré sous Linux
seulement, comme les autres journaux qui dépendent du poste.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Le builder délègue à un rôle en lecture seule (explorer, code-reviewer,
planner) sans passer par /tool/exec : src/chat/subagent.rs porte
toolSubagent. Mêmes refus avant tout appel au moteur (rôle inconnu,
verifier compris, tâche vide, récursion), contexte isolé servi par le Go
(POST /internal/v1/turn/subagent : prompt du rôle + briefing machine,
outils, capacités ; subagentView et subagentFraming partagés), tour
imbriqué run_turn avec l'endpoint, le raisonnement, l'écho et l'arrêt du
tour qui délègue, température 0,6, travail annexe (second slot, effacé à
la fin), télémétrie à part. Seul le texte écrit après le dernier outil
revient, rogné, préfixé du rôle, tronqué à 6000 runes.
Le tour du rôle est porté par SubHost, qui délègue tout à son GoTurnHost
sauf un nouvel appel à subagent ; les outils de la façade passent dans
GoTurnHost::exec_tool_base, que seul le tour principal complète de
subagent (le type du tour imbriqué ne se contient pas lui-même).
Parité : journal tiré du Go contre un faux moteur scripté
(parity_subagent_test.go → tests/parity_subagent/subagent.json : refus,
réponse directe, rapport après deux outils, texte vide relancé, rapport
tronqué, cadrage du planner, moteur qui refuse ; requêtes du rôle),
rejoué par un tour complet de la façade (tests/lot6.rs) ;
TestSubagentVueInterne côté Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Les deux outils qui rappellent un bloc archivé par la compaction ne passent
plus par /tool/exec du Go : src/recall.rs porte toolRecall et
toolRecallSearch (id tolérant comme recallIDToSeq, mots-clés en minuscules
rune par rune comme strings.ToLower, score libellé double et plus récent
d'abord, limites, extraits et textes du Go). L'archive reste au Go, seul à
tenir la séquence de la base, qui la sert à la façade
(GET /internal/v1/recall/block?seq=N et /internal/v1/recall/blocks, blocs
chiffrés sous une mémoire verrouillée exclus) ; ses propres outils passent
par les mêmes lectures. Go injoignable : l'appel passe par lui.
Parité : journal de 41 appels sur une archive de 35 blocs tiré du Go
(parity_recall_test.go → tests/parity_recall/recall.json), rejoué par
tests/recall.rs ; un tour de la façade qui rappelle et cherche
(tests/lot6.rs) ; TestRecallInterneLecture côté Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
La façade exécute les deux outils qui montrent une image au modèle
(src/imagetools.rs, port de chat_vision_tool.go, chat_screenshot.go et
prepareImageForModel) : préparation de l'image comme les pièces jointes
(orientation EXIF, grand côté sous 1568 px, mêmes pixels), sonde du moteur
(/props authentifié, gardée 10 s ; preset externe : sa case), see_image aux
textes du Go, web_screenshot par Playwright (adresses locales quand internet
est coupé, mêmes arguments, erreur résumée, capture rangée dans le dossier
de la discussion, ménage à 20 captures et 40 Mo, relayée au modèle s'il
voit les images). Le Go garde les siens pour ses sous-agents et tâches IA.
Parité : journal tiré du Go avec ses fichiers, un faux moteur et un faux
Playwright (tests/parity_images/images.json), rejoué par
tests/imagetools.rs ; un tour de la façade sans passer par le Go
(tests/lot6.rs) ; tests unitaires.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
La façade tient la session Chrome des outils browser_* (src/browser/,
port de computer_cdp.go, computer_use.go, browser_grid.go et
cu_autoshot.go) : lancement (profil temporaire, mêmes arguments, tué avec
la façade), client CDP multiplexé sur websocket, attente du chargement et
du calme réseau, scripts injectés copiés à l'octet près, les neuf outils
aux textes du Go (snapshot dédoublonné, recherche, clics, saisie et listes
déroulantes, touches, défilement, navigation et ses erreurs), capture
rangée propre et quadrillée pour le modèle (mêmes pixels), aperçus JPEG en
anneau servis par /api/chat/cushot.
Un seul navigateur : quand la façade tient la conversation, le Go lui
passe les outils de ses sous-agents et tâches IA, ses aperçus et la
fermeture (POST /internal/facade/v1/browser) ; façade injoignable, le
navigateur du Go prend le relais. Couper l'interrupteur ferme les deux.
Parité : journal de 42 étapes tiré du Go contre un faux Chrome (script +
faux serveur CDP qui note chaque commande, scripts résumés par leur
empreinte, pixels de la grille), rejoué par tests/browser.rs ; un vrai
Chromium de bout en bout avec LOKI_CU_SMOKE=1 ; un tour de la façade qui
ouvre une page (tests/lot6.rs) ; TestFacadeNavigateur côté Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
bash_bg et bash_tail des tours de la façade ne passent plus par /tool/exec
du Go : src/tools/jobs.rs porte code_jobs.go. Refus avant lancement
(commande vide ou dangereuse, optimisation en cours), journal
.loki/jobs/<id>.log dans le dossier de la discussion, processus qui survit
au tour, shell sans le jeton interne ; relecture bornée (40 lignes par
défaut, 200 au plus), état et durée, arrêt relu aussitôt comme le Go.
paths::go_mkdir_all reproduit os.MkdirAll, erreur comprise.
Les jobs d'une discussion supprimée s'arrêtent avec elle. L'optimiseur du
Go compte ceux de la façade (POST /internal/facade/v1/bgjobs) avant de
mesurer ; façade injoignable : les siens seulement.
Parité : journal de 24 étapes tiré du Go (parity_jobs_test.go →
tests/parity_jobs/jobs.json), rejoué par tests/jobs.rs ; un tour de la
façade qui lance un job (tests/lot6.rs) ; TestFacadeJobsArrierePlan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Les outils web des tours de la façade (web_search, web_images, web_open,
web_read, web_grep) ne passent plus par /tool/exec du Go : src/webtools.rs
porte chat_internet_tools.go, chat_internet.go et chat_web_images.go. Le
cache des pages (10 min, clé MD5 des options comme le Go) est dans la
façade ; client Crawl4AI au corps près, réponses lues comme json.Unmarshal ;
résultats DuckDuckGo tirés du markdown (uddg, entités, extraits) ;
recherche d'images avec le jeton vqd et les cookies gardés par hôte ;
web_grep sur les regex du Go, erreurs au texte de regexp.
Le moteur intégré lit le HTML avec go-readability et html-to-markdown, que
rien en Rust ne reproduit au caractère près : la conversion d'une page et la
lecture des résultats restent au Go, derrière /internal/v1/web/fetch et
/internal/v1/web/search (sans état). web_screenshot reste au Go.
Le traducteur go_regex refuse la répétition d'une répétition et les comptes
au-delà de 1000, et rend littéral un « { » qui n'ouvre pas de répétition,
comme le Go (motifs retry_patterns compris).
Parité : journal de 74 appels tiré du Go (parity_web_tools_test.go →
tests/parity_web/outils.json : résultats, requêtes reçues par les faux
Crawl4AI et DuckDuckGo, pages demandées au moteur intégré), rejoué par
tests/webtools.rs ; un tour de la façade qui ouvre une page (tests/lot6.rs) ;
TestInternalMoteurWebIntegre côté Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
L'essai qui ne répond jamais (tests/tune.rs, essai_muet) ne liait pas le
port choisi par l'optimiseur : pendant les 2 s d'attente, un autre test
lancé en parallèle pouvait le prendre et répondre à /health à sa place
(« /props illisible » au lieu de « pas prêt après 2s », vu sur la CI). Il
le lie désormais sans jamais accepter, comme un moteur bloqué au
chargement.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Avec LOKI_BACKENDS=rust, la façade compile elle-même llama.cpp sous Linux
et macOS (backends/build.rs) : install {force}, update {clean} et
install-custom {repo, name, ref}, port de backend_build.go, des jobs
lcRunInstall / lcRunUpdate / lcBuildAndSwitch et d'installCustomBackend.
- plan complet (Metal, CUDA avec racine du toolkit et architectures, HIP,
Vulkan), build/ déplacé ou propre mis de côté en build.old ;
- étapes suivies : sorties des deux flux dans un même tube, découpage du
sinkWriter (git) et du scanner (CMake), configure.log et build.log,
fin des journaux remontée en cas d'échec ;
- outils manquants installés au mieux, compilateur vérifié ;
- service moteur arrêté le temps du build et relancé même en échec ;
- refus comme le Go : moteur de l'image, URL absente, optimisation en
cours, job déjà lancé.
Côté Go, facadeBackendsRoutes gagne les trois routes hors Windows (la
chaîne MSVC reste au Go, qui rapporte ses étapes au job de la façade).
Parité : internal/loki/parity_build_test.go tire un journal de 15 étapes
(tests/parity_backends/build.json) que tests/backends.rs rejoue par la
façade ; tests unitaires du découpage, du plan, du cache et des erreurs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Quand la façade tient la mémoire (LOKI_MEM=rust), les outils mem_search,
mem_read, mem_add, mem_edit et tracker de ses tours s'exécutent chez elle
(src/memory/tools.rs, port de chat_memory.go et de l'outil de tracker.go) :
mêmes résultats et diffs, mêmes lectures et écritures que les routes de la
mémoire (scellées selon le chiffrement, .bak, index tenu par le code, seed
au nom du projet). Recherche classée par couverture puis pertinence,
extrait coupé à l'octet comme le Go ; trackers vus niveau par niveau.
ChatShared porte la même mémoire que les routes ; tracker_add prend l'heure
en paramètre (tracker_add_at). Côté Go, l'heure des trackers passe par
trackerNow, que le journal fixe.
Parité : journal de 57 appels tiré du Go (parity_mem_tools_test.go →
tests/parity_mem/outils.json : résultats, diffs, fichiers de la mémoire et
trackers), rejoué par tests/memory.rs ; un tour de la façade qui crée une
page (tests/lot6.rs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
L'anneau des complétions (perf_log.go) est porté en Rust (src/perfring.rs) :
perte de cache contre la complétion précédente de la même discussion et de
la même nature, ce qui s'est intercalé, seuil d'alerte du preset (dans
l'ordre du lancement, 2048 sur un modèle hybride), ligne [perf], résumé de
/api/perf/summary. ChatShared le porte : tours, vérifications, résumés de
compaction et préchauffage de la façade y rangent leurs complétions sans
aller-retour au Go.
Côté Go, quand la façade tient la conversation, perfRecord range chez elle
(/internal/facade/v1/perf/record) et perfSnapshot relit son anneau
(optimiseur) ; /api/perf/summary lui est remis. Façade injoignable : l'anneau
du Go prend le relais. La route /internal/v1/turn/perf, plus appelée, est
retirée.
Parité : journal tiré du Go (parity_perf_test.go → tests/parity_perf/
perf.json : 14 complétions, résumé, onze seuils), rejoué par
src/perfring/tests.rs ; deux tours qui perdent le cache (tests/lot6.rs),
l'API de l'anneau pour le Go, et TestFacadePerfAnneau côté Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
L'outil par lequel le modèle tient les critères d'acceptation de sa tâche
(poser, marquer faits, lister, effacer) partait au Go, qui renvoyait la
liste à la façade comme delta. Il s'exécute maintenant dans le tour de la
façade (chat/crittool.rs, port de toolCriteria) : mêmes messages, ids en
nombre ou en chaîne, vingt critères au plus, `passed` réservé à la passe de
vérification, rangement scellé (chat/crit:<id>), liste poussée à l'écran à
chaque changement par un nouveau champ `deltas` de l'hôte du tour.
Les critères sont rangés sous la discussion du tour (le Go prenait la
discussion active : elles ne diffèrent que si l'utilisateur en change
pendant la génération). see_image reste au Go (préparation de l'image).
Parité : journal de 22 appels tiré du Go (parity_criteria_test.go →
tests/parity_tools/criteria.json), rejoué par tests/tools.rs ; le contrat
du mode Code de tests/lot6.rs est désormais vérifié par la façade seule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Quand la façade tient les tâches (LOKI_TASKS=rust), les quatre outils par
lesquels le modèle gère ses tâches planifiées — task_list, task_create,
task_update, task_delete — s'exécutent dans ses tours (src/tasks/tools.rs,
port de tasks_tools.go), sans passer par le Go : mêmes messages, projet
actif, fuseau de l'utilisateur, tâche unique « dans N minutes » ou « à
HH:MM », fréquence validée, script vérifié, compte-rendu coupé comme le Go.
ChatShared porte la même base de tâches que les routes.
Côté Go, les outils prennent l'heure des tâches (tasksNow) au lieu de
time.Now : rien ne change en service, le journal de parité la fixe.
Parité : journal de 44 appels tiré du Go
(parity_tasks_tools_test.go → tests/parity_tasks/tools.json), rejoué par
tests/tasks.rs ; un tour de la façade qui crée une tâche (tests/lot6.rs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Quand la façade tient les serveurs MCP (LOKI_MCP=rust), un appel mcp__*
d'un de ses tours partait au Go (/internal/v1/tool/exec), qui le renvoyait
aussitôt à la façade (/internal/facade/v1/mcp/call) : deux allers-retours
pour revenir au même pool. Le Go n'ajoute rien au résultat d'un outil MCP.
ChatShared porte désormais le pool (le même que celui des routes, créé une
fois) ; GoTurnHost::exec_tool l'appelle directement — même résultat que la
route interne, reconnexion comprise. Une reconnexion annonce la version du
Go (/internal/v1/auth), lue une fois et retenue. Sans pool tenu, rien ne
change. Le Go garde la route interne pour les tours qu'il exécute encore.
Test : un tour de la façade contre un faux serveur MCP stdio — rien par le
Go, résultats tels que le modèle les reçoit, erreur après reconnexion,
version du Go annoncée.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
105 mutations du verrou et du job ; le premier passage en tuait 39. Les
survivantes le sont désormais par des tests ajoutés :
- l'essai noté dans le verrou pendant la mesure, la sauvegarde notée avant
d'écrire, l'application refusée pendant la mesure, la cible inconnue ;
- l'application gardée : preset qui n'est plus en service, API externe,
clé changée ailleurs pendant l'application (défaite) ;
- l'application planifiée : clé sur option refusée sans l'option ;
- l'essai muet (délai de chargement), /props sans
default_generation_settings, SIDE_SLOT sur un seul slot, threads comptés
sur les cœurs de la sonde, aucun changement de moteur signalé quand son
build est inconnu ;
- VRAM mesurée par un faux nvidia-smi, budget, profondeur, notes et poids,
écarts de mesure, essais écartés, moteur piloté par la façade.
nvidia-smi, la pause entre deux lectures de VRAM et l'unité du budget
deviennent réglables (tests). Le test de l'essai orphelin lit l'identité
du processus une fois l'exec visible dans /proc : lue trop tôt, elle
portait l'exécutable du test (échec intermittent en CI).
Les vingt survivantes restantes, équivalentes ou hors de portée d'un test
(courses, estimation seulement), sont décrites dans la spec.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Les neuf routes que la façade sert toujours (ping, status, config, ligne
de commande du moteur, couches du modèle, chemins, RAM, VRAM, journal du
service) arrivent désormais à la façade par le tunnel du relais aussi :
withFacadeChat les lui remet dès qu'elle tourne (facade_core.go). Les
handlers du Go ne servent plus que sans façade (LOKI_FACADE=0).
Côté Rust, une requête relayée par le Go (jeton interne) n'est pas
réauthentifiée, comme pour les routes des lots.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
PROJ_SNAPSHOT était le dernier réglage qui gardait la conversation au
Go : la façade la tient désormais dans tous les cas (LOKI_CHAT=go reste
la porte de sortie). facadeChatBlocker et la décision prise une fois par
processus disparaissent.
Go (facade_projsnap.go) :
- /internal/v1/turn/view, avec la clé et l'agent : tête du tour portant
le bloc figé (ou le vivant à rafraîchir), préambule sans date ni
dossier, et la décision projsnap (rafraîchir, mise à jour à poser,
instantané à ranger, clé et système du tour) ; dry pour le
préchauffage ;
- /internal/v1/turn/projsnap/refresh : bloc vivant et instantané qu'il
devient, pour une compaction en plein tour.
Rust :
- ChatService::assemble_turn, partagé par le tour et la relance du
builder : instantané rangé avec la discussion, mise à jour posée en tête
du dernier message utilisateur (prependCtxUpdate), historique nettoyé
au rafraîchissement ;
- TurnHost::project_refresh après une compaction ou une réduction en
plein tour (replaceProjectHead) ; l'instantané posé devient celui de la
discussion ;
- le préchauffage demande la vue en dry et garde les mises à jour tant
que le bloc n'est pas rafraîchi.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
PREWARM ne garde plus la conversation au Go. La façade tient
l'historique : elle assemble et envoie la requête du prochain tour ; le
Go garde le compteur des requêtes en vol et ce qui l'annule.
Go (facade_prewarm.go) :
- /internal/v1/prewarm/check : moteur libre d'un vrai travail, loin de
minuit, en bonne santé, slot visé ;
- /internal/v1/prewarm/begin : rechargement SLOT_PERSIST puis inscription
comme LA requête en vol ; son annulation est relayée à la façade ;
- /internal/v1/prewarm/end : place rendue, cache repris noté ;
- /internal/v1/oai/begin : prewarm_keep pour l'étape qui le prolonge ;
- fin de tâche planifiée : le déclencheur part à la façade.
Rust (chat/prewarm.rs) :
- assemblage par les mêmes fonctions que le tour, sentinelle « . », un
seul jeton, non streamé ; abstention (capacités inconnues, tour en
cours, discussion vide, compaction attendue, assemblage inattendu) ;
- déclencheurs : fin de tour, fin de tâche, changement de discussion ou
de projet (full : après 3 s) ;
- le tour garde le préchauffage qu'il prolonge (empreintes des messages
et des options de rendu) et coupe tout de suite les autres ;
- télémétrie kind=prewarm.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
KEEP_TURN_IMAGES ne garde plus la conversation au Go. L'état du tour vit
dans la façade ; les fichiers et la vision restent au Go.
Go (facade_keepimg.go) :
- /internal/v1/turn/keepimg : vision réellement active, budget des images
gardées ;
- /internal/v1/turn/keepimg/store : une image relayée rangée par
référence (refRelayImage), relue à l'octet près.
Rust :
- chat/keepimg.rs : port de keepImages (image à l'essai, mesure par le
moteur répartie au prorata des octets, budget, publication, fin de tour).
- La boucle du tour : budget demandé pour le fil principal, image rangée
par le Go, historique réécrit publié sans les relais non gardés ; les
images gardées partent avant tout résumé (filet réactif, en cours de
tour, fenêtre pleine).
- Début et fin de tour : retirées si le contexte le demande ou si elles
ne doivent plus partir (clé retirée, vision absente).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
SIDE_SLOT et SLOT_PERSIST ne gardent plus la conversation au Go. La
machinerie reste au Go (configuration, requêtes en vol, tampon du slot) ;
la façade la consulte avant chaque requête au moteur local.
Go :
- /internal/v1/turn/slot (facade_slots.go) rend engineSlotFor et, pour
une étape de tour, recharge d'abord l'état gardé de la discussion
(slotPersistRestore). La sauvegarde à la bascule voit les étapes de la
façade : /internal/v1/oai/served marque le slot (engineMarkMain).
- facadeChatBlocker : SIDE_SLOT et SLOT_PERSIST retirés.
Rust :
- TurnHost::engine_slot : l'id_slot posé dans chaque requête (0 pour le
fil de la discussion, 1 pour la vérification), le ticket « en vol »
ouvert sur le second slot.
- Cache KV plein en plein calcul (sideSlotPoolError) : rejoué deux fois
tel quel, sans compaction, puis l'erreur.
- Résumés de compaction comptés en vol comme postSummary : transcription
sur le second slot, continuation sur le slot 0.
- La passe de vérification est un travail annexe (engineSideJob) : le Go
efface le slot à la fin s'il a servi (écart du lot 7 levé).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
main a reçu en parallèle la télémétrie du tour tenu par la façade
(/internal/v1/turn/perf), les tickets « en vol » du moteur et la
conversation tenue par la façade par défaut (LOKI_CHAT=go pour revenir
au Go). Le lot 29 faisait la même chose par /internal/v1/perf/record :
un seul chemin est gardé, celui de main.
- TurnHost::perf_record (main) remplace TurnHost::perf ; la nature est
posée par l'hôte (verify pour la passe isolée seulement, main pour les
corrections du builder, comme le Go).
- Les résumés de compaction passent par le même point d'entrée, nature
compact : chat/perf.rs ne garde que le relevé de la réponse entière
(perfRecFromWire) et l'envoi.
- La sonde du gabarit reste à la façade (lot 23) : /internal/v1/turn/perf
ne la lance plus côté Go.
- facade_perf.go retiré ; ses cas (perte, intercalé, alerte, preset
externe, incomplète, GET) rejoignent web_internal_turnperf_test.go.
- README : LOKI_CHAT défaut Rust, avec ce que les lots 19, 23, 29 et 30
y ajoutent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
La surcharge retry_patterns (tableau JSON de regex du Go, chacune en
(?mi)) ne garde plus la conversation au Go : la façade la lit au
démarrage de sa conversation, comme le Go la compile une fois par
processus ; illisible ou vide, les motifs embarqués s'appliquent, une
regex illisible est sautée.
Les regex restent celles du Go : chat::stream::go_regex réécrit pour le
crate regex ce qui n'a pas le même sens — classes Perl et limites de mot
ASCII (\w \d \s \b et leurs contraires, dans et hors d'une classe),
\Q…\E (refusé dans une classe, comme le Go), [ && -- ~~ dans une classe,
octal permis, référence arrière refusée.
Parité : parity_retry_test.go compile côté Go une soixantaine de regex
(dont celles qu'il refuse) et note leur première correspondance sur onze
textes (accents, chiffres non ASCII, espaces insécables, crochets) ;
tests/retry.rs rejoue le journal à l'identique et vérifie la lecture de
la surcharge au démarrage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Quand la façade tient la conversation (LOKI_CHAT=rust), ses complétions
rejoignent l'anneau de télémétrie du Go (perf_log.go) au lieu de passer
inaperçues : /api/perf/summary les compte, l'optimiseur en tire ses
poids, et l'interface reçoit à nouveau la perte de cache.
- chat/perf.rs : perfRecFromStats, perfRecFromWire (lu sans échec
possible, mêmes tolérances que perfNum, timings d'un autre type
présents mais vides comme le Go) et perfPrefix (FNV-1a cumulé) ;
- la boucle du tour range chaque complétion lue, numérotée dans le tour,
avec ses empreintes si elle est complète ; une complétion coupée par un
stop est rangée incomplète ; la dernière statistique porte la nature,
la perte, ce qui s'est intercalé et l'alerte ;
- natures : main (tour), verify (vérification du mode Code), compact
(résumé de compaction, rangé dès que la réponse se lit) ;
- le Go (/internal/v1/perf/record) range le relevé comme les siens et
calcule l'alerte au seuil du preset actif, jamais pour un preset
externe.
Tests : relevés et empreintes (chat/perf.rs), télémétrie de la boucle
(complète, coupée), du vrai hôte et des résumés (tests/lot6.rs),
TestFacadePerf côté Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Avec LOKI_TUNE=rust (expérimental, Unix seulement), la façade sert
/api/tune, /api/tune/status, /api/tune/cancel, /api/tune/apply et
/api/tune/last, et déroule l'optimisation sans perte (rust/loki-rs/src/tune) :
vrai moteur arrêté, essais composés à blanc puis lancés par les « loki
serve » du Go (LOKI_TUNE_TRIAL), chacun dans son groupe de processus,
mesurés par le bench complet à profondeur fixe, retenus au-delà du
bruit ; vrai moteur relancé. Rien n'est écrit sans un clic : copie du
preset, ou le preset lui-même — redémarrage, sonde, et ancienne version
rétablie si elle échoue.
- tune/core.rs : la partie pure (EXTRA_ARGS, réglages, essais de chaque
étape, garde-fous, diff et réécriture du preset, score) ;
- tune/lock.rs : le verrou tune.lock au format du Go, identité des
processus comme procIdentity (la reprise du Go reconnaît la façade
vivante, et écarte son verrou si elle meurt), ménage d'un verrou
périmé (essai orphelin arrêté, application interrompue défaite) ;
- tune/job.rs : routes, job, essais et application.
Le verrou de génération reste celui du Go, pris par le bail du moteur au
motif « tune » (refus de l'optimiseur pour les autres, stop du chat qui
annule). Le Go prête /internal/v1/tune/context (bench, génération, jobs
du mode code, état du moteur, télémétrie) et le démarrage ou l'arrêt du
moteur sans le refus de l'optimiseur (service/action « os »). Hors
Unix, la façade passe LOKI_TUNE vide au Go, qui garde l'optimiseur.
serde_json lit désormais les flottants à l'arrondi exact
(float_roundtrip), comme le Go.
Parité : parity_tune_test.go tire du Go un journal (partie pure, contenu
du verrou, résultat enregistré, vérification après application) rejoué
par tests/tune_core.rs. tests/tune.rs déroule de vraies optimisations, le
binaire de test jouant « loki serve » ; les tests du verrou du Go sont
portés ; TestFacadeTune côté Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
84 mutations du port Rust, 81 tuées ; trois équivalentes documentées
dans la spécification (annulation déjà vue au premier tour du select,
progression d'un job remplacé, PID nul du moteur).
- bench/tests.rs : un mini moteur qui garde les corps des requêtes ;
tranche pile à la fin du corpus, tour sans attendu, débits nuls
recalculés, niveau rejoué une seule fois, préchauffage et ligne courte
d'un run rapide, profondeur arrêtée (contexte plein, corpus épuisé,
réponse reprise, note de reprise hybride ou gabarit) ;
- tests/bench.rs : second lancement pendant la prise du bail, preset
relu au départ du job, mesure enregistrée sous le preset actif ;
- journal de parité : un prompt juste au-delà de la prose.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Avec LOKI_BENCH=rust (expérimental), la façade sert /api/bench,
/api/bench/status, /api/bench/cancel et /api/bench/last, et déroule le
protocole de mesure contre le moteur local (rust/loki-rs/src/bench) :
mode rapide (préchauffage, ligne courte) et mode complet (prefill à froid
à profondeur réelle, trois tours qui reprennent le cache). Corpus,
requêtes, calculs et messages sont ceux du Go ; seule une mesure complète
est enregistrée (last_bench, bench_presets, empreinte de configuration du
Go).
Le verrou de génération reste celui de la conversation du Go : la façade
le prend par le bail du moteur, avec le motif « bench ». Le Go note alors
benching et donne aux autres le refus du benchmark ; changer de
discussion ne rompt pas le bail ; benchRunning() le voit (clients /v1,
préchauffage, optimiseur). Un stop du chat, un Reset ou un bail perdu
annulent le bench. Le Go prête aussi le travail annexe (effacement du
slot à la fin, /internal/v1/engine/sidejob) et la télémétrie
(/internal/v1/bench/perf).
Correction des tests du Go : trois tests du préchauffage échouaient dix
minutes par jour, avant minuit (prewarmNearMidnight lisait l'heure
réelle). L'heure du garde-fou devient remplaçable (prewarmNow), fixée à
midi dans les tests.
Parité : parity_bench_test.go tire du Go un journal rejoué par
tests/bench.rs contre le même faux moteur ; chaque requête reçue par le
moteur est comparée (longs textes réduits à leur taille et à leur
empreinte). Il couvre les refus, les deux modes, les échecs de chaque
phase, le raisonnement refusé puis rejoué, la profondeur partielle et
l'annulation. Les tests du Go sont portés (bench/tests.rs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Avec LOKI_TASKS=rust (expérimental), la façade sert les routes
/api/tasks* et fait tourner le planificateur, que le Go ne démarre plus.
Elle exécute elle-même les tâches « script seul » (shell de l'agent,
limite de 10 min, arrêt à la main). Une tâche IA passe par le verrou de
génération et les outils de l'agent, restés au Go : la façade la lui fait
exécuter (/internal/v1/tasks/run), comme son propre planificateur l'aurait
fait. Les notifications partent par la façade si elle les tient, sinon par
le Go (/internal/v1/push/send).
- src/tz.rs : les fuseaux IANA comme le Go (TZif v1/v2 et règle POSIX,
time.Local, time.Date et ses heures qui sautent ou se répètent) ;
- tasks/cron.rs : les quatre formes de fréquence, avec ParseDuration du Go
et ses messages d'erreur ; instants sur 128 bits (dates au-delà de
2262) ;
- tasks/mod.rs : la base, l'exécution, le planificateur (un tic par
minute, une tâche due par tic) et les sept routes.
Le Go reçoit un crochet de test pour l'heure des tâches (tasksNow).
Correction de la façade : les appels à l'API interne du Go étaient tous
bornés à 10 s. Un outil exécuté par le Go qui dure plus longtemps (page
lente, navigateur, MCP tenu par le Go) était coupé, alors qu'il continuait.
Ces appels sont désormais bornés à 30 min ; une tâche IA est attendue
jusqu'à 24 h.
Parité : parity_tasks_test.go tire du Go un journal rejoué par
tests/tasks.rs, l'heure des tâches fixée pas à pas. Il couvre
l'enregistrement (chaque forme et chaque erreur de fréquence, plusieurs
fuseaux), la bascule, l'interrupteur maître, la suppression, les
lancements et les tics du planificateur. Les mutations (79, dont 77
tuées et 2 équivalentes) ont fait trouver et corriger cinq écarts du
port : dates lointaines, intervalle en jours démesuré, heure qui saute au
printemps, compte-rendu coupé au milieu d'un caractère, noms de fuseau
contenant « .. ».
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Port de backend_strata.go (rust/loki-rs/src/strata) : la machine (cartes
par nvidia-smi, Python 3.10+ et venv, RAM totale et disponible, RAM du
moteur Strata en marche, place libre), ce qui y tient et la
recommandation, la configuration de lancement en clair des modèles
installés (placement des experts drop/ram/arena/disk, carte d'aide,
réglages du preset), les réglages d'un modèle installé et l'installation
(job des moteurs : paquet figé vérifié par SHA-256, installeur lancé et
journalisé comme le sinkWriter du Go, preset écrit puis activé par le Go).
Tenu seulement avec LOKI_BACKENDS=rust ; le lancement (loki serve) reste
au Go, qui relaie les routes du tunnel.
Correction du Go : /api/strata/settings décidait « en service » après
avoir écrit les nouveaux réglages, donc jamais pour un réglage changé ;
décidé désormais avant l'écriture, comme SavePresetApplying. Le port suit.
Parité : parity_strata_test.go (crochets de test : release, meminfo,
place libre, action de service) tire du Go un journal rejoué par
tests/strata.rs ; les tests du Go portés dans strata/tests.rs ;
TestFacadeStrata côté Go. Mutations : 71 tuées sur 71.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Port de web_engine.go, web_engine_reco.go et backend_engine_oci.go
(rust/loki-rs/src/engineimg) : statut du moteur (build, source, variante,
versions installées, avis, version recommandée, version précédente,
relevé du rendu), dernier build publié, plan (dernière ou recommandée,
risque de rendu du raisonnement au défaut de b10763), mise à jour depuis
le registre officiel (couches du sommet, liens, runtime ignoré, essai à
blanc, REASONING_PRESERVE=off dans le preset actif ou la configuration,
ménage, vérification du rendu avant/après par la sonde de gabarit),
bascules, suppression et retour à la version précédente.
Tenu seulement avec LOKI_BACKENDS=rust : la mise à jour est un job des
moteurs. La sonde est celle de la conversation quand la façade la tient.
/api/engine/cmdline reste au Go, qui relaie les routes du tunnel.
Parité : parity_engineimg_test.go tire du Go un journal (faux registre
OCI, faux moteur local pour la sonde) rejoué par tests/engineimg.rs ;
TestFacadeMoteurImage côté Go. Les aides de fichiers des rejeux passent
dans tests/common/parity.rs. Mutations : 71 tuées sur 74, trois
équivalentes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Port de chat_tplprobe.go : la façade sonde le gabarit du moteur après ses
complétions (/health, /props, /apply-template), avec les verdicts
preserves_history, prefix_stable et renders_reasoning, le cache par
build/modèle/gabarit/forme et la relance bornée.
- l'écho du raisonnement suit le verdict de la sonde (echo_policy_for) ;
- NUDGE_IN_TOOL ne garde plus la conversation au Go : le rappel de budget
va dans le dernier résultat d'outil quand le préfixe n'est pas stable ;
- le Go lit le verdict de la façade (/internal/facade/v1/chat/tpl) quand
elle tient la conversation.
Parité : parity_tplprobe_test.go tire du Go un journal d'échanges rejoué
octet pour octet par tests/tplprobe.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
La façade (rust/loki-rs/src/backends) sert le statut du moteur llama.cpp
(dépôt git, binaire, moteur de l'image, précompilé, moteur téléchargé,
plan de build et conseil), la vérification git, la dernière release et
l'installation des binaires précompilés (extraction avec liens, VERSION,
ménage), la bascule fast/opt, les backends custom et leur désinstallation,
et les GPU vus par un moteur (caches mémoire et disque).
Elle détient LE job des moteurs (state/build_job + loki-build.log, repris
au démarrage). Les jobs que le Go fait encore tourner (compilation depuis
les sources, moteur de l'image, Strata) lui rapportent chaque étape par
/internal/facade/v1/build-job/* ; le Go garde le garde de l'optimiseur et
ne restaure plus de job quand la façade tient le lot.
Correction du Go : prebuiltVersionFormat lisait le format comme 3e champ
du marqueur VERSION ; hors CUDA Windows (« b300 fmt2 ») il n'y en a que
deux, et chaque vérification retéléchargeait la release au lieu de dire
« déjà à jour ». Le port suit.
Parité : parity_backends_test.go (Linux) → tests/parity_backends, rejoué
par tests/backends.rs (fausses machines dans le PATH, faux GitHub, dépôt
git reproductible) ; TestFacadeBackends pour les étapes transmises.
Mutations : 55 tuées sur 57, deux inobservables sous Linux (cudart
Windows, garde d'un avertissement).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
En conteneur, l'image se met à jour par Docker : /api/update rend la
version du Go sans mise à jour disponible et la note Docker,
/api/update/apply la refuse (409). La façade sert ces deux routes quand
LOKI_UPDATE=rust et LOKI_CONTAINER=1 (sysupdate.rs) ; hors conteneur,
loki-rs ne passe pas la variable et le Go garde le remplacement du
binaire.
Parité : parity_update_test.go tire du Go les quatre réponses en
conteneur, rejouées par tests/sysupdate.rs ; TestFacadeMiseAJour côté Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
La façade tient les serveurs MCP quand LOKI_MCP=rust (expérimental ; le
Go reste le défaut) : configuration (state/mcp), catalogue embarqué (le
même fichier que le Go, copié dans l'étape Rust de l'image), état et
routes /api/mcp*. Un client MCP en Rust (mcp/client.rs) parle JSON-RPC
sur stdio et en Streamable HTTP (réponses JSON ou SSE, session suivie,
en-têtes configurés, ping du serveur servi). Le pool suit mcp_client.go :
session gardée entre les tours, connexions parallèles et partagées,
session invalidée pendant sa connexion jamais republiée, délais npx/uvx,
appel borné avec une reconnexion, sortie plafonnée à la fenêtre.
Un seul pool, celui de la façade : le Go, qui exécute encore les outils
de l'agent, lui demande les outils MCP, leurs appels et le
pré-chauffage (/internal/facade/v1/mcp/*, facade_mcp.go).
Parité : parity_mcp_test.go tire du Go les fonctions pures, seize
mutations de configuration, et un vrai serveur du SDK MCP Go (onze
outils) : ce qu'il met sur le fil et ce que le client Go en tire (état,
outils annoncés, appels). tests/mcp.rs le rejoue contre un faux serveur
qui resert ces réponses, et vérifie stdio, reconnexion, routes, catalogue
et session invalidée de bout en bout ; TestFacadeMCP côté Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Quand la façade tient la conversation, /api/chat/export, /api/chat/mode
et /api/chat/criteria rejoignent ses routes (chatx.rs, facadeChatRoutes
côté Go), sans nouvelle variable. L'export Markdown ou JSON part de la
conversation vivante (le Go relisait la copie enregistrée, en retard d'un
tour au besoin) : raisonnement replié, outils dans leur état final,
compaction signalée, export partiel annoncé, JSON indenté et HTML
échappé, sans mise à jour de contexte ni image en base64. Le mode et les
critères (scellés, vingt au plus, statuts ramenés) vont sur le fil de la
façade. Le reste du mode Code (indice, travaux de fond, outil criteria)
reste au Go et lit les mêmes clés.
Parité : parity_chatx_test.go tire du Go l'export d'une conversation
riche sous 22 jeux d'options et 23 appels au mode et aux critères (base
et fil après chacun, erreurs JSON imbriquées comprises), rejoués octet
pour octet par tests/chatx.rs ; tests du relais mis à jour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
La façade sert /api/projects quand LOKI_PROJECTS=rust (expérimental ; le
Go reste le défaut) : liste avec discussions, pages et trackers comptés,
création (slug translittéré et unique, index amorcé), renommage,
description, suppression (discussions, trackers et mémoire du projet,
actif réassigné, dernier projet indestructible) et bascule, refusées
pendant une génération.
Le projet actif passe toujours par le Go, qui en garde une copie en RAM.
Les discussions sont à qui tient la conversation : la façade bascule et
supprime elle-même quand elle la tient, sinon le Go garde
/internal/v1/project/switch et /internal/v1/project/drop-convs
(facade_projects.go).
Parité : parity_projects_test.go tire du Go 40 appels sur une base
amorcée (pages, discussions, trackers piégés), avec le registre, l'actif,
memory/, les trackers et l'index après chacun, rejoués par
tests/projects_api.rs contre un faux Go qui bascule et supprime comme le
vrai ; bascule et suppression par la façade de bout en bout ;
TestFacadeProjets côté Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Sans LOKI_CHAT, c'est désormais la façade qui tient la conversation ;
LOKI_CHAT=go la rend au Go (porte de sortie, aussi listée dans le compose
Unraid). Le Go la garde de toute façon si un réglage que la façade ne
sait pas encore honorer est posé (PROJ_SNAPSHOT, KEEP_TURN_IMAGES,
NUDGE_IN_TOOL, SIDE_SLOT, SLOT_PERSIST, PREWARM, motifs retry_patterns),
décidé au démarrage comme avant.
Vérifié de bout en bout avec les vrais binaires et un faux moteur, dans
les deux modes : deux tours (texte, puis appel d'outil), même état, même
journal rejoué, discussion intacte après relance, et un panneau perf
identique (complétions, tours, pertes, sonde du gabarit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Trois choses manquaient au tour tenu par la façade, actives même sans
clé opt-in, et qu'il fallait avant d'en faire le défaut :
- la télémétrie de chaque complétion (perfRecord) : /api/perf/summary et
l'alerte « cache perdu » posée sur les stats finales d'une étape. La
boucle Rust la remet au Go (/internal/v1/turn/perf) au même moment que
la boucle Go, avec ses empreintes de messages ; le Go range, calcule la
perte et rend nature, jetons perdus et alerte, posés sur la copie finale
des stats ;
- la sonde du gabarit, lancée dans les mêmes conditions (requête de forme
normale du moteur local) ;
- le comptage des requêtes en vol : chaque requête au moteur local ouvre
un ticket (/internal/v1/oai/begin), marqué servi à la réponse 200
(engineServed, engineMarkMain pour une étape de la discussion), rendu en
fin de lecture du flux. Le slot de la discussion n'est ainsi ni effacé
pendant une requête ni pris pour un autre.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parity_engine_test.go appelle lastErrorLine (sys_service_rust_linux.go)
et suppose la supervision par fichier PID du conteneur ; la façade ne
tourne qu'en conteneur Linux. La CI macOS ne compilait plus les tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
La façade sert /api/internet (moteur web, serveur Crawl4AI et sa clé
jamais rendue, joignabilité avec son cache de 30 s), /api/computer
(interrupteur du navigateur, binaire trouvé, vision), /api/apikey (clé de
complétion, repli sur config.env, adresse /v1 à coller dans un client
tiers) et /api/network (écoute du moteur) quand LOKI_SYSTEM=rust
(expérimental ; le Go reste le défaut).
Restent au Go derrière son API interne (facade_system.go) : le cache de
joignabilité de ses outils web (/internal/v1/web/reach/reset), le
Chromium des outils browser_* (/internal/v1/cdp/shutdown) et le
redémarrage qui applique une clé neuve quand la façade ne pilote pas le
moteur (/internal/v1/service/action). Le relais du tunnel garde l'hôte
et X-Forwarded-Proto du client.
Correction en passant : maskAPIKey paniquait sur une clé d'un octet, et
/api/apikey tombait à chaque lecture.
Parité : parity_system_test.go tire du Go 61 appels de route avec la
base et les sondes reçues par trois faux Crawl4AI après chacun, et huit
dispositions du dossier de Playwright, rejoués par tests/system.rs ; ce
qui reste au Go vérifié de bout en bout ; TestFacadeSysteme,
TestFacadeRelaisGardeHote et TestMaskAPIKeyCourte côté Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
La façade sert /api/start, /api/stop, /api/restart, /api/vram/unload et
/api/vram/reload quand LOKI_ENGINE=rust (expérimental ; le Go reste le
défaut). Elle lance elle-même « loki-rs engine <action> », le superviseur
que le Go appelait déjà en conteneur, avec l'environnement qu'il lui
passait et de quoi joindre l'API interne du Go ; sa dernière ligne
d'erreur devient l'erreur rendue. Le contrôle d'avant démarrage
(preflightEngine : preset externe, Strata, BIN, MODEL, tranches) et le
bilan de VRAM du déchargement (gpuSettle) sont portés.
La façade ne tient ce lot qu'en conteneur : sous systemd ou avec
LOKI_ENGINE_SUPERVISOR=go, loki-rs ne passe pas la variable au Go.
Restent au Go derrière son API interne (facade_engine.go) : le verrou de
l'optimiseur avant tout démarrage (/internal/v1/service/prestart), la
réponse en cours quand il tient la conversation
(/internal/v1/chat/generating) et l'arrêt de la dictée quand il la tient
(/internal/v1/dictate/stop).
Parité : parity_engine_test.go tire du Go dix bilans de VRAM, vingt-six
installations passées au préflight, les messages du superviseur et dix
réponses de route, rejoués par tests/enginectl.rs ; démarrage, arrêt,
redémarrage, déchargement chiffré et rechargement vérifiés de bout en bout
avec un faux superviseur et un faux nvidia-smi ; TestFacadeMoteur côté Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
La façade sert la liste des presets (pastilles, mesures du bench quand
elles correspondent à la configuration), leur ordre, l'éditeur (lecture,
graine d'un preset neuf, enregistrement avec nom de fichier unique,
prompt système), la suppression (refusée pour l'actif, modèle compris sur
demande), les aperçus de l'éditeur (cache de prompts, second slot, avis
MoE et de chargement, réutilisant le port de loki serve), la copie en
placement auto, les presets externes (la clé n'est jamais rendue, test
par un vrai appel de complétion) et la bascule, quand LOKI_PRESETS=rust
(expérimental ; le Go reste le défaut).
Ce qui touche au moteur reste au Go jusqu'à son lot : facade_presets.go
expose /internal/v1/preset/apply (bascule avec état du slot gardé et
moteur relancé ou arrêté, preset en service réappliqué) et
/internal/v1/tune/guard (verrou de l'optimiseur), et remet les routes du
tunnel à la façade.
Parité : parity_presets_test.go tire du Go les fonctions pures et 70
appels de route avec presets/, models/ et la base après chacun, rejoués
par tests/presets_api.rs ; bascule et application vérifiées de bout en
bout contre le faux Go, presetApply côté Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
loki-rs engine serve fait ce que faisait loki serve : lire la
configuration (par l'API interne du Go), sonder le monde (aide du moteur,
--version, --list-devices, GGUF, RAM, VRAM, cgroup, cpuset, variables
LLAMA_ARG_*/GGML_*), composer la ligne de llama-server, vérifier le port,
garder la commande pour l'interface, puis exec. Le superviseur le lance à
la place de loki serve quand LOKI_SERVE=rust (expérimental ; le Go reste
le défaut).
serve/ porte backend_serve*.go fonction par fonction (couches GPU, --fit
et sa marge, threads, files CUDA, second slot, mode tensor, cache de
prompts, points de reprise, décodage spéculatif, fidélité, LOAD_GUARD,
SLOT_PERSIST), toute la décision restant pure. Strata, l'essai de
l'optimiseur et un lancement sans API interne passent toujours par
loki serve (Go). La façade donne au Go l'adresse de son API interne
(LOKI_GO_ADDR), héritée par le superviseur.
Parité : parity_serve_test.go tire du Go 1 872 lancements (12 machines ×
112 presets × 3 aides de moteur × 13 jeux de variables) et les sondes
pures, rejoués argument par argument par tests/serve.rs ; deux essais de
bout en bout lancent le vrai binaire contre un faux Go et un faux moteur.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
La façade sert l'éditeur des pages mémoire (lecture, enregistrement,
renommage, suppression, déplacement vers un autre projet, index MEMORY.md
tenu par le code), le panneau des trackers, le mode mémoire du projet
actif et les interrupteurs du mode agent, quand LOKI_MEM=rust
(expérimental ; le Go reste le défaut).
Les pages restent des fichiers memory/<projet>/*.md écrits de façon sûre
(.bak, temporaire synchronisé puis renommé, relecture vérifiée), les
trackers des valeurs du bucket tracker. Le chiffrement reste au Go : la
façade fait ouvrir et sceller ses contenus par /internal/v1/crypt/*, et
n'écrit jamais en clair quand la mémoire est verrouillée.
gojson.rs reproduit le décodage d'encoding/json (première valeur, EOF,
erreurs du scanner et de type, structs anonymes) pour renvoyer au client
les mêmes messages que le Go ; il servira aux lots suivants.
Côté Go : facade_mem.go remet ces routes à la façade quand elles arrivent
par le tunnel ; coffre, instantanés, sauvegarde et projets restent au Go.
Parité : parity_gojson_test.go (81 corps) → tests/gojson.rs ;
parity_mem_test.go (noms, titres, slugs, modes, 36 dates, 102 appels de
route avec le disque et la base après chacun) → tests/memory.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
La façade sert la clé VAPID et les (dés)abonnements, et pousse elle-même
les notifications « réponse prête » quand LOKI_PUSH=rust (expérimental ;
le Go reste le défaut) : chiffrement RFC 8291 (aes128gcm, enregistrement
complété à 4096 octets comme webpush-go), signature VAPID RFC 8292 (JWT
ES256), abonnements purgés sur 404/410, par ring déjà présent via rustls.
Mêmes clés en base que le Go.
Côté Go : facade_push.go remet les routes du tunnel à la façade, et
sendPushToAll (fin de tour, fin de tâche) lui délègue l'envoi.
Parité : parity_push_test.go tire du Go les durées et onze appels de route
avec la liste des abonnements en base, rejoués par tests/push.rs ; le
chiffrement reproduit l'exemple de la RFC 8291 à l'octet près, et un envoi
de bout en bout est déchiffré côté navigateur.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
tests/engine.rs (redemarre) a échoué en CI sur « Text file busy » : un
test écrivait son faux binaire pendant qu'un test voisin, dans un autre
thread du même process, lançait un enfant — qui héritait du descripteur
d'écriture le temps de son exec, et l'exec du script échouait. Les faux
binaires (moteur, serveur de dictée) sont désormais écrits par un sh à
part : le process des tests ne tient jamais le fichier ouvert en
écriture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
La façade sert le dépôt des pièces jointes (morceaux base64, 1 Gio au
plus, espace disque vérifié), le panneau Fichiers, la suppression, le
téléchargement (fiche, tranches base64 pour le tunnel E2E, brut avec
plage d'octets), l'affichage des images (type reniflé, CSP fermée,
vignette redressée et réduite par moyennage) et l'archive .zip, quand
LOKI_FILES=rust (expérimental ; le Go reste le défaut).
Côté Go : la nouvelle route /internal/v1/conv/workspace donne le dossier
de la discussion active (le Go la connaît toujours), facade_files.go
remet ces routes à la façade quand elles arrivent par le tunnel, et les
.part d'envois ne sont plus vidés par le Go au démarrage.
Parité : parity_files_test.go tire du Go les noms, slugs, en-têtes EXIF,
orientations et réductions (pixel pour pixel), reniflages, puis 42 appels
de route sur une arborescence fixe, rejoués par tests/convfiles.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
La façade sert /api/prefs, /api/sysprompt, /api/reasoning-effort et
/api/oai/public quand LOKI_SETTINGS=rust (expérimental ; le Go reste le
défaut), sur les mêmes clés de la base : préférences fusionnées et
nettoyées (prénom borné, avatars en liste fermée, valeurs vides
effacées), prompt système du preset actif avec repli sur le global,
intensité du raisonnement en liste blanche, adresse publique normalisée.
Côté Go, facade_settings.go remet ces routes à la façade quand elles
arrivent par le tunnel du relais.
Parité : parity_settings_test.go tire du Go 19 adresses, 11 intensités et
29 appels de route avec le contenu de la base après chacun, rejoués par
tests/settings.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
La façade sert /api/transcribe et /api/dictate/* quand LOKI_DICTATE=rust
(expérimental ; le Go reste le défaut) et supervise elle-même le serveur
de transcription sherpa-onnx : lancé à la demande sur la boucle locale,
GPU masqués et sans le jeton interne, prêt dès que sa socket répond, mort
au démarrage expliquée, éteint après dix minutes d'inactivité ou quand
les réglages changent. Modèles Parakeet téléchargés et décompressés au fil
de l'eau (tar.bz2, entrées hors du dossier refusées, liens ignorés),
protocole WebSocket identique (en-tête de 8 octets, trames de 10 Kio).
Côté Go, facade_dictate.go remet ces routes à la façade quand elles
arrivent par le tunnel du relais, et asrShutdownVite (libération de la
VRAM, optimiseur) lui demande l'arrêt par l'API interne.
Parité : parity_dictate_test.go tire du Go la lecture de treize WAV, les
chemins d'archive, réglages, catalogue, arguments et treize appels de
route, rejoués par tests/dictate.rs ; supervision testée contre un faux
sherpa-onnx qui parle le protocole.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
La variable LOKI_DOWNLOADS devient LOKI_MODELS et couvre, en plus des
téléchargements, la liste du sélecteur (avec qui référence chaque .gguf),
la suppression (refus si en service, 409 avec les presets sans force,
tranches emportées), les dossiers de modèles et le catalogue Hugging Face
(recherche, fichiers d'un dépôt avec verdict mémoire, jeton vérifié puis
enregistré, cache vidé à son changement).
Côté Go, facade_models.go remet ces routes à la façade quand elles
arrivent par le tunnel du relais.
Parité : parity_models_test.go tire du Go les fonctions pures du catalogue
et un scénario sur disque de 20 appels (liste, dossiers, suppressions),
rejoués à l'identique par tests/models.rs ; catalogue et routes testés de
bout en bout contre un faux Hugging Face.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
La façade Rust mène les téléchargements de .gguf quand LOKI_DOWNLOADS=rust
(expérimental ; le Go reste le défaut) : sonde, lancement, suivi,
annulation, avec les mêmes contrats et la même mécanique que le Go
(tranches rapatriées en entier, plages d'octets parallèles, reprise, place
disque vérifiée sur le total, jeton réservé aux hôtes Hugging Face, refus
traduits, .part vidés au démarrage).
Côté Go : facade_downloads.go remet ces routes à la façade quand elles
arrivent par le tunnel du relais, et le Go ne vide plus les .part qu'elle
mène.
Parité : journal des fonctions pures tiré du Go (parity_dl_test.go) et
rejoué en Rust ; téléchargements de bout en bout contre un faux Hugging
Face (tests/downloads.rs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Conversation, tours, routes du chat et outils fichiers/shell/git en Rust derrière LOKI_CHAT=rust (expérimental, Go par défaut) ; boucle du contrat du mode Code ; changement de projet par la façade ; parité Go/Rust par journaux de référence (19 tours, 7 tours complets, 70 appels d'outils). Repris de la PR #25 : le sélecteur de l'en-tête ne charge que des presets.
Le sélecteur de l'en-tête proposait, sous les presets, un groupe « Modèles
(.gguf) » listant les fichiers du disque. En choisir un écrivait MODEL= et
rien d'autre : le contexte, le NGL et l'échantillonnage du modèle
précédent restaient en place — un 27B chargé par-dessus les réglages d'un
4B partait sur 100k de contexte et mourait en OOM, sans que rien ne dise
pourquoi. Charger un modèle, c'est appliquer tous ses réglages : ça
s'appelle un preset, et c'est désormais le seul chemin.
/api/models/use disparaît avec le groupe qu'elle servait, ainsi que
switchModelFile, loadModelSwitchFiles et MS_FILES côté interface. Le title
du sélecteur le dit : « preset actif — en changer recharge tous ses
réglages et relance le moteur ».
Repris tel quel de la PR #25, dont l'historique (antérieur à la réécriture
de main) ne peut plus y être fusionné. index.html régénéré par
tools/assemble-ui.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
En mode Code, le Go n'annonce web_screenshot que si Playwright est
installé : présent dans le conteneur qui avait tiré les journaux, absent
des machines de la CI, d'où deux tours de référence qui ne s'y
retrouvaient plus. Le test retire Playwright du PATH : le journal ne
dépend plus de la machine qui le tire (Linux et macOS vérifiés sur la
sortie de la CI).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
internal/loki/parity_turn_test.go passe sept tours par la vraie route
/api/chat/send du Go contre un faux moteur — réponse simple, raisonnement,
outil puis réponse, « pensé sans agir », erreur du moteur, et en mode Code
un contrat déjà tenu et un contrat abandonné après deux corrections — et
range dans rust/loki-rs/tests/parity_turn/ le journal d'affichage du tour
(coalescé, mesures et horodatages masqués), l'historique rangé, les corps
envoyés au moteur, ainsi que les réponses du Go que la façade demandera
pendant ce tour (/turn/start, /turn/view, /turn/verifier). Ce que le prompt
de la machine porte de propre au lancement (dossiers temporaires,
discussion, date, hôte) est masqué des deux côtés.
tests/lot6.rs (parite_du_tour_complet) rejoue les sept tours par
chat::generate et retrouve journal, historique et requêtes à l'identique,
numérotation du journal coalescé comprise.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
git_status, git_diff et git_clone (rust/loki-rs/src/tools/git.rs) quand la
façade tient la conversation : dépôt deviné comme le Go (sous-dossier
demandé, racine, unique sous-dossier dépôt), sorties mêlées et bornées,
jamais d'invite, clone limité à https:// et git@hôte: et rangé dans le
dossier de la discussion. Toujours sur cette machine, cible distante ou
non, comme le Go.
Parité : TestPariteOutilsGit (deux dépôts, l'un modifié, l'autre sans
commit ; aucun appel réseau) range tests/parity_tools/git.json ;
tests/tools_git.rs le retrouve à l'identique. Le rejeu des journaux des
outils est partagé (tests/common/toolsfx.rs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Avec LOKI_CHAT=rust, changer de projet passait par projectSwitch du Go,
qui enregistrait SA copie de la discussion — chargée au démarrage, périmée
— par-dessus celle de la façade, ouvrait une autre discussion que la façade
ignorait, et ne voyait pas un tour de la façade en cours. Désormais :
- /api/projects (switch) remet le changement à la façade
(/internal/facade/v1/project/switch) : elle enregistre sa discussion avec
le projet quitté, fait changer le projet actif au Go
(/internal/v1/project/activate), ouvre la plus récente du nouveau projet
(une neuve s'il n'en a pas) ; refusé pendant un tour (409) ;
- supprimer un projet : la façade le quitte d'abord s'il est actif, puis
supprime ses discussions (/internal/facade/v1/project/drop).
Réglages que la façade ne sait pas honorer (PROJ_SNAPSHOT,
KEEP_TURN_IMAGES, NUDGE_IN_TOOL, SIDE_SLOT, SLOT_PERSIST, PREWARM, motifs
retry_patterns) : le Go garde la conversation, décidé une fois au démarrage
(facadeChatBlocker) ; la façade le lit (/internal/v1/chat/holder) avant de
servir quoi que ce soit, au lieu d'échouer en plein tour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Port de codeVerifyLoop (rust/loki-rs/src/chat/verify.rs) : à la fin d'un
tour de build en mode Code, relances du builder sur les critères encore
ouverts, passe de vérification sur un contexte isolé, corrections, abandon
dit dans le fil après deux corrections — mêmes deltas que le Go (role,
verify_done, mesures ctx_isolated), relances et corrections rangées dans
l'historique, vérifications non.
Le Go garde les critères (l'outil criteria s'y exécute) : la façade les lit
en base (chat/crit:<id>) et reçoit le contexte isolé du vérificateur par
POST /internal/v1/turn/verifier (prompt du rôle et faits de la machine,
outils fermés, rôle verifier, seul autorisé à marquer passed). Toute
compaction en mode Code rend l'état des critères au résumé, comme
compactMessages.
generate : vue du tour factorisée (turn_view), question à l'utilisateur
suivie (une question posée suspend la vérification).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Quand la façade tient la conversation (LOKI_CHAT=rust), read, grep, glob,
write, edit, bash et ask s'exécutent dans rust/loki-rs/src/tools au lieu de
repartir au Go par /internal/v1/tool/exec : mêmes garde-fous dans le même
ordre (commande catastrophique, « & » final en mode Code, bac à sable du
mode Code, dossier mémoire interdit, lu avant d'écrire), même diff pour
l'UI, même nettoyage de la sortie du shell. Le jeton de l'API interne est
retiré de l'environnement du shell de l'agent.
L'état partagé reste au Go, en un seul exemplaire (sous-agents et tâches
le lisent aussi) : POST /internal/v1/tool/context (dossier de travail,
dossier mémoire, cible distante), /tool/tracker (lecture notée, contrôle
avant écriture), /tool/wrote (note après écriture, diagnostics LSP).
Repli sur le Go pour une cible distante ou un contexte indisponible.
Parité : internal/loki/parity_tools_test.go passe 58 appels à execTool sur
une arborescence de test et range le journal dans tests/parity_tools ;
tests/tools.rs le retrouve à l'identique avec exec_local. Écarts assumés
consignés dans docs/superpowers/specs/2026-10-15-rust-migration-lot7-outils-design.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
internal/loki/parity_test.go rejoue 19 scénarios dans runChatTools contre
un faux moteur aux réponses scriptées, outils aux résultats fixés
(execToolHook, nil en service) : réponse simple, raisonnement, outils
seuls et en parallèle, appel répété, arguments illisibles, outil inconnu,
budget dépassé, « pensé sans agir » une et deux fois, pensée en ligne,
échantillonnage et intensité, raisonnement coupé, 500 une et deux fois,
erreur du moteur, appel d'outil écrit en texte, API externe, historique
déjà outillé. Les corps envoyés au moteur, les événements d'affichage
(texte regroupé, mesures ignorées), l'historique ajouté et l'erreur sont
rangés dans rust/loki-rs/tests/parity/*.json ; le test Go échoue si la
boucle ne les produit plus (LOKI_PARITY_UPDATE=1 pour régénérer).
rust/loki-rs/tests/parity.rs rejoue les mêmes scénarios dans
chat::turn::run_turn et retrouve les 19 journaux à l'identique.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Une requête qui porte le jeton interne vient du Go, qui l'a déjà
authentifiée (tunnel du relais) : les routes du chat la servent sans
redemander la clé. L'API interne de la façade, réservée à ce jeton, reçoit
les événements d'affichage nés au Go (mode Code, critères) et le
rechargement après déverrouillage : une discussion chargée vide mémoire
verrouillée revient au déverrouillage, jamais par-dessus un fil rempli
(sinon le premier message aurait écrasé la discussion chiffrée).
Le relais par la façade de /api/chat/mode et /api/chat/criteria et les
critères rendus par l'outil disparaissent : le Go pousse ses événements
lui-même, une seule source quel que soit le chemin.
README : LOKI_CHAT=rust et ce qu'il ne prend pas encore en charge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Avec LOKI_CHAT=rust, la façade sert les routes du chat. Deux chemins
arrivent pourtant au Go sans passer par elle — le tunnel du relais et sa
redistribution /api/e2e/req — et liraient la conversation du Go, périmée,
voire l'écriraient en base par-dessus celle de la façade. facade_chat.go :
- routes du chat remises à la façade (après requireWebAuth), avec le jeton
interne qui y tient lieu d'authentification, flux compris ;
- /api/e2e/chat lit le flux de la façade et le rechiffre événement par
événement (nombres repris tels quels) ;
- les événements d'affichage nés ici (bascule du mode Code, critères)
vont au journal de la façade (/internal/facade/v1/delta) ;
- l'export lit la discussion enregistrée ;
- le déverrouillage de la mémoire fait recharger la façade.
L'outil d'exécution ne rend plus les critères : critNotify les pousse lui-
même, d'où qu'il soit appelé. Sans LOKI_CHAT=rust, rien ne change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Avec LOKI_CHAT=rust, la façade charge la discussion active au démarrage
et sert elle-même /api/chat (flux SSE : rejeu puis direct, battement de
cœur toutes les 4 s, trames regroupées), /api/chat/send, stop, reset,
state, compact, tool-result et /api/conversations{,/new,/switch,/rename,
/delete}, avec les réponses des handlers Go. /api/chat/mode et
/api/chat/criteria restent au Go ; la façade relaie puis journalise
l'état rendu, pour que tous les appareils suivent. Pièces jointes,
fichiers, images et export restent au Go, qui suit la discussion active.
Le Go reçoit LOKI_CHAT=rust et l'adresse de la façade (étape suivante :
lui remettre le trafic de chat du tunnel). Sans la variable, rien ne
change : tout part au Go comme avant.
ChatService gagne reset (fichiers effacés par le Go), la compaction
manuelle, le résultat complet d'un outil et le relais d'un événement ;
les critères changés par l'outil criteria sont journalisés pendant le
tour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
/internal/v1/tool/exec rend la liste des critères après un appel de
l'outil criteria qui l'a changée (add, set, clear) : critNotify la pousse
au journal du Go, une conversation tenue par la façade la reçoit ici.
/internal/v1/conv/dropped accepte files_only : Reset vide la discussion
et ses fichiers, sans la supprimer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Port d'EnqueueOrStart, StartTurn, generate, drainQueued,
startQueuedIfAny, dropQueued et compactAndPublish :
- envoi : réessai reconnu (cid), tour préparé par le Go (/turn/start), mis
en file si un tour tourne, sinon moteur prêt (/health), verrou du tour
puis bail du moteur du Go, bulle journalisée et enregistrée tout de suite ;
- tour : compaction de début de tour, vue d'envoi (tête du Go, historique
sans <context_update>, préambule fusionné comme InjectSkills), boucle
dont chaque événement devient un delta du journal, historique rangé
(outils, réponse, raisonnement renvoyé, rappels en porte-à-faux retirés),
contexte estimé si le moteur ne le compte pas, compaction de fin de tour ;
- fin : turn_done, journal coalescé, verrou et bail rendus, discussion
enregistrée, file abandonnée après un stop ou message suivant lancé,
sinon Web Push ;
- stop : le tour en cours, ou le détenteur Go du moteur.
chat::history porte historyFromView, dropStrayNudges, le rappel des pages
mémoire lues, l'assemblage d'InjectSkills et la forme des tool_used du
journal, avec leurs tests Go. ConvState garde ctxUsedLen et genPeak en
mémoire seulement. EngineLease::interrupted ne garde plus de verrou à
travers une attente.
Écarts : boucle de vérification du mode Code, critères pour la
compaction, préchauffage, télémétrie, retry_patterns personnalisés.
Tests de bout en bout : tour complet (outil, réponse, bail rendu,
discussion enregistrée, doublon), message en file injecté entre deux
étapes, moteur tenu par une tâche planifiée (409, stop), moteur pas prêt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Les outils exécutés par le Go lisent la discussion active (dossier de la
discussion, mode Code, critères) : chaque chargement, création ou bascule
la lui annonce (hook activated, /internal/v1/conv/active). Une discussion
supprimée fait nettoyer ses fichiers, travaux, critères, mode et résultats
d'outils (/internal/v1/conv/dropped).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Ce qui décrit un tour reste au Go le temps d'être porté (lot 7) ; la
façade le demande ici :
- /turn/start : pièces jointes et vision (userMessageContent), capacités
demandées et celles du tour (rôle du mode Code figé), nom du modèle,
puce « mode Code » (une fois par discussion) ;
- /turn/view : la tête de turnView sans PROJ_SNAPSHOT (prompt du preset,
consignes du dépôt, contexte du projet), le préambule d'InjectSkills et
les outils du tour, MCP compris ; 409 sous PROJ_SNAPSHOT ;
- /turn/done : notification Web Push de fin de tour ;
- /conv/active : la discussion active des outils suit la façade ;
- /conv/dropped : fichiers, travaux, critères, mode et résultats d'outils
d'une discussion supprimée.
injectPrefix et codeHintDue sortent d'InjectSkills et de maybeCodeHint,
sans changement de comportement.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
GoTurnHost est le TurnHost de la façade : envoi par EngineHttp (clé de
l'endpoint, pannes classées comme friendlyLLMError), outils par l'API du Go
avec leurs suites (capture, aperçu, image relayée), résultats complets
confiés à l'écrivain des discussions (id « <discussion>.<aléa> »),
compaction par Compactor avec un résumeur qui interroge le moteur
(EngineSummarizer) et l'archive recall du Go (StoreRecall), images jointes
lues sur disque et ouvertes par le Go, en cache borné à 64 Mio.
RecallArchive devient asynchrone ; la boucle consulte la mémoire des
refus de compaction avant d'annoncer une compaction en cours de tour.
Écarts : pas de résumé en continuation (état du slot tenu par le Go), ni
conseil de cache, ni garde d'écriture du mode Code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
/internal/v1/tool/exec rend aussi ce que la boucle Go tire du résultat
après l'exécution, et qui demande le disque ou le navigateur
(toolStepExtras) : capture de web_screenshot pour la bulle, aperçu du
navigateur après une action qui change la page (jamais après une erreur),
message qui relaie l'image au modèle (capture avec la vision, see_image).
/internal/v1/recall/archive range un bloc verbatim pour l'outil recall :
son numéro vient de la séquence bbolt, seul ce process peut le donner. 423
si la mémoire est verrouillée, rien d'écrit en clair.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Port de runChatLoop : complétions enchaînées jusqu'à la réponse, sur les
modules déjà portés (charge utile, flux, compactage). Les filets du Go y
sont : reprises réseau et statuts passagers, flux externe coupé (rejoué ou
poursuivi après le texte affiché), intensité refusée par le gabarit,
raisonnement renvoyé refusé ou trop long, compaction réactive puis
réduction forcée, 500 (tool_choice none puis outils retirés), réponse vide
(rejeu si fenêtre pleine, relances « pensé sans agir »), appel écrit en
texte, budget souple, appels identiques court-circuités, arguments
illisibles jamais exécutés, pré-vol du mode Code, compaction en cours de
tour, file d'attente injectée entre deux étapes.
Aucune entrée-sortie dans la boucle : envoi, outils, compaction, résultats
complets et mémoires des refus passent par TurnHost ; l'arrêt par Cancel.
Non portés (clés opt-in ou machinerie du moteur local restée au Go) :
KEEP_TURN_IMAGES, PROJ_SNAPSHOT, NUDGE_IN_TOOL, SIDE_SLOT, SLOT_PERSIST,
préchauffage, sonde de gabarit, télémétrie.
15 tests sur un faux hôte aux réponses scriptées (temps suspendu pour les
reprises).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Port de llm_budget.go (palier AGENT_BUDGET, rappels qui reviennent à
chaque palier), appendNudge, steerSystem, dedupableTool,
repeatedCallResult et toolResultPreview : ce qui manquait à la boucle
d'un tour en plus des modules déjà portés.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Port des opérations de chat_sessions.go et de LoadConversation :
discussion active créée au besoin (reprise du fil unique d'avant les
discussions, clé d'origine gardée), chargement au démarrage qui réessaie et
n'écrit rien sur une base illisible, liste du projet actif, création,
bascule (la courante enregistrée sous SON identifiant), renommage,
suppression (l'écriture en vol ne la fait pas renaître ; bascule sur la plus
récente restante ou une neuve : toujours une discussion active).
Les opérations sont sérialisées ; l'index partage son verrou avec
l'écrivain (StoreSink::index_lock, convIndexMu en Go). Fichiers, travaux,
critères, mode, résultats d'outils d'une discussion supprimée et
préchauffage après bascule passent par SessionHooks. Store gagne
get_open / put_seal (getStoreBytesErr / putStoreBytes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Port de l'état de Conversation et de subscribeSink : état persisté et
discussion chargée, epoch (reset, bascule), verrou du tour et son arrêt,
ajout au journal avec rejet des deltas d'un tour périmé, coalescence de fin
de tour, instantané numéroté sous le même verrou, /api/chat/state.
Subscription rend les lots à émettre : rejeu (padding, reset si le curseur
vient d'une autre discussion, pagination, caught_up, contexte), puis le
direct, avec {reset} et l'en-tête paginé quand l'epoch change. Réveil par
compteur de version marqué lu avant la relecture de l'état : aucun réveil
perdu.
Tests Go de l'abonnement portés.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
EngineLease prend le verrou de génération du Go, le renouvelle au tiers de
sa durée en tâche de fond et signale au tour un stop passé par le Go ou la
perte du bail (Reset, bascule, expiration). Rendu par release, ou en tâche
de fond s'il est lâché. engine_stop arrête un détenteur Go (tâche, bench,
optimiseur). Store::internal_post garde le statut HTTP pour distinguer les
refus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Le verrou de génération (conv.Generating) est partagé par le chat, les
tâches planifiées, le bench et l'optimiseur. Une conversation tenue par la
façade doit prendre ce verrou-là : sinon une tâche partirait vers le même
moteur en plein tour.
/internal/v1/engine/lease prend le verrou (409 avec la raison du refus et
la tâche en cours), /renew le prolonge et remonte un stop passé par le Go,
/release le rend (seulement s'il le tient encore), /engine/stop arrête un
détenteur Go. Sans renouvellement le bail expire : une façade tuée en plein
tour ne garde pas le moteur. Reset et les bascules de discussion rompent le
bail comme ils libèrent un tour Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
Port de chat_persist.go et de la partie index de chat_sessions.go, au même
format en base que le Go (chat/conv:<id>, chat/index, toolres/<clé>, tous
scellés par l'API interne) :
- ConvState : JSON de Conversation à l'octet près (tranches vides en null,
bloc projet omis), relecture tolérante comme json.Unmarshal et guérison
stripImageParts ; instantané en plein tour avec le journal compacté ;
- index : ConvMeta, titre déduit (rappels de loki et <context_update>
exclus, 60 caractères), tri, projets, renommage, rafraîchissement d'une
entrée ;
- écrivain unique (Persister) : lots fusionnés, jamais un état plus ancien
qu'un état écrit, résultats d'outils servis depuis la mémoire jusqu'à
l'écriture, suppressions qui ne font rien renaître, file jamais bloquée
par un écrivain qui panique ;
- StoreSink : écriture par l'API interne du Go. Mémoire verrouillée : rien
en clair ; index illisible : jamais réécrit.
Tests Go de l'écrivain portés, plus des tests de bout en bout contre le
faux Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
La partie « protocole » de runChatLoop devient une machine à états pure et
incrémentale (StreamParser) : lignes data:, décodage strict de streamChunk
façon encoding/json (un champ mal typé saute le chunk), télémétrie tolérante
(perfWire/applyPerf), timings et usage, accumulation des tool_calls par
index, découpage </think> côté client, frappe en direct (argPreview,
bodyTail), détection des appels d'outils écrits en texte (motifs embarqués
codés à la main, fenêtre retryScanStart), pré-vol du mode Code, stats de
secours sans timings. Plus toolCallLabel, repairToolCall, l'assemblage des
appels (badArgs, ids de secours) et l'encodage json.Marshal de Go.
L'état global Go (REASONING, preset externe, REASONING_ECHO, retry_patterns,
noteEnginePrompt, codeWriteGuard, tReq) devient des paramètres explicites
(StreamConfig). HTTP, reprises, compactage et exécution restent à la boucle.
Tests Go portés un pour un, et les flux enregistrés des tests de bout en
bout rejoués sur le parseur.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le port du compactage et celui de la charge utile avaient chacun leur
version d'echoPolicy / echoTokens / echoable / hasBody. Une seule reste,
celle de wire.rs ; les 111 tests du chat passent inchangés.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port fidèle de chat_compact.go et chat_compact_cont.go dans
chat/compact.rs : estimation des jetons (raisonnement renvoyé compris),
seuils et garde de marge de génération, bornes tête/torse/queue, archivage
recall, torse dégraissé, résumé sur transcription ou en continuation,
demande en cours réinjectée, garantie de réduction de 20 %, pré-contrôle,
mémoire des refus et des échecs de la continuation, filet shrinkToFit.
Prompts et marqueurs repris à l'octet près.
L'état global Go devient explicite : CompactConfig (configuration, fenêtre,
modèle, politique d'écho), trait Summarizer (appel au moteur, tampon du
slot, appel d'outil écrit en texte), trait RecallArchive, CompactMemory,
critères du mode Code en paramètre, vue d'envoi du tour fournie par
fermeture. Tests Go portés un pour un avec un faux moteur.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port fidèle du côté envoi du client de chat : wireMessages et ses étapes
(normalisation du système, contexte projet en tête du premier message
utilisateur, retrait des marques img_relay/ctx_upd, consigne de fin quand
les outils sont neutralisés), le point de sortie REASONING_ECHO
(politique, écho, comptage, mémoire des refus, erreur de gabarit), le
repli de reasoning_effort (llm_effort.go) et turnReasoning,
l'échantillonnage du preset, buildChatPayload, chatEndpoint et
completionsURL.
L'état global Go devient paramètre : configuration, clé locale, mémoire
des traductions d'intensité et des refus d'écho, verdict de la sonde de
gabarit, lecture des images rangées par référence. Aucune copie de
l'historique quand rien ne change (Cow), comme les tranches Go.
Tests Go portés un pour un (normalisation, consigne de fin, écho,
intensité, échantillonnage, endpoint, images, corps identique à
l'ancien assemblage).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port fidèle de la partie déterministe de chat_conversation.go et des trames
SSE de web_chat.go : ajout au journal avec plafond de 200 000 événements,
compaction de fin de tour (compactLog : blocs texte fusionnés avec toks, ts0
et seq0, seul le done=true des outils gardé, aperçu navigateur retiré),
rejeu coalescé (coalesceReplay, replace quand le client a vu le début du
bloc), pagination (historyCut, historyHead, 20 échanges par défaut), plan
d'abonnement (padding 2048 / 16384, reset de discussion périmée, caught_up,
ctx_used), direct par dichotomie sur le Seq (decorateEvent), et trames SSE
avec l'échappement HTML de json.Marshal et le regroupement 128 Kio / 256
événements.
Ni goroutine, ni verrou, ni base : la discussion active, le contexte utilisé
et l'horloge arrivent en paramètres, pour bâtir l'acteur de conversation
dessus. Les tests Go correspondants sont portés un pour un.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sceller / ouvrir un blob par le Go (mémoire verrouillée : erreur dédiée,
rien d'écrit en clair) et faire exécuter un appel d'outil par sa boucle
— les deux ponts dont la conversation Rust aura besoin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Les discussions sont chiffrées en base quand la mémoire l'est, avec une
clé de données qui ne vit qu'en mémoire du Go après déverrouillage. Une
conversation tenue par la façade fera sceller et ouvrir ses blobs ici :
la clé ne sort jamais. Mémoire verrouillée : 423, jamais d'écriture en
clair ni de perte — l'appelant attend le déverrouillage, comme le Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Envoi de la requête de complétion (clé de CET endpoint seulement, jamais
celle du serveur local vers une API externe), découpe du flux en lignes
comme bufio.Scanner (8 Mio au plus par ligne, arrêt net au-delà), et les
primitives de reprise de llm_retry_net.go : statuts rejouables, délai
800 ms doublé et plafonné à 5 s, classement des pannes réseau et messages
de friendlyLLMError / streamCutError à l'identique. La décision de rejouer
reste à la boucle.
HTTPS pour les presets externes : reqwest + rustls (ring), derrière une
fonctionnalité Cargo « tls » activée par défaut — le contrôle croisé Linux
sous Windows s'en passe, faute d'en-têtes libc Linux. L'étape Alpine de
l'image installe gcc pour ring.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Message, ToolCall, Tool, événements de flux, statistiques, journal :
sérialisation JSON identique aux types Go (omitempty, contenu any). Socle
commun des ports parallèles qui suivent (charge utile, flux, compactage,
journal).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lot 6, étape 1, sans changement de comportement : le switch des outils de
la boucle de chat (une cinquantaine d'outils) devient execTool, extrait
tel quel ; seule la question « ask », qui émettait un événement, le rend
désormais à l'appelant qui l'émet au même moment.
C'est la frontière par laquelle une boucle de chat écrite en Rust fera
exécuter ses outils par le Go tant qu'ils ne seront pas portés : API
interne /internal/v1/tool/exec, même fonction, donc mêmes garde-fous
(commande dangereuse refusée avant exécution, cible distante, écriture
hors du dossier de travail) ; la coupure de la connexion interrompt la
commande comme le bouton stop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
L'en-tête de l'interface interroge /api/status en boucle : il est
désormais assemblé par la façade, à partir des aides portées (état du
service par le fichier PID, sonde /health, preset externe, cause d'un
échec de chargement lue dans le journal, pourcentage de chargement, preset
actif, alertes de volume et de FUSE). Sur une base réelle, sa réponse est
octet pour octet celle du Go.
Journal du moteur, emplacements et nombre de couches du modèle suivent.
L'état du moteur s'appuie sur la résolution des modèles de gguf.rs plutôt
que sur sa propre copie.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gguf.rs reprend backend_gguf.go à l'identique : en-tête, clés et table des
tenseurs lus sous les mêmes bornes (chaînes, compteurs, profondeur des
tableaux, alignement, premier octet du dernier tenseur), v1/v2/v3 et
gros-boutiste, modèles en tranches (shardFamily porté avec), cache par
empreinte taille/date, mêmes messages d'erreur. Un panic éventuel reste
rattrapé en « GGUF illisible ». Tous les tests Go sont portés un pour un,
avec le même constructeur de GGUF synthétique.
Pour /api/model/layers : resolveModelPath / resolveServeModelPath et leurs
dossiers de modèles (models/, state/model_dirs, LOKI_MODEL_DIRS), la liste
stockée étant passée par l'appelant qui la lit via le store ; le corps JSON
de handleModelLayers est reproduit tel quel.
paths.rs sert /api/paths avec le même JSON que lokiPaths. Le « binaire en
cours » reste celui du serveur Go supervisé, passé par l'appelant, comme le
projet actif (state/active_project) et LOKI_WORKSPACE. Les équivalents de
filepath.Clean/Join/Dir/Abs/IsAbs garantissent les mêmes chaînes que le Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Les aides qui nourrissent /api/status et /api/service/log passent dans
loki-rs/src/engstatus.rs, à l'identique du Go :
- model_load_error_from / is_backtrace_frame (modelLoadErrorFrom) : même
ordre des cas, même sensibilité à la casse cas par cas, libellés
identiques à l'octet près ;
- service_log_tail : la fin du journal du moteur en conteneur ;
- engine_load_pct (Linux, -1 ailleurs) : /proc/<pid>/io et VmRSS rapportés
à la taille du modèle, tranches comprises ; la résolution de MODEL= est
portée localement (models/, $LOKI_MODEL_DIRS, et les dossiers de la base
via engine_load_pct_dirs) en attendant crate::paths ;
- llm_port, is_external, health_check (sonde /health sur localhost, 3 s,
prêt seulement sur un 200, preset externe toujours prêt).
Tests Go repris un pour un (LOAD_GUARD, Strata, OOM avant la pile
d'appels, preset externe, tranches), plus une sonde contre un serveur
hyper local : 200, 503, port fermé, connexion muette.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RAM lue dans /proc/meminfo, jauges des cartes par un seul nvidia-smi
partagé entre tous les onglets (cache de deux secondes, une minute quand
nvidia-smi est absent), comme le Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le module sysinfo de loki-rs reprend, à l'identique, ce dont les routes
/api/ram, /api/vram et le bandeau de /api/status ont besoin côté Go.
La RAM se lit dans /proc/meminfo (MemTotal moins MemAvailable, en Mo) ;
hors Linux la façade rend (0, 0) et l'interface masque le bloc. Les jauges
GPU interrogent nvidia-smi, bornées à dix secondes, et passent par un cache
partagé : deux secondes de validité, une minute quand le binaire est
introuvable, et un verrou tenu pendant la requête pour que les onglets
simultanés attendent la même lecture au lieu d'en lancer chacun une. La
structure GpuStat sérialise name, used, total, util et temp, les noms que
handleVram envoie déjà.
L'alerte « /data n'est PAS un volume monté » et le conseil fuse.shfs
d'Unraid gardent leurs textes, leur règle du montage le plus long et leur
cache d'une minute. Les dossiers de modèles venant de la base, le conseil
reçoit leur lecture en fermeture asynchrone, appelée seulement quand le
cache expire.
Les tests Go de sys_storagefuse_test.go et la partie cache de
web_vram_test.go sont portés un pour un ; le test hors conteneur, qui
touche à l'environnement, vit seul dans tests/sysinfo.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Même empreinte (vecteurs officiels et empreinte du Go toujours vérifiés par
les tests), cinquante lignes de cryptographie en moins à maintenir.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Portage fidèle de ListPresets, configFingerprint et activePresetID
(backend_presets.go, web_sysprompt.go) dans loki-rs/src/presets.rs, pour
que /api/status puis GET /api/presets puissent se servir côté façade.
Le Go lisait lui-même LOKI_HOME, la configuration active et l'ordre choisi
par glisser-déposer ; ici tout arrive en paramètres : home, la config
(Store::config) et l'ordre (seau state, clé preset_order, décodé par
parse_preset_order comme le faisait getJSON). Le reste suit le Go à la
lettre : création du dossier presets, fichiers illisibles sautés, tri par
nom puis ordre stocké, clés « appareil » exclues de l'empreinte.
L'empreinte reste un SHA-1 identique octet pour octet à celle du Go, car
elle est aussi stockée ailleurs (réglage auto, résultats de tune). Faute
de crate SHA-1 disponible, une implémentation directe, vérifiée sur les
vecteurs officiels et sur une empreinte calculée côté Go.
presetCtx (pastille de contexte de /api/presets) vient avec, ainsi que
les tests Go correspondants : empreinte insensible à la mise en forme,
ordre des presets, tailles de contexte, preset actif après bascule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La surface compatible OpenAI ne fait plus le détour par le Go : la façade
authentifie avec la clé des complétions (préflight CORS exempté), réécrit
le corps vers le second slot quand il est en service (id_slot à 1, valeurs
gardées telles quelles), retire les X-Forwarded-* et relaie au moteur sans
tampon — Host d'origine, sauf pour Strata.
Les décisions restent au Go, qui tient l'état de la conversation : la
façade lui demande où router, ouvre un ticket « en vol » avant de relayer
(refus pendant un benchmark ou une optimisation, avec Retry-After) et le
rend quand le flux se termine, que le client parte ou que le moteur soit
injoignable. Sans ticket obtenu, la requête est refusée : le Go pourrait
sinon effacer le slot en plein calcul.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La façade Rust va relayer /v1 directement au moteur. Les décisions restent
ici, parce qu'elles dépendent de l'état de la conversation que ce process
tient en mémoire : route (port, Strata, clé des complétions, second slot
en service), begin (requête « en vol » : le slot de la discussion n'est
pas effacé pendant ce temps, préchauffage annulé si besoin ; refus pendant
un benchmark ou une optimisation ; trace de perte de cache), end.
Mêmes étapes, même ordre qu'oaiHandler. Un ticket jamais rendu (façade
tuée en plein flux) expire au bout de six heures, sans quoi le compteur
de requêtes en vol resterait bloqué.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deux routes de pilotage de plus en Rust, lues dans la base à travers le
Go. La configuration sort clés triées, comme json.Encoder le fait d'une
map Go. Faux serveur Go de test à store en mémoire, réutilisable par les
lots suivants.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le Go expose sa base à la façade par l'API interne : lecture, écriture,
suppression d'une clé et lecture d'un bucket entier. Il reste l'unique
propriétaire du fichier bbolt ; les écritures passent par putBytes/update
et invalident son cache comme une écriture locale. Base illisible : 503,
pour que Rust distingue « rien » de « pas pu lire ».
Côté Rust : client du store (seul point de passage, que la migration
finale de la base ne touchera qu'ici), routeur /api avec contexte partagé
et garde d'authentification réutilisable ; /api/ping y emménage. Modules
réservés aux aides portées ensuite (système, état du moteur, presets,
chemins, GGUF).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
En conteneur, start/stop/restart/status du moteur passent par
`loki-rs engine <action>`, port à l'identique de la supervision Go :
`loki serve` lancé chef de session, fichier PID et journal aux mêmes
chemins (serviceIsActive, l'UI et `loki logs` lisent les mêmes fichiers),
contrôle à 2 s avec la fin du journal en cas d'échec, arrêt de tout
l'arbre — session, groupe et filiation, pour le moteur de Strata qui prend
sa propre session — avec les mêmes délais (SIGTERM, 4 s, SIGKILL, 20 s).
Le Go garde ses garde-fous (optimisation en cours, preset externe) puis
délègue au loki-rs voisin de son binaire ; la dernière ligne d'erreur du
superviseur devient l'erreur montrée par l'UI. LOKI_ENGINE_SUPERVISOR=go
revient à l'ancienne supervision. `loki serve`, qui construit la ligne de
commande du moteur, ne change pas : il aura son propre lot.
Le contrôle « démarré ? » regarde si l'enfant est sorti plutôt que le
signal 0 : un enfant mort reste un zombie tant que son parent vit, et un
zombie répond encore au signal 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
L'interface (index, marked, service worker, manifeste, polices) est
désormais servie par loki-rs, embarquée par build.rs depuis la même source
que le go:embed — les deux binaires d'un build servent la même UI. Mêmes
en-têtes de cache que le Go, gzip à la première demande puis gardé en
mémoire, jamais sur les polices ni en HEAD.
L'authentification de pilotage est portée à l'identique (requireWebAuth,
crossSiteReject, localHostName, Bearer en temps constant, mêmes messages,
mêmes codes) ; les cas de TestCrossSiteReject sont repris un pour un. La
clé est demandée à l'API interne du Go à chaque requête ; si elle ne
répond pas proprement, la façade ferme (503). /api/ping est la première
route authentifiée servie par Rust.
Le jeton de l'API interne est tiré au hasard à chaque démarrage et passé
au Go par l'environnement. La façade rend 404 sur /internal/* venu de
l'extérieur et efface X-Loki-Internal entrant avant tout relais.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lot 2 de la migration : la façade va authentifier elle-même ses routes
/api/*. La clé de pilotage vit dans la base bbolt, verrouillée par ce
process ; la façade la demande donc à GET /internal/v1/auth, à chaque
requête — un changement de clé prend effet tout de suite, comme avant.
La réponse contient un secret, d'où trois verrous : route branchée
uniquement sur l'écoute TCP directe (le tunnel du relais sert le mux nu),
jeton tiré au hasard par la façade à chaque démarrage et exigé en
temps constant, 404 sans rien révéler sinon. Base illisible : 503, la
façade fermera l'accès plutôt que de l'ouvrir.
Spec du lot 2 jointe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
L'image embarque loki-rs (étape Alpine, binaire musl statique) et
l'entrypoint en fait la porte d'entrée : `loki-rs serve` sur le port
public, le Go derrière lui sur 127.0.0.1:8091. LOKI_FACADE=0 revient à
l'ancien schéma si la façade pose problème.
Au démarrage, le loki-rs de l'image installe la dernière release
« rolling » dans /data/bin/<build>/ ; l'entrypoint prend ensuite loki et
loki-rs du même dossier (which-bin), et un lien dans /usr/local/sbin fait
que `docker exec loki loki …` vise lui aussi la version installée. C'est
toujours le loki-rs de l'image qui met à jour : un binaire publié
défectueux ne peut pas bloquer le correctif suivant.
CI : job Rust (fmt, clippy -D warnings, tests). Publication : la release
« rolling » part à chaque push sur main avec le même numéro de run que
l'image (LOKI_BUILD) ; IMAGE_ABI, source unique, empêche d'installer des
binaires qui exigent une image plus récente. manifest.json est envoyé en
dernier.
Message corrigé quand l'image est plus récente que la release : rien à
tirer dans ce cas.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Premier lot de la migration vers Rust. Le binaire loki-rs :
- serve : prend le port public (8090), lance `loki web 8091` en écoute
locale et lui relaie tout — HTTP, flux SSE trame par trame, WebSocket.
Les en-têtes de bout en bout passent intacts (Host, Authorization,
Origin, Sec-Fetch-*, X-Forwarded-Proto : le Go en tire ses contrôles
d'accès) ; X-Loki-Via entrant est effacé, un client ne doit jamais
pouvoir se faire passer pour le tunnel. Go mort : relancé avec attente
croissante, 502 lisible entre-temps.
- self-update : lit la release « rolling », installe loki et loki-rs dans
$LOKI_HOME/bin/<build>/ après vérification SHA-256, bascule par
réécriture atomique de release.json. Les versions s'ordonnent par numéro
de run CI ; une ABI d'image différente n'est jamais installée. Toute
erreur laisse l'état précédent, le démarrage n'est jamais bloqué.
Le HTTPS passe par curl (présent dans l'image) : pas de pile TLS ici.
- which-bin : dossier d'où l'entrypoint prend les deux binaires, toujours
ensemble.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La façade Rust du conteneur prend le port public et lance `loki web`
derrière elle : le Go doit alors n'écouter qu'en local, sinon il resterait
joignable en direct sans passer par la façade. Défaut inchangé (0.0.0.0).
Plan du lot 1 de la migration Rust joint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La réécriture en Rust se fait par lots, Rust en production dès le premier :
une façade loki-rs prend le port 8090 et proxifie vers le Go, qui passe en
écoute locale sur 8091. Chaque lot suivant lui retire des routes.
Le même lot fait que le conteneur prend la dernière version au démarrage :
release GitHub « rolling » publiée à chaque push sur main, binaires vérifiés
par SHA256SUMS et posés dans /data/bin, garde-fou d'ABI d'image, jamais de
boot bloqué.
La façade efface X-Loki-Via à l'entrée : sur l'écoute directe, l'en-tête
pouvait être forgé pour contourner le contrôle d'hôte anti-rebinding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- strataDetect mesure l'espace libre de /data/strata (et non /data) : un SSD
monté à cet endroit est celui qui compte pour l'installation.
- L'installeur reçoit XDG_CACHE_HOME=/data/strata/cache : les caches pip et
Hugging Face ne remplissent plus /root, c.-à-d. l'image Docker d'Unraid.
- docker-compose.unraid.yml + README : monter un pool SSD en /data/strata.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XLbFcZj6mJjDpWvCpcChGA
Choisir le modèle lui-même comme brouillon dans l'éditeur de preset
écrivait MODEL_DRAFT=<le modèle>, et Loki passait -md vers ce fichier :
common_speculative_init_result charge alors un second exemplaire complet
du modèle, au lieu de greffer le contexte MTP sur le modèle déjà en VRAM.
Swift 27B : « allocating 9628.34 MiB on device 0 » pour le brouillon,
moteur mort ; quand ça passait, couches renvoyées sur le CPU et 5 t/s au
lieu de 22 (prefill 290 t/s au lieu de 890).
probeSpec reconnaît ce cas (os.SameFile), specArgs l'ignore et le dit :
la tête MTP intégrée sert de brouillon sans -md. L'éditeur ne propose
plus le modèle principal dans la liste des brouillons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wfx8uW1un1WwWJ6xzAbkV
En conteneur, userSvcStop n'attendait que le processus `loki serve`. Avec
Strata, le moteur qui tient la VRAM (cache d'experts, ~9 Go) est un autre
processus qui finit de s'arrêter après le serveur Python : la relance
suivante démarrait llama-server sur une carte encore pleine (« cudaMalloc
failed: out of memory » à l'encodeur de vision) et Strata mesurait la RAM
disponible avec l'ancien moteur encore chargé (mode mmap choisi à tort).
stopProcTree relève l'arbre (session, groupe et descendants) avant tout
signal, SIGTERM puis SIGKILL aux survivants, et ne revient qu'une fois
tout parti.
Diagnostic : une pile d'appels citant load_model ne passe plus pour une
nouvelle tentative (la cause mémoire était perdue), et le chat donne
l'échec de chargement au lieu de « il démarre, réessaie ».
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wfx8uW1un1WwWJ6xzAbkV
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>
Repris d'AJEAN 0.17.6. La discussion est enregistrée au début du tour
(StartTurn persiste), mais la liste ne se rafraîchissait qu'à la fin de
la réponse. Elle se recharge maintenant à l'arrivée du message, groupée
quand plusieurs événements se suivent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 0.17.5.
- task_create accepte in_minutes (dans N minutes) ou at (« HH:MM », prochaine
occurrence, ou « AAAA-MM-JJ HH:MM ») pour une tâche unique, calculée côté
serveur : le modèle n'a plus à écrire un cron ni à jongler avec les
fuseaux pour un simple rappel. Elle s'écrit « @once AAAA-MM-JJ HH:MM » et
se désactive après son passage ; manquée pendant un arrêt, elle part au
redémarrage.
- Le navigateur envoie son fuseau (Europe/Paris…) avec chaque message ; il
est retenu et sert aux tâches que l'IA planifie. Le conteneur tourne en
UTC : « à 17h » tombait deux heures à côté. La réponse de task_create
nomme le fuseau utilisé.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 0.17.5. Sur une fenêtre de 65k, un tour qui lit plusieurs
gros fichiers d'un coup passait de 60 % à plus de 130 % sans jamais
compacter, et l'erreur remontait telle quelle.
- Le test de compactage en cours de tour compte aussi les résultats
d'outils de l'étape, que le moteur n'a pas encore vus. Sans usage côté
serveur, l'estimation porte toujours sur tout l'historique.
- Tout résultat d'outil est borné à 30 000 caractères dans la vue du
modèle, avec une mention qui l'invite à cibler. L'interface garde le
résultat complet (« voir plus »).
- Dernier recours quand le moteur refuse encore le prompt et que le
compactage n'a rien pu faire : shrinkToFit tronque les gros résultats
d'outils puis retire les plus vieux échanges, vers 60 % de la fenêtre
corrigés par la taille réelle lue dans l'erreur. Deux essais au plus.
- Tâches planifiées : la note de tâche passe en tête du message utilisateur
au lieu du système. Le préfixe reste celui du chat et le cache de prompt
de llama-server sert aux deux (l'amont mesurait 12 s de recalcul pour un
« salut » après une tâche).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 0.17.4.
- Compactage : la dernière demande du torse n'est plus réinjectée quand la
queue contient déjà un message utilisateur (le modèle répondait une
seconde fois à une vieille question), ni quand le résumé a échoué (elle
est déjà dans le torse dégraissé et réapparaissait après ses propres
résultats d'outils).
- Le texte écrit avant un appel d'outil n'est plus rangé deux fois dans
l'historique du modèle (message tool_calls ET réponse finale) : du
contexte gaspillé à chaque tour d'outil.
- Deux appareils qui envoient en même temps : le second message part en
file au lieu d'un 409. Une tâche planifiée garde le refus.
- Un raisonnement qui reprend après du texte de réponse ouvre une nouvelle
bulle sous la réponse au lieu de remplir l'ancienne, repliée au-dessus.
- Bascule de preset par identifiant : le numéro seul visait un autre
preset quand la liste avait bougé sur un autre appareil. Le numéro reste
accepté.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 0.16.4 et 0.17.4.
- Flux coupé en cours de réponse vers une API externe (Wi-Fi, VPN, proxy
qui décroche) : le tour reprend au lieu d'être abandonné, jusqu'à 5 fois
avec attente croissante. Rien d'affiché : la requête est rejouée (le
raisonnement montré est retiré). Du texte affiché : il est rendu au
modèle avec « continue là où tu t'es arrêté », et la suite s'ajoute à
l'écran. Un outil à moitié écrit est clos puis réémis. Jamais pour le
llama-server local : coupé, il a planté. Une coupure après le dernier
chunk (finish_reason reçu) garde la réponse telle quelle.
- Appels d'outils parallèles : chaque morceau est rangé selon son champ
index, plus selon sa position dans le chunk. Un serveur qui envoie un
appel par chunk les fusionnait (noms écrasés, JSON « {…}{…} »).
- Outils coupés et « réponds maintenant » seulement sur un 500 (appel mal
formé). Un 401, 429 ou 502 d'une API externe coupait les outils et
rejouait le tour, masquant la vraie cause.
- Débit de décodage mesuré côté Loki quand le serveur n'envoie pas de
timings ; le débit de lecture, inconnu, n'est plus affiché à 0.
- Windows : la connexion refusée (WSA 10061) est reconnue comme telle par
la reprise réseau (TestConnexionRefuseeEstRejouee échouait sous Windows).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
La reprise faite en parallèle sur l'autre branche (b667fd4, jamais poussée)
couvrait des trous de celle-ci. Ses ajouts, posés sur la version en place :
- chat_template_kwargs, propre à llama.cpp, ne part plus vers une API
distante — ni au chat ni au résumé de compactage. Une API stricte
(OpenAI) répond 400 à un argument inconnu : toute compaction échouait.
- Vision : case « le modèle accepte les images » (EXTERNAL_VISION) dans la
fenêtre API externe. Elle remplace, en externe, MMPROJ et la sonde
/props : un modèle distant multimodal ne pouvait jamais recevoir d'image.
- Le benchmark refuse de tourner sur un preset externe : healthCheck le dit
prêt sans moteur, et la mesure tapait un port arrêté ou un moteur resté
en vie, attribuée à tort à ce preset.
- Le badge de modèle des réponses porte le nom du modèle distant.
- Éditer le preset en service l'applique aussitôt (SavePresetApplying).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 0.14.0, adapté.
AJOUT EN COURS DE RÉPONSE
Il fallait arrêter la génération pour glisser une précision (le serveur
répondait 409). Un message envoyé pendant une réponse part désormais EN
FILE : runChat l'injecte à la prochaine frontière d'étape (après un appel
d'outil) — le modèle en tient compte dans la SUITE de sa réponse — ou, si
le tour se termine avant, il devient le tour suivant, dans l'ordre. Le
bouton envoyer réapparaît à côté de stop dès qu'il y a du texte ; le
message s'affiche « en attente » au-dessus de la carte jusqu'à ce que le
flux le confirme. Stop abandonne la file (queue_dropped, le client retire
ses pastilles).
Écarts avec l'amont :
- Les messages en attente sont posés AU-DESSUS de la carte, pas dans le
fil qui s'écrit encore (ils s'y seraient intercalés entre deux bulles).
- Dédoublonnage par identifiant d'envoi (cid) : l'UI réessaie un envoi dont
la réponse s'est perdue sur le tunnel. Le 409 « déjà en cours » faisait
office de garde ; sans lui, le réessai mettait le message deux fois en
file.
- Une tâche planifiée qui occupe le modèle garde le refus 409 (sa fin ne
dépile rien).
- Pas d'injection après un stop : la boucle repassait en tête une fois
l'outil interrompu et journalisait le message en file (vu en test).
- runChat prend l'injecteur en paramètre variadique : les appels hors chat
(tâches, vérification du mode Code, tests) ne changent pas.
DEUX FILS FUSIONNÉS
Un appareil déconnecté pendant qu'un autre changeait de discussion se
réabonnait avec un `from` hérité de l'ancienne : les Seq n'étant pas
globaux, la fin de la nouvelle discussion se greffait sur le début de
l'ancienne restée à l'écran. caught_up et reset portent maintenant l'id de
la discussion ; le client le renvoie (conv_id) et, s'il ne correspond plus,
le serveur ordonne un reset avant de rejouer. La garde « from au-delà du
dernier Seq » émet elle aussi ce reset (elle rejouait par-dessus l'écran
sans le vider).
Vérifié dans le navigateur avec un faux moteur : précision injectée après
l'outil dans le même tour, message en file devenu tour suivant, stop qui
abandonne la file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Les deux reprises d'AJEAN arrivées en parallèle du chiffrement écrivaient
en clair à côté de discussions chiffrées.
- Résultats complets des outils (« voir plus ») : bucket toolres ajouté à
encryptedBuckets, écrits et relus par putStoreBytes / getStoreBytesErr.
Mémoire verrouillée : rien n'est écrit, le flux porte le résultat entier.
Le nettoyage des orphelins ne tourne plus quand la mémoire est
verrouillée : l'index des discussions revenait vide et TOUT passait pour
orphelin.
- Images du fil (chatimg/) : même enveloppe que les pages mémoire ;
verrouillée, l'image reste en base64 dans le message. Elles n'étaient
jamais effacées : comme un fil rechargé perd ses images (stripImageParts),
celles de plus de 24 h partent au démarrage et à chaque bascule de
discussion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 0.15.1 → 0.15.7 (tool_results.go adapté : pas de
chiffrement chez nous).
VOIR PLUS
Chaque résultat d'outil partait en entier dans le flux (jusqu'à 12 000
caractères) et dans le journal rejoué à chaque ouverture : un long fil
d'agent en transportait des centaines. Le flux ne porte plus qu'un aperçu
de 1 600 caractères, la taille réelle (le « ~N tok » de la bulle reste
juste) et un id. Le résultat complet est rangé dans le bucket `toolres`,
sous « <discussion>.<aléa> » — un id propre, pas le tool_call_id que
certains parseurs recyclent. « voir plus » le charge au clic via
/api/chat/tool-result et lève le plafond de hauteur du bloc ; il se range
avec « copier » dans une barre au coin du résultat. Les résultats partent
avec leur discussion (convDelete) ; les orphelins sont nettoyés toutes les
200 écritures.
COUPES ANNONCÉES
- Terminal : une sortie de plus de 8 000 caractères était coupée en
silence ; le modèle croyait tout voir. Elle commence désormais par
« [sortie tronquée : N caractères au total, seuls les 8000 derniers sont
gardés] ».
- MCP : l'amont transmet désormais la réponse en entier. On s'en approche
sans renoncer au garde-fou (65k de contexte) : le plafond suit la
fenêtre (CTX caractères, ≈ un quart de la fenêtre, jamais sous les
12 000 d'avant), et la coupe dit la taille réelle.
Vérifié dans le navigateur avec un faux moteur qui appelle bash : aperçu
de 1 600 caractères, « voir plus » charge les 8 099, mention de coupe en
tête.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 0.15.7.
HISTORIQUE PAGINÉ
Rouvrir une longue conversation d'agent rejouait TOUT le journal : des Mo
à travers le tunnel, et des milliers de bulles construites avant
d'afficher la moindre chose. Le client annonce désormais `tail` : au
chargement (et à l'ouverture d'une autre discussion), le serveur ne rejoue
que les 20 derniers échanges, précédés de {history_more: N}. Un bouton en
tête du fil redemande le rejeu complet en gardant la position de lecture.
Un client qui n'envoie pas `tail` (app relais…) reçoit tout, comme avant.
Écart avec l'amont : les états que le client garde et que la partie
masquée avait posés — mode Chat/Code, critères du mode Code — sont réémis
à la coupe (historyHead), ainsi que ctx_used à l'ouverture d'une
discussion. Sans ça, une discussion en mode Code rouverte s'affichait en
mode Chat, et la jauge de contexte à zéro.
iOS : LES TAPS PERDUS PENDANT UNE RÉPONSE
iOS annule un appui si la position de défilement change pendant qu'il a
lieu. Le fil se recalant en bas à chaque token, presque chaque tap tombait
dessus — y compris sur stop. Le suivi est suspendu pendant un contact (et
350 ms après), scrollTop n'est plus réécrit quand on est déjà en bas, et
stop agit dès l'appui sur écran tactile (anti-doublon pour le clic qui
suit).
Vérifié dans le navigateur sur 23 échanges : 20 affichés + « afficher les
3 échanges précédents », puis rejeu complet à position de lecture égale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Annule c32fd40. Le replay borné aux 4000 derniers événements coupait au
milieu d'un tour, perdait l'état du mode Code (critères, jauge de contexte)
porté par la partie masquée et s'imposait à tous les clients. Le fenêtrage
par échanges repris d'AJEAN (commit suivant) coupe aux frontières d'échange
et renvoie cet état en tête.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 0.15.6, pour la partie qui nous concerne. Côté serveur, la
liste était déjà légère (l'index ne porte que des métadonnées, aucune
conversation n'est relue) ; c'est le DOM qui coûtait : des centaines de
lignes construites d'un coup à chaque rafraîchissement de la liste.
On en dessine 40, puis la suite quand la ligne « afficher N de plus »
arrive à l'écran (IntersectionObserver, ou clic). La discussion active
reste toujours visible, même au-delà. La recherche filtre toujours la
liste ENTIÈRE, qui reste en mémoire ; changer la requête repart de la
première page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 0.15.7 (chat_images.go et son test), adapté.
Une image jointe (ou une capture de web_screenshot) vivait en base64 dans
les messages de la conversation, et persist() réécrit la conversation
ENTIÈRE à chaque fin de tour : une photo de téléphone, c'étaient des Mo
remis sur disque à chaque réponse. Elle est désormais rangée une fois sous
$LOKI_HOME/chatimg/<empreinte>.<ext> (hors du dossier de travail, que l'IA
peut vider) ; l'historique n'en garde que « loki-img:<nom> ». Juste avant
l'envoi, expandImageRefs remet les octets exacts (cache mémoire borné à
64 Mo) : pour le modèle, rien ne change.
DEUX ÉCARTS AVEC L'AMONT
- Copie à l'écriture : l'amont remplace l'URL en place. Chez nous, persist
peut tourner pendant qu'un runChat sérialise ces mêmes maps (compactage
en vol, bascule de projet) — « concurrent map read and map write », fatal.
Les parties modifiées sont donc recopiées.
- Le rechargement reste éphémère : stripImageParts retire toujours les
images d'une conversation rouverte (garde-fou contre un modèle sans vision
qui lirait le base64 comme du texte). L'amont, lui, les garde ; on
pourra le suivre plus tard si l'on veut.
Pas de migration des archives : une conversation rouverte est allégée par
stripImageParts, comme avant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UN VRAI BUG D'ABORD
normalizeLoadFlags traduisait --mlock seul en « --load-mode mlock ». Or,
pour llama.cpp (common/arg.cpp), « mlock » veut dire PAS de mmap + résident :
le modèle entier montait en RAM au lieu d'être mappé. L'ancien --mlock, lui,
gardait le mmap — son équivalent est « mmap+mlock ». Sur un modèle plus gros
que la RAM (Qwen3.8-Flash-Next, 82 Go pour 64 Go), un preset qui avait
coché « Garder en RAM » tournait donc à l'OOM dès qu'un moteur récent
prenait le relais. Correspondance alignée sur AJEAN 0.16.0 :
--mlock seul → mmap+mlock · --no-mmap → none · les deux → mlock.
DANS LES DEUX SENS
Un moteur ancien (ou un fork) ne connaît pas --load-mode et sort en erreur
si on le lui passe : downgradeLoadMode le retraduit en anciens drapeaux
(dio, inconnu de ces moteurs, est abandonné avec un avertissement).
UN SÉLECTEUR À LA PLACE DE DEUX INTERRUPTEURS
« Garder en RAM » et « Charger tout en mémoire » ne disaient pas leur
combinaison. L'éditeur de preset propose les six modes de llama.cpp (auto,
mmap, none, mlock, mmap+mlock, dio), avec ce que fait chacun en sous-titre,
et écrit la forme moderne. Un preset aux anciens drapeaux s'affiche sur le
bon mode sans modification.
Au passage, q5_1 porte la mention « lent sur CUDA » dans la liste du cache
KV (voir le commit du port occupé : non accéléré en Flash-Attention).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 0.15.7 (web_gzip.go et son test, tels quels).
L'UI (~590 Ko de HTML/JS/CSS en un seul fichier) et les gros JSON
(historique relu, liste des sessions, journal) partaient en clair. gzip
les divise par 4 à 8 — ça se sent sur un téléphone en Wi-Fi ou à travers
le tunnel.
La décision se prend au premier octet, sur le Content-Type posé par le
handler : les flux SSE (chat, complétions /v1 en streaming) et les
binaires passent tels quels — un flux compressé retiendrait ses
événements dans le tampon de gzip. Flush reste transmis pour les réponses
progressives, et Unwrap laisse le WebSocket des postes distants atteindre
le Hijacker d'origine.
Seul l'écouteur local est enveloppé : le tunnel sert le mux nu, comme
avant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 0.13.6. Changer de preset sur le téléphone laissait l'onglet
du PC afficher l'ancien, sélection de la liste comprise, jusqu'à un
rechargement à la main. /api/status (sondé toutes les 5 s) expose l'id du
preset actif ; loadStatus rappelle loadPresets quand il change sans qu'on
y ait touché ici.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 0.13.12.
PAR PROJET
Le mode mémoire était un réglage global de config.env. Il vit désormais
sur le projet (champ mem_mode) : un chantier de code peut couper la
mémoire pendant qu'un projet perso la garde proactive. Un projet sans
réglage propre — tous ceux d'avant — retombe sur l'ancien MEM_MODE global :
rien ne change tant qu'on n'y touche pas. L'API /api/memory et `loki
memory` agissent sur le projet actif ; l'UI le dit, et se recharge déjà au
changement de projet.
AUTO, EN DEUX SAVEURS
- « auto, injectée » (always, le défaut) : l'index des pages est en tête
de conversation. La consigne dit maintenant d'y lire directement la
bonne page ; mem_search ne sert plus qu'à chercher par contenu. Avant,
on injectait l'index ET on exigeait une recherche préalable — un appel
d'outil par tour pour retrouver ce que le modèle avait sous les yeux.
- « auto, recherche » (nouveau) : rien d'injecté, l'IA cherche avant
chaque tâche. Contexte plus léger, démarrage plus rapide.
memProactive regroupe les deux là où seul « proactif » compte.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 0.14.0 (chat_mem_pinned.go et ses tests, tels quels).
Le compactage résume le torse, résultats de mem_read compris. Une page qui
portait TOUTES les règles d'une tâche se retrouvait réduite à trois mots
dans le résumé, et le modèle, après compactage, les oubliait. Le rappel ne
recopie rien : il LISTE les pages lues (bornées aux 24 plus récentes) et
invite à les relire si elles concernent la tâche. Il s'accumule d'un
compactage à l'autre — l'ancien rappel est relu comme source, puisque le
mem_read d'origine a disparu.
Posé seulement en mode agent (sans lui, pas de mem_read pour y donner
suite). Comme le contexte projet, le rappel est sorti du bloc système
commun vers le premier message utilisateur (isProjectSystem) : il est
propre à la conversation et casserait sinon le cache du système partagé.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 0.15.8, adapté : chez nous le contexte projet n'est jamais
persisté, il est injecté à chaque tour sous forme de messages système
préfixés — ce qui rend le déplacement trivial et sans migration.
LE PROBLÈME
Sur un modèle hybride (Qwen3.5 et suivants, couches récurrentes), llama.cpp
ne peut reprendre un prompt déjà calculé qu'à un point de sauvegarde, et il
n'en pose qu'au DÉBUT d'un message utilisateur. La description du projet,
l'index mémoire et l'index des trackers étaient fusionnés dans le bloc
système : deux projets divergeaient AVANT le premier point de reprise, et le
premier message après un changement de projet recalculait tout (l'amont
mesure ~8 s pour 5 000 tokens sur un 27B).
LE CORRECTIF
normalizeSystemMessages sort ces messages (isProjectSystem, par leurs
préfixes) du bloc système et les place, dans un bloc <project_context>, en
tête du PREMIER message utilisateur de la séquence envoyée. Le système
commun reste identique d'un projet à l'autre, donc en cache. L'historique
persisté et l'affichage ne changent pas (copie, entrée non mutée — testé).
InjectSkills ne fusionne plus son préambule DANS un message projet placé en
tête (cas d'un preset sans prompt système) : il y aurait perdu son préfixe
et serait resté dans le bloc commun.
Le gain ne vaut qu'entre projets de même mode mémoire : la consigne mémoire
fait partie du système commun.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 0.13.12 / 0.13.13. Couper le mode agent retirait bien les
outils, la mémoire et le préambule Loki, mais laissait passer deux choses :
- Le prompt système du PRESET et le contexte du projet (description, index
mémoire, trackers). Ils décrivent des outils et une mémoire que ce mode
n'a pas : le modèle, persuadé de pouvoir agir, écrivait des <tool_call>
en clair dans sa réponse. Agent off, generate n'injecte plus rien — le
modèle reçoit les messages nus, comme un llama-server direct.
- Un tool_call que le moteur parvient à parser alors qu'aucun outil n'a été
annoncé (agent off, ou relance sans outils après un 500). Il était exécuté,
répondait « outil inconnu », et la boucle repartait. Il est désormais
ignoré : le modèle répond en texte.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 0.16.3. Les benchmarks sont rangés par id de preset. Un
preset dont on changeait le modèle — ou supprimé puis recréé sous le même
nom — affichait les mesures d'un AUTRE modèle, qui n'avaient rien à voir.
- benchMatchesPreset : un bench n'est affiché que si le modèle mesuré est
celui du preset (le nom de fichier est enregistré avec la mesure depuis
toujours, il ne servait simplement à rien).
- DeletePreset oublie le bench, comme il oubliait déjà le prompt système.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 0.15.5.
COMPACTAGE DE SECOURS
Sur n'importe quel refus du moteur — appel d'outil mal formé, modèle en
cours de chargement, erreur de template — runChat résumait ~75 % de la
conversation avant de rejouer, même quand elle tenait en trois messages.
contextOverflow ne laisse passer que les vrais débordements : libellés de
llama.cpp (« exceeds the available context size ») et des API
OpenAI-compatibles (context_length_exceeded), ou, à défaut de libellé, une
conversation déjà à 90 % de la fenêtre.
ARGUMENTS D'OUTIL ILLISIBLES
Un JSON tronqué était neutralisé en {} (indispensable : le template les
re-parse à chaque requête) PUIS exécuté tel quel, d'où des erreurs
trompeuses (« fichier manquant », « commande vide ») qui envoyaient le
modèle sur une fausse piste. L'appel n'est plus exécuté ; le modèle reçoit
une erreur qui dit exactement quoi renvoyer. Des arguments VIDES restent
valides (outil sans paramètre).
RELANCE SANS OUTILS
La consigne imposait le français. Elle demande désormais la langue de
l'utilisateur.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 0.16.0 (chat_diff.go et son test, repris tels quels : le
fichier n'avait pas bougé chez nous depuis la 0.13.5).
- Un fichier de 500 lignes écrit par l'agent affichait « +500 » pendant la
frappe puis retombait à « +120 » : le diff envoyé à l'UI est plafonné à 120
lignes, et l'UI recomptait sur la version tronquée. lineDiff et addedDiff
rendent désormais les VRAIS totaux, calculés avant la coupe, et ils
voyagent dans le journal (added/removed) — donc survivent au refresh.
Repli sur l'ancien décompte pour les conversations d'avant.
- Une retouche d'une ligne dans un bloc de plus de 400 lignes s'affichait en
remplacement complet (le LCS n'y était pas tenté). Les lignes communes en
tête et en queue sont écartées d'abord : on voit le vrai changement, avec
trois lignes de contexte, et il n'est plus repoussé hors de la fenêtre.
- Un contenu terminé par un saut de ligne ne compte plus une ligne de trop,
côté serveur comme dans la bulle en cours de frappe (bodyLineCount).
Le vérificateur du mode Code (code_verify.go) journalise les mêmes champs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deux reprises d'AJEAN 0.15.9, côté `loki serve`.
PORT OCCUPÉ
Un llama-server orphelin (un stop qui n'a pas tué, un relancement trop
rapide) peut encore tenir le port quand le suivant démarre. Deux moteurs
sur le même port se partagent alors les requêtes au hasard : VRAM saturée,
réponses du mauvais modèle. waitPortFree tente une connexion TCP (fiable
même en SO_REUSEADDR, là où un Listen de test réussirait), laisse 5 s à un
moteur qu'on vient d'arrêter pour libérer, puis refuse avec un message qui
dit quoi faire. argValue lit la DERNIÈRE occurrence de --port/--host, comme
llama-server, puisque EXTRA_ARGS peut les surcharger.
CACHE KV LENT
llama.cpp CUDA, compilé avec ses options par défaut (FA_ALL_QUANTS=OFF),
n'accélère en Flash-Attention que f16, bf16, q8_0 et q4_0 — et seulement à
l'identique pour K et V. C'est le cas du moteur de l'image server-cuda et de
ceux installés par OCI. q5_1 ou un couple mixte q8_0/q4_0 marchent mais sont
reconvertis en f16 à chaque pas. L'amont ne prévient que pour son binaire
précompilé ; chez nous tous les moteurs sont concernés, donc l'avertissement
part toujours dans loki-engine.log.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repris d'AJEAN 0.15.5. Sans clé de pilotage (le défaut), l'API /api était
ouverte à tout ce qui savait joindre le port — y compris une page web
quelconque ouverte dans un navigateur du réseau local. Un POST « simple »
(text/plain) ne déclenche aucune pré-vérification CORS : la page pouvait
changer des réglages et, mode agent actif, faire exécuter des commandes.
requireWebAuth passe désormais par crossSiteReject, AVANT le test de clé :
- Sec-Fetch-Site: cross-site → 403 ;
- Origin présent et différent de l'hôte appelé (ou « null ») → 403. curl,
les scripts et les apps n'envoient pas d'Origin : non concernés ;
- sans clé seulement, l'hôte appelé doit être local (IP, localhost, nom sans
point, .local/.lan/.home…, nom du conteneur) : c'est ce qui coupe le DNS
rebinding, où un domaine malveillant se fait résoudre en IP locale.
DEUX ÉCARTS AVEC L'AMONT, DUS AU CONTENEUR
- LOKI_TRUSTED_HOSTS : derrière un reverse proxy, le nom public n'est ni
local ni celui du conteneur. Plutôt qu'imposer une clé, on peut lister ce
nom. Documenté dans le README.
- Le trafic du tunnel (marqué par withLocalAuth, authentifié par le relais)
est dispensé du contrôle d'hôte : son Host est celui du relais.
À SAVOIR : un accès existant par nom de domaine SANS clé ni
LOKI_TRUSTED_HOSTS est désormais refusé (403, message explicite).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le rattrapage de l'amont (navigateur piloté, chiffrement de la mémoire,
notifications, tâches script, sous-agents, anglais) était parti sur main
sous le numéro 0.12.3 : huit fonctionnalités sous un numéro de correctif.
Bump des trois porteurs de version — la constante Go, versioninfo.json et
les .syso Windows régénérés (go generate) — et notes de release réécrites
pour 0.13.0. Sans ça, `loki update` et le bandeau de mise à jour comparaient
une version qui ne bougeait pas.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6CAgoLJzufeA8rZTSpSpY
Reprise de l'idée des sous-agents d'OpenFox, sur la mécanique déjà en place
pour la passe de vérification : un runChat isolé, non persisté.
L'outil `subagent` délègue une question bornée à un rôle qui travaille dans
SON propre contexte et ne rend que sa réponse. Sur un modèle local, c'est la
fenêtre de contexte qu'on sauve : « trouve où est géré le cache » coûte dix
lectures de fichiers qui restaient ensuite dans l'historique jusqu'à la
compaction, alors que seule la réponse comptait.
Les rôles explorer et code-reviewer, jusqu'ici définis mais jamais appelés,
deviennent utilisables. Tous les rôles délégués sont en LECTURE SEULE : pas
de write/edit (ce qui modifie le dépôt reste dans le fil principal, sous les
yeux de l'utilisateur), pas de subagent (aucune récursion), pas de mémoire ni
de web. Seul le planner pose des critères — son prompt le lui demande — et
marquer un critère « passé » reste le privilège de la passe de vérification.
Le rôle verifier n'est PAS délégable : le builder se décernerait son propre
satisfecit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6CAgoLJzufeA8rZTSpSpY
Inspiré du fenêtrage du fil d'OpenFox (2.0.151+). À chaque ouverture
d'onglet, le serveur rejouait TOUT le journal d'affichage : sur une
discussion de plusieurs centaines de tours, des dizaines de milliers
d'événements traversaient le flux, puis autant de bulles s'installaient dans
le DOM que le navigateur devait traîner à chaque rendu.
Le replay initial est désormais borné à ses 4000 derniers événements. Rien
n'est tronqué sur le disque : le serveur annonce combien d'événements sont
restés en arrière, l'interface l'affiche en tête du fil et le bouton
« charger le début » se réabonne en demandant le journal entier.
Jamais borné sur une reprise de flux (from > 0) : là, le client a déjà le
début à l'écran et attend la suite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6CAgoLJzufeA8rZTSpSpY
L'amont tient une table de clés FR/EN et marque chaque texte d'un data-i18n.
Reproduire ça ici demanderait de réécrire tous les écrans d'un coup, avec le
risque d'en casser un pour une clé oubliée — et un « settings.memory.title »
affiché en production.
Chemin additif : la source RESTE le français, et « English » applique un
dictionnaire sur le texte affiché (correspondance exacte, nœud par nœud).
Une chaîne absente du dictionnaire reste en français ; le dictionnaire
s'enrichit sans toucher au reste de l'interface.
Jamais traduits : le fil de discussion (#chat), le code, les zones de
saisie, et tout ce qui porte data-no-i18n. Un observateur couvre les
panneaux rendus en JS après le chargement. Revenir au français recharge la
page — le texte d'origine a été remplacé dans le DOM, c'est le moyen sûr de
le retrouver intact.
Couverture de départ : navigation des réglages, intitulés de sections,
libellés de lignes, boutons et interrupteurs. Sélecteur dans Apparence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6CAgoLJzufeA8rZTSpSpY
Reprise d'AJEAN (mem_crypto/mem_vault/mem_store/mem_io/mem_migrate/
mem_snapshots/mem_health/mem_fsutil + backup_bundle), adaptée à Loki.
Chiffrement à enveloppe : une DEK tirée une fois chiffre les données en
AES-256-GCM ; elle est enfermée dans un coffre par une KEK dérivée en
Argon2id. Ce qui ouvre le coffre : la clé de pilotage de l'appareil (le
serveur n'en a que l'empreinte, il ne peut pas ouvrir seul) ou la clé de
récupération donnée une fois. La DEK ne vit qu'en RAM.
Périmètre chiffré, choisi pour Loki : les pages mémoire, les discussions
(journal ET index, qui porte les titres), les blocs archivés au compactage
— du verbatim de conversation — et les trackers. Pas les buckets de
réglages : le coffre lui-même y vit, les chiffrer serait une boucle.
Verrouillé, rien n'est écrasé : putStoreBytes REFUSE d'écrire du clair
par-dessus du chiffré, la liste des discussions se lit vide et les pages
s'affichent « 🔒 chiffré ». Le déverrouillage recharge la discussion et
rattrape ce qui serait resté en clair. Une migration interrompue reprend au
démarrage, un snapshot est pris avant chaque bascule, et aucune donnée n'est
supprimée avant relecture vérifiée de son remplaçant.
Piège corrigé au passage : chiffrer À L'INTÉRIEUR d'une transaction bbolt se
bloquait sur le verrou de la base (memEncActive relit la config, donc la
base). L'état du chiffrement est désormais résolu AVANT la transaction
(memEncoderNow), qui ne reçoit plus qu'un encodeur pur.
Sauvegarde : le paquet chiffré {mémoire, presets, réglages} s'exporte et
s'importe en FICHIER (/api/backup/export, /api/backup/import). La
sauvegarde vers le relais ajean.link n'est pas reprise — c'est le service de
l'auteur amont ; ici le fichier reste chez soi.
Le dossier mémoire n'était déjà plus joignable qu'aux outils mem_* : c'était
la condition de ce chiffrement (un `cat memory/…` aurait rendu du binaire).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6CAgoLJzufeA8rZTSpSpY
Reprise d'AJEAN : le serveur pousse une notification vers les navigateurs
abonnés, directement via leur service de push — donc app fermée et téléphone
verrouillé, là où une notification côté page ne peut rien (un onglet caché
relâche son flux SSE).
Deux déclencheurs : la fin d'un tour utilisateur (sauf interruption par le
bouton stop : celui qui a coupé est devant l'écran) et — ajout propre à Loki
— la fin d'une TÂCHE PLANIFIÉE, succès comme échec. C'est le cas qui compte
le plus : une tâche tourne justement quand personne ne regarde.
Clés VAPID générées à la première demande et rangées dans la base ;
abonnements persistés et purgés quand le service de push répond 404/410.
Corps de notification générique, sans extrait de réponse : elle transite par
Apple ou Google. /sw.js et /manifest.webmanifest sont servis à la racine
(un service worker doit venir de l'origine) ; le worker ne fait QUE recevoir
les push, sans cache — mettre l'UI en cache servirait une interface périmée
après une mise à jour de l'image.
Interrupteur dans Réglages → Mode agent, à armer sur chaque appareil. L'UI
dit ce qui manque plutôt que d'échouer : HTTPS requis, notifications
bloquées, ou iPhone à ajouter d'abord à l'écran d'accueil.
Nouvelle dépendance : github.com/SherClockHolmes/webpush-go (RFC 8291).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6CAgoLJzufeA8rZTSpSpY
Deux reprises d'AJEAN autour des tâches planifiées.
Dossier de scripts (/data/scripts) : un dossier durable À CÔTÉ de memory,
hors du workspace. Le workspace est jetable — supprimer une discussion
emporte ses fichiers — donc un script qu'on veut garder n'y avait pas sa
place. Le briefing machine l'annonce à l'IA, qui y écrit et y lance ses
scripts normalement.
Tâche « script seul » (Task.Kind/Script) : le planificateur lance le script
sans charger le modèle ni consommer un token, sa sortie devient le
compte-rendu, et l'UI l'affiche comme n'importe quelle tâche. Elle tourne
hors du verrou de génération — d'où un registre à part pour l'afficher « en
cours » et l'arrêter (/api/tasks/stop), et un « tester » qui n'attend ni le
verrou ni le moteur. Sélecteur Consigne IA / Script seul dans la modale.
Outils task_list/create/update/delete : l'IA se donne elle-même rappels et
veilles récurrentes, cloisonnés par projet (dans un projet, elle ne voit et
ne pilote que ses tâches). Le budget du préambule passe de 8600 à 11000
caractères, avec le palier documenté dans le test.
Dossier mémoire réservé à ses outils : bash, write et edit ne le touchent
plus (guardToolOnly*). Un `cat memory/…` contournait l'index MEMORY.md, et
c'est la condition d'un chiffrement de la mémoire à venir.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6CAgoLJzufeA8rZTSpSpY
Le flux n'envoie plus tout le résultat d'un outil : un aperçu de 1600
caractères, sa taille RÉELLE et l'id de l'appel. Le bouton « voir plus »
charge le reste à la demande (/api/chat/tool-result), et déplie vraiment le
bloc (plafond de 280 px levé) au lieu de le laisser scroller. « voir plus »
et « copier » vivent dans la même barre, ancrée en bas à droite d'un
bloc-parent non scrollant : elle reste au coin quand on scrolle le résultat.
Le compteur « ~N tok » de la bulle disait la taille de ce que l'UI avait
reçu, donc toujours le plafond sur un long résultat. Il lit maintenant
result_chars, la taille réelle.
MCP : plus de troncature à 12000 caractères avant le modèle (même règle que
la lecture mémoire). Un serveur MCP est configuré exprès pour ce qu'il rend ;
le couper au milieu rendait la réponse inutilisable. L'UI, elle, n'en affiche
que l'aperçu.
Reprises d'AJEAN 0.15.1, 0.15.2 et 0.15.4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6CAgoLJzufeA8rZTSpSpY
L'amont (AJEAN 0.15) donne à l'IA le contrôle d'un navigateur ; Loki
embarquait déjà un Chromium pour `web_screenshot` sans jamais le piloter.
Contrôle du navigateur (computer_use.go, computer_cdp.go, browser_grid.go,
portés d'AJEAN) : l'IA ouvre une page en CDP, en reçoit les éléments
interactifs NUMÉROTÉS et agit par numéro (browser_open/snapshot/find/click/
type/key/scroll). Aucune vision requise — un petit modèle texte s'en sort.
Avec un projecteur chargé s'ajoutent browser_screenshot (image quadrillée)
et browser_click_xy. Interrupteur dédié (Réglages, `loki computer`,
/api/computer), sous le mode agent : cliquer et taper dans une page sont des
actions réelles, même niveau de confiance que bash.
Adaptation au conteneur : chromePath cherche D'ABORD le Chromium de
Playwright (PLAYWRIGHT_BROWSERS_PATH, /opt/pw-browsers), le seul navigateur
de l'image — sinon la fonctionnalité se déclarait absente là où le
navigateur est présent. LOKI_CHROME force un chemin, LOKI_CU_HEADFUL ouvre
une vraie fenêtre.
Préparation des images (web_upload_orient.go, porté d'AJEAN) : l'orientation
EXIF est cuite dans les pixels — le projecteur l'ignore et voyait les photos
de téléphone couchées — et le grand côté ramené sous 1568 px. Appliquée aux
pièces jointes, à see_image et aux captures.
Dédup des appels d'outils : bash, bash_bg, bash_tail, see_image et les
browser_* ne sont plus court-circuités sur un appel identique. Relancer la
même commande après avoir modifié un fichier est légitime, et un outil qui
porte une image dans un message à part renvoyait « [déjà fait] » SANS
l'image — le modèle tournait en boucle.
Mode code : le builder ne publie plus de lui-même (pas de commit/push/reset/
rebase ni de redémarrage de service sans demande explicite), reprise de la
leçon d'AJEAN 0.15.4 ; l'inspection en lecture seule reste encouragée.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6CAgoLJzufeA8rZTSpSpY
Trois apports repérés chez AJEAN (v0.13.8) et OpenFox (2.0.118), portés et
adaptés à Loki.
## Reprise réseau du tour (llm_retry_net.go)
La requête de complétion partait une fois : un Do() qui échoue ou un statut
d'erreur tuait le tour. Les messages d'erreur le disaient eux-mêmes —
« réessaie dans quelques secondes » — autrement dit on demandait à
l'utilisateur de refaire à la main ce que le code pouvait faire seul. Une
tâche planifiée tombée pendant un redémarrage du moteur échouait pour de
bon, sans personne pour recliquer.
Trois reprises consécutives, 0,8 → 1,6 → 3,2 s, plafonnées, interruptibles
par un /stop. La règle de sûreté ne souffre pas d'exception : on ne rejoue
que TANT QU'AUCUN OCTET N'A ÉTÉ DIFFUSÉ, sinon la moitié de la réponse
déjà chez l'utilisateur serait dupliquée. Le compteur repart à zéro dès
qu'une réponse arrive.
500 n'est pas un statut de reprise : c'est ce que llama.cpp rend pour un
appel d'outil malformé ou un prompt trop long, deux échecs déterministes
que les filets sémantiques traitent déjà. Restent les codes qui disent
« pas maintenant » : 429, 502, 503, 504.
## Presets externes (backend_external.go, web_external.go)
Un preset avec EXTERNAL=1 route le chat vers une API OpenAI-compatible
distante (OpenAI, Groq, OpenRouter, un vLLM sur une autre machine) au lieu
du llama-server local. C'est un preset COMME UN AUTRE : même liste, même
bascule, même prompt système par preset. La différence ne vit qu'à deux
endroits — l'inférence (resolveChatEndpoint) et la bascule, qui arrête le
moteur local au lieu de le redémarrer.
La clé du serveur local ne part jamais chez un tiers : chaque endpoint
porte la sienne. La clé du preset n'est jamais renvoyée en clair à
l'interface, et un champ vide ne l'efface pas — il faut y avoir touché.
Une fenêtre dédiée plutôt que l'éditeur habituel : un modèle distant n'a
ni quantification, ni couches GPU, ni moteur. Un bouton teste la connexion
avant d'enregistrer, et rend le message de l'API plutôt que le JSON brut.
Le résumé de compactage part au même endroit que le chat : le laisser
taper le moteur local aurait cassé toute compaction sur un preset externe.
## see_image (chat_vision_tool.go)
Loki savait voir une pièce jointe et une capture qu'il venait de prendre,
mais pas un fichier qui dort sur le disque : « regarde ~/photos/bug.png »
n'avait aucune réponse, `read` rendant des octets binaires. L'outil charge
l'image et la réinjecte dans un message utilisateur multimodal — même
chemin que les pièces jointes.
Même règle ÉPHÉMÈRE que les captures (et non celle de l'amont, qui persiste
l'image) : l'image va dans le tour en cours, pas dans l'historique. Un
base64 persisté repartirait à chaque tour et finirait par dépasser la
fenêtre pour de bon.
Le marqueur de perte à la compaction existait déjà mais n'offrait qu'un
recours, « reprends la capture » — ce qui enverrait photographier une page
web alors que l'image perdue est un PNG du disque. Formulation généralisée.
## Au passage
toolCallLabel est extrait de runChat. Cette table nom d'outil → argument a
une double fonction — libellé affiché ET argument principal — donc un outil
absent s'exécute sur une chaîne vide : see_image répondait « chemin de
fichier manquant » quoi qu'on lui passe, sans que rien d'autre ne bronche.
Une table pareille se teste.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129sffVC43rezAXUMQuzUog
TestLoadConversationAbsenceLegitimeResteRapide vérifie qu'une absence
légitime — première installation, rien d'enregistré — ne déclenche aucune
reprise. Il le mesurait au chronomètre : « moins de 250 ms », la durée
d'une attente de reprise.
Le temps de mur ne dit pas ça. Il mêle le coût du PREMIER essai (créer et
ouvrir la base bbolt, fsync compris) à l'attente qu'on cherche à exclure.
Sur un runner CI chargé, cet essai unique a pris 435 ms : le test a échoué
alors qu'aucune reprise n'avait eu lieu, et le même commit passait une
minute plus tôt sur un autre runner.
LoadConversation compte désormais ses reprises (loadConvRetries), et le
test lit ce compteur. Ce qu'il affirme est enfin ce qu'il mesure, et il ne
dépend plus de la charge de la machine.
Le test de la base illisible vérifie le compteur dans l'autre sens (quatre
reprises attendues) : sans ça, un compteur bloqué à zéro rendrait la
première assertion vraie sans rien prouver.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129sffVC43rezAXUMQuzUog
Un modèle téléchargé depuis l'éditeur d'un preset survit à la suppression
de ce preset (c'est voulu : on le réutilise ailleurs). Il devenait pourtant
inatteignable — impossible de refaire un preset dessus, impossible de
l'effacer.
Trois causes, trois correctifs.
1. « Le modèle existe déjà » n'est plus une erreur. La recherche Hugging
Face proposait le quant, le clic lançait la sonde, et le téléchargement
répondait en rouge « le modèle existe déjà » — fin du parcours. La sonde
constate maintenant la présence du fichier (sans même sortir sur le
réseau) et renvoie la valeur à écrire dans MODEL= : l'interface le
SÉLECTIONNE, et la file continue (le projecteur vision, par exemple).
Les quants déjà présents portent une pastille « déjà installé » dans la
liste du dépôt, avant le clic.
2. Le sélecteur de modèle reconnaissait mal ce qu'il avait sous les yeux.
/api/models comparait le dossier de chaque .gguf à LokiHome() alors que
les téléchargements atterrissent dans $LOKI_HOME/models : la comparaison
ne pouvait jamais être vraie. Conséquences : l'étiquette « dossier loki »
ne s'affichait nulle part, et un preset écrit MODEL=modele.gguf
s'affichait « introuvable ; ajoute son dossier ci-dessous » — le fichier
étant juste à côté. Un nom simple est désormais résolu comme le fait le
moteur : dossier de loki d'abord, puis les autres.
3. Une liste « Modèles installés », dans le groupe Modèle de l'éditeur.
La route /api/models/delete existait depuis longtemps ; aucun bouton ne
l'appelait. Le seul moment où un .gguf pouvait disparaître, c'était en
cochant « supprimer aussi le fichier » à la suppression de son preset.
La liste montre taille, dossier, tranches manquantes, et qui s'en sert :
le modèle en service ne s'efface pas (le moteur l'a ouvert, la place ne
serait même pas rendue), celui que des presets nomment prévient en les
nommant puis obéit. La suppression dit ce qu'elle a libéré.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129sffVC43rezAXUMQuzUog
Parakeet TDT 0.6B v3 (NVIDIA), servi par sherpa-onnx. La v3 et non la v2 :
c'est la seule des deux qui parle français — la v2 est anglais seul.
CE QUI DISPARAÎT DU DOCKERFILE
Deux étapes de compilation, dont une CUDA de ~200 fichiers nvcc qui a été tuée
par l'OOM du runner plus d'une fois, et avec elles tout l'appareillage de
garde-fous qu'elles réclamaient (GGML_NATIVE=OFF contre le SIGILL en
production, bornage des architectures CUDA, deux binaires CPU/CUDA à choisir à
l'exécution). À la place : le téléchargement d'un binaire statique de 35 Mo,
version épinglée et empreinte SHA-256 vérifiée — le binaire s'exécute sur la
machine de l'utilisateur, une release remplacée en amont ne doit pas passer en
silence.
CE QUI CHANGE DANS LE CODE
La forme est la même — un processus local supervisé, éteint après dix minutes
d'inactivité. Le dialogue, lui, change : whisper-server exposait du HTTP
multipart, sherpa-onnx n'expose qu'un WebSocket dont le protocole tient en deux
entiers et des flottants. D'où un client WebSocket et une conversion WAV →
float32 côté serveur. Le parcours des blocs du WAV n'est pas du zèle : l'offset
44 codé en dur transforme un bloc LIST intercalé en craquement au début de
chaque phrase.
DEUX RÉGLAGES DISPARAISSENT, ET C'EST LE MOTEUR QUI L'IMPOSE
La LANGUE : Parakeet la détecte lui-même, il n'a aucun drapeau pour la forcer.
Le réglage n'aurait servi qu'à mentir. À surveiller : whisper avait précisément
écarté la détection automatique parce qu'elle se trompait sur des tranches
courtes.
Le GPU : le build livré est le statique CPU. Annoncer un sélecteur de carte
sans pouvoir l'honorer serait pire que de ne rien annoncer — et un modèle de
0,6 B en int8 sur des tranches de quelques secondes n'en a pas besoin, la carte
reste au moteur de chat.
MIGRATION
Un réglage enregistré du temps de whisper retombe sur le défaut au lieu de
casser la dictée. Les modèles ggml de /data/whisper/ ne servent plus à rien
mais ne sont PAS effacés : ce sont des données que personne n'a demandé de
perdre. Ils sont à supprimer à la main.
Le paquet UI regénéré ici couvre aussi les sources du commit précédent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trois apports repris d'AJEAN 0.12.9 → 0.13.5, adaptés au fork.
MÉMOIRE LONGUE DE LA CONVERSATION (chat_recall.go)
Le compactage résumait, donc perdait. Chaque gros bloc du torse est désormais
ARCHIVÉ verbatim sous un identifiant court (r7…) dans bbolt AVANT d'être
résumé ; le résumé cite l'id, et le modèle le rappelle avec recall(id) ou le
retrouve par mots-clés avec recall_search. Le contexte reste plat, l'archive
grossit sur disque. En mode code, un fichier lu ou un diff produit tôt dans la
session n'est plus perdu au compactage suivant.
Au passage : garde-fou anti-résumé-dégénéré, budget de résumé indexé sur la
fenêtre, queue ramenée à 20 % (compacter plus large ne coûte plus de perte), et
fin de l'épinglage du 1er message user — le modèle répondait à l'ancienne
demande au lieu de continuer la tâche en cours.
PROJETS (projects.go)
Un projet cloisonne une mémoire, ses discussions et ses trackers. La couture
est memoryDir(), qui pointe sur le projet actif : tout le code mémoire en
hérite sans le savoir. Migration automatique au premier démarrage (memory/*.md
→ memory/generale/, discussions et tâches orphelines rattachées). Une tâche
planifiée vise un projet et l'exécution le force, pour qu'une veille n'écrive
pas dans la mémoire du chantier affiché à l'écran.
TRACKERS (tracker.go)
3e type de mémoire : les données datées qui s'accumulent. On ne les lit jamais
en entier — consultation par niveaux (vue d'ensemble → année → mois →
événements), et la dernière valeur de chaque tracker est donnée d'emblée au
modèle, qui répond sans appeler l'outil.
Aussi : index MEMORY.md tenu par le CODE et injecté en tête de conversation
avec la description du projet (le modèle ne peut plus le désynchroniser) ;
prompt système rattaché au PRESET et non plus global — stocké en base, pas
dans le .env, qui ne saurait pas porter un texte multiligne.
Deux correctifs du lot précédent voyagent ici, faute de pouvoir séparer les
fichiers : msgText signale la présence d'une image au compactage, et le
garde-fou « pensé sans agir » passe à deux relances (nudgeCount/maxNudges).
Le paquet UI est regénéré dans le commit suivant, qui touche les mêmes sources.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Au démarrage, une lecture ratée de la base (verrou bbolt transitoire pendant
le chevauchement des process au redémarrage du conteneur) était confondue
avec une conversation absente. Pire que l'amont chez nous : getStr rend ""
dans les deux cas, donc convEnsureActive forgeait un NOUVEL identifiant et
l'écrasait — le fil en cours devenait orphelin, en silence. On sonde
désormais la base avec son erreur AVANT toute écriture, on réessaie quatre
fois, et un échec durable est journalisé sans que rien ne soit touché.
- Une capture d'écran disparaissait sans laisser de trace : stripImageParts
aplatissait le message en ne gardant que sa légende. Le marqueur
imageLostMarker rend la perte VISIBLE, pour que le modèle sache reprendre
une capture au lieu de la redécrire de mémoire.
- maxLogEvents 20000 → 200000 : un seul tour à très long raisonnement
tronquait déjà le journal de rejeu, et l'utilisateur perdait le début de sa
conversation à l'écran.
- Keepalive WebSocket des postes distants (ping toutes les 25 s, des DEUX
côtés). Sans lui, un poste au repos était coupé au bout de ~60-100 s par les
intermédiaires qui ferment les canaux inactifs, puis reconnecté après
backoff — les déconnexions à répétition sur tous les postes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CTX passé à 100000 dans l'éditeur, « enregistré »… et la carte du chat
qui affiche toujours 32768. Seul le fichier du preset changeait :
config.env gardait l'ancienne valeur, le moteur tournait avec, et plus
aucun preset n'était détecté actif (empreinte différente). Il fallait
penser à rebasculer dessus à la main.
SavePresetApplying décide AVANT l'écriture si le preset édité est celui
en service (empreinte de l'ancien fichier == configuration courante) ;
si oui, la nouvelle version est installée comme une bascule
(applyPresetFile : réglages machine et moteur préservés) et le service
redémarre en arrière-plan. Un preset inactif ou nouveau reste un simple
fichier. L'UI le dit et rafraîchit l'état — la jauge de contexte suit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFhcmBMUXdK6UesdGUgFPH
Il ne se remplissait qu'à l'ouverture ; tout ce que l'agent écrivait
ensuite (rapports, captures, scripts) et chaque pièce jointe déposée
restaient invisibles tant qu'on ne cliquait pas « rafraîchir ».
filesOnActivity, regroupé à 400 ms : appelé à chaque résultat d'outil,
en fin de tour et après un dépôt. Rien pendant le rejeu du journal ni
panneau fermé — l'ouverture recharge de toute façon.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFhcmBMUXdK6UesdGUgFPH
Trois maux d'une conversation agentique longue :
- Chaque appel d'outil s'écrivait en de nombreux événements (annonce,
frappe du corps, arguments) jusqu'au done=true qui porte déjà l'état
final. Le journal enflait jusqu'à maxLogEvents et tronquait les plus
VIEUX événements : les premiers messages disparaissaient à l'affichage.
Seul le done=true est conservé au compactage.
- Un événement non-outil (stats, raisonnement) glissé entre l'annonce et
le résultat faisait émettre l'outil DEUX fois au replay et à l'export
Markdown. L'annonce reste en attente jusqu'au done.
- Ouvrir une session plus ancienne avec le curseur de la précédente
(Seq plus élevés) sautait tout : conversation vide. Curseur au-delà du
dernier Seq → on repart du début.
- L'export JSON embarquait les images en base64 (fichier énorme) : les
pièces jointes sont réduites à leur descriptif.
Repris de l'amont AJEAN v0.12.7, avec ses tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7fwdmmVbvLHznF9v1npzN
Un mot fréquent noyait les bonnes pages : la recherche ne comptait que
les occurrences. Elle pondère maintenant chaque terme par sa rareté dans
la mémoire et favorise les pages qui couvrent plusieurs mots de la
requête.
Repris de l'amont AJEAN v0.11.7, avec ses tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7fwdmmVbvLHznF9v1npzN
Moteur qui tourne, carte déjà pleine : l'énumération des cartes plante
en « CUDA error: out of memory » et ne rend qu'une liste tronquée — le
groupe Cartes graphiques de l'éditeur se cachait (moins de deux cartes)
et le tensor-split était perdu après un simple rechargement de l'UI,
qui vide le cache mémoire.
La dernière énumération réussie est persistée dans $LOKI_HOME/devices.json
et servie en repli (stale) quand le moteur sort en erreur. Identité, ordre
et mémoire totale sont des faits matériels stables ; seule la mémoire
libre y est périmée, sans importance pour répartir.
Repris de l'amont AJEAN v0.10.8 et v0.11.4.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7fwdmmVbvLHznF9v1npzN
onchange ne part que sur une interaction : créer un preset et laisser
l'interrupteur décoché n'écrivait pas REASONING=, et la clé absente
laisse le moteur suivre le gabarit du modèle — il raisonnait malgré le
switch affiché sur off. À l'enregistrement, l'état de l'interrupteur
est désormais matérialisé : off explicite, ou on si aucune valeur active
plus précise (auto/deepseek) n'est déjà là.
Repris de l'amont AJEAN v0.12.1 (issue #46).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7fwdmmVbvLHznF9v1npzN
Depuis 6f6a321 le moteur est un réglage de machine : le preset ne
l'impose plus, la bascule garde le courant. En conteneur l'éditeur ne
proposait de toute façon qu'une seule case, « Image », que personne ne
pouvait changer — et le BIN figé dans le preset servait encore à lister
les cartes graphiques : un vieux preset interrogeait le moteur de
l'image au lieu du moteur mis à jour.
Le groupe disparaît de l'éditeur ; la liste des GPU interroge le moteur
courant (config_bin), mémorisé au préchauffage. CSS mort retiré.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7fwdmmVbvLHznF9v1npzN
Le moteur mis à jour depuis l'interface (Réglages → Moteur, BIN →
/data/engine/server-cuda-b10680/llama-server) ne tenait pas : à la
bascule de preset suivante — changer de modèle, tâche planifiée —
/app/llama-server revenait, et le modèle qui chargeait cinq minutes
plus tôt mourait sur « unknown model architecture: 'qwen4exp' ». Le
journal alternait les deux moteurs sans qu'aucun réglage visible ait
bougé, et le panneau Moteur affichait « fourni par l'image » juste
après un « ✓ moteur mis à jour ».
Cause : chaque preset créé depuis l'UI embarque BIN (newPresetSeedKeys),
donc le chemin de l'image de l'époque, et applyPresetFile remplace TOUTE
la configuration par le preset — BIN n'était pas dans preservedKeys.
Le moteur est un réglage de machine : le courant est conservé à la
bascule, sauf si le preset désigne un backend personnalisé (compilé pour
un modèle précis — là, c'est un vrai choix par modèle, il gagne). Et un
nouveau preset ne fige plus le moteur de l'image ni un moteur téléchargé.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01961iDyM6pwn2dE2SW23gYX
docker-entrypoint.sh reposait BIN=/app/llama-server à chaque démarrage
du conteneur, sans regarder ce qu'il y avait avant. L'intention était
saine — une mise à jour d'image ne doit pas laisser un BIN obsolète —
mais elle écrasait aussi le moteur téléchargé depuis l'interface
(« mettre à jour le moteur », /data/engine/<version>/), pourtant choisi
par l'utilisateur et toujours présent sur le volume.
Symptôme vécu : Qwen3.8-Flash-Next chargeait avec server-cuda-b10680 ;
au redémarrage suivant, retour silencieux au moteur de l'image et
« unknown model architecture: 'qwen4exp' », sans qu'aucun réglage ait
bougé. Le journal alternait /app et /data/engine sans raison visible.
On ne garde BIN que s'il désigne un moteur installé sous
$LOKI_HOME/engine/ et encore exécutable ; sinon, comportement d'avant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01961iDyM6pwn2dE2SW23gYX
Relecture du bouton « Libérer la VRAM » (009b585) — six défauts, tous
sur la même feature :
- gpuUsedSettled comptait une lecture ratée de nvidia-smi comme « 0 Mo »
(pilote en réinitialisation juste après l'arrêt) : freed = before, et
l'interface annonçait 14 Gio rendus alors que rien n'avait bougé. Elle
rendait aussi la main au premier palier, AVANT que le pilote ait réagi
(taskkill et Process.Kill reviennent avant le ménage CUDA) : « 0 Mo »
après un déchargement qui marchait. gpuSettle saute les lectures
ratées, n'accepte un palier qu'une fois la baisse observée, et est
testable (sampler injecté).
- /api/vram/unload et /reload acceptaient GET : sans clé de pilotage,
une balise <img> sur une page tierce suffisait à couper le moteur.
405 + Allow: POST.
- whisperShutdown prend wsrvMu, que whisperEnsure garde jusqu'à deux
minutes pendant un chargement de modèle : le geste dépassait le délai
du navigateur, moteur pourtant déjà arrêté. whisperShutdownVite tue le
processus en train de démarrer (poignée atomique hors verrou) au lieu
d'attendre derrière lui.
- Sous systemd, une unité en crash-loop répond « activating », pas
« active » : le stop était sauté et systemd relançait llama-server
toutes les trois secondes pendant que l'UI disait « déjà arrêté ».
engineNeedsStop élargit aux états transitoires.
- Un moteur planté au chargement était présenté comme « modèle
déchargé — recharge-le » : LOAD_ERROR distingue les deux, le conseil
renvoie vers l'erreur affichée dans le moniteur.
- Deux clics concurrents (moniteur + réglages) lançaient un stop au
milieu d'un start ; VRAM_BUSY fait verrou, et le bouton se repeint
depuis l'état renvoyé par le serveur, pas depuis l'ancien poll.
Quelques Mo de bruit entre deux lectures ne font plus « VRAM libérée :
0.0 Gio ».
Au passage, le 404 d'un téléchargement nomme le fichier manquant : sur
un dépôt qui publie six fragments sur sept (table PLE livrée à part),
« la révision a pu être réécrite » envoyait chercher au mauvais endroit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01961iDyM6pwn2dE2SW23gYX
Le modèle occupe la mémoire vidéo tant que le moteur tourne : une autre
application qui réclame la carte (jeu, encodage, autre serveur d'inférence)
ne trouvait plus rien à prendre. Le geste existait — « arrêter » le service,
au fond des réglages — mais son nom ne disait pas qu'il libérait la VRAM, et
il laissait tourner le serveur de dictée, qui garde la carte lui aussi.
- POST /api/vram/unload : arrête le moteur ET la dictée, attend que le pilote
rende la mémoire (une lecture immédiate rapporte « 0 Mo libérés » après un
déchargement pourtant réussi) et renvoie le bilan chiffré. Refuse pendant une
génération, sauf {force:true} — la couper perdrait la réponse en cours.
- POST /api/vram/reload : relance le moteur, préflight compris (BIN/MODEL
absents = la vraie raison tout de suite, pas un « chargement… » sans fin).
- Bouton sur les jauges du moniteur, là où l'on regarde la VRAM ; il devient
« Recharger le modèle » dès que le moteur est arrêté, d'après /api/status et
non d'un drapeau local (un second onglet afficherait sinon un bouton qui ment).
- Même commande dans Réglages → Moteur → Service du moteur.
- Carte de saisie : « Modèle déchargé — Recharger le modèle » au lieu de
« Le modèle charge », qui promettait un chargement qui ne viendrait jamais.
La lecture nvidia-smi de /api/vram passe dans web_vram.go (gpuStats), partagée
avec le bilan du déchargement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8n9tDn5sZudAcUT8U6uGg
.preset-add est en display:none par défaut, et n'était révélé que par
« details[open]>summary>.preset-add ». Les panneaux de réglages ont
migré vers <section class="set-pane"><div class="set-pane-h">, donc ce
sélecteur ne matchait plus : le « + » des Presets restait invisible et
il n'y avait plus aucun moyen d'en créer un depuis l'interface.
Le même bouton dans « Discussions » est resté visible, lui, parce qu'il
vit encore dans un <details open><summary> — d'où un défaut qui ne
touchait que les presets.
Règle ajoutée pour le nouveau conteneur, puis UI réassemblée
(tools/assemble-ui).
Vérifié dans l'application lancée : display passe de none à block, et le
« + » apparaît dans l'en-tête du volet Presets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
Le build de 51 minutes a échoué au lien final : « libcuda.so.1 not found »,
références cuMem* non résolues, et une libggml-cuda.so PARTAGÉE alors que
BUILD_SHARED_LIBS=OFF est censé être passé. Ce dernier détail était le vrai
indice : les drapeaux n'atteignaient pas cmake du tout.
ARG WHISPER_CMAKE_FLAGS="…" était déclaré entre deux étapes. Règle Docker :
un ARG posé après un FROM appartient à l'étape où il apparaît ; les « ARG »
nus des étapes whisperbuild-* héritent, eux, du scope GLOBAL — où rien
n'était défini. Valeur vide, cmake sans aucun drapeau : ggml en bibliothèques
partagées (échec de lien contre les stubs du pilote), et surtout
-march=native — le SIGILL de la PR #18 revenu en silence sur les deux
binaires. Les tests du Dockerfile n'y voyaient rien : ils vérifiaient le
texte, pas les règles de portée de Docker.
- L'ARG remonte avant le premier FROM, à côté de LLAMACPP_IMAGE.
- Chaque étape vérifie désormais SON binaire : un ldd qui montre libggml ou
libwhisper en dynamique fait échouer le build sur-le-champ, au lieu de
laisser partir un binaire qui ne trouvera pas ses .so dans l'image finale.
- TestDockerfileArgFlagsGlobal verrouille la position de l'ARG, ancré en
début de ligne — une première version se laissait berner par une
occurrence en commentaire, sa contre-épreuve l'a montré.
libcuda.so.1 reste une dépendance dynamique normale du binaire CUDA : c'est
le pilote, injecté à l'exécution par le NVIDIA Container Toolkit, et
l'édition de liens la résout via les stubs du toolkit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3ewMAsXhkw9RY11kb9Dc9
Depuis la fusion de la PR #21, chaque push sur main échouait en moins d'une
minute avec zéro job lancé : le YAML du workflow était devenu invalide.
La PR #21 ajoutait cache-from/cache-to après build-args, alors que le bloc
with: les portait DÉJÀ en fin de liste. Une clé dupliquée dans un mapping
YAML fait rejeter le workflow avant même son démarrage — d'où des échecs
immédiats, sans le moindre journal.
Rectification au passage : le cache n'était pas absent, il était déjà actif.
Les trente-sept minutes venaient de la nouveauté de l'étape CUDA (cache
froid) et de l'absence de borne d'architectures, corrigée elle pour de bon.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3ewMAsXhkw9RY11kb9Dc9
Le build ne mourait plus, mais tournait encore après trente-sept minutes. Et
il aurait produit un binaire inutilisable sur la moitié du matériel visé.
Les GPU de la machine cible sont une RTX 5060 Ti (Blackwell, sm_120) et une
RTX 3060 (Ampere, sm_86). L'étape de compilation était épinglée sur
nvidia/cuda:12.4.1 : ce nvcc-là ne CONNAÎT pas Blackwell. Il refuse
l'architecture, et le binaire ne tournerait au mieux que par recompilation PTX
au chargement. 12.8.1 sur Ubuntu 24.04 est désormais utilisé — exactement ce
avec quoi llama.cpp bâtit l'image amont qui fournit libcudart.
Pour la durée, deux causes distinctes :
- Sans borne, ggml compile pour TOUTES les architectures qu'il connaît, de
Maxwell à Blackwell. CMAKE_CUDA_ARCHITECTURES ramène le travail à quatre
(Turing → Blackwell), et l'ARG CUDA_ARCHS permet d'élargir sans toucher au
fichier pour un GPU plus ancien.
- Surtout : ce binaire ne dépend d'AUCUN fichier du dépôt, et il était pourtant
recompilé à chaque push. Le cache de couches GitHub Actions le réutilise tant
que ses lignes du Dockerfile ne bougent pas ; seuls le code Go et
l'assemblage de l'image se refont.
Un test vérifie le plancher CUDA et la présence de la borne d'architectures :
ces deux fautes ne se voient pas à la lecture et coûtent quarante minutes à
découvrir.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3ewMAsXhkw9RY11kb9Dc9
Le premier build de l'image avec whisper CUDA a échoué après huit minutes
sans écrire une seule ligne d'erreur : le journal s'arrête à 92 % de la
compilation, puis « Complete job ». Un compilateur qui échoue dit pourquoi ;
un compilateur tué ne dit rien.
Ces 92 % étaient atteints en TREIZE secondes. Ce ne sont pas des compilations
terminées, ce sont des nvcc lancés simultanément.
« cmake --build -j » sans nombre autorise chez Make un parallélisme illimité.
ggml-cuda compte environ 200 fichiers d'instanciation de gabarits et chaque
nvcc réclame 1 à 2 Go : le runner (16 Go) mourait d'un OOM. L'étape CPU y
survivait — peu de fichiers, compilation légère — ce qui a rendu le piège
invisible jusqu'à l'arrivée de CUDA.
- -j"$(nproc)" sur les deux étapes, et un test le vérifie désormais : cette
faute est indétectable à la lecture et ne se manifeste qu'en CI.
- Le runner libère dotnet, android et ghc avant de bâtir. L'empilement
nvidia/cuda-devel + runtime CUDA + Chromium approche les 14 Go disponibles ;
cette part-là reste une hypothèse, mais elle coûte une minute à écarter.
- whisper-server CUDA est strippé : les symboles de débogage d'un binaire
ggml-cuda pèsent plusieurs centaines de mégaoctets dans l'image finale.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3ewMAsXhkw9RY11kb9Dc9
Le choix du matériel et du modèle n'existait nulle part : la dictée prenait
le seul modèle codé en dur et le seul chemin possible. Sur une machine à
plusieurs GPU, rien ne permettait de dire lequel prêter à whisper.
Le panneau liste les GPU avec leur VRAM libre (/api/vram donne déjà tout), et
chaque modèle avec sa taille de téléchargement et sa mémoire. Un modèle absent
le dit dans sa propre étiquette : le choisir n'est pas un réglage instantané
mais un transfert de plusieurs centaines de mégaoctets, et le taire ferait
passer l'attente pour une panne.
Le test du micro capte trois secondes, affiche le niveau mesuré puis la
transcription, sans toucher au champ de saisie. Vu de l'extérieur, un micro
muet, un niveau trop faible et un moteur en panne se ressemblent tous — il
fallait bricoler ce diagnostic à la main pour les distinguer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3ewMAsXhkw9RY11kb9Dc9
Chaque dictée relançait whisper-cli, qui relisait le modèle depuis le disque
avant de transcrire. Ce chargement dominait le temps de réponse — 3,2 s de
calcul pour 3,4 s d'audio — et aucun réglage ne pouvait le rattraper.
whisper-server garde le modèle en mémoire entre deux phrases. Loki le
supervise : démarrage au premier clic sur le micro, extinction après dix
minutes sans dictée. Une dictée n'est pas un service permanent, et garder un
modèle chargé toute la journée priverait le moteur de chat de sa VRAM.
- Réglages serveur (modèle, langue, matériel, réactivité) dans bkState, avec
leurs routes. La langue par défaut passe de « auto » à « fr » : sur quelques
secondes d'audio la détection se trompe, et une langue mal détectée produit
du charabia — des suites de caractères géorgiens ont été observées.
- Catalogue de quatre modèles, de small à large-v3 ; large-v3-turbo par
défaut. Téléchargement par identifiant, dans un .part renommé à la fin : un
transfert interrompu ne laisse plus un .bin tronqué qu'on croirait bon.
- Le GPU se choisit par CUDA_VISIBLE_DEVICES, posé en REMPLAÇANT toute valeur
héritée. Dupliquée, la variable laisse le gagnant dépendre de la libc.
- Les marqueurs de whisper ([BLANK_AUDIO], (silence)) ne sont plus collés dans
le champ de saisie comme s'ils étaient du texte dicté.
L'image construit DEUX binaires whisper-server, CPU et CUDA. LLAMACPP_IMAGE
accepte la variante CPU de l'image amont ; sur cette base les .so CUDA sont
absentes et un binaire lié à CUDA n'a même pas de quoi démarrer, donc aucun
repli n'est possible depuis le programme. Le garde-fou du Dockerfile vérifie
maintenant les deux étapes : les drapeaux de portabilité de la PR #18 doivent
survivre à l'arrivée de CUDA.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3ewMAsXhkw9RY11kb9Dc9
Sept tâches, de la persistance des réglages au panneau de l'interface. Le
temps réel (WebSocket + découpeur) fait l'objet d'un second plan : ce plan-ci
livre déjà un logiciel utilisable — bon modèle, GPU au choix, lenteur
supprimée — sans le direct.
Contraintes relevées dans le dépôt et inscrites en tête du plan : la CI
compile pour Windows (donc pas de Setsid), staticcheck doit rester à zéro, et
index.html est généré par tools/assemble-ui, jamais édité à la main.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3ewMAsXhkw9RY11kb9Dc9
La dictée marche depuis la PR #18 mais comprend mal et n'écrit qu'à l'arrêt du
micro. La lenteur est structurelle : chaque requête relance whisper-cli, qui
recharge le modèle depuis le disque. Découper en tranches par-dessus ce
mécanisme reviendrait à recharger le modèle toutes les quelques secondes.
La conception retient whisper-server (modèle chargé une fois, supervisé par
Loki comme llama-server), un découpage en tranches calé sur les silences et
piloté côté Go, et un panneau Dictée pour choisir GPU, modèle, langue et
réactivité.
Deux points tranchés en cours de route :
- Deux binaires whisper-server, CPU et CUDA, pas un seul. LLAMACPP_IMAGE
accepte la variante CPU de l'image amont ; sur cette base les .so CUDA sont
absentes et un binaire lié à CUDA n'a même pas de quoi démarrer. Un binaire
unique condamnerait cette variante.
- Le seuil de silence est adaptatif. Un seuil fixe ne coupe jamais dans une
pièce calme avec un micro discret, et coupe sans arrêt près d'un ventilateur.
Les drapeaux GGML_NATIVE=OFF restent exigés sur les deux cibles : le SIGILL de
la PR #18 ne doit pas revenir par la porte du build CUDA.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3ewMAsXhkw9RY11kb9Dc9
La dictée n'a jamais transcrit une seule phrase. Le micro enregistrait bien —
contexte HTTPS, autorisation accordée, bouton rouge — mais le serveur répondait
500 sur chaque envoi :
whisper-cli : AMX is not ready to be used!
read_audio_data: reading audio data from '/tmp/loki-dictee-...wav' ...
read_audio_data: trying to decode with miniaudio
Deux indices dans ces trois lignes. « AMX is not ready to be used » ne sort que
d'un binaire COMPILÉ avec les instructions AMX, celles d'un Xeon récent — le
runner GitHub. Et le journal s'arrête net : ni « failed to decode », ni texte
transcrit. Le processus ne renvoie pas une erreur, il meurt.
ggml compile en -march=native par défaut. Le binaire partait donc taillé pour
le processeur qui l'avait construit, et rencontrait une instruction illégale
dès qu'on le posait ailleurs.
- GGML_NATIVE=OFF à l'étape whisperbuild, AVX-512 et AMX explicitement coupés.
Reste la ligne de base AVX2/FMA/F16C de ggml, présente sur tout x86-64 depuis
2013. Un test lit le Dockerfile et garde ces drapeaux : rien à l'exécution ne
rappellerait leur raison d'être au prochain qui touchera cette étape.
- Le handler ne renvoyait que la dernière ligne de stderr. Sur une mort par
signal, cette ligne est celle d'AVANT le coup fatal : elle ressemble à une
explication sans en être une, et envoie chercher du côté de l'audio. Il dit
maintenant comment le processus a fini — « signal: illegal instruction » vaut
diagnostic à lui seul.
L'image doit être reconstruite : whisper-cli y est compilé.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3ewMAsXhkw9RY11kb9Dc9
Le correctif précédent ne pouvait pas s'appliquer. Il ne posait « -ngl auto »
que si la clé NGL était ABSENTE de config.env — or defaultConfig() y sème
NGL=999 sur chaque installation neuve. La quasi-totalité des configurations la
portent donc sans que personne ne l'ait choisie, le code la lisait comme un
choix délibéré, et l'abandon revenait intact :
W common_fit_params: failed to fit params to free device memory:
n_gpu_layers already set by user to 999, abort
999 n'a jamais été un nombre de couches : c'est la sentinelle historique
« toutes ». Sur un moteur qui sait mesurer la VRAM libre, elle devient donc
« -ngl auto », et Loki le dit sur stderr plutôt que de le faire en douce. Tout
autre nombre reste intouché — c'est un vrai choix. NGL=all force l'ancien
comportement, NGL=auto n'envoie toujours aucun drapeau, et un moteur ancien
reçoit toujours 999 (l'omettre le ferait tourner 100 % CPU).
- La décision sort dans nglArgs(), fonction pure : la seule question qui
demande le moteur arrive déjà tranchée, donc elle se teste sans lancer
llama-server. 10 cas couverts.
- binFitsLayersItself() double la lecture fine de l'aide par la présence de
--load-mode. Les deux sont arrivés dans la même vague ; une description
reformulée ou une colonne plus large ne doit pas faire conclure « moteur
incapable » et réimposer 999.
- L'aide de la clé et le sous-titre du champ disent la nouvelle règle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les llama-server récents ont fusionné --mlock, --mmap et --no-mmap dans un
seul --load-mode, et refusent désormais d'ajuster les couches GPU dès qu'on
leur impose un nombre : « n_gpu_layers already set by user to 999, abort ».
Le moteur pousse alors tout sur le GPU et meurt en cudaMalloc, ou se replie
à moitié sur le CPU — débit effondré, GPU à 100 %.
- Traduction des drapeaux dépréciés AU LANCEMENT, pas dans le preset :
--mlock → --load-mode mlock, --no-mmap → --load-mode none, --mmap →
--load-mode mmap. Les interrupteurs de l'interface continuent d'écrire
l'ancienne forme, et un moteur antérieur la reçoit telle quelle. Un
--load-mode écrit à la main dans EXTRA_ARGS gagne sur tout.
- -ngl auto par défaut quand le moteur propose la valeur, 999 sinon (l'omettre
sur un moteur ancien le ferait tourner 100 % CPU). NGL=<nombre> dans le
preset reste souverain.
- --parallel et -ngl ne sont plus ajoutés en double quand EXTRA_ARGS les porte
déjà : le moteur râlait (« specified multiple times ») et, pire, c'est la
dernière occurrence qui gagne — le réglage explicite du preset était donc
silencieusement écrasé par le défaut de Loki.
- Les capacités se lisent dans « <bin> --help », une seule fois par binaire
(binHelp met en cache) au lieu d'un lancement par question.
Dictée : quand le téléchargement du modèle whisper échoue (conteneur sans
réseau, Hugging Face injoignable), le serveur renvoyait bien la raison mais
l'interface affichait quand même « téléchargement (0 %) ». L'erreur cachée
laissait cliquer indéfiniment sur un micro qui n'écrira jamais rien.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Le bouton « vérifier les mises à jour » revient dans la carte Moteur : il
ne fait QUE regarder, là où « mettre à jour » télécharge et bascule après
confirmation. Deux gestes qui n'engagent pas pareil, deux boutons.
- Les boutons « catalogue » / « + ajouter un serveur » (MCP) étaient des
pilules arrondies au milieu de boutons rectangulaires : même gabarit que
leurs voisins (4 px, 12 px, mêmes marges).
- « Réglages » devient « Paramètres » dans la barre latérale et en tête de
la modale.
Version 0.12.3.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
La carte d'outil qui porte une capture (web_screenshot) échappe au plafond
de 280 px — l'image se montrait par le trou d'une serrure — et au repli
automatique : l'image EST le résultat, la replier la cachait sitôt prise.
Elle garde son max-height propre (420 px) et s'ouvre plein écran au clic.
Retirée de la liste de repli du tour pour survivre aussi au repli de fin de
tour. Au rejeu du journal, elle arrive repliée comme le reste de
l'historique. Version 0.12.2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Une carte (mémoire, raisonnement, outil…) reste ouverte pendant que SA
section travaille — bornée à 280 px, défilement qui suit l'écriture — et se
replie dès qu'elle est finie : l'outil au retour de son résultat, le
raisonnement quand le bloc suivant démarre, le reste en fin de tour. On
retrouve les fenêtres distinctes par section, sans qu'elles envahissent la
page. Version 0.12.1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sélecteur de modèle fonctionnel, dictée vocale, cartes de raisonnement qui
suivent l'écriture, moyenne tok/s de la conversation, % de chargement réparé
à chaud, tri des discussions par création, bouton Réglages en pied de barre.
Notes de version à jour ; .syso régénérés.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Le défilement interne des cartes bornées ne suivait PAS la génération :
le rendu en attente porte la bulle entière (.msg), pas .body — le test
« parent = bodywrap » ne passait jamais et l'épinglage bas était mort.
On cherche maintenant le bodywrap DANS la bulle : la carte suit le texte
au fur et à mesure, et une remontée manuelle est respectée. Les cartes
d'outil suivent aussi la ligne en cours, puis remontent en tête une fois
l'appel terminé.
- Bouton micro : il gardait la bordure des boutons génériques, décalé de
ses voisins. Ajouté aux règles communes des boutons de saisie (34 px,
sans bordure, fond au survol) avec #attach et #files-btn.
- La carte du temps total (au-dessus de la saisie) affiche la vitesse
moyenne de la conversation : « en cours — 12 s · moy 21.3 tok/s ».
Alimentée par les événements stats journalisés (gen_tokens/gen_ms,
cumulatifs par complétion — seul le delta est ajouté), donc rejouée au
chargement : la moyenne survit au refresh. Remise à zéro au reset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Quatre retouches d'interface :
- Le sélecteur de l'en-tête liste maintenant les MODÈLES du disque (groupe
« Modèles (.gguf) ») en plus des presets : en choisir un le charge —
POST /api/models/use écrit MODEL seul (contexte, NGL et échantillonnage
conservés) et redémarre le service en arrière-plan. Sans preset créé, le
sélecteur n'offrait aucun choix.
- Pourcentage de chargement : sur un redémarrage à chaud, le modèle est déjà
dans le cache disque — llama-server ne lit rien (read_bytes reste à 0) et
la pastille passait de « 0 % » à « prêt » sans jamais monter. On prend le
plus avancé de read_bytes et de la mémoire résidente (VmRSS), qui grandit
cache ou pas.
- Discussions triées par date de CRÉATION (récentes en tête) : une
discussion garde sa place, écrire dans un vieux fil ne le fait plus
remonter.
- Bouton Réglages ancré en pied de barre latérale, juste au-dessus du
moniteur Performance — toujours au même endroit, quel que soit le nombre
de discussions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Un bouton micro dans la carte de saisie : un clic enregistre, un second
arrête, transcrit et pose le texte dans le champ sans écraser ce qui s'y
trouve. Anneau rouge pulsé pendant l'enregistrement, garde-fou à 90 s.
La transcription est 100 % locale : POST /api/transcribe → whisper-cli
(whisper.cpp, compilé CPU en statique dans une étape dédiée du Dockerfile).
Le modèle ggml-small-q5_1 (~190 Mo, multilingue) n'est pas dans l'image :
téléchargé au premier usage dans /data/whisper/ avec progression (503
{downloading, pct} en attendant), il survit aux recréations du conteneur.
L'audio est encodé en WAV 16 kHz mono côté navigateur (whisper.cpp ne lit
que du PCM) — pas de ffmpeg dans l'image. Le micro n'existe qu'en contexte
sécurisé (HTTPS ou localhost) : le bouton l'explique au lieu d'échouer en
silence.
Vérifié bout en bout avec le binaire whisper.cpp officiel : téléchargement
du modèle par le handler, puis une sinusoïde 440 Hz transcrite « (beeping) ».
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Un long raisonnement (des milliers de tokens) faisait grandir la page de
plusieurs écrans et finissait par figer l'affichage : le bloc entier était
re-parsé en Markdown à chaque tick, en O(n²). Deux mesures :
- Les cartes raisonnement/outils ont une hauteur bornée (280 px) avec
défilement interne, collé en bas pendant la génération (un défilement
manuel vers le haut est respecté).
- En direct, un bloc de raisonnement géant n'est re-parsé que sur sa fin
(REASON_TAIL) ; le texte complet est posé au rendu de fin de bloc. La
finalité voyage avec le rendu en attente (renderPending.final) : le timer
déjà armé passait final=false et consommait le rendu final en ne posant que
la queue — le début du raisonnement n'était jamais rendu.
Version 0.11.0 (const, versioninfo, .syso régénérés) ; README et
RELEASE_NOTES à jour sur les nouveautés du fork.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
La barre latérale ne garde que les discussions et le moniteur machine : tout
le bloc « Réglages » (11 sections empilées en details) devient une modale
avec sa nav de sections à gauche et LE panneau choisi à droite — chaque
section a enfin toute la largeur, et la barre latérale respire.
Les panneaux réutilisent tels quels les gabarits existants (.slist,
.switchrow, .subhead) ; les ids historiques (svc-log-box, lc-details,
tasks-details…) sont conservés sur les panneaux, et openDetails() les résout
vers openSettings(panneau) : pastille d'état du moteur, pastille
d'installation et jobs llama.cpp rouvrent la modale au bon endroit sans
changer leurs appelants. Le chargement paresseux du journal du moteur
(ancien ontoggle) devient un hook d'affichage du panneau.
Discussions : la plus récente en tête. Le serveur triait déjà par date
d'activité, mais son tri stable laissait une discussion toute neuve SOUS
celle qu'on venait de quitter (touchées dans la même seconde) : l'UI
départage par date de création puis par ordre d'index.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Le nom du modèle (fichier .gguf, sans dossier ni extension) est journalisé
avec la borne de tour côté serveur : chaque réponse garde le modèle qui l'a
réellement produite, y compris après un rechargement de page (rejeu du
journal) et même si un autre modèle est chargé depuis.
Côté UI, une pastille discrète s'affiche dans l'étiquette de la bulle de
réponse. Elle vit dans dataset.model pour survivre au repeint des libellés
lors d'un changement d'identité (applyIdentity).
Vérifié en conditions réelles (moteur simulé, rechargement en pleine
génération) : badge, tok/s des bulles et chrono « en cours » sont bien
rejoués après un refresh.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Le jeton ne vivait que dans la variable d'environnement HF_TOKEN : découvrir
depuis l'interface qu'un dépôt est verrouillé, c'était devoir éditer un
docker-compose et recréer le conteneur pour y répondre.
- Éditeur de preset → Modèle → « Jeton Hugging Face » : ligne repliée comme
« Dossiers de modèles », qui affiche l'état (aucun / masqué / fourni par
l'environnement). Le bandeau d'un dépôt verrouillé y mène d'un clic.
- Le jeton est VÉRIFIÉ auprès de /api/whoami-v2 avant d'être enregistré (le
compte s'affiche) : un jeton mal collé accepté en silence rendrait le 401
qu'on cherchait à expliquer. Il est rangé avec les secrets en base d'état, pas
dans config.env que le changement de preset réécrit en bloc, et n'est jamais
renvoyé en clair — seulement masqué.
- Priorité : jeton enregistré, puis HF_TOKEN. Rien d'enregistré = comportement
d'avant à l'identique. L'enregistrement vide le cache des réponses obtenues
sans jeton.
- Le jeton n'est envoyé qu'aux adresses Hugging Face : un lien collé vers un
autre hébergeur n'a aucune raison de recevoir un secret.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hz1QWmvZqW3t53YC5SJBKC
Un dépôt « gated » (orcarouter/Qwen3.8-27B-Uncensored-GGUF, vécu) laisse lire
son arborescence sans rien : Loki listait donc ses seize quantifications avec
leur verdict mémoire, puis échouait sur « HTTP 401 depuis la source » au
premier octet. Le message ne disait ni que le dépôt était verrouillé, ni qu'il
fallait accepter ses conditions, ni où poser un jeton.
- Le refus est maintenant traduit à partir de X-Error-Code (GatedRepo,
RepoNotFound, EntryNotFound…) et nomme le dépôt, l'action à faire et l'état
du jeton : absent (il en faut un) ou présent mais sans accès. 404, 416, 429
et les pannes de la source y gagnent aussi une phrase utile.
- Le verrou se voit AVANT de choisir une quantification : la recherche demande
`expand[]=gated` et la fiche du dépôt est lue à l'ouverture, d'où une
pastille « accès restreint » dans la liste et un avertissement en toutes
lettres au-dessus des fichiers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hz1QWmvZqW3t53YC5SJBKC
Portage des v0.9.9 → v0.10.2 de l'amont, la dernière vraie fonctionnalité qui
nous manquait. Une consigne, une fréquence (« @every 2h », « tous les jours à
9h », ou une expression cron à 5 champs, dans le fuseau du navigateur), et l'IA
l'exécute seule en arrière-plan. Preset épinglé par tâche (bascule de modèle
avant l'exécution, attente du rechargement), accès mémoire et web réglables,
interrupteur maître pour tout suspendre, bouton « tester maintenant ».
Le planificateur est une goroutine, un tic par minute, dans le process qui
détient la conversation et parle au moteur. Une seule tâche part par tic : de
toute façon une seule inférence tourne à la fois, et étaler les départs évite
qu'une rafale monopolise le modèle. Occupé ou modèle en cours de chargement
n'est pas un échec — la tâche repasse au tic suivant.
Une tâche ne partage QU'UN point avec le chat : le verrou de génération. Ni
messages, ni journal d'affichage, ni epoch — elle construit son fil éphémère et
le jette, ne gardant que son texte final comme compte-rendu (borné à 4000
caractères, réinjecté au passage suivant pour la continuité).
Deux adaptations, parce que loki n'est pas l'amont :
— Dossier de travail. Ici il appartient à la DISCUSSION ouverte : une tâche y
aurait déposé ses fichiers, et en aurait changé en cours de route si
l'utilisateur changeait de discussion — pour disparaître avec elle à la
suppression. Chaque tâche a donc le sien (workspace/tasks/<id>/), stable d'un
passage à l'autre. La bascule ne touche que les points d'entrée des OUTILS
(agentCwd) : le panneau Fichiers, les dépôts et les liens des messages
continuent de suivre la discussion de l'utilisateur.
— Capacités. Mem est posé explicitement à MemOff quand l'agent est coupé : le
zéro de MemMode est la chaîne vide, qu'EnabledTools ne reconnaît pas comme
« coupée » — une tâche sans agent se serait vu offrir les outils mem_*. Le
mode code reste off : rôles, critères et passe de vérification n'ont pas de
sens sans personne en face.
Le refus d'un message pendant qu'une tâche tourne dit maintenant LAQUELLE occupe
le modèle : « génération en cours » sur un fil vide et immobile n'expliquait
rien.
Interface : section repliable dans les réglages (liste, état, prochain passage,
pastille), modale d'édition bâtie sur le gabarit de l'éditeur de preset, panneau
« dernier résultat » en markdown. Vérifié dans un vrai navigateur — création,
rendu, réouverture en édition, bascule intervalle/cron, interrupteur maître,
suppression — sans une seule erreur JS.
Le fork est parti de la v0.9.4 ; l'amont en est à la v0.10.7. Reprise de ce
qui manque VRAIMENT ici, en laissant de côté ce que loki a déjà résolu à sa
façon (contexte MTP via --parallel 1, jauge de contexte, chrono de tour,
vignettes d'images, ligne d'état de génération).
Rendu du chat cadencé puis lissé (amont v0.9.5 issue #24, v0.10.5). Chaque
token re-parsait le Markdown du bloc ENTIER : du O(n²) qui faisait ramer
l'interface sur un long raisonnement — le moteur débitait toujours autant, mais
les tokens semblaient arriver au ralenti et un simple rafraîchissement
« réparait » tout. Le texte s'accumule désormais et n'est re-rendu qu'à
intervalle adaptatif (16 ms sur un petit bloc, jusqu'à 500 ms sur un énorme),
soldé à chaque frontière (outil, bascule de rôle, fin de tour, erreur, rejeu).
Par-dessus, un lissage d'apparition découple l'arrivée de l'affichage : le
décodage spéculatif rend les tokens par rafales, le texte sautait par paquets ;
il s'écoule maintenant à cadence régulière. Rejeu exclu — relire un fil ne doit
pas être une lente réécriture.
Échantillonnage réglable par preset (amont v0.9.5/v0.9.6) : TEMP, TOP_P, TOP_K,
MIN_P, PRESENCE_PENALTY, REPEAT_PENALTY, injectés dans chaque requête (donc sans
redémarrage du moteur), vide = défaut du serveur. Sans ça seule la température
voyageait et le reste retombait sur les défauts de llama.cpp, rarement ceux que
recommande le modèle. Différence avec l'amont : REASONING_EFFORT n'est PAS
traité là — loki lui réserve un chemin plus riche, et l'écrire ici écraserait
`chat_template_kwargs`, donc la consigne « aucune ».
Un seul message système, en tête, à l'envoi (amont v0.9.8, issue #26).
steerSystem ne couvrait que les consignes de loki ; un historique venu
d'ailleurs peut encore en porter deux, et Qwen3.x en --jinja répond alors
« System message must be at the beginning ». Copie normalisée : l'historique
affiché et persisté garde sa forme.
Détection Vulkan multi-distro (amont issues #28, #29) : le chemin Debian codé en
dur est invisible sur Fedora/RHEL/Atomic, où le plan de build retombait sur le
CPU. ldconfig d'abord, puis les chemins connus.
Dossier de travail (amont v0.10.2) : la consigne dit maintenant ce que le
dossier EST — l'endroit par défaut de tout ce que le modèle produit — et nomme
les dossiers système à ne pas toucher, au lieu d'interdire vaguement d'en sortir.
Non repris : les tâches planifiées (~1200 lignes + interface, à décider), et le
quoting cmd.exe par .bat temporaire (loki tourne en conteneur Linux).
Qwen3.8-27B valide `reasoning_effort` au lieu de l'ignorer : il connaît
xhigh/medium/low, pas « high » — le niveau que l'interface enregistre par
défaut. Chaque message partait donc en 500, avec une trace jinja affichée
en guise d'erreur, et le 500 tombait dans la branche « prompt trop long »
de runChat : loki compactait l'historique pour rien avant d'abandonner.
Le refus dit lui-même ce que le gabarit accepte. On le lit (llm_effort.go),
on traduit le niveau demandé vers le plus proche sur l'échelle
none/minimal/low/medium/high/xhigh — à égalité, le plus fort, dégrader en
silence étant pire que générer un peu plus longtemps — et on rejoue le tour,
historique intact. Sans liste annoncée, le champ est simplement retiré.
« aucune » n'est jamais traduite : c'est une coupure, portée par
`enable_thinking` que tous les gabarits comprennent.
La traduction est retenue par modèle : les messages suivants ne repaient pas
l'aller-retour. Le repli est tracé sur stderr, sinon l'intensité choisie dans
l'interface n'est pas celle qui part au moteur sans que rien ne le dise.
« maximale » (xhigh) rejoint la liste des niveaux proposés : aucun gabarit ne
les connaît toutes, et sans elle le maximum d'un Qwen3.8 restait hors
d'atteinte. Le repli couvre les gabarits qui la refusent.
Le commit précédent a versé backend/ et frontend/ dans le dépôt — 94 Mo
de node_modules et d'artefacts de build, restes du fork pré-Go qui
traînaient encore dans le répertoire de travail. Ils ne sont plus suivis,
et .gitignore les refuse désormais avec les caches Python et npm.
L'image Docker n'était pas concernée : le Dockerfile ne copie que cmd/,
internal/, tools/ et les fichiers de module.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trois défauts sans rapport entre eux, tous visibles à l'écran.
Jauge de contexte bloquée à « 0 / N (0 %) » : le comptage exact vient de
usage.prompt_tokens (include_usage), qu'un llama-server récent a cessé de
renvoyer. On note désormais si l'usage est arrivé ; sinon la fin de tour
publie l'estimation qui pilote déjà la compaction — approximative, mais
jamais absente. Et la souscription envoie la valeur courante juste après
le rattrapage : une page rechargée affichait zéro sur une discussion
pourtant pleine, faute d'événement ctx_used dans la fenêtre rejouée.
Espace disque : « il ne reste que 11,9 Go » sur un partage Unraid qui en
a des centaines. /mnt/user est servi par shfs, un FUSE qui agrège
plusieurs disques — statfs y renvoie l'espace d'un seul, et ce chiffre
REFUSAIT l'installation du modèle. Les systèmes de fichiers qui
approximent (FUSE, overlay, NFS, CIFS, Ceph, Gluster) sont reconnus par
leur magie : leur chiffre reste affiché, précédé d'un « ~ », mais ne
bloque plus rien.
Panneau Fichiers : le panneau entier défilait, emportant vers le haut
l'en-tête, le fil d'Ariane et le pied (occupation disque, bouton .zip)
dès qu'il y avait quelques fichiers. Seule la liste défile maintenant ;
le pied reste en bas et l'état vide se centre.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trois défauts d'affichage, une cause commune pour deux d'entre eux.
Le panneau des critères et la puce « mode Code » étaient insérés AVANT
#composer, qui est en position:absolute : ils restaient dans le flux de
la page et passaient donc SOUS la carte de saisie. Ils vivent maintenant
DANS le composeur, alignés sur sa colonne, et les critères s'ouvrent
repliés (cinq lignes dépliées mangeaient la moitié de l'écran).
Chrono du tour complet au-dessus de la carte : le temps entre l'envoi du
message et le moment où l'on peut reparler — appels d'outils, passes de
vérification et compaction compris. Les durées portées par les cartes ne
mesurent que la génération de texte ; celle-ci répond à « ça a pris
combien de temps, en tout ? ». Il avance à la seconde puis se fige sur le
total.
Anneau d'activité de la liste des discussions : l'animation portait sur
le <svg>, dont l'origine de rotation dépend de transform-box — défaut
qui a changé au fil des versions, d'où un anneau figé. Elle porte
désormais sur le conteneur HTML, qui tourne partout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La capture n'apparaissait dans le fil QUE si le modèle recopiait la ligne
markdown rendue par l'outil. Un petit modèle l'oublie, et l'utilisateur
ne voyait jamais l'image qu'il avait demandée — la capture existait
pourtant bien sur le disque.
L'événement d'outil porte désormais le chemin de l'image et l'interface
la rend directement dans la bulle web_screenshot (cliquable pour la
loupe, comme les autres images du fil). Ce que le modèle en écrit
ensuite reste possible, mais n'est plus la condition de l'affichage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le modèle cherchait à vérifier visuellement son travail, ne trouvait pas
web_screenshot (offert seulement avec l'accès internet), en concluait
« pas de Chromium local » et partait installer puppeteer — alors qu'un
Chromium headless est dans l'image depuis toujours.
L'outil est donc offert aussi au mode code sans internet, mais borné aux
adresses locales (localhost, 127.0.0.1, ::1, *.localhost) : photographier
son propre serveur de dev est une vérification locale, pas une sortie sur
le web — l'interrupteur d'accès internet garde tout son sens. Le prompt
du builder dit explicitement que le navigateur est déjà là et qu'il ne
faut installer aucun paquet de navigateur.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les llama-server récents ouvrent 4 slots par défaut : des tampons de
calcul GPU multipliés par quatre, pour un serveur qui ne sert qu'un
utilisateur. Résultat vécu après une mise à jour du moteur : une config
27B qui tournait très bien meurt en « cudaMalloc failed: out of
memory » en pleine génération, sans qu'aucun réglage n'ait changé. On
repasse explicitement à un slot — le comportement historique — et
PARALLEL=n reste disponible dans le preset pour qui veut du parallèle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Un llama-server qui plantait au démarrage (backtrace __libc_start_main
dans le journal) laissait la pastille sur « chargement 0 % » pour
l'éternité : aucun des motifs de modelLoadError ne reconnaissait un
crash. Ajoutés : backtrace (signal, segfault, abort), mémoire GPU
insuffisante (out of memory / cudaMalloc failed — avec le conseil de
réduire CTX), et erreur CUDA (GPU indisponible après un redémarrage du
serveur). Pas de motif « libggml » : les logs de chargement normaux
citent les .so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La pastille « chargement… » restait identique de longues minutes sur un
gros GGUF — indiscernable d'un plantage. llama-server n'expose aucun
progrès, mais charger c'est LIRE le fichier : on rapporte les octets lus
par le process (/proc/<pid>/io, shards compris) à la taille du modèle et
la pastille affiche « chargement 43 % » (rafraîchie toutes les 5 s,
plafonnée à 99 — c'est /health qui dit prêt).
Carte Moteur : bouton « vérifier la version » retiré — « mettre à jour »
fait sa propre vérification et dit s'il n'y a rien de neuf.
Éditeur de preset : en conteneur, l'option de moteur « Personnalisé »
disparaît (rien à compiler dans l'image, la mise à jour passe par la
carte Moteur) ; un BIN non reconnu retombe sur le moteur de l'image.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le raisonnement n'étant pas conservé entre les tours (coût de contexte),
un modèle à reasoning fort re-déduisait TOUT son plan avant chaque appel
d'outil — des minutes de <think> pour écrire un fichier de 200 octets. Le
prompt du builder impose maintenant : plan écrit UNE fois dans PLAN.md,
une à deux phrases de réflexion entre les appels, chaque fichier complet
en un seul write.
UI : les outils du mode code (criteria, read, grep, glob, git_*, ask,
bash_bg/bash_tail) ont leurs étiquettes — ils retombaient tous sur
« mémoire ».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bouton « .zip » dans le pied du panneau : la discussion entière — dossiers
et sous-dossiers compris — part en une archive, nommée d'après le titre de
la discussion. Même danse en trois temps que le téléchargement de fichier
(meta → brut, ou tranches base64 derrière le tunnel chiffré), liens
symboliques ignorés, parcours borné comme le calcul de taille du panneau.
Le scope « hors discussion » a son propre zip, sans le dossier des
discussions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- gofmt sur trois fichiers oubliés par le dernier commit (ci/test rouge).
- gopls@latest exige Go ≥ 1.26 alors que l'image de build est en 1.25 avec
GOTOOLCHAIN=local : l'étape Docker échouait net. GOTOOLCHAIN=auto télécharge
le toolchain requis, borné à l'étape de build (build-push GHCR rouge).
- Serveurs MCP : npx/uvx téléchargent leur paquet au premier lancement, et le
délai de connexion de 20 s tombait dessus (« enregistré mais connexion
échouée : context deadline exceeded » depuis le catalogue). Délai à froid
de 3 min pour ces deux lanceurs, et erreur de délai réécrite pour dire quoi
faire (réessayer : le paquet reste en cache).
- README : note sur ce premier lancement, ligne mode Code dans le tableau
des différences.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sélecteur Chat|Code par discussion dans le pied du composeur ; en mode
chat, une détection serveur suggère la bascule (puce ignorable, jamais
automatique). Conception reprise d'OpenFox (MIT), réécrite en Go — voir
NOTICE.md.
- Outils fichiers : read (lignes numérotées, borné), grep, glob, et le
tracker « lu avant d'écrire » qui refuse write/edit sur un fichier non
lu ou modifié depuis la lecture ; edit préserve les fins de ligne CRLF.
- Politique d'exécution : commandes catastrophiques refusées (rm -rf /,
mkfs, reboot…), chemins bornés au dossier de la discussion en mode
code, mutex par fichier.
- Critères d'acceptation : contrat posé via l'outil criteria (éditable
dans l'UI), passe de vérification indépendante sur contexte isolé —
seule habilitée à marquer « passed » — puis corrections plafonnées.
- Rôles embarqués (agents/*.md) : builder, planner, verifier, explorer,
code-reviewer ; badge de rôle dans le fil.
- LSP : gopls / typescript-language-server / pyright (inclus dans
l'image), diagnostics injectés dans le retour de write/edit.
- Git natif : git_status, git_diff, git_clone (borné à la discussion).
- Jobs d'arrière-plan bash_bg/bash_tail (serveur de dev, build long).
- Auto-retry : un appel d'outil écrit en texte (default_api:…,
<tool_call>…) relance le tour une fois avec consigne corrective.
- Outil ask : question à choix rendue en carte à boutons.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Modèles, discussions et fichiers disparaissaient à chaque recréation du
conteneur quand /data n'était pas un volume : tout vivait dans la couche
éphémère. Loki le détecte maintenant (mountinfo) et le dit — bandeau rouge
dans l'UI (/api/status warn), avertissement en tête du journal, et
l'entrypoint exporte LOKI_HOME pour ses sous-commandes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le sélecteur envoyait seulement `reasoning_effort` dans le corps de la
requête. Ce champ n'agit que sur les gabarits qui le LISENT (gpt-oss et
apparentés). Un modèle hybride à la Qwen3 ne connaît que `enable_thinking` :
il recevait `reasoning_effort: none`, son gabarit l'ignorait, et il
continuait de réfléchir pendant que l'interface affichait « aucune ». Les
llama-server récents traduisent eux-mêmes `none` en `enable_thinking=false`,
mais les binaires plus anciens laissent simplement tomber le champ.
On joint donc `chat_template_kwargs` à la requête — même procédé que la
compaction, qui coupe déjà la réflexion ainsi :
aucune (ou REASONING=off explicite) → {"enable_thinking": false}
basse/moyenne/haute → {"reasoning_effort": "<niveau>"}
auto → rien, le gabarit garde son défaut
Une clé REASONING absente n'est PAS une interdiction : sans consigne on ne
touche à rien. Une clé inconnue d'un gabarit est ignorée sans erreur.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RodUJoPJDBvVKhA6S5msvb
La grande nouveauté de cette version : AJEAN sait enfin voir les images.
# Loki 0.15.0
## La vision, configurable par preset
Une version entièrement consacrée à la vitesse, avec une règle tenue de bout en
bout : **ne jamais dénaturer le modèle**. Rien ne touche à la quantification
des poids, à l'échantillonnage, à la température, au budget de raisonnement ni
au contenu du contexte. Ce qui est sans perte est actif d'office ; tout ce qui
changerait ce que voit le modèle, le placement des poids ou les slots du moteur
attend une clé. Sans clé, la requête et la ligne de commande restent celles
d'avant.
Jusqu'ici rien dans l'interface ne permettait de donner des yeux à un modèle. Il fallait glisser l'option `--mmproj` à la main dans la configuration brute, et son chemin n'était même pas résolu comme celui du modèle. L'éditeur de preset gagne un champ **Vision** : tu y choisis le projecteur multimodal (fichier `mmproj`) qui accompagne le modèle, et c'est tout. Au démarrage du moteur, AJEAN le charge tout seul.
La section **Performance** du README rassemble toutes les clés, leur défaut et
leur prix.
Le champ ne liste que les projecteurs (les fichiers dont le nom contient `mmproj`), pas les modèles de plusieurs Go, pour que le choix reste lisible. Et s'il te manque un projecteur, le champ « télécharger un modèle » accepte aussi un lien vers un `mmproj` : une fois récupéré, il se place directement dans le champ Vision.
## Actif d'office, sans perte
## Les images arrivent vraiment au modèle
**La conversation n'est plus recalculée pour rien.**
Avant, une image collée dans le chat était simplement déposée comme un fichier dans le dossier de travail, à charge pour le modèle de l'ouvrir avec ses outils (ce qui ne donnait qu'un tas d'octets illisibles). Désormais, quand un projecteur est configuré, l'image part au modèle en contenu multimodal : il la voit, et peut en parler.
- **Cache de prompts dimensionné** : le moteur garde en RAM une copie exacte des
conversations qu'il quitte. Son défaut ne tenait pas une conversation de 30 à
65 k jetons à côté d'un vérificateur ou d'un sous-agent : il l'évinçait, et
tout était recalculé (30 à 80 s sur un 27B). Il est maintenant agrandi pour un
modèle tout-GPU, et le slot est effacé après chaque travail annexe pour que la
conversation revienne du cache.
- **Préfixe stable** : les rappels de Loki entrent dans l'historique tels
qu'envoyés, la relance après un 500 ne change plus les outils, la ligne MCP ne
manque plus au premier tour après un démarrage, l'ordre des trackers ne bouge
plus. Chacun de ces écarts faisait recalculer toute la boucle d'outils.
- **Compaction moins tôt** : le raisonnement que Loki ne renvoie jamais ne
compte plus dans le contexte. La compaction (qui perd de l'information)
partait vers 44-47 k au lieu de 49 k sur une fenêtre de 65 k. Son résumé n'est
plus amputé de l'état d'avancement.
- **Hybrides (Qwen3.5/3.6) sur un moteur ancien** : `--checkpoint-min-step 2048`
d'office quand la RAM le permet, pour ne plus retomber jusqu'à ~8 k jetons en
arrière à chaque reprise.
**Le matériel est mieux employé.**
- **Threads CPU** : « auto » veut dire les cœurs physiques, plus tous les
threads logiques — c'est le décodage des experts MoE sur CPU qui payait.
- **Deux GPU** : file de lancements CUDA élargie quand le pipeline entre cartes
est possible (même calcul ; seul le prompt peut gagner).
**Loki lui-même pèse moins.**
- Écrire un gros fichier ne coûte plus un temps quadratique côté Go (jusqu'à
8 s de CPU volées au décodage sur un MoE).
- L'enregistrement de la discussion ne retarde plus le premier jeton.
- Un seul `nvidia-smi` pour tous les onglets, aucun pour un onglet caché, un
seul à l'ouverture de l'éditeur de preset.
- Unraid : un `/data` ou `/models` servi par la couche FUSE est signalé, avec
le remède.
**On peut enfin mesurer.**
- Chaque réponse dit ce qu'elle a repris du cache, recalculé et accepté du
brouillon ; `GET /api/perf/summary` en fait la synthèse par nature de requête
(`lost_events`, `lost_tokens` : le cache perdu, et après quoi).
- **Bench** honnête, en tâche de fond : mode complet à profondeur réelle, tours
qui reprennent le cache, rien d'enregistré sans timings réels.
- Une sonde du gabarit de chat dit, sans rien envoyer au modèle, si le rendu
reste stable d'un message à l'autre.
**Garde-fous.** Un cache KV quantifié, `--context-shift` ou `--cache-reuse`
posés à la main sont signalés. Un mode de chargement résident voué à l'échec
(poids restés en RAM au-delà de 90 %) est refusé avec la raison en clair —
`LOAD_GUARD=off` pour passer outre ; le refus n'est plus relancé en boucle par
systemd, et un moteur Vulkan sur cartes mixtes n'est plus refusé à tort.
## En opt-in : à essayer, à mesurer
| Clé | Ce qu'elle fait |
|---|---|
| `PREWARM=on` | prépare le prochain tour pendant que tu lis (`full` : aussi la discussion qu'on ouvre) |
| `COMPACT_CONTINUATION=on` | le résumé de compaction prolonge le prompt en cache au lieu d'être calculé à froid |
| `PROJ_SNAPSHOT=on` | bloc projet figé par discussion, changements livrés à part ; date et dossier sortent du système |
| `REASONING_ECHO=on` | renvoie au moteur local la réflexion du modèle, au format entraîné |
| `SPEC=auto` / `mtp` | décodage spéculatif MTP (tête détectée dans le fichier, garde-fous VRAM) |
| `SPEC=ngram` / `mtp+ngram` | n-grammes du contexte, utiles en mode Code |
| `SIDE_SLOT=on` | second slot pour les travaux annexes (VRAM en double) |
| `SLOT_PERSIST=on` | état du slot gardé à la bascule de preset |
| `SPLIT_MODE=tensor` | parallélisme de tenseurs entre cartes (expérimental) |
| `FIT_TARGET`, `OP_OFFLOAD_MIN_BATCH`, `CUDA_GRAPH_OPT` | réglages d'expert pour cartes inégales et MoE |
| `KEEP_TURN_IMAGES=on` | **zone grise** : garde les images des outils dans l'historique |
| `NUDGE_IN_TOOL=on` | **zone grise** : rappel de budget dans le résultat d'outil |
marges et placement sur un moteur d'essai isolé ; rien n'est écrit sans un
clic, et l'application est vérifiée puis défaite si le moteur ne tient pas.
- **« Dupliquer en placement auto… »** pour un MoE aux experts placés à la
main : une copie du preset où `--fit` les répartit.
## Ordre de test conseillé
Une étape à la fois, mesurée avant la suivante :
1.**Bench complet** du preset tel quel : la référence.
2.**`PREWARM` + `COMPACT_CONTINUATION`**, une vraie session de travail, puis
`/api/perf/summary` : le cache perdu (`lost`) doit baisser.
3.**Optimiseur** sur le preset en service.
4.**MoE** : « Dupliquer en placement auto… », bench complet des deux.
5.**Mise à jour du moteur** vers la version recommandée (encart du panneau
Moteur).
6.**`SPEC=auto`** (ou `mtp`) si le modèle a une tête MTP.
## Moteur : version recommandée et retour arrière
Quand le moteur officiel est antérieur à `b10864`, le panneau Moteur dit ce
qu'une mise à jour apporte (points de reprise des hybrides, MTP rapide) et la
propose — sur clic seulement, version vérifiée sur le registre. La version
quittée est gardée : **Revenir à la version précédente** y ramène sans réseau.
Depuis `b10763`, llama-server garde par défaut la réflexion des tours passés :
un gabarit comme Qwen3.6 la rendrait vide. Avant de basculer, Loki le détecte et
propose `REASONING_PRESERVE=off` ; après, il compare le rendu de l'ancien et du
nouveau moteur et signale le premier écart.
## Ce qui a été écarté
- **`--cache-reuse`** : réutilise du cache calculé sous un autre contexte, donc
change les sorties.
- **`--context-shift`** : retire des jetons du contexte, et ne marche pas sur
les hybrides.
- **Cache KV quantifié par défaut** : `q8_0` n'est pas exact, `q4_0` perd
mesurablement, pour ~2 Gio gagnés sur un 27B hybride. Reste un choix
explicite (`KV_TYPE`).
## Autres corrections
- Une compaction en plein tour ne fait plus disparaître le rappel des pages
mémoire lues.
- Un `loki tune` tué net ne laisse plus le moteur arrêté ; lancé avec un autre
`LOKI_HOME` que l'interface (`sudo`), il refuse au lieu de charger un essai à
côté d'un moteur vivant.
- L'ancienne option « n-grammes (mod) » de l'éditeur rejoint `SPEC=ngram`, avec
une migration proposée (jamais faite d'office).
- Accès refusé : l'interface dit pourquoi et comment le lever.
## Mise à jour
```
ajean update
docker compose pull && docker compose up -d
```
Non vérifié sur cette version : le résultat final dépend d'un modèle vision et de son projecteur compatibles (Qwen2.5-VL, Gemma 3, etc.). À noter, l'image reste dans l'historique de la conversation et repart au moteur à chaque tour tant que la conversation dure.
Installation systemd (`loki install`) : l'unité du moteur gagne
`RestartPreventExitStatus=78`. `loki update` ne réécrit pas les unités ;
`sudo loki install`, ou `sudo systemctl edit loki-engine` avec cette ligne dans
# Migration Rust lot 1 — façade + mise à jour au démarrage — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Mettre un binaire Rust `loki-rs` en frontal du conteneur (proxy vers le Go) et faire que le conteneur prenne les derniers binaires publiés à chaque démarrage.
**Architecture:**`loki-rs serve` écoute sur `LOKI_WEB_PORT` (8090), lance `loki web 8091` (Go, `LOKI_WEB_BIND=127.0.0.1`) en enfant supervisé et lui proxifie tout (HTTP, SSE, WebSocket). `loki-rs self-update` télécharge la release GitHub `rolling` dans `/data/bin/<build>/`, `loki-rs which-bin` dit à l'entrypoint quel dossier de binaires utiliser.
- Source unique de l'ABI : fichier `IMAGE_ABI` à la racine (contenu `1`), copié dans l'image en `/etc/loki/IMAGE_ABI`. Build de l'image : `ENV LOKI_BUILD=<run_number>` (0 en build local).
- Les binaires de `/data/bin/<build>/` ne sont utilisés que si `abi == IMAGE_ABI`**et**`build > LOKI_BUILD`.
-`LOKI_AUTO_UPDATE=0` : aucune requête réseau. Le boot n'est jamais bloqué (code 0, timeouts bornés).
- La façade efface `X-Loki-Via` entrant ; transmet `Host`, `Authorization`, `Cookie`, `Origin`, `Sec-Fetch-*`, `X-Forwarded-Proto` intacts ; retire les hop-by-hop ; ne bufferise rien.
**Interfaces :**`update::run(cfg:&UpdateConfig) -> Outcome` (jamais d'erreur remontée au CLI : journalise et rend `Outcome::{Installed(u64), UpToDate, AbiMismatch, Failed(String), Disabled}`) ; `UpdateConfig { base_url, data_bin, image_build, image_abi, enabled, timeout }` construit par `UpdateConfig::from_env()` (`LOKI_UPDATE_URL` défaut `https://github.com/R0m1k3/Loki/releases/download/rolling`, `LOKI_HOME/bin`, `LOKI_BUILD`, `/etc/loki/IMAGE_ABI` puis `LOKI_IMAGE_ABI`, `LOKI_AUTO_UPDATE`).
Algorithme : manifeste → décision → `SHA256SUMS` → télécharge `loki-linux`, `loki-rs-linux` dans `<data_bin>/.tmp-<build>/` → vérifie chaque hash → chmod 755 (unix) → renomme en `loki`, `loki-rs` → écrit `release.json` → renomme le dossier en `<data_bin>/<build>` → écrit `<data_bin>/release.json` (tmp + rename) → supprime les autres `<n>/` et `.tmp-*`. Toute erreur : supprime `.tmp-<build>`, `Failed`.
- [ ] Tests d'intégration (faux serveur hyper sur 127.0.0.1:0 servant des fichiers en mémoire) : installation OK (fichiers présents, release.json, select_dir pointe dessus) ; hash faux → rien installé, ancien état intact ; abi ≠ → rien téléchargé hormis le manifeste ; serveur absent → Failed, pas de panic ; `enabled=false` → Disabled sans requête ; seconde exécution → UpToDate et nettoyage des anciens builds.
Comportement : hyper http1 + `with_upgrades` ; chaque requête → client hyper-util vers `backend` (URI `http://backend{path_and_query}`), Host d'origine conservé ; retire hop-by-hop (`connection`, `keep-alive`, `proxy-connection`, `te`, `trailer`, `transfer-encoding`, `upgrade`, + noms listés dans `Connection`) et `x-loki-via` ; si requête d'upgrade (`Connection: upgrade` + `Upgrade`), garde `Connection: upgrade` + `Upgrade`, attend 101, puis `copy_bidirectional` entre les deux côtés upgradés ; corps en streaming dans les deux sens ; erreur de connexion → 502 `text/plain` « Loki (Go) redémarre — réessaie dans un instant ».
- [ ] Tests (faux backend hyper) : GET relayé avec Host/Authorization/Origin/X-Forwarded-Proto intacts et `X-Loki-Via` absent côté backend ; POST corps relayé ; SSE : backend envoie 1er événement puis attend 500 ms — le client reçoit le 1er avant 300 ms ; WebSocket écho via tokio-tungstenite ; backend arrêté → 502.
**Interfaces :**`child::Supervisor::spawn(go_bin:PathBuf, port:u16) -> Supervisor` (relance avec backoff 1 s → 30 s, remis à 1 s après 60 s de vie ; stdin null, stdout/stderr hérités ; env `LOKI_WEB_BIND=127.0.0.1`) ; `Supervisor::shutdown(self, grace:Duration)` (SIGTERM unix / kill windows, puis kill après `grace`).
`main serve` : go_bin = `LOKI_GO_BIN` sinon `loki` à côté de l'exe courant ; attend jusqu'à 30 s que `127.0.0.1:LOKI_GO_PORT` accepte (sinon ouvre quand même, 502 en attendant) ; `proxy::serve` sur `0.0.0.0:LOKI_WEB_PORT` ; SIGTERM/SIGINT → arrêt du proxy + `shutdown(10 s)`.
Spec couvert : façade (T4/T5), X-Loki-Via (T4), bind Go (T1), rolling + manifeste + ABI (T2/T3/T6), repli et boot non bloquant (T3/T6), désactivation (T3), tests (T2–T4), CI (T6). Écart assumé vs spec : ordre des versions par `build` (run_number) et non par commit, et installation dans `/data/bin/<build>/` (bascule atomique par `release.json`).
`runChatTools` → `runChatLoop` (payload, slot, requête au moteur, analyse du
flux, reprises, compactage réactif, **switch des outils inline**
llm_client.go:2363-2549, compactage en cours de tour) → retour dans
`generate` (validation mode Code `codeVerifyLoop`, compactage de fin de
tour, persistance, file suivante, préchauffage).
## Coupures possibles
| | Coupure | Ce qui traverse | Risque |
|---|---|---|---|
| A | Journal + SSE en Rust, boucle au Go | flux de deltas Go→Rust, epoch, reset | Log persisté DANS le même blob chiffré que Messages |
| B | Client LLM en Rust (protocole, flux, reprises) | appel d'outil Go en RPC, porte du moteur, compactage | fonction de 1 469 lignes, appels récursifs (sous-agents, vérificateur) |
| C | Machine d'état de la conversation en Rust | turnView, compactage, outils, persistance en RPC | verrou `Generating` partagé avec tâches/bench/optimiseur |
| D | Sessions + persistance | — | écrivain unique bbolt, chiffrement, ordre des résultats d'outils |
## Stratégie proposée
Pas de bascule partielle de la discussion de l'utilisateur : le cœur chat
est réécrit **en entier** côté Rust, derrière `LOKI_CHAT=rust` (Go par
défaut), puis comparé au Go sur des conversations rejouées avant de devenir
le défaut. Étapes, chacune livrée et testée :
1.**Go, sans changement de comportement** : extraire le switch des outils en
`execTool(ctx, name, args, caps) → résultat` et l'exposer en API interne
(`/internal/v1/tool/exec`), ainsi que le chiffrement des blobs
(`/internal/v1/crypt/seal|open`, la clé reste au Go) et le bail du moteur
(`Generating` → `/internal/v1/engine/lease`).
2.**Rust, bibliothèque** : protocole du moteur (payload OpenAI, analyse SSE
llama.cpp/Strata, reasoning, appels d'outils natifs et textuels, effort,
reprises réseau), testé contre des flux enregistrés.
# Lot 11 — fichiers de la discussion tenus par la façade
## Contexte
Les fichiers rangés par discussion vivaient dans le Go :
-`web_upload.go` : dépôt, téléchargement, image ;
-`web_files.go` : panneau Fichiers ;
-`web_files_zip.go` : archive ;
-`web_thumb.go` et `web_upload_orient.go` : vignettes ;
-`chat_convfiles.go` : chemins.
Lot 11 : la façade les sert quand `LOKI_FILES=rust` (`convfiles.rs`,
expérimental ; sans la variable rien ne change).
| Route | Rôle |
|---|---|
| `POST /api/chat/upload` | dépôt en morceaux base64 (8 Mio), fichier de 1 Gio au plus, espace disque vérifié, `.part` renommé à la fin sous un nom sûr et unique |
| `GET /api/chat/files` | panneau : dossiers d'abord puis du plus récent au plus ancien, tailles récursives (liens non suivis), vue « hors discussion » |
| `POST /api/chat/file/delete` | suppression ; refus de la racine et du dossier des discussions |
| `GET /api/chat/file` | fiche, tranche base64 (tunnel E2E) ou brut en pièce jointe (plage d'octets) |
| `GET /api/chat/image` | type reniflé sur le contenu, en ligne sous une CSP fermée ; vignette JPEG `thumb=<px>` redressée (EXIF) et réduite par moyennage |
| `GET /api/chat/files/zip` | archive construite à la fiche, puis servie en tranches ou en brut |
Le dossier de la discussion active reste décidé par le Go : la façade le lui
demande à chaque requête par la nouvelle route `/internal/v1/conv/workspace`
(`{dir, root, id, title}`). Il connaît la discussion active, qu'il tienne la
conversation ou que ce soit la façade. Les liens des anciens messages
(`captures/<id>/…`, `uploads/…` à la racine) restent résolus comme avant.
`facade_files.go` remet les routes du tunnel à la façade. Il ne vide plus les
`.part` d'envois au démarrage : la façade le fait au sien.
## Écarts assumés
- Octets des vignettes JPEG : l'encodeur diffère. Les dimensions, le
redressement et la réduction (algorithme porté pixel pour pixel) sont les
mêmes.
- Horodatage des entrées `.zip` en UTC.
- Une seule plage d'octets par requête de téléchargement : le Go en acceptait
plusieurs, en multipart.
## Parité
`internal/loki/parity_files_test.go` tire du Go :
- 10 noms de dépôt et 6 slugs ;
- 6 en-têtes EXIF ;
- les 8 orientations d'une image de 5×3 et 4 réductions, pixel pour pixel ;
- 10 reniflages ;
- 42 appels de route sur une arborescence fixe, dates comprises :
- panneau, téléchargement, image et vignette ;
- dépôt en un morceau, en deux morceaux, envoi expiré, fichier vide,
base64 illisible ;
- suppressions ;
- archive de la discussion et « hors discussion », contenu comparé
entrée par entrée ;
- les fichiers restants après chaque POST.
`tests/convfiles.rs` rejoue le tout par la façade, plus une plage d'octets,
un lien vers l'extérieur (refusé, non compté) et le nom d'archive tiré du
titre. Deux mutations (ordre du panneau, suffixe des doublons) font échouer
- les handlers de `web_api.go` et `web_external.go` ;
- les aperçus de l'éditeur (`cacheRAMPreview`, `sideSlotPreview`,
`moePreview`, `autoPlacementPreset`).
Lot 15 : la façade tient ces routes quand `LOKI_PRESETS=rust`
(`presets_api.rs`, expérimental). Sans la variable, rien ne change.
| Route | Rôle |
|---|---|
| `GET /api/presets` | liste, avec pastilles (contexte, vision, quantification, raisonnement, Strata, externe) et mesure du bench si elle correspond à la configuration actuelle |
| `POST /api/presets/order` | ordre choisi par glisser-déposer |
| `GET /api/preset?id=` | contenu et prompt système ; sans id, la graine d'un preset neuf (BIN personnalisé, HOST, PORT) |
| `POST /api/preset/save` | crée (nom de fichier unique) ou met à jour ; le preset EN SERVICE est appliqué tout de suite |
| `POST /api/preset/delete` | refusé pour le preset actif ; emporte prompt système et mesure, et le modèle sur demande |
| `POST /api/preset/cacheram`, `/api/preset/moe` | aperçus de l'éditeur, calculés comme au lancement (lot 14) |
| `POST /api/preset/autoplace` | copie en placement automatique d'un preset MoE placé à la main |
| `GET /api/preset/external?id=`, `POST /api/preset/external/save`, `/api/preset/external/test` | presets externes ; la clé n'est jamais rendue, le test fait un vrai appel de complétion |
| `POST /api/switch` | bascule de preset |
Les presets restent des fichiers `presets/*.env`, et leur prompt système une
clé de la base. Les aperçus réutilisent le port de `loki serve` (lot 14),
sondes comprises.
## Restent au Go
Tout ce qui touche au moteur reste au Go, jusqu'au lot qui le portera. Le Go
expose deux routes internes pour cela (`facade_presets.go`).
`POST /internal/v1/preset/apply {path, mode}` applique un preset à la
configuration vivante :
-`switch` : l'état du slot est relevé avant (SLOT_PERSIST), puis le moteur
est relancé, ou arrêté pour un preset externe ;
-`save` : application, puis relance ;
-`apply` : la configuration seule (preset externe en service modifié).
`GET /internal/v1/tune/guard` dit si une optimisation tient le moteur. La
réponse est alors un 409 `{ok: false, tuning: true}`, comme le Go.
## Écarts assumés
- La suppression d'un preset retire sa mesure sans réécrire les autres avec
leurs champs à zéro, comme le faisait la struct du Go. Le Go les relit
pareil.
- Le message d'un échec de réseau du test externe est celui de reqwest.
## Parité
`internal/loki/parity_presets_test.go` tire du Go deux séries.
Les fonctions pures :
- nom d'affichage, guillemets, format `.env`, noms refusés ;
- quantification, raisonnement, modèle ;
- URL de complétions, contenu d'un preset externe, message d'erreur d'API ;
- arguments rejoints, clé réécrite dans un preset ;
- huit copies en placement auto ;
- empreinte de mesure, moteur imposé par un preset.
Puis 70 appels de route, avec après chacun les dossiers `presets/` et
`models/`, et les clés touchées en base. Ils couvrent :
- le pilotage du navigateur : `handleComputer`, `chromePath`,
`visionEnabled` ;
- la clé de complétion du moteur : `handleAPIKey`, `backend_config.go`,
`oaiBaseURL` ;
- l'écoute réseau du moteur : `handleNetwork`, `sys_network.go`.
Lot 17 : la façade tient ces routes quand `LOKI_SYSTEM=rust` (`system.rs`,
expérimental). Sans la variable, rien ne change.
| Route | Rôle |
|---|---|
| `GET\|POST /api/internet` | interrupteur, moteur web (`go` ou `crawl4ai`), URL et clé du serveur Crawl4AI (la clé n'est jamais rendue, seulement ses quatre derniers caractères), joignabilité |
| `GET\|POST /api/computer` | interrupteur du navigateur, binaire trouvé, vision du modèle |
| `GET\|POST /api/apikey` | clé de complétion : génération, saisie, effacement ; adresse `/v1` à coller dans un client tiers |
| `GET\|POST /api/network` | moteur ouvert au réseau ou à cette machine seulement (`HOST`), adresse de l'endpoint |
Les réglages restent des clés de la base, écrites comme le Go : une chaîne
vide efface la clé, et l'URL comme les clés ne laissent rien traîner dans la
configuration.
La joignabilité de Crawl4AI suit `crawlReachable` :
-`/health`, puis `/`, en moins de deux secondes en tout ;
- clé envoyée en `Authorization: Bearer` ;
- moins de 500 = joignable ;
- cache de 30 s par URL, vidé quand l'URL, la clé ou le moteur changent.
## Restent au Go
Le Go expose trois routes internes (`facade_system.go`) :
-`POST /internal/v1/web/reach/reset` : le cache de joignabilité des outils
web, encore exécutés par le Go, est vidé à chaque changement ;
-`POST /internal/v1/cdp/shutdown` : le Chromium des outils `browser_*` se
ferme quand l'interrupteur se coupe ;
-`POST /internal/v1/service/action {action, if_active}` : une clé neuve ne
s'applique qu'au redémarrage du moteur. La façade le relance elle-même si
elle le pilote (lot 16), sinon le Go, seulement s'il tourne.
`GET /internal/v1/tune/guard` (lot 15) refuse un changement de clé pendant une
optimisation.
Le relais du tunnel vers la façade garde désormais l'hôte et
`X-Forwarded-Proto` du client : l'adresse de la clé de complétion est celle
que le client a tapée, pas celle de la façade.
## Correction en passant
`maskAPIKey` paniquait sur une clé d'un seul octet (`k[len(k)-2:]`).
`/api/apikey` tombait alors à chaque lecture tant que la clé restait en place.
Une clé d'un ou deux octets s'affiche maintenant « … », côté Go comme côté
Rust.
## Écarts assumés
- Hors Windows, aucun pare-feu n'est piloté : la façade ne tourne qu'en
conteneur Linux, où le Go répondait déjà « inconnu ».
- La clé masquée est coupée sur les octets, comme le Go. Un caractère coupé
devient U+FFFD, octet par octet, comme son encodeur JSON.
## Parité
`internal/loki/parity_system_test.go` tire du Go 61 appels de route, avec
après chacun les clés touchées en base et les en-têtes reçus par trois faux
serveurs Crawl4AI (200, 500, 404). Ils couvrent :
- moteurs inconnus, URL avec et sans slashs, injoignable, sans schéma ;
- clés longues, courtes, d'un caractère, résiduelle en configuration ;
- erreurs de type qui laissent passer les autres champs, `null` ;
- vision par projecteur, preset externe ou Strata ;
- clé générée (masquée dans le journal), saisie, de treize octets, repli sur
`config.env`, adresse publique, `X-Forwarded-Proto`, refus pendant une
optimisation ;
- écoute ouverte, fermée, `localhost`, `::1`, port illisible.
Il tire aussi huit dispositions du dossier de Playwright pour le Chromium
retenu.
`tests/system.rs` rejoue le tout par une seule façade, cache de
joignabilité compris. Il vérifie de bout en bout ce qui reste au Go : cache
vidé, navigateur fermé, moteur relancé par le Go, ou par le superviseur
quand la façade pilote le moteur. Une mutation de chaque règle fait échouer
les tests.
Côté Go :
-`TestFacadeSysteme` vérifie le relais et les trois routes internes ;
-`handleProjects` et `projectDTOs` (`web_projects.go`) ;
- les mutations de `projects.go` : `createProject`, `renameProject`,
`setProjectDesc`, `deleteProject` ;
-`projectSwitch` (`chat_sessions.go`).
Lot 18 : la façade tient `/api/projects` quand `LOKI_PROJECTS=rust`
(`projects_api.rs`, expérimental). Sans la variable, rien ne change.
| Action | Rôle |
|---|---|
| `GET` | les projets, le plus ancien d'abord, avec leurs discussions, pages (`.md` visibles, index exclu) et trackers comptés ; le projet actif |
| `create` | slug translittéré (`Réseau NAS` → `reseau-nas`), unique (`-2`, `-3`…), `projet` à défaut, 40 octets au plus ; nom de 60 runes au plus ; dossier `memory/<slug>` et son index amorcé |
| `rename` | le libellé seul : slug, chemins et discussions ne bougent pas |
| `desc` | 2 000 runes au plus, vide = effacée |
| `switch` | la conversation passe sur le projet (la plus récente de ses discussions, ou une neuve) |
| `delete` | discussions, trackers et mémoire du projet ; le projet actif est réassigné ; le dernier projet est indestructible |
Le registre reste dans la base (`state/projects`, écrit comme `json.Marshal`
avec le HTML échappé), la mémoire de chaque projet dans `memory/<slug>`.
## Restent au Go
Le projet actif passe toujours par le Go (`/internal/v1/project/activate`),
qui en garde une copie en RAM pour la mémoire, les outils et les tâches.
Les discussions sont à qui tient la conversation.
Quand la façade la tient (`LOKI_CHAT=rust`), elle bascule et supprime
elle-même. Avant de supprimer le projet actif, elle enregistre sa discussion
et en ouvre une du premier projet restant.
Sinon le Go garde deux routes internes (`facade_projects.go`) :
-`POST /internal/v1/project/switch` : `projectSwitch` (discussion du projet
quitté enregistrée, plus récente du projet rejoint ouverte, ou une neuve) ;
-`POST /internal/v1/project/drop-convs` : les discussions du projet
supprimées (`convDelete`, fichiers et résultats compris).
Une génération en cours refuse la bascule et la suppression (409). La façade
lit la sienne, ou celle du Go (`/internal/v1/chat/generating`, lot 16).
## Écarts assumés
- Pendant une tâche planifiée, le Go voit comme actif le projet de la tâche ;
la façade lit le projet actif enregistré.
- Pendant un tour de la façade, une bascule vers un projet inconnu répond 409,
comme quand le Go tient la conversation. Le Go, relayant à la façade,
répondait 400 (« projet introuvable ») dans ce cas.
## Parité
`internal/loki/parity_projects_test.go` part d'une base amorcée :
- pages visibles, cachées, hors `.md`, dans un sous-dossier ;
- discussions de Générale, d'avant les projets, d'un projet à venir et d'un
projet disparu ;
- trackers, dont `alphabet/x` qui ne doit pas tomber avec `alpha`.
Il tire du Go 40 appels, avec après chacun le registre, le projet actif,
l'arbre `memory/`, les trackers et l'index des discussions. Ils couvrent :
- créations (doublons, translittération, sans lettres, longues, vides) ;
- renommages et descriptions (longues, coupées sur une espace, effacées) ;
- bascules (sur l'actif, inconnue, vers un projet sans discussion, pendant
une génération) ;
- suppressions (de l'actif, inconnue, pendant une génération, jusqu'au
dernier projet) ;
- actions inconnues, corps vide ou illisible, erreur de type, méthode
refusée.
`tests/projects_api.rs` rejoue le tout par la façade, contre un faux Go qui
bascule et supprime les discussions comme le vrai. La bascule et la
suppression quand la façade tient la conversation sont vérifiées de bout en
bout. Une mutation de chaque règle fait échouer les tests.
Côté Go, `TestFacadeProjets` vérifie le relais et les deux routes internes.
# Lot 19 — export, mode Code et critères de la discussion
## Contexte
Avec `LOKI_CHAT=rust`, la façade tient la conversation (lot 6), mais trois
routes de la discussion ouverte restaient au Go :
-`/api/chat/export` (`chat_export.go`). Il relisait la copie enregistrée,
qui peut avoir un temps de retard sur la conversation vivante ;
-`/api/chat/mode` (`code_mode.go`) : mode Chat ou Code de la discussion, et
ses critères ;
-`/api/chat/criteria` : critères édités à la main.
Lot 19 : ces trois routes rejoignent celles de la conversation. La façade
les sert quand elle la tient (`chatx.rs`, `facadeChatRoutes` côté Go), sans
nouvelle variable. Sans `LOKI_CHAT=rust`, rien ne change.
| Route | Rôle |
|---|---|
| `GET /api/chat/export` | Markdown (fait pour être lu) ou JSON (fait pour être retraité), tiré de la conversation vivante ; options `format`, `reasoning`, `tools`, `results`, `turns` |
| `GET\|POST /api/chat/mode` | mode de la discussion ouverte (`chat/mode:<id>`, absent = chat) et ses critères ; un changement part sur le fil (`{mode}`) |
| `POST /api/chat/criteria` | critères édités : vides écartés, vingt au plus, ids posés, statuts ramenés à `pending`/`passed`/`failed` ; scellés comme le contenu des discussions (`chat/crit:<id>`), sur le fil (`{criteria, ts}`) |
L'export suit `chat_export.go` :
- Markdown : un bloc par tour, le raisonnement replié dans un `<details>`,
les outils en liste (seul l'état final d'un outil compte, comme au
rejeu), la compaction signalée, un export partiel annoncé en tête ;
- JSON : indenté et HTML échappé comme `json.MarshalIndent`, vue modèle sans
mise à jour de contexte (`PROJ_SNAPSHOT`) ni image en base64 (élidée avec
sa taille), journal filtré par les mêmes cases ;
- les derniers échanges comptent à partir des messages de l'utilisateur, pas
des rappels de Loki.
## Restent au Go
Le reste du mode Code reste au Go et lit les mêmes clés : l'indice « ça
ressemble à du code », les travaux de fond et l'outil `criteria` de l'agent.
## Écarts assumés
- L'export part de la conversation vivante, pas de sa copie enregistrée :
le dernier tour y est, même tout juste fini.
- Les images d'un tour en cours restent dans l'export JSON, élidées. Une
conversation relue par le Go les avait déjà perdues.
## Parité
`internal/loki/parity_chatx_test.go` tire du Go l'export d'une conversation
riche sous 22 jeux d'options lus dans la requête. La conversation contient :
- pièces jointes, raisonnement en deltas ;
- outils finis, en cours, avec et sans sortie, labels piégés pour le
Markdown ;
- statistiques, compaction ;
- images en base64 et en URL, rappel de Loki ;
- mise à jour de contexte marquée, et une tapée par l'utilisateur.
Plus les exports d'une conversation vide.
Il tire aussi 23 appels aux routes du mode et des critères, avec la base et
les événements du fil après chacun : erreurs JSON, y compris celles d'un
critère mal typé (`Criterion.criteria.id`), et au-delà de vingt critères.
`tests/chatx.rs` rejoue l'export octet pour octet et les routes par la
façade. Il vérifie aussi les en-têtes du téléchargement et le maintien des
routes au Go sans la conversation. Une mutation de chaque règle fait échouer
Le Go tenait les serveurs MCP (Model Context Protocol) :
-`mcp_config.go` : la configuration, dans `state/mcp` ;
-`mcp_catalog.go` : le catalogue embarqué ;
-`mcp_client.go` : le pool de sessions, avec le SDK MCP Go ;
- les routes `/api/mcp*` de `web_api.go`.
Le pool sert deux usages : l'état affiché dans l'interface, et les outils
`mcp__<serveur>__<outil>` annoncés au modèle et exécutés pour l'agent.
Lot 20 : la façade tient tout cela quand `LOKI_MCP=rust` (`mcp/`,
expérimental). Sans la variable, rien ne change.
| Route | Rôle |
|---|---|
| `GET /api/mcp` | état de chaque serveur, connexion tentée pour les activés (en parallèle) |
| `GET /api/mcp/catalog` | catalogue embarqué (le même fichier que le Go), `{workspace}` résolu, entrées déjà configurées marquées, lanceur (npx, uvx) présent ou non |
| `POST /api/mcp/save` | ajoute ou remplace (actif par défaut) ; nom contrôlé, un seul transport |
| `POST /api/mcp/delete`, `/api/mcp/toggle`, `/api/mcp/tool` | retire, active ou éteint, masque ou démasque un outil |
| `POST /api/mcp/test` | reconnexion forcée |
## Client MCP en Rust
`mcp/client.rs` parle JSON-RPC 2.0 sur deux transports, comme le SDK Go :
- stdio : un processus local, un message par ligne ;
- Streamable HTTP : un POST par message, réponse en JSON ou en flux SSE,
session suivie par `Mcp-Session-Id`, en-têtes configurés ajoutés.
La session s'ouvre par `initialize` puis `notifications/initialized`. Elle
sert `tools/list` (première page, comme le Go) et `tools/call`. Un `ping` du
serveur reçoit sa réponse.
Le pool (`mcp/mod.rs`) suit `mcp_client.go` :
- une session gardée d'un tour à l'autre ;
- une entrée publiée avant la connexion, pour que deux appelants partagent
la même tentative ;
- une session invalidée pendant sa connexion n'est jamais republiée ;
- délai de 3 min pour npx et uvx (premier téléchargement), 20 s sinon ;
- un appel borné à 120 s, une reconnexion si la session est morte ;
- la sortie plafonnée à la fenêtre de contexte (12 000 au moins), une erreur
d'outil signalée, une sortie vide rendue « [ok] (aucune sortie) ».
## Le Go
Un seul pool, celui de la façade. Le Go, qui exécute encore les outils de
l'agent, lui demande les outils à annoncer, leurs appels et le
# Lot 24 — panneau « moteur de l'image » tenu par la façade
## Contexte
Le panneau Moteur permet de mettre à jour llama.cpp sans reconstruire
l'image. Le moteur est pris dans l'image de conteneur officielle
(`ghcr.io/ggml-org/llama.cpp:server-cuda-b#####`), dont on ne télécharge que
les couches du sommet, celles qui portent `/app`. Trois fichiers du Go le
servent :
-`web_engine.go` : statut, dernier build publié, mise à jour, bascule,
suppression ;
-`web_engine_reco.go` : version recommandée, risque de rendu du
raisonnement (défaut de b10763), retour à la version précédente,
vérification du rendu du gabarit avant/après la bascule ;
-`backend_engine_oci.go` : dialogue avec le registre, extraction des
couches, essai à blanc.
La mise à jour est un job, le job des moteurs que la façade tient depuis le
lot 22. Le risque de rendu et la vérification du rendu reposent sur la sonde
de gabarit, portée au lot 23.
## Le lot
Avec `LOKI_ENGINE_IMAGE=rust`**et**`LOKI_BACKENDS=rust` (expérimental), la
façade (`rust/loki-rs/src/engineimg`) sert les routes suivantes :
| Route | Rôle |
| --- | --- |
| `/api/engine` | statut : binaire, build, source, variante, versions installées, avis, encart « version recommandée », version précédente, relevé du rendu |
| `/api/engine/check` | dernier build publié pour la variante, comparé au courant |
| `/api/engine/update` | job : recherche, téléchargement des couches du sommet, essai à blanc, REASONING_PRESERVE=off si demandé, bascule, ménage, vérification du rendu |
| `/api/engine/use` | bascule vers une version installée ou vers le moteur de l'image |
| `/api/engine/remove` | suppression d'une version installée (refusée pour celle qui tourne) |
| `/api/engine/plan` | sur clic : version visée (dernière ou recommandée) et risque de rendu |
| `/api/engine/rollback` | retour au moteur d'avant la dernière bascule |
Sans les backends, loki-rs ne passe pas la variable au Go : le panneau
reste au Go et le dit au démarrage. `/api/engine/cmdline` reste au Go.
La sonde de gabarit est celle de la conversation quand la façade la tient
(`LOKI_CHAT=rust`) : le relevé du rendu rejoue alors la forme de ses vraies
requêtes. Sinon, la façade a sa propre sonde, qui sonde une requête nue.
Pour REASONING_PRESERVE=off posée dans le preset actif, la façade réutilise
l'enregistrement du lot 15 (`save_applying`). Le service est arrêté puis
relancé comme au lot 22 : par la façade si elle pilote le moteur, sinon par
le Go (`/internal/v1/service/action`).
Côté Go, `facade_engineimg.go` relaie à la façade les routes reçues par le
tunnel.
## Parité
`internal/loki/parity_engineimg_test.go` (Linux) tire du Go un journal
rejoué par `tests/engineimg.rs`. Il tourne contre :
- un faux registre OCI : jeton, index multi-architecture, manifeste annoté,
couches .tar.gz ;
- un faux moteur local (le `fakeTpl` du Go, porté) pour la sonde et le rendu.
Le journal couvre :
- **le statut** : moteur de l'image, téléchargé ou compilé, avec la version
précédente (absente quand c'est le moteur courant), le relevé du rendu,
sans moteur d'image, sans aucun moteur ;
- **le dernier build** : publié, sans numéro, ou refusé (jeton refusé ou
illisible, manifeste refusé ou illisible, aucune image pour
l'architecture) ;
- **le plan** :
- dernière version (avec ou sans numéro) ou recommandée (build trop
ancien, tag figé absent, sans numéro), et déjà à jour ;
- chaque verdict du risque de rendu : moteur arrêté, gabarit vide,
REASONING_PRESERVE posée, EXTRA_ARGS, gabarit sans les variables, gabarit
qui garde la réflexion, build antérieur au défaut, preset externe ;
- **la mise à jour** :
- couches du sommet vers la base ; une couche du dessus masque celle du
dessous ; liens absolus ramenés dans `/app` ; marqueurs de suppression et
sous-dossiers ignorés ; couche sans fichier du moteur ; couche du runtime
ignorée ; phase du téléchargement ;
- essai à blanc ;
- risque de rendu rappelé dans le journal, établi ou impossible à vérifier
(moteur local arrêté) ;
- REASONING_PRESERVE=off : posée dans le preset actif ou dans la
configuration, déjà posée, ou inconnue du moteur ;
- ménage : versions triées par build, dossier sans moteur épargné, version
précédente gardée ;
- rendu comparé avant/après : changé, identique, ou impossible sur le
nouveau moteur ;
- déjà à jour, corps illisible ;
- **les échecs** :
- optimisation en cours, version inconnue, image sans serveur ou sans
version annoncée, manifeste sans couche, runtime au sommet, couche
introuvable, tag illisible ;
- moteur qui ne démarre pas (12 dernières lignes de sa sortie) ou qui ne
voit aucune carte ;
- **les bascules** : vers une version installée, vers l'image, vers une
version absente, pendant une optimisation ; retour, deux fois, et en GET ;
- **la suppression** : version (`../` neutralisé), version absente, moteur
utilisé ;
- **après chaque appel** : BIN, REASONING_PRESERVE, `engine/previous-bin` et
l'arborescence de `engine/`.
Autres tests :
- la façade vérifie le service arrêté puis relancé autour d'une mise à jour
et redémarré par une bascule (`service_arrete_puis_relance`), le relevé
« non vérifié à temps », le 503 sans backends, les variantes et les
vérifications périmées ;
- le Go (`TestFacadeMoteurImage`) vérifie que les sept routes partent à la
façade, et elles seulement.
## Mutations
74 mutations du port Rust, toutes tuées sauf trois, équivalentes :
- la garde « moteur d'image absent » de la source : `same_path` ne
rapproche jamais un chemin vide ;
- l'essai à blanc qui relance l'inventaire des cartes quand l'ancien moteur
est le nouveau : même résultat ;
- une vérification du rendu périmée qui continue d'attendre son moteur : son
relevé est de toute façon écarté au rangement.
## Écarts assumés
- Les erreurs réseau (registre injoignable) gardent le texte de `reqwest`.
- Un manifeste dont un champ a le mauvais type ne donne pas le message du
Go : le nom Go du type anonyme n'est pas reproduit.
- La vérification du rendu attend son délai à la cadence choisie (5 s), sans
le contexte Go qui coupe aussi une requête en cours au bout de 20 min.
- la liste du sélecteur (`handleModels`, `backend_models_refs.go`) ;
- les dossiers de modèles (`backend_model_dirs.go`) ;
- la suppression (`deleteModelFile`) ;
- les téléchargements (`backend_models.go`, `backend_models_shards.go`) ;
- le catalogue Hugging Face (`backend_hf.go`, `web_hf.go`, `backend_catalog.go`).
Lot 8 : la façade tient tout cela quand `LOKI_MODELS=rust` (expérimental ;
sans la variable rien ne change, tout part au Go).
## Ce que sert la façade
| Route | Module | Rôle |
|---|---|---|
| `GET /api/models` | `models.rs` | `.gguf` des dossiers déclarés (tranches 2..N exclues), taille de la famille, tranches manquantes, qui les référence (presets, configuration en service) |
| `POST /api/models/delete` | `models.rs` | refus si en service ; 409 avec la liste des presets sans `force` ; le modèle part avec toutes ses tranches, octets libérés rendus |
| `GET\|POST /api/models/dirs` | `models.rs` | dossiers de modèles, nombre de modèles, espace libre ; ajout (dossier existant) et retrait |
| `POST /api/models/download/probe` | `downloads.rs` | taille du modèle (toutes ses tranches), espace libre, « ça tient ? », ou « déjà là » avec la valeur de `MODEL=` |
| `POST /api/models/download` | `downloads.rs` | lance en arrière-plan ; 409 si déjà en cours ; « déjà là » si toutes les tranches sont présentes |
| `GET /api/models/download/status` | `downloads.rs` | état de chaque téléchargement de ce lancement |
| `POST /api/models/download/cancel` | `downloads.rs` | annule ; ce que le téléchargement a écrit est effacé |
| `GET /api/hf/files` | `hf.rs` | modèles, projecteurs et poids MTP d'un dépôt, verdict mémoire de chaque modèle (contexte du preset actif, projecteur du dépôt) |
| `GET\|POST /api/hf/token` | `hf.rs` | jeton vérifié auprès de Hugging Face avant d'être enregistré, jamais rendu en clair |
Mêmes contrats (corps, codes, messages) et même mécanique que le Go, entre
autres :
- téléchargement :
- lien de page Hugging Face (`/blob/`) converti en lien direct ;
- modèle en tranches rapatrié en entier ;
- place disque vérifiée sur le total, avec 256 Mio de marge ; jamais de
refus sur FUSE, overlayfs, NFS, SMB, Ceph ou GlusterFS ;
- plages d'octets en parallèle (`LOKI_DL_CONNS`) et reprise d'une
connexion coupée ;
- jeton envoyé aux seuls hôtes Hugging Face ;
- catalogue : réponses mises en cache (60 s pour une recherche, 10 min pour un
dépôt), cache vidé à l'enregistrement d'un jeton ;
-`.part` orphelins vidés au démarrage — par la façade : le Go ne le fait
plus, il effacerait un téléchargement qu'elle mène.
## Côté Go (`facade_models.go`)
Le tunnel du relais et la redistribution E2E arrivent au Go sans passer par
la façade : `withFacadeChat` lui remet ces routes, authentifiées par le
jeton interne, comme celles du chat. Façade injoignable : 502 « modèles
indisponibles : … ».
La façade transmet `LOKI_MODELS=rust` et son adresse au Go qu'elle lance.
## Écarts assumés
- Une source qui ignore les plages n'est plus lue en entier pendant la sonde.
- Erreurs de transport (réseau, TLS) au format de reqwest.
- Message d'un corps JSON illisible : celui de serde (l'interface n'en envoie
jamais).
- Hôte d'un lien collé en majuscules converti comme les autres.
- Espace libre et mémoire vive inconnus hors Unix/Linux (la façade n'est
livrée que pour Linux).
-`/api/models/download/status` trié par date de début.
- Nombres flottants du matériel écrits `24.0` (le Go écrit `24`) : même
valeur JSON.
## Parité
Journaux tirés du Go et rejoués en Rust (`LOKI_PARITY_UPDATE=1` pour
-`dictate_models.go` : catalogue Parakeet, téléchargement et extraction ;
-`dictate_server.go` : supervision de `sherpa-onnx-offline-websocket-server`
et protocole WebSocket ;
-`web_transcribe.go` : routes.
Lot 9 : la façade la tient quand `LOKI_DICTATE=rust` (expérimental ; sans la
variable rien ne change).
## Ce que sert la façade (`rust/loki-rs/src/dictate.rs`)
| Route | Rôle |
|---|---|
| `POST /api/transcribe` | un WAV (≥ 1000 octets, ≤ 32 Mio) entre, le texte sort ; modèle absent : 503 `{downloading, pct}` et téléchargement lancé (relancé après un échec, qui est dit) |
| `GET\|POST /api/dictate/config` | modèle (validé contre le catalogue) et réactivité ; enregistrer arrête le serveur, qui repart au prochain mot avec le nouveau modèle |
| `GET /api/dictate/models` | catalogue trié par taille, présence des quatre fichiers |
| `GET\|POST /api/dictate/models/download` | un téléchargement à la fois, progression sur les octets reçus |
| `GET /api/dictate/state` | serveur allumé (et depuis quand), modèle, présence, téléchargement |
| `POST /internal/facade/v1/dictate/stop` | (le Go, jeton interne) arrêt sans attendre derrière un démarrage |
Même mécanique que le Go :
**Archive `.tar.bz2`**
- Décompressée au fil du téléchargement, dans un dossier `.part` renommé à la fin.
- Entrées absolues ou qui remontent refusées, liens ignorés.
**Serveur de transcription**
- Lancé à la demande sur un port libre de la boucle locale, GPU masqués, sans
le jeton interne dans son environnement.
- Prêt dès que sa socket répond.
- Une mort au démarrage est rapportée avec sa cause et la dernière ligne du
journal ; un processus qui ne répond pas fait échouer le démarrage au bout
de 2 minutes.
- Éteint après dix minutes d'inactivité.
-`shutdown_fast` tue le processus qui démarre au lieu d'attendre le verrou.
**Protocole**
- En-tête de 8 octets (fréquence, nombre d'octets), échantillons float32 en
trames de 10 Kio, une trame texte en retour.
- Annotations (`[musique]`, `(rires)`) écartées.
## Côté Go (`facade_dictate.go`)
-`withFacadeChat` remet les routes du tunnel et de la redistribution E2E à la
façade. Si elle ne répond pas : 502 « dictée indisponible : … ».
-`asrShutdownVite` passe par la façade quand elle tient la dictée : son
serveur n'est pas un enfant de ce process. Cela couvre la libération de la
VRAM et l'arrêt du moteur principal par l'optimiseur.
## Écarts assumés
- Messages d'erreur de transport (WebSocket, HTTP) et de décompression au
format des crates Rust.
- Fin de connexion sans réponse : « lecture du résultat : connexion fermée ».
## Parité
`internal/loki/parity_dictate_test.go` tire du Go ce journal :
You are in CODE MODE: an autonomous coding agent working in this discussion's folder.
Method — in this order:
1. If the task has no acceptance criteria yet, set them FIRST with the criteria tool (action=add): 2-6 short, testable statements of what DONE means. For a trivial task (one obvious change), skip criteria and just do it.
2. Explore before you change: read the relevant files (read, grep, glob, git_status). Never edit a file you have not read.
3. Implement with edit (small patches) or write (new files). Match the style of the surrounding code.
4. Prove it: run the build/tests with bash. A change that was never run is not done. For a web page or UI, start the dev server with bash_bg then use web_screenshot on its localhost URL — a headless Chromium is ALREADY installed, so NEVER install puppeteer, playwright or a browser.
5. Report briefly what changed and how you verified it.
Rules:
- PLAN ONCE. If the task needs a plan, write it to PLAN.md in ONE short write, then follow it. Between tool calls, think one or two sentences at most — NEVER restate or re-derive the plan: it is in PLAN.md and in your criteria, read them instead.
- Write each file COMPLETE in a single write call. Many small writes waste turns.
- When a criterion is done AND checked, mark it completed (criteria set). Verification starts once none is left pending; only it may mark passed.
- If a command fails, read the error and fix the cause; do not retry the same command unchanged.
- Ask the user (ask tool) only for decisions that are genuinely theirs; decide the rest yourself.
- Making the change is your job; shipping it is not. Without an explicit request, never commit, push, reset or rebase, never deploy, restart a service or replace a running binary. Read-only inspection (git_status, git_diff) is fine and encouraged.
You are the CODE REVIEW pass of code mode. Review the current changes (git_diff) for real defects.
Look for, in this order: correctness bugs (wrong logic, unhandled errors, races), security issues (injection, path traversal, secrets), then significant simplifications. Ignore style and taste.
For each finding: file:line, one-line problem, one-line fix. If the diff is sound, say so in one sentence. Do not modify anything.
You are the EXPLORATION pass of code mode. Answer questions about the codebase without modifying anything.
Use read, grep, glob and git tools to locate and understand code. Cite files as path:line. Be concise: report what you found and where, not everything you read. If the answer spans several places, list them with one line each.
You are the PLANNING pass of code mode. Break the user's task down BEFORE any code is written.
Produce:
1. A short restatement of the goal (one sentence).
2. Acceptance criteria via the criteria tool (action=add): 2-6 short, testable statements of what DONE means.
3. The implementation steps, ordered, with the files each step touches.
Explore the code (read, grep, glob, git_status) as needed to make the plan concrete — real file paths, real function names. Do not modify anything. End by asking the user to confirm the plan or amend it.
You are the VERIFICATION pass of code mode. You independently check the acceptance criteria against the actual code — you did not write it, trust nothing.
For each criterion still pending or failed:
1. Understand what it actually requires.
2. Check the real changes: git_diff, read the touched files, run the relevant build/test command with bash when applicable.
3. Record the verdict with the criteria tool: action=set, status=passed (satisfied) or failed (with a note saying exactly what is missing — actionable, one line).
Rules:
- Trivial or non-code criteria that are plainly satisfied: pass them without exploring.
- Only fail a criterion that genuinely misses its requirement; a different-but-valid implementation passes.
- Do NOT fix anything yourself. You only verify and report.
- Be fast: check the diff first, read only what the criteria require.
// Nommer le fichier : sur un modèle en fragments, c'est souvent UN
// fragment qui manque — certains dépôts n'en publient que six sur
// sept et livrent la table PLE à part, à lier soi-même — et sans
// le nom, on cherche une révision réécrite qui n'existe pas.
returnfmt.Errorf("fichier %s absent du dépôt (HTTP 404) — dépôt réorganisé depuis la copie du lien, ou fragment jamais publié (table PLE livrée à part ?)",dlFileLabel(dlURL))
case"RevisionNotFound":
returnfmt.Errorf("révision introuvable dans le dépôt (HTTP 404)")
}
returnfmt.Errorf("lien introuvable (HTTP 404)")
case416:
returnfmt.Errorf("la source refuse la plage d'octets demandée (HTTP 416) — fichier modifié pendant le transfert ?")
case429:
returnfmt.Errorf("trop de requêtes vers la source (HTTP 429) — réessaie dans quelques minutes")
}
ifresp.StatusCode>=500{
returnfmt.Errorf("la source est en panne (HTTP %d) — réessaie plus tard",resp.StatusCode)
}
returnfmt.Errorf("HTTP %d depuis la source",resp.StatusCode)
}
// dlFileLabel : « nom.gguf » entre guillemets pour un message, ou vide si le
// lien n'a pas de nom de fichier lisible.
funcdlFileLabel(dlURLstring)string{
u,err:=url.Parse(dlURL)
iferr!=nil{
return""
}
name:=path.Base(u.Path)
ifname==""||name=="/"||name=="."{
return""
}
return"« "+name+" »"
}
// dlAccessReason nomme la raison du refus. repo vide = source hors Hugging Face
// (lien direct vers un autre hébergeur) : on ne parle alors pas de conditions à
// accepter, qui n'existent que là-bas.
funcdlAccessReason(code,repostring)string{
switchcode{
case"GatedRepo":
ifrepo==""{
return"dépôt à accès restreint : ses conditions doivent être acceptées sur huggingface.co"
}
return"dépôt à accès restreint : accepte ses conditions sur huggingface.co/"+repo
case"RepoNotFound":
ifrepo==""{
return"dépôt privé ou inexistant"
}
return"dépôt privé ou inexistant : "+repo
}
ifrepo!=""{
return"accès refusé par Hugging Face sur "+repo
}
return"accès refusé par la source"
}
// dlTokenHint complète la raison par l'état du jeton. Deux situations opposées
// se cachent derrière le même 401 : pas de jeton du tout, ou un jeton qui n'a
// pas accès à CE dépôt — et le geste à faire n'est pas le même.
funcdlTokenHint()string{
if!hfTokenSet(){
return" — puis renseigne le jeton Hugging Face (réglage « Jeton Hugging Face », ou variable d'environnement HF_TOKEN)"
}
return" — le jeton Hugging Face enregistré n'y donne pas accès (expiré, ou conditions non acceptées avec ce compte)"
}
// dlProbe asks the server for the first byte to learn the total size and
// whether ranges are supported (206 + Content-Range).
t.Errorf("échec relancé dans la même salve : %d lectures",calls.Load())
}
}
Loaded 100 of 735 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.