Mnemosyne OS
Nouveau La documentation est en ligne : chaque moteur, pas à pas. docs.mnemosyne-os.io →
← Tous les articles

Présent ne veut pas dire chargeable

Publié le 10 août 2026 7 min de lecture

electronpackagingesmpnpmcipostmortem

Un paquet peut être là, dans l’archive, à la bonne version, sans rien qui manque sur le disque, et rester introuvable pour le code qui l’importe. C’est la classe de bug qui vit dans l’angle mort de tous les contrôles d’empaquetage.

Nous l’avons appris de la façon habituelle. Mnemosyne OS v1.3.7 est sortie le 10 août avec une promesse simple : toute la boucle tourne sur votre machine. Modèle, mémoire, récupération, sans clé, sans compte. Dans l’application installée, elle ne chargeait aucun modèle local.

Voici comment une version a franchi tous nos garde-fous et s’est quand même publiée cassée, quels étaient les trois défauts, et quel test de dix secondes se tient désormais entre un paquet et sa signature. Si vous avez déjà vu un ERR_MODULE_NOT_FOUND n’apparaître qu’en production, vous connaissez déjà la fin ; l’intéressant est de comprendre pourquoi aucun de nos contrôles ne pouvait le voir venir.

Ce que cette version apportait vraiment

Ce qui rendait la 1.3.7 importante, c’est un chiffre qui abîmait silencieusement l’expérience locale. Notre précédent modèle d’entrée de gamme, celui qu’une machine à 4 Go choisit par défaut, était Phi-3 Mini, avec une fenêtre de contexte de 4096 tokens. Mnemosyne ancre ses réponses dans les souvenirs qu’elle récupère, et ces souvenirs font partie du prompt. Dès la première question ancrée en mémoire, le contexte débordait. La promesse « ça marche hors ligne » était techniquement vraie et pratiquement fausse.

Le catalogue local rafraîchi embarque 8 modèles avec des fenêtres de contexte de 262k tokens. C’est le contraste sur lequel repose cette version : 4k à 262k. Ce n’est pas un argument de performance, un petit modèle local se souvient toujours moins bien qu’un gros modèle cloud, et nous n’allons pas prétendre le contraire, mais c’est la différence entre une boucle capable de contenir vos souvenirs et une boucle qui n’y arrivait pas.

La même version corrige trois autres points qui méritent d’être nommés :

  • En BYOK, la liste des modèles vient désormais du fournisseur lui-même. Six fournisseurs étaient morts parce que nous passions un identifiant de catalogue là où un nom de modèle d’API était attendu. Un identifiant interne n’est pas le vocabulaire d’un endpoint.
  • Un journal local du coût de chaque appel, aux tarifs que vous saisissez vous-même. Aucune table de prix n’est embarquée dans l’application, les prix changent, et une table périmée qui a l’air faisant autorité est pire que pas de table du tout.
  • La gouvernance des coffres : marquer un coffre MAXIMUM tient enfin réellement la route cloud fermée.

Le premier clic

Puis l’application installée a tenté de charger un modèle :

Cannot find package 'lifecycle-utils' imported from
  …/app.asar.unpacked/node_modules/node-llama-cpp/dist/index.js

Trois défauts, tous de la même forme : quelque chose est présent, mais pas là où le code qui en a besoin va le chercher.

1. asarUnpack couvrait le paquet, pas sa fermeture de dépendances

Nous dépaquetons node-llama-cpp parce qu’il embarque des binaires natifs. Dépaqueté, son dist/index.js s’exécute depuis app.asar.unpacked, et résout ses imports sur le vrai système de fichiers. Il ne peut jamais atteindre une dépendance restée dans app.asar. Dépaqueter un paquet sans dépaqueter son arbre transitif donne un module qui échoue au premier maillon resté empaqueté.

Le plus rageant : cette règle exacte était déjà écrite dans le même fichier de config, douze lignes plus haut, pour la fermeture d’un autre paquet. Elle n’avait simplement jamais été appliquée ici. Quatre-vingt-huit entrées manquaient, toutes déductibles du graphe résolu.

2. Le script d’aplatissement pnpm élisait une version et jetait les autres, en silence

Notre script aplatit un store pnpm en un node_modules ordinaire pour le packaging. Quand deux versions d’un même nom existaient, il gardait la première rencontrée et comptait toutes les autres comme « déjà présente ». L’arbitre était l’ordre des répertoires, c’est-à-dire l’alphabet.

signal-exit@3.0.7 (CommonJS) a battu 4.1.0 (ESM), et le consommateur qui a besoin de l’export nommé de la v4 est mort avec :

Named export 'onExit' not found

