Mnemosyne OS
New The documentation explains every engine, step by step. docs.mnemosyne-os.io →
← All posts

Present is not the same as loadable

Published August 10, 2026 6 min read

electronpackagingesmpnpmcipostmortem

A package can sit inside the archive at the right version. Nothing is missing from disk. The code that imports it still cannot reach it. Every packaging check we had was blind to this class of bug.

We learned it the usual way. Mnemosyne OS v1.3.7 went out on August 10th with a simple headline: the whole loop runs on your machine. Model, memory, retrieval, no key, no account. In the installed app, it loaded no local model at all.

This post tells how the release passed every gate we had and still shipped broken. It names the three defects. It describes the ten-second test that now runs between a package and its signature. If you have ever watched an ERR_MODULE_NOT_FOUND appear only in production, you already know the ending. The interesting part is why none of our checks could see it coming.

What the release was about

Version 1.3.7 mattered because of one number. That number had been quietly ruining the local experience. A 4 GB machine picks the entry-level local model by default. Until this release that model was Phi-3 Mini, with a 4096-token context window. Mnemosyne OS grounds an answer in the memories it retrieves, and those memories are part of the prompt. On the very first memory-grounded question, the context overflowed. The promise “it works offline” was true on paper and false in practice.

The refreshed local catalogue ships 8 models with 262k-token context windows. The release rests on that contrast: 4k to 262k. This is not a performance claim. A small local model still recalls less well than a large cloud one, and we will not pretend otherwise. But a 262k window can hold your memories in the prompt. A 4k window could not.

The same release fixed three other things:

  • BYOK model lists now come from the provider itself. Six providers were dead because we passed a catalogue id where the API expected a model name. The endpoint does not know our internal identifiers.
  • A local journal of what each call costs, at rates you enter yourself. The app ships no price table. Prices change, and a stale table that looks authoritative is worse than no table.
  • Vault governance: marking a vault MAXIMUM now actually holds the cloud route closed.

The first click

Then the installed app tried to load a model, and:

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

We found three defects. All three have the same shape: something is present, but not where the code that needs it goes looking.

1. asarUnpack covered the package, not its dependency closure

We unpack node-llama-cpp because it has native binaries. Unpacking means its dist/index.js runs from app.asar.unpacked, and resolves its imports against the real filesystem. It can never reach a dependency that stayed inside app.asar. If you unpack a package without its transitive tree, the module fails at the first packed link.

That exact rule was already written in the same config file, twelve lines above, for another package’s closure. Nobody had applied it here. Eighty-eight entries were missing. Every one of them could be derived from the resolved graph.

2. The pnpm flattening script kept one version and dropped the rest in silence

Our script flattens a pnpm store into a plain node_modules for packaging. When two versions of a name existed, it kept the first one it met and counted every other as “already present”. Directory order decided, which means the alphabet decided.

signal-exit@3.0.7 (CommonJS) beat 4.1.0 (ESM). The consumer that needs the named export from v4 died with:

Named export 'onExit' not found

Both consumers were right. One wants ^3, the other ^4, and no single version serves both. The fix is what npm has always done: nest the satisfying version under each dependent. The script now also says what it nested, instead of incrementing a mute counter. The first run reported 8 conflicts. One of them was node-llama-cpp, which needed a newer lifecycle-utils than the flat tree had kept.

3. A closure computed before nesting misses what nesting introduces

Nesting a v7 package pulled in a transitive dependency that no earlier pass had seen. That dependency was left packed. This defect appeared after defects 1 and 2 were fixed and the build had been declared good. It appeared twice.

Every gate asked “is the file there?”

This is the real lesson. It applies far beyond Electron.

v1.3.7 passed a sealed-asar check, a size check and a signature check. Each of them asked a presence question. None of them asked whether the thing loads. Presence and resolvability are two different properties. Packaging bugs live in the gap between them.

So we wrote the check that asks the other question. It imports the native entry point from the built package, exactly as Electron will. That means ESM resolution against the real filesystem, the same resolution that cannot reach a dependency stranded inside the archive. The check takes ten seconds. It needs no install and no GPU, because importing the module does not load a model. A non-zero exit fails the job, before signing. It is wired into all three build jobs (Windows, macOS, Linux). It locates app.asar.unpacked by searching the release directory, because the three platforms bury it in three different places. A hardcoded path per job would be one rename away from a gate that silently checks nothing.

The negative test reproduces the shipped failure word for word. We had to see the guardrail fail once before we trusted it.

What we did about the release already out there

We switched the release to pre-release roughly twenty minutes after publication. Our updater ignores pre-releases. From that moment, no installation was offered the broken build. The macOS and Linux artifacts were downloaded zero times before we pulled them.

We have since rebuilt every installer with the three defects fixed and the import check in place. v1.3.7 is public again.

What we saw while the machine was choking

The same week gave us one more observation, and it points the other way.

One machine with 31 GB of RAM and an RTX 4050 was running all of this at once. A long benchmark run was in progress. The OS itself had a 2B model resident on the GPU. 26 vaults were mounting, the embedding pipeline was working, and Electron builds were running. Roughly 5 GB of RAM was left. The logs show what that costs:

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

The machine was throttled, and inference suffered for it. It did not run comfortably under load. What we care about is how it suffered. The metrics sampler gave up and said so. Vaults kept mounting. Inference ran to completion, and the answer arrived. The system degraded legibly instead of dying. Mnemosyne OS promises that you can check what it remembers. So the way it fails is part of the product.

One test to add to your own build

If you package a native module into a desktop app, add one test. It imports your real entry point from the artifact you are about to sign, in the same module system your runtime uses. A file listing does not do this. An import does.

Ours cost ten seconds and would have caught all three defects.

Mnemosyne OS is a local-first memory OS. The current build is on the download page.

Share

Cite this post

A blog post has no DOI. This citation points to the page itself.

APA

Mnemosyne OS. (August 10, 2026). Present is not the same as loadable. Mnemosyne OS. https://mnemosyne-os.io/blog/a-release-that-could-not-load-a-model

BibTeX
@misc{mnemosyne-a-release-that-could-not-load-a-model,
  author       = {{Mnemosyne OS}},
  title        = {Present is not the same as loadable},
  year         = {2026},
  month        = {8},
  day          = {10},
  howpublished = {Blog post, Mnemosyne OS},
  url          = {https://mnemosyne-os.io/blog/a-release-that-could-not-load-a-model}
}