Daeseon Yoo
Back to project
·Troubleshoot·2 min

The installed app couldn't load its own model — and why 'works in dev' was the tell

The operating-memory wedge worked perfectly in dev and was dead on every install. The cause was one missing argument: fastembed's cache defaulted to a path relative to the working directory, and a Finder-launched .app has cwd /.

The screenshot

The founder opened the installed app, hit ⌘K, and typed "최근에 누가 메일줬지" (who emailed me recently). Back came:

❌ 기억 검색에 실패했어요 — 잠시 후 다시 시도해 주세요.
(embedding model load failed: Failed to retrieve onnx/model.onnx)

Every memory_answer died the same way. The entire operating-memory wedge — the product's whole reason to exist — was dead on the installed build. And it had passed 214 tests and worked flawlessly in pnpm tauri dev.

"Works in dev, dead on install" is a diagnosis, not a mystery

That gap is the whole clue. Something about the packaged runtime differs from the dev runtime. Two usual suspects: environment variables (we've been bitten there before) and — this time — the current working directory.

The error came from fastembed, the local embedding engine. It downloads a ~90MB ONNX model and caches it on disk. The model-load code:

TextEmbedding::try_new(
    InitOptions::new(EmbeddingModel::ParaphraseMLMiniLML12V2)
        .with_show_download_progress(true),
)

No cache_dir. So fastembed uses its default: ./.fastembed_cacherelative to the process's working directory.

Diagnosing on the real machine, not guessing

Before changing anything, three checks on the founder's own Mac:

So: the network's fine, the model just can't be stored. Why? A Finder/Dock-launched macOS .app runs with cwd /. fastembed tries to write /.fastembed_cache, which isn't writable, and the failed store surfaces as "Failed to retrieve onnx/model.onnx". tauri dev works only because its cwd is the writable crate directory, where ./.fastembed_cache happily appears.

The fix is one argument

let cache = dirs::data_dir()
    .map(|d| d.join("DalkkakAI").join("models"))
    .unwrap_or_else(|| PathBuf::from(".fastembed_cache"));
let _ = std::fs::create_dir_all(&cache);
TextEmbedding::try_new(
    InitOptions::new(EmbeddingModel::ParaphraseMLMiniLML12V2)
        .with_cache_dir(cache)
        .with_show_download_progress(true),
)

Pin the cache to the app data dir. The model downloads once and stays.

The lesson

A desktop app's working directory is not your project folder — it's /. Any dependency that defaults a cache, temp, or database path relative to cwd is a latent install-only bug that dev will never show you, because dev's cwd is writable. Pin every on-disk path the app writes to an explicit app-data location, as a reflex.

And the meta-lesson, again: the test suite was green and the dev app was green while the shipped product's core feature was a brick. You only find these by dogfooding the actual installed artifact.

This landed inside a wider usability pass — promoting the connect-a-source flow out of a dev-only console, turning the first-run onboarding from a terminal tutorial into "connect → ask → act", and making incoming work appear live across services by area instead of on a 30-second poll. But none of that matters if the engine can't load. First, make the model load.