Mnemosyne OS
Nuevo La documentación explica cada motor, paso a paso. docs.mnemosyne-os.io →
← Todos los artículos

Presente no es lo mismo que cargable

Publicado el 10 de agosto de 2026 7 min de lectura

electronpackagingesmpnpmcipostmortem

Un paquete puede estar dentro del archivo, en la versión correcta. No falta nada en el disco. El código que lo importa sigue sin poder alcanzarlo. Todos nuestros controles de empaquetado eran ciegos a esta clase de bug.

Lo aprendimos de la forma habitual. Mnemosyne OS v1.3.7 salió el 10 de agosto con una promesa simple: todo el bucle funciona en tu máquina. Modelo, memoria, búsqueda, sin clave, sin cuenta. En la aplicación instalada, no cargaba ningún modelo local.

Esta entrada cuenta cómo la versión superó todos nuestros controles y aun así se publicó rota. Nombra los tres defectos. Describe el test de diez segundos que ahora corre entre un paquete y su firma. Si alguna vez has visto un ERR_MODULE_NOT_FOUND aparecer solo en producción, ya conoces el final. Lo interesante es por qué ninguno de nuestros controles podía verlo venir.

Qué cambiaba esta versión

La 1.3.7 importaba por una sola cifra. Esa cifra llevaba tiempo arruinando en silencio la experiencia local. Una máquina de 4 GB elige por defecto el modelo local de entrada. Antes de esta versión ese modelo era Phi-3 Mini, con una ventana de contexto de 4096 tokens. Mnemosyne OS ancla sus respuestas en los recuerdos que recupera, y esos recuerdos forman parte del prompt. En la primera pregunta anclada en memoria, el contexto se desbordaba. La promesa «funciona sin conexión» era cierta sobre el papel y falsa en la práctica.

El catálogo local renovado trae 8 modelos con ventanas de contexto de 262k tokens. La versión descansa sobre ese contraste: de 4k a 262k. No es un argumento de rendimiento. Un modelo local pequeño sigue recordando peor que uno grande en la nube, y no vamos a fingir lo contrario. Pero una ventana de 262k puede sostener tus recuerdos dentro del prompt. Una ventana de 4k no podía.

La misma versión arregla otras tres cosas:

  • En BYOK, la lista de modelos viene ahora del propio proveedor. Seis proveedores estaban muertos porque pasábamos un identificador de catálogo donde la API esperaba un nombre de modelo. El endpoint no conoce nuestros identificadores internos.
  • Un registro local del coste de cada llamada, a las tarifas que introduces tú. La aplicación no incorpora ninguna tabla de precios. Los precios cambian, y una tabla obsoleta con aire de autoridad es peor que ninguna tabla.
  • Gobernanza de las bóvedas: marcar una bóveda como MAXIMUM mantiene por fin cerrada de verdad la ruta hacia la nube.

El primer clic

Entonces la aplicación instalada intentó cargar un modelo:

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

Encontramos tres defectos. Los tres tienen la misma forma: algo está presente, pero no donde el código que lo necesita va a buscarlo.

1. asarUnpack cubría el paquete, no su cierre de dependencias

Desempaquetamos node-llama-cpp porque lleva binarios nativos. Desempaquetado, su dist/index.js se ejecuta desde app.asar.unpacked y resuelve sus imports contra el sistema de ficheros real. Nunca puede alcanzar una dependencia que se quedó dentro de app.asar. Si desempaquetas un paquete sin su árbol transitivo, el módulo falla en el primer eslabón que siguió empaquetado.

Esa misma regla ya estaba escrita en el mismo fichero de configuración, doce líneas más arriba, para el cierre de otro paquete. Nadie la había aplicado aquí. Faltaban ochenta y ocho entradas. Cada una podía deducirse del grafo resuelto.

2. El script de aplanado de pnpm se quedaba con una versión y descartaba el resto en silencio

Nuestro script aplana un store de pnpm en un node_modules corriente para el empaquetado. Cuando existían dos versiones de un mismo nombre, se quedaba con la primera que encontraba y contaba todas las demás como «ya presente». Decidía el orden de directorios, es decir, el alfabeto.

signal-exit@3.0.7 (CommonJS) ganó a 4.1.0 (ESM). El consumidor que necesita la exportación nombrada de la v4 murió con:

Named export 'onExit' not found

