Deux defauts trouves en relisant le code de l'envoi en morceaux.
Le verrou global etait tenu pendant l'ecriture disque et le renommage.
Or le client lance un envoi PAR FICHIER JOINT, de front : trois fichiers
s'attendaient donc les uns les autres, chacun bloquant les suivants
jusqu'a sa derniere tranche. Le verrou global ne protege plus que la
table des sessions ; l'ecriture se fait sous le verrou de la session. Un
drapeau `busy` empeche le menage de fermer un fichier sous les pieds de
la requete qui l'ecrit. Test : 3 fichiers de 20 Mo en parallele, contenus
distincts, aucun melange.
Rien ne verifiait la place disque : un envoi d'un gigaoctet pouvait
remplir le volume de la machine qui fait tourner le modele, ou vivent
aussi llama-server, la base et les journaux. Le client annonce la taille
au premier morceau, le serveur refuse en 507 s'il ne reste pas la place
plus 512 Mo de marge. La taille annoncee n'engage que le client : le
plafond reel reste verifie morceau par morceau.
Aussi : Sync() avant publication (un tampon non vide donne un fichier
incomplet), et cote client une tranche vide sans marque de fin arrete le
telechargement au lieu de boucler indefiniment.
Le proxy chiffre (relay_e2e.go) reemballe TOUTE reponse en JSON avant de
la renvoyer : une reponse non-JSON est enveloppee en chaine. Du binaire
n'y survit pas — les octets non-UTF8 sont massacres — et le navigateur
telechargeait l'enveloppe JSON a la place du fichier. Le probleme est
structurel, pas propre au telechargement : il touchait aussi l'export
Markdown de conversation, qui revenait entre guillemets, echappements
compris.
handleChatFile prend donc deux formes supplementaires : `meta=1` rend une
fiche {name, size, e2e}, et `b64=1&offset=&len=` une tranche en base64.
Le client demande la fiche d'abord — minuscule, elle passe partout — et
en tire le transport a utiliser : binaire direct en local, tranches
base64 derriere le tunnel. Le client ne peut pas le deviner autrement, le
proxy lui rendant des reponses JSON parfaitement ordinaires.
Le decoupage en tranches de 8 Mo evite de tenir le fichier entier en
memoire pour le transporter, comme a l'envoi. Verifie sur 120 Mo : les
deux chemins rendent exactement les memes 125 829 120 octets, et le test
Go compare octet a octet sur des donnees non-UTF8, celles que le
reemballage JSON detruisait.
relay_e2e.go marque desormais les requetes venues du tunnel
(X-Ajean-E2E) : c'est le seul moyen pour un handler de savoir que sa
reponse sera reemballee.
La limite de 24 Mo n'etait pas arbitraire : le fichier partait dans un
seul corps JSON en base64, tenu entier en memoire par le navigateur puis
par le serveur. Relever la constante seule aurait demande ~1,4 Go de RAM
de chaque cote pour un fichier de 1 Go.
Le transfert est donc decoupe en tranches de 8 Mo. Le navigateur lit une
tranche a la fois (Blob.slice), le serveur la decode, l'ecrit dans un
.part et l'oublie ; seul le descripteur reste ouvert. Mesure sur 120 Mo :
la RAM du process n'a pas bouge (95 -> 96 Mo).
uploadMaxBytes (1 Go) borne desormais le DISQUE ; c'est uploadChunkMax
(8 Mo) qui borne la memoire, et lui seul.
Une session interrompue est balayee au bout de 10 min, et les .part
orphelins sont effaces au demarrage du service : les sessions vivent en
memoire, aucun n'est reprenable apres un arret. Un identifiant inconnu
est refuse en 409 plutot que de reprendre un fichier en son milieu.
La vignette affiche l'avancement en pourcentage au-dela d'une tranche.
Les fichiers circulent enfin dans les deux sens.
Envoi : trombone dans le composeur, glisser-deposer sur toute la fenetre,
ou Ctrl+V. Le fichier est depose dans uploads/ du dossier de travail de
l'agent et son chemin est annonce au modele, qui en fait ce qu'il veut
avec ses outils. Rien n'est interprete au passage, donc tout type de
fichier passe. Le depot n'a lieu qu'a l'ENVOI du message : retirer un
fichier de la liste ne laisse aucune trace sur le disque. Transport en
base64 dans du JSON et non en multipart, seule forme que le tunnel E2E
dispatche : l'acces distant marche sans code specifique.
Reception : le modele ecrit un lien Markdown ordinaire vers un fichier de
son dossier de travail, et le clic telecharge. Il repondait jusqu'ici par
un chemin serveur, inutilisable depuis un navigateur. Le clic passe par
fetch + blob (la cle de pilotage voyage dans un en-tete qu'une navigation
ne porterait pas, et le chemin de base change derriere le tunnel).
Markdown refusant les espaces non echappes dans une cible de lien, ils
sont encodes avant rendu : sans ca [x](mon rapport.pdf) ne produisait
rien du tout.
Perimetre du telechargement strictement limite au dossier de travail :
'..', chemins absolus et liens symboliques qui en sortent sont refuses,
et le fichier est toujours servi en piece jointe avec nosniff — un .html
produit par le modele ne doit pas s'executer dans l'origine de l'UI, ou
il lirait la cle de pilotage.
Preambule du mode agent : ~2350 -> ~1770 tokens, sans perdre une regle.
Le poids etait dans les schemas d'outils (le double du prompt), pas dans
le prompt. Celui-ci reenumerait des outils que les schemas decrivent
deja, et la regle « write, jamais echo » y figurait trois fois. Trois
tests gardent le budget : ce prompt a deja regrossi ligne par ligne deux
fois.
Aussi : l'export mentionne les pieces jointes (un message sans texte
donnait une section vide), et le decompte de contexte perd le mot
« contexte » au profit du nom du modele.
Trois choses ne tenaient pas debout dans la fenetre.
Les options CHANGEAIENT selon le format : le JSON proposait un obscur
« journal d'affichage » a la place des trois cases du Markdown.
Personne ne pouvait deviner ce que ca recouvrait, et cocher une case
n'avait pas le meme sens selon le format choisi juste au-dessus. Les
trois memes options valent desormais pour les deux ; le format ne
decide que du contenant. Cote JSON, le journal est filtre par les
memes criteres.
La taille du fichier annoncee sous le curseur ne servait a rien et
justifiait a elle seule une sonde cote serveur. Supprimee, sonde
comprise : le nombre d'echanges vient maintenant de /api/chat/state,
qui est deja l'instantane leger de la conversation.
La fenetre reprend le vocabulaire visuel de l'editeur de preset
(modal-card, pe-group, pe-row, interrupteurs pe-switch) au lieu de son
balisage bricole, et chaque option porte son sous-titre. Conversation
vide : un simple « Rien a exporter » remplace les reglages, plutot que
d'offrir de tailler un fichier qui serait vide de toute facon.
Le menu de portee proposait « 25 derniers echanges » sur un fil qui en
comptait trois, et ne disait nulle part combien il y en avait. Un
curseur le remplace, borne au nombre REEL d'echanges, butee droite =
toute la conversation. Sous la piste : le compte (« 3 derniers echanges
sur 12 ») et le POIDS du fichier pour les options courantes, sonde
cote serveur (probe=1, rendu puis jete, donc jamais un second calcul de
taille qui divergerait).
Le format passe en controle segmente (.seg), celui du choix de moteur
dans l'editeur de preset : deux cases cote a cote au lieu de deux
boutons radio empiles.
Deux details qui faussaient l'affichage : une sonde encore en vol
revenait ecraser le « … » par la taille de l'etat PRECEDENT (le poids
retardait d'un cran sur le curseur), et fmtSize arrondissait 2585
octets en « 3 KB » dans le seul endroit qui pretend annoncer la taille
reelle.
Deux boutons d'export cote a cote ne laissaient aucun choix : tout ou
rien. Un bouton unique ouvre desormais une fenetre ou l'on choisit le
format (Markdown a lire / JSON a retraiter), ce qu'on emporte
(raisonnements, appels d'outils, sorties d'outils, ou journal
d'affichage cote JSON) et la portee (tout le fil ou les N derniers
echanges). Une ligne dit ce que l'export contiendra, et l'en-tete du
fichier signale un export partiel : relu plus tard, il ne doit pas
passer pour le fil complet.
Memes options en ligne de commande : --last N, --no-reasoning,
--no-tools, --no-results, --messages-only. Sans option, l'export reste
complet, endpoint compris.
« clear chat » descend en derniere position et seul sur sa ligne :
c'est la seule action destructive du panneau, elle n'a rien a faire
sous le doigt qui visait « refresh ».
Au passage, issue #20 : le prompt systeme et la description de l'outil
mem_search annoncaient encore la memoire sous MEMORY/, nom d'avant la
0.8. Le modele lisait donc un chemin qui n'existe plus. Corrige, avec
les deux commentaires restes au meme nom.
Trois retours d'utilisateurs apres la 0.8.3.
Export de la conversation (regression de la 0.8 : conversation.json a
disparu dans ajean.db, verrouille et binaire). Bouton dans l'UI +
commande 'ajean export [--json] [fichier]', endpoint /api/chat/export.
Ecoute reseau du moteur : interrupteur 'joignable depuis le reseau local'
dans le panneau Acces OpenAI (HOST + regle de pare-feu Windows), commande
'ajean network [on|off|status]'. HOST devient un reglage de machine
preserve au changement de preset.
Modeles GGUF decoupes en tranches -00001-of-000NN : une famille = un
modele. Telechargement de toutes les tranches depuis n'importe quel lien,
sonde d'espace disque sur le total, liste et suppression regroupees,
demarrage refuse avec le nom du fichier manquant.
Bug 1 (chat bloque, remonte par Nathan) : trois causes cumulees.
- runShell naissait d'un contexte independant du tour : stop n'arretait rien.
La commande herite desormais du contexte du tour, et la boucle d'outils
s'interrompt des que l'arret est demande.
- cmd.WaitDelay : une commande laissant un process detache gardait les tubes
ouverts, donc Wait ne revenait JAMAIS (ni delai, ni stop).
- Reset debloque toujours l'etat de generation : « nouvelle conversation » etait
le geste tente quand le chat coince, il ne servait a rien. Un tour perime ne
libere plus le tour courant.
Trouve en testant : le workspace disparu cassait TOUTES les commandes suivantes
(chdir: no such file or directory) jusqu'au redemarrage ; il est recree au besoin.
Bug 2 (raisonnement, remonte par Nathan) : l'UI ecrivait « off » en EFFACANT la
ligne REASONING, or sans drapeau le moteur suit le gabarit du modele, qui
raisonne. L'UI ecrit maintenant on|off et « off » part en --reasoning off, apres
avoir demande au binaire s'il connait le drapeau (ik_llama.cpp ne le connait pas
et refuserait de demarrer). Presets existants inchanges ; l'editeur dit desormais
« non precise : le modele decide » quand la cle est absente.
- start/restart : preflight (BIN, MODEL, .gguf) et fin du faux « [ok] activating »
sur un service qui redemarre en boucle (SubState auto-restart).
- MODEL en chemin absolu accepte hors des dossiers declares (lecture seule) ;
l'allowlist reste sur la suppression de fichiers.
- ajean edit : squelette commente des cles au lieu d'un fichier quasi vide.
- install : les prochaines etapes n'oublient plus « ajean llamacpp install ».
- Presets : un nouveau preset ne recopie plus la config active (issue #17),
seulement BIN/HOST/PORT.
- Presets : plus de guillemet interne mange a la relecture (Go et interface),
et EXTRA_ARGS decoupe en respectant les guillemets.
Deux defauts trouves a l'usage :
1. Impossible de saisir la cle de pilotage dans l'UI : le chargement lance une
dizaine d'appels /api/* en parallele, chaque 401 rouvrait la modale, et
celle-ci vide son champ a l'ouverture. Les caracteres disparaissaient au fur
et a mesure de la frappe. Une seule demande de cle est desormais partagee par
tous les appels concurrents. Au passage, une modale ouverte n'est plus
ecrasee en silence (sa promesse restait pendante pour toujours).
2. migrate_07 n'importait pas .authorized_users : les identites appairees pour
le chat chiffre. Symptome trompeur — le tunnel s'etablit, la machine
s'affiche en ligne, mais le portail refuse toute conversation. Importe et
couvert par un test.
Le champ 'config' de /api/paths est devenu 'database' : la ligne Configuration
s'affichait vide. Le panneau montre desormais la base, les modeles, les presets,
la memoire, le workspace et les backends.
- suppression des fonctions et types inutilises (staticcheck U1000)
- sources UI, workflows, README passes en ajean
- release.yml ne publie plus que les binaires ajean-*
- notes de release redigees
- suppression des migrations d'agencement, d'unites et de dossier de donnees
- store bbolt unique (ajean.db) : config, prefs, conversation, cles, drapeaux
- arborescence backends/ bin/ presets/ memory/ models/ workspace/
- plus aucune variable, unite ou asset nomme jean
- Preset actif : liseré gauche droit, rogné par l'arrondi de la carte, un peu plus large
- Puce de l'actif dessinée en CSS (même taille et même assiette sur Windows et macOS)
- Survol et sélection moins appuyés dans la liste
- Bascule : retour visuel immédiat (barre qui glisse depuis la gauche, atténuée puis
pleine) ; animations rejouées seulement quand la sélection change vraiment
- /api/switch répond sans attendre le redémarrage du service : bascule instantanée
- Bouton envoyer et touche Entrée désactivés tant que le modèle n'est pas chargé
Le remplacement du binaire renommait l'ancien en un nom FIXE (.old). Si un
processus tournait encore depuis ce .old (mise a jour precedente), le fichier
n'etait ni supprimable ni ecrasable : le renommage sortait en "Acces refuse"
(errno 5), que os.IsPermission rapporte comme un defaut de DROITS. L'UI
affichait donc "droits insuffisants, relance en administrateur", conseil faux :
aucun privilege ne remplace l'image d'un executable en cours d'execution.
- renameAside() ecarte sous un nom UNIQUE (.old-<horodatage>) : le renommage
passe meme si un ancien ecartement est verrouille (verifie sur banc d'essai
avec deux processus vivants)
- les trois endroits qui faisaient la manoeuvre l'utilisent (update, install,
premier lancement), et le menage ramasse le nom fixe herite + les uniques
- message d'erreur : Windows renvoie le meme code pour "droits insuffisants" et
"fichier utilise", les deux causes sont donc nommees
Aussi : la section Cartes graphiques restait affichee avec une seule carte.
L'attribut hidden etait sans effet, .pe-group{display:flex} l'emportant sur la
regle par defaut du navigateur.
- moteur precompile : la version installee est celle qui tourne (on elisait le
dossier le plus ancien), et les anciennes extractions sont purgees
- presets : le moteur precompile reste reconnu apres une mise a jour, et un BIN
pointant une release disparue suit la version installee au lieu d'echouer
- build CUDA : le toolkit est cherche avant le nvcc du PATH, sa racine est
passee a CMake (fin du "CUDA Toolkit not found" avec CUDA installe)
- job d'installation : etat persiste sur disque, une operation interrompue par
un redemarrage du service est annoncee, pastille de rappel sur mobile
- editeur de modele : section Cartes graphiques (--device / --tensor-split), la
liste vient du moteur du preset car les identifiants dependent du backend
- reglages : plus de menu replie, interrupteurs regroupes, decodage speculatif
- CUDA_VISIBLE_DEVICES et WEB_ENGINE survivent au changement de preset, sauf si
le preset les definit lui-meme
Moteur web integre en pur Go (http + readability + html-to-markdown),
selectionnable par WEB_ENGINE. Crawl4AI reste disponible pour le rendu
JavaScript et reste le defaut des installations qui l'ont deja configure.
Le schema de web_open n'annonce plus actions/wait_for/dismiss_popups avec
le moteur integre, et une page riche en HTML mais sans texte est diagnostiquee
comme rendue en JavaScript, pour que le modele change de source au lieu de
rouvrir la meme URL en boucle.
Le renommage des dossiers avait ete repercute dans release.yml mais pas
dans ci.yml, qui compilait encore ./cmd/jean : les deux jobs echouaient.
Une occurrence trainait aussi dans mac-build.yml (cmd/jean/icon.ico), que
le remplacement precedent n'avait attrapee que pour icon.png.
Un cas n'etait pas qu'un commentaire : tools/assemble-ui utilisait
"internal/jean/ui" comme chemin par defaut. La directive go:generate passe
le dossier en argument, donc la generation fonctionnait, mais l'invocation
directe documentee dans son propre en-tete (go run ./tools/assemble-ui)
aurait echoue.
Le reste etait des commentaires designant les anciens chemins, corriges
pour que le depot ne mente plus sur sa propre structure.
Job ci rejoue en entier en local : gofmt, vet, build, tests et les quatre
cross-compilations de release.
Surface visible passee en AJEAN : aide de la CLI, messages, invite du chat
terminal, commentaires, README, metadonnees Windows du binaire
(InternalName / OriginalFilename).
Les reglages s'appellent desormais AJEAN_* (AJEAN_HOME, AJEAN_MODEL_DIRS,
AJEAN_DL_CONNS, AJEAN_LINK_URL, AJEAN_ACME_EMAIL, AJEAN_SERVICE), avec
l'ancien nom JEAN_* toujours lu en second via envAny(). Ces variables sont
posees a la main dans des shells, des units, des docker-compose et des
scripts de deploiement qu'on ne peut pas retrouver : cesser de les lire
ferait retomber silencieusement un reglage sur sa valeur par defaut.
Migration one-shot du dossier de donnees retentee dans la fenetre ou
l'installateur Windows a arrete toutes les instances, seule fenetre ou le
rename peut aboutir. Le chemin du binaire est recalcule juste apres, sinon
on reinstallerait dans le dossier qu'on vient de deplacer.
Deux choses volontairement NON renommees, parce que les renommer casserait
des utilisateurs sans rien apporter :
- "model": "jean", le nom expose sur l'endpoint compatible OpenAI. Des
integrations externes le configurent en dur.
- les cles localStorage (jean-theme, jean.sys...). Les renommer
reinitialiserait theme, preferences d'affichage et prompt systeme dans le
navigateur de chaque utilisateur.
Le plist launchd recevait /usr/local/bin/jean en dur : le chemin est
desormais injecte, sinon le daemon macOS aurait refuse de demarrer apres le
renommage.
index.html regenere depuis ui/src via go generate.
Socle du renommage jean -> AJEAN, sans aucun impact visible pour le parc
installe. Trois briques :
- module github.com/nathaninline/ajean, cmd/ajean, internal/ajean,
package ajean. Purement interne.
- AjeanHome() remplace JeanHome(). Les defauts deviennent
%ProgramData%\ajean et /etc/ajean, avec migration du dossier existant
au premier lancement. La migration est un os.Rename dans le dossier
parent de l'ancien chemin : intra-volume, donc atomique et instantane
meme avec des .gguf de plusieurs dizaines de Go. Si le rename echoue
(handle ouvert sous Windows, droits, volume en lecture seule) on
CONTINUE sur l'ancien chemin et on retentera : jamais de copie, jamais
de suppression, le pire cas est "rien n'a bouge".
$AJEAN_HOME et $JEAN_HOME restent tous deux honores, ainsi que
/etc/default/ajean et /etc/default/jean : un chemin impose a la main
est un ordre, on ne le migre pas.
- l'auto-MAJ accepte desormais ajean-<os>-<arch> ET jean-<os>-<arch>,
dans cet ordre. Indispensable dans les deux sens : les binaires deja
installes ne connaissent que jean-*, et ce binaire-ci doit pouvoir
s'installer depuis une release anterieure au renommage.
Couvert par des tests, dont le repli quand un handle ouvert bloque le
rename sous Windows.