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.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>
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>
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.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.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>
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
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>
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
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
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
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
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>
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>
- 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.
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>
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>