Les deux consommateurs avaient raison. L’un exige ^3, l’autre ^4, et aucune version unique ne peut servir les deux. Le correctif est ce que npm fait depuis toujours : imbriquer la version satisfaisante sous chaque dépendant, et dire ce qui a été imbriqué, au lieu d’incrémenter un compteur muet. Le premier passage a signalé 8 conflits, dont node-llama-cpp qui réclamait un lifecycle-utils plus récent que celui gardé par l’arbre aplati.

3. Une fermeture calculée avant l’imbrication rate ce que l’imbrication introduit

L’imbrication d’un paquet en v7 a tiré une dépendance transitive qu’aucun passage précédent n’avait vue, et qui est donc restée empaquetée. Celle-là est apparue après la correction des défauts 1 et 2, alors que le build avait été déclaré bon. Deux fois.

Tous les garde-fous demandaient « le fichier est-il là ? »

C’est la vraie leçon, et elle ne parle pas d’Electron.

La 1.3.7 a passé un contrôle d’asar scellé, un contrôle de taille et un contrôle de signature. Chacun posait une question de présence. Aucun ne demandait si la chose se charge. Présence et résolvabilité sont deux propriétés différentes, et les bugs de packaging vivent exactement dans l’écart entre les deux.

Nous avons donc écrit le contrôle qui pose l’autre question. Il importe le point d’entrée natif depuis le paquet construit, exactement comme le fera Electron : résolution ESM contre le vrai système de fichiers, ce qui est précisément ce qui rend inatteignable une dépendance restée prisonnière de l’archive. Il prend dix secondes, ne demande aucune installation ni GPU, importer le module ne charge pas de modèle. Une sortie non nulle fait échouer le job, avant la signature. Il est câblé dans les trois jobs de build (Windows, macOS, Linux), et il localise app.asar.unpacked en cherchant dans le répertoire de release, parce que les trois plateformes l’enterrent à trois endroits différents et qu’un chemin codé en dur par job est à un renommage près d’un garde-fou qui ne vérifie plus rien.

Le test négatif reproduit l’erreur publiée au mot près. Un garde-fou que vous n’avez jamais vu échouer est un garde-fou que vous n’avez pas testé.

Ce que nous avons fait de la version déjà en ligne

La release a été basculée en pre-release environ vingt minutes après sa publication. Notre updater ignore les pre-releases : à partir de cet instant, aucune installation ne s’est vu proposer le build cassé. Les artefacts macOS et Linux ont été téléchargés zéro fois avant d’être retirés.

Tous les installeurs ont depuis été reconstruits avec les trois défauts corrigés et le test d’import en place, et la 1.3.7 est de nouveau publique.

Ce que nous avons vu pendant que la machine étouffait

Une observation de la même semaine mérite d’être rapportée, parce qu’elle va dans l’autre sens.

Sur une seule machine à 31 Go avec une RTX 4050, tournaient en même temps : un long run de benchmark, l’OS lui-même avec un modèle 2B résident sur le GPU, 26 coffres en cours de montage, le pipeline d’embedding, et des builds Electron. Environ 5 Go de RAM libre. Les logs montrent exactement ce que ça coûte :

event-loop stall: 34355ms
metrics.worker: no reply within 10000ms

Ce n’est pas une histoire de « ça tourne confortablement sous charge ». La machine était étranglée et l’inférence en a souffert. Ce qui nous intéresse, c’est comment elle en a souffert : l’échantillonneur de métriques a abandonné en le disant, les coffres ont continué à se monter, l’inférence est allée au bout, et la réponse est arrivée. Ça a dégradé lisiblement au lieu de mourir. Pour un système dont toute la proposition est que vous pouvez vérifier ce qu’il retient, le mode de défaillance fait partie du produit.

Ce qui vaut la peine d’être volé

Si vous empaquetez un module natif dans une application de bureau, ajoutez un test qui importe votre vrai point d’entrée depuis l’artefact que vous vous apprêtez à signer, dans le même système de modules que celui de votre runtime. Pas une liste de fichiers. Un import.

Le nôtre coûte dix secondes et aurait attrapé les trois défauts.

Mnemosyne OS est un OS de mémoire local-first. Le build courant est sur la page de téléchargement.

Partager

Citer cet article

Pas de DOI, un billet de blog n'est pas un dépôt. La citation porte sur la page elle-même.

APA

Mnemosyne OS. (10 août 2026). Présent ne veut pas dire chargeable. Mnemosyne OS. https://mnemosyne-os.io/fr/blog/a-release-that-could-not-load-a-model

BibTeX
@misc{mnemosyne-a-release-that-could-not-load-a-model,
  author       = {{Mnemosyne OS}},
  title        = {Présent ne veut pas dire chargeable},
  year         = {2026},
  month        = {8},
  day          = {10},
  howpublished = {Blog post, Mnemosyne OS},
  url          = {https://mnemosyne-os.io/fr/blog/a-release-that-could-not-load-a-model}
}