From 2cb6ff38aa7149ea30869c16e1018370ad4b2370 Mon Sep 17 00:00:00 2001 From: roboomp Date: Thu, 18 Jun 2026 02:22:00 +0000 Subject: [PATCH] fix(mnemopi): forced fastembed onnxruntime override - Forced the on-demand fastembed runtime manifest to override fastembed's archived onnxruntime-node 1.21.0 transitive dependency with the package's pinned 1.26.0 runtime.\n- Treated native dlopen failures from the direct fastembed import as recoverable so macOS ARM64 can fall back to the controlled runtime cache.\n- Added coverage for the runtime install plan and cache key policy.\n\nFixes #2920 --- packages/mnemopi/CHANGELOG.md | 4 ++ .../mnemopi/src/core/fastembed-runtime.ts | 41 ++++++++++++++----- .../mnemopi/test/fastembed-runtime.test.ts | 17 +++++++- 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/packages/mnemopi/CHANGELOG.md b/packages/mnemopi/CHANGELOG.md index c6ddf71c2..844583a98 100644 --- a/packages/mnemopi/CHANGELOG.md +++ b/packages/mnemopi/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Forced the on-demand fastembed runtime install to override fastembed's archived `onnxruntime-node@1.21.0` transitive pin with Mnemopi's `onnxruntime-node@1.26.0` pin, fixing local embedding startup on macOS ARM64. ([#2920](https://github.com/can1357/oh-my-pi/issues/2920)) + ## [16.0.6] - 2026-06-18 ### Changed diff --git a/packages/mnemopi/src/core/fastembed-runtime.ts b/packages/mnemopi/src/core/fastembed-runtime.ts index a1e8277e6..f04cb5593 100644 --- a/packages/mnemopi/src/core/fastembed-runtime.ts +++ b/packages/mnemopi/src/core/fastembed-runtime.ts @@ -5,6 +5,7 @@ import { getFastembedRuntimeDir, installRuntimeModuleResolver, logger, + type RuntimeInstallSpec, resolveRuntimeModule, } from "@oh-my-pi/pi-utils"; import type * as Fastembed from "fastembed"; @@ -12,13 +13,21 @@ import packageManifest from "../../package.json" with { type: "json" }; type FastembedModule = typeof Fastembed; +/** Runtime install inputs for the optional fastembed embedding stack. */ +export interface FastembedRuntimeInstallPlan { + /** Cache directory key; changes when runtime resolution policy changes. */ + versionKey: string; + /** Dependency graph written to the runtime cache package manifest. */ + install: RuntimeInstallSpec; +} + /** * `fastembed` and `onnxruntime-node` are optional peers (~270MB of native * assets across platforms), never bundled and never installed eagerly. When * the direct import cannot resolve — bundled `dist/cli.js`, compiled binary, - * or a consumer that skipped the optional peers — the pinned pair is - * `bun install`ed into a per-version runtime cache on first use and loaded - * from there (#2389). + * a consumer that skipped the optional peers, or a native loader failure from + * fastembed's nested ORT — the pinned pair is `bun install`ed into a + * per-version runtime cache on first use and loaded from there (#2389, #2920). * * The pins live in `peerDependencies` as exact versions (not `catalog:`) so * this module reads concrete specs even when the workspace manifest is @@ -27,6 +36,16 @@ type FastembedModule = typeof Fastembed; const FASTEMBED_SPEC = packageManifest.peerDependencies.fastembed; const ORT_SPEC = packageManifest.peerDependencies["onnxruntime-node"]; +/** Build the deterministic fastembed runtime install plan used by local embeddings. */ +export function fastembedRuntimeInstallPlan(): FastembedRuntimeInstallPlan { + return { + versionKey: `fastembed-${FASTEMBED_SPEC}_ort-${ORT_SPEC}_forced-ort`.replace(/[^A-Za-z0-9._-]/g, "_"), + install: { + dependencies: { fastembed: FASTEMBED_SPEC, "onnxruntime-node": ORT_SPEC }, + overrides: { "onnxruntime-common": ORT_SPEC, "onnxruntime-node": ORT_SPEC }, + }, + }; +} let fastembedLoad: Promise | null = null; export function loadFastembed(): Promise { @@ -42,15 +61,15 @@ async function loadFastembedOnce(): Promise { // native addons and may be absent at runtime — a static import would load // the addon at module-init and crash every consumer without the peers. try { - // Preload ORT 1.24 before fastembed's nested ORT 1.21 — only on Windows, + // Preload the pinned ORT before fastembed's nested ORT — only on Windows, // where loading the older binding first triggers a DLL-reuse crash. if (process.platform === "win32") { await import("onnxruntime-node"); } return await import("fastembed"); } catch (error) { - if (!isModuleResolutionError(error)) throw error; - logger.debug("mnemopi: fastembed not resolvable, using on-demand runtime install", { + if (!isRecoverableFastembedLoadError(error)) throw error; + logger.debug("mnemopi: fastembed not loadable, using on-demand runtime install", { error: String(error), }); return loadFromRuntimeInstall(); @@ -58,10 +77,10 @@ async function loadFastembedOnce(): Promise { } async function loadFromRuntimeInstall(): Promise { - const versionKey = `fastembed-${FASTEMBED_SPEC}_ort-${ORT_SPEC}`.replace(/[^A-Za-z0-9._-]/g, "_"); + const plan = fastembedRuntimeInstallPlan(); const runtimeDir = await ensureRuntimeInstalled({ - runtimeDir: path.join(getFastembedRuntimeDir(), versionKey), - install: { dependencies: { fastembed: FASTEMBED_SPEC, "onnxruntime-node": ORT_SPEC } }, + runtimeDir: path.join(getFastembedRuntimeDir(), plan.versionKey), + install: plan.install, probePackage: "fastembed", }); const nodeModules = path.join(runtimeDir, "node_modules"); @@ -80,10 +99,10 @@ async function loadFromRuntimeInstall(): Promise { return requireRuntime(entry) as FastembedModule; } -function isModuleResolutionError(error: unknown): boolean { +function isRecoverableFastembedLoadError(error: unknown): boolean { if (typeof error !== "object" || error === null) return false; const { name, code, message } = error as { name?: unknown; code?: unknown; message?: unknown }; if (name === "ResolveMessage") return true; - if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") return true; + if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND" || code === "ERR_DLOPEN_FAILED") return true; return typeof message === "string" && /cannot find (module|package)/i.test(message); } diff --git a/packages/mnemopi/test/fastembed-runtime.test.ts b/packages/mnemopi/test/fastembed-runtime.test.ts index 14e0405b3..7bab8ace5 100644 --- a/packages/mnemopi/test/fastembed-runtime.test.ts +++ b/packages/mnemopi/test/fastembed-runtime.test.ts @@ -1,13 +1,15 @@ import { describe, expect, test } from "bun:test"; import rootManifest from "../../../package.json" with { type: "json" }; import packageManifest from "../package.json" with { type: "json" }; +import { fastembedRuntimeInstallPlan } from "../src/core/fastembed-runtime"; // The fastembed/onnxruntime-node peers are pinned as exact versions (not // `catalog:`) because `core/fastembed-runtime.ts` reads them to `bun install` // the on-demand embedding runtime — including from bundles where the inlined // manifest would otherwise carry an uninstallable `catalog:` spec (#2389). // This pins the contract: the runtime install materializes exactly the -// versions the workspace develops and tests against. +// versions the workspace develops and tests against, and forces fastembed's +// transitive ORT request away from its archived 1.21.0 pin (#2920). describe("fastembed runtime version pins", () => { const catalog = rootManifest.workspaces.catalog; @@ -20,4 +22,17 @@ describe("fastembed runtime version pins", () => { expect(packageManifest.peerDependencies.fastembed).toMatch(/^\d+\.\d+\.\d+$/); expect(packageManifest.peerDependencies["onnxruntime-node"]).toMatch(/^\d+\.\d+\.\d+$/); }); + + test("runtime install overrides fastembed's transitive onnxruntime pin", () => { + const plan = fastembedRuntimeInstallPlan(); + expect(plan.install.dependencies).toEqual({ + fastembed: packageManifest.peerDependencies.fastembed, + "onnxruntime-node": packageManifest.peerDependencies["onnxruntime-node"], + }); + expect(plan.install.overrides).toEqual({ + "onnxruntime-common": packageManifest.peerDependencies["onnxruntime-node"], + "onnxruntime-node": packageManifest.peerDependencies["onnxruntime-node"], + }); + expect(plan.versionKey).toContain("forced-ort"); + }); });