Ambos consumidores tenían razón. Uno quiere ^3, el otro ^4, y ninguna versión única sirve a los dos. El arreglo es lo que npm hace desde siempre: anidar la versión que satisface bajo cada dependiente. El script ahora también dice qué anidó, en lugar de incrementar un contador mudo. La primera pasada reportó 8 conflictos. Uno de ellos era node-llama-cpp, que pedía un lifecycle-utils más reciente que el conservado por el árbol plano.

3. Un cierre calculado antes del anidado no ve lo que el anidado introduce

Anidar un paquete en v7 arrastró una dependencia transitiva que ninguna pasada anterior había visto. Esa dependencia quedó empaquetada. Este defecto apareció después de corregir los defectos 1 y 2, con el binario ya declarado bueno. Apareció dos veces.

Todos los controles preguntaban «¿está el fichero?»

Esa es la lección de verdad, y va mucho más allá de Electron.

La 1.3.7 pasó un control de asar sellado, uno de tamaño y uno de firma. Cada uno hacía una pregunta de presencia. Ninguno preguntaba si la cosa carga. Presencia y resolubilidad son dos propiedades distintas. Los bugs de empaquetado viven en el hueco entre ambas.

Así que escribimos el control que hace la otra pregunta. Importa el punto de entrada nativo desde el paquete construido, exactamente como hará Electron. Eso es una resolución ESM contra el sistema de ficheros real, la misma resolución que no puede alcanzar una dependencia atrapada dentro del archivo. El control tarda diez segundos. No necesita instalación ni GPU, porque importar el módulo no carga ningún modelo. Una salida distinta de cero hace fallar el job, antes de la firma. Está conectado a los tres jobs de compilación (Windows, macOS, Linux). Localiza app.asar.unpacked buscándolo en el directorio de release, porque las tres plataformas lo entierran en tres sitios distintos. Una ruta fija por job estaría a un renombrado de convertirse en un control que no comprueba nada.

El test negativo reproduce el fallo publicado palabra por palabra. Queríamos ver fallar el control una vez antes de fiarnos de él.

Qué hicimos con la versión ya publicada

Pasamos la release a pre-release unos veinte minutos después de su publicación. Nuestro updater ignora las pre-releases. Desde ese momento, a ninguna instalación se le ofreció el binario roto. Los artefactos de macOS y Linux se descargaron cero veces antes de retirarlos.

Desde entonces hemos reconstruido todos los instaladores con los tres defectos corregidos y el test de importación en su sitio. La 1.3.7 vuelve a ser pública.

Lo que vimos mientras la máquina se ahogaba

La misma semana nos dejó otra observación, y va en el otro sentido.

Una sola máquina de 31 GB con una RTX 4050 ejecutaba todo esto a la vez. Una pasada larga de benchmark estaba en marcha. El propio OS tenía un modelo 2B residente en la GPU. 26 bóvedas se estaban montando, el pipeline de embeddings trabajaba y había compilaciones de Electron en curso. Quedaban unos 5 GB de RAM. Los logs muestran lo que eso cuesta:

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

La máquina estaba estrangulada y la inferencia lo sufrió. No funcionaba cómodamente bajo carga. Lo que nos importa es cómo lo sufrió. El muestreador de métricas se rindió y lo dijo. Las bóvedas siguieron montándose. La inferencia llegó hasta el final, y la respuesta apareció. El sistema se degradó de forma legible en vez de morir. Mnemosyne OS promete que puedes comprobar qué recuerda. Así que su forma de fallar forma parte del producto.

Un test que añadir a tu propia compilación

Si empaquetas un módulo nativo en una aplicación de escritorio, añade un test. Importa tu punto de entrada real desde el artefacto que estás a punto de firmar, en el mismo sistema de módulos que usa tu runtime. Un listado de ficheros no hace eso. Un import sí.

El nuestro cuesta diez segundos y habría cazado los tres defectos.

Mnemosyne OS es un OS de memoria local-first. El binario actual está en la página de descarga.

Compartir

Citar este artículo

Una entrada de blog no tiene DOI. La cita apunta a la página.

APA

Mnemosyne OS. (10 de agosto de 2026). Presente no es lo mismo que cargable. Mnemosyne OS. https://mnemosyne-os.io/es/blog/a-release-that-could-not-load-a-model

BibTeX
@misc{mnemosyne-a-release-that-could-not-load-a-model,
  author       = {{Mnemosyne OS}},
  title        = {Presente no es lo mismo que cargable},
  year         = {2026},
  month        = {8},
  day          = {10},
  howpublished = {Blog post, Mnemosyne OS},
  url          = {https://mnemosyne-os.io/es/blog/a-release-that-could-not-load-a-model}
}