- 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>
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 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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.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>
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.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.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
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
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
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
- 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>
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
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>
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.
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>
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>
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>
Deux manques dans la présentation d'un tour :
1. Rien ne disait sur QUELLE discussion un agent était en train de
travailler. La ligne concernée porte maintenant un anneau qui tourne,
à gauche du titre — et l'en-tête aussi, puisque la barre latérale est
escamotée sur téléphone. L'état vient de deux sources : le flux SSE
(instantané) et /api/conversations, qui expose désormais `busy` pour
que la page ouverte en cours de génération s'anime sans attendre le
rejeu du journal. L'indicateur est posé et retiré SANS redessiner la
liste : un redessin relancerait l'animation à chaque événement.
2. Le pied des réponses ne montrait que la vitesse du moteur, jamais le
temps que le tour avait pris. Il porte maintenant « travail 1 min 04 s »
— de la question envoyée à la fin du tour, raisonnement, appels
d'outils et attentes compris. La durée avance à la seconde pendant le
tour et se fige au turn_done ; elle apparaît aussi sur l'indicateur
« … », là où il n'y a pas encore de réponse sous laquelle écrire.
La bulle de raisonnement gagne au passage sa propre durée.
La mesure est prise sur les HORODATAGES SERVEUR (événements `user` et
`turn_done`) : elle est donc juste en direct comme au rejeu, où tout
arrive d'un bloc côté client. Le compteur vivant se recale sur l'écart
entre l'horloge du navigateur et celle du serveur, réévalué à chaque
événement reçu en direct — un téléphone n'est pas à la même heure que la
machine.
« Masquer la vitesse de génération » ne masque plus que la vitesse : la
durée n'est pas une mesure de moteur, c'est ce que la réponse a coûté.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5UndZ9DedRXPuAoRXbmDb
Les fichiers du chat vivaient dans un pot commun : on changeait de discussion
et on revoyait les mêmes pièces jointes, et supprimer une discussion laissait
derrière elle tout ce qu'on y avait déposé ou fait écrire à l'agent (seules ses
captures, déjà rangées par identifiant, partaient avec).
Chaque discussion a désormais son dossier, <workspace>/discussions/<id>/ :
- les dépôts (uploads/), les captures (captures/) et ce que l'agent écrit y
atterrissent ; le shell et les chemins relatifs du modèle y sont résolus ;
- le panneau Fichiers s'ouvre sur ce dossier et n'en sort pas, et se redessine
quand la discussion change (bascule ou vidage, signalés par le flux SSE) ;
- supprimer une discussion — ou la vider — emporte ses fichiers. Les deux gestes
le disent maintenant avant de demander confirmation ; « clear chat » en
demandait aucune.
La racine du dossier de travail reste la borne de sécurité : les liens des
anciens messages (uploads/x.pdf, captures/<id>/y.jpg) continuent d'ouvrir leur
fichier par un chemin de repli. Au démarrage, une migration range les captures
dans le dossier de leur discussion et rend chaque dépôt à la discussion qui le
mentionne dans son journal ; ce que personne ne réclame reste à la racine,
atteignable par le bouton « hors discussion » du panneau, qui disparaît une fois
le ménage fait.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
Le journal moteur de production a livré la vraie cause première des 500 en
cascade : « request (55407 tokens) exceeds the available context size
(32768) ». Le message multimodal qui relaie une capture d'écran au modèle
était PERSISTÉ dans l'historique ; son base64 (des dizaines de milliers de
tokens) repartait à chaque tour, et la conversation dépassait
définitivement la fenêtre — plus aucun tour ne passait, et l'exception
Jinja du rattrapage (corrigée au commit précédent) masquait tout.
- L'image devient ÉPHÉMÈRE : jointe au tour en cours, jamais à l'historique.
Le modèle la regarde maintenant ; sa description textuelle, elle, reste.
- stripImageParts guérit les conversations déjà empoisonnées au chargement
et à la bascule : parties image retirées, texte aplati.
- engineSeesImages : l'image n'est envoyée que si llama-server DÉCLARE la
vision (/props, modalities.vision, cache 10 s). La clé MMPROJ ne suffit
pas — projecteur d'un autre modèle ou modèle sans vision (gpt-oss), le
gabarit sérialise le base64 en texte. La description de l'outil suit le
même état : ne jamais promettre une image qui n'arrivera pas.
Deux défauts trouvés à l'analyse, au passage :
- renderBody reconstruit le DOM à chaque delta du streaming : une image déjà
affichée était RE-TÉLÉCHARGÉE à chaque token arrivé après elle. Cache de
blobs par URL, une seule requête par capture.
- les opérations de discussions (création, bascule, renommage, suppression)
entrelaçaient leurs lectures-écritures d'index sous requêtes simultanées :
sérialisées par un verrou dédié.
Tests : historique guéri (aplati sans l'image), transmission conditionnée à
la sonde, description alignée ; suite complète, vet, staticcheck verts.
Trois causes d'encombrement traitées :
- JPEG au lieu de PNG (Playwright déduit le format de l'extension). Sur une
vraie page web — photos, dégradés — le JPEG pèse 3 à 10 fois moins ; le PNG
ne gagnait que sur les aplats, cas minoritaire ici.
- Pleine page DÉSACTIVÉE par défaut : un article long capturé en entier fait
plusieurs milliers de pixels de haut, donc plusieurs Mo, alors que « montre
cette page » veut presque toujours dire le premier écran. Le modèle peut
toujours demander full_page.
- Ménage automatique : au plus 20 captures et 40 Mo par discussion, les plus
anciennes partant en premier. La capture qui vient d'être prise n'est
JAMAIS supprimée — une capture plus lourde que le plafond se serait effacée
elle-même, et le modèle aurait renvoyé un lien mort.
Les captures sont désormais rangées PAR DISCUSSION (captures/<id>/…) :
supprimer une discussion, ou la vider, emporte ses images. Sans ce rangement,
des fichiers que plus aucun message n'affiche restaient sur le disque.
Tests : ménage (nombre et octets), survie de la capture courante, et
suppression qui n'emporte que les captures de la discussion visée.