diff --git a/packages/mnemopi/CHANGELOG.md b/packages/mnemopi/CHANGELOG.md index 938ea7844..567bc5032 100644 --- a/packages/mnemopi/CHANGELOG.md +++ b/packages/mnemopi/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Fixed working-memory TTL trim silently deleting restored or imported durable rows: rows keeping `consolidated_at = NULL` with an old `timestamp` are no longer trimmed when flagged `IMPORTED`, `importFromDict` stamps imported rows as consolidated, and every working-memory delete path (trim, `forgetWorking`, force-import overwrite) now cascades linked annotations, embeddings, facts, memoria projections, gists, and graph edges instead of leaving orphans. ([#4819](https://github.com/can1357/oh-my-pi/issues/4819)) +- Fixed Mnemopi local embeddings on Windows loading an unrelated `onnxruntime.dll` from the inherited system path instead of fastembed's cached ORT runtime. ([#4849](https://github.com/can1357/oh-my-pi/issues/4849)) ## [16.3.9] - 2026-07-06 diff --git a/packages/mnemopi/src/core/fastembed-runtime.ts b/packages/mnemopi/src/core/fastembed-runtime.ts index 3a8978fed..272fa0f76 100644 --- a/packages/mnemopi/src/core/fastembed-runtime.ts +++ b/packages/mnemopi/src/core/fastembed-runtime.ts @@ -48,6 +48,67 @@ export function fastembedRuntimeInstallPlan(): FastembedRuntimeInstallPlan { } let fastembedLoad: Promise | null = null; +/** Inputs for selecting the Windows DLL directory paired with a fastembed installation. */ +export interface WindowsFastembedRuntimeOptions { + /** Resolved fastembed package entry whose dependency graph owns the ORT binding. */ + fastembedEntry: string; + /** Directory containing fastembed's manifest and nested dependency graph. */ + fastembedPackageDir: string; + /** Native architecture to select; defaults to the current process architecture. */ + arch?: string; + /** Environment receiving the DLL search path; defaults to the subprocess environment. */ + env?: NodeJS.ProcessEnv; +} + +/** The ORT module and DLL directory selected from fastembed's own dependency graph. */ +export interface WindowsFastembedRuntime { + /** Resolved entry for fastembed's own ONNX Runtime dependency. */ + ortEntry: string; + /** Package directory containing the selected ORT manifest and native assets. */ + ortPackageDir: string; + /** Directory prepended to `PATH` so Windows finds the paired native DLL. */ + dllDir: string; +} + +/** + * Prepend the ORT DLL directory paired with fastembed before Bun loads its + * native binding. Compiled Windows binaries extract `.node` files to a + * temporary directory, so the default DLL search can otherwise select an + * unrelated `onnxruntime.dll` from the inherited system path. + */ +export async function prepareWindowsFastembedRuntime({ + fastembedEntry, + fastembedPackageDir, + arch = process.arch, + env = process.env, +}: WindowsFastembedRuntimeOptions): Promise { + const nestedNodeModules = path.join(fastembedPackageDir, "node_modules"); + const rootNodeModules = path.dirname(fastembedPackageDir); + const nestedOrtEntry = resolveRuntimeModule(nestedNodeModules, "onnxruntime-node"); + const ortEntry = nestedOrtEntry ?? resolveRuntimeModule(rootNodeModules, "onnxruntime-node"); + const ortPackageDir = path.join(nestedOrtEntry ? nestedNodeModules : rootNodeModules, "onnxruntime-node"); + if (!ortEntry) { + throw new Error(`Cannot find module onnxruntime-node beside ${fastembedEntry}`); + } + const dllGlob = new Bun.Glob(`bin/napi-*/win32/${arch}/onnxruntime.dll`); + let dllDir: string | undefined; + for await (const dll of dllGlob.scan({ cwd: ortPackageDir, absolute: true, onlyFiles: true })) { + dllDir = path.dirname(dll); + break; + } + if (!dllDir) { + throw new Error(`Cannot find module onnxruntime-node Windows DLL for ${arch} beside ${ortEntry}`); + } + + const currentPath = env.PATH; + const normalizedDllDir = path.resolve(dllDir).toLowerCase(); + const alreadyPresent = currentPath + ?.split(path.delimiter) + .some(entry => path.resolve(entry).toLowerCase() === normalizedDllDir); + if (!alreadyPresent) env.PATH = currentPath ? `${dllDir}${path.delimiter}${currentPath}` : dllDir; + return { ortEntry, ortPackageDir, dllDir }; +} + export function loadFastembed(): Promise { fastembedLoad ??= loadFastembedOnce().catch(error => { fastembedLoad = null; @@ -57,16 +118,14 @@ export function loadFastembed(): Promise { } async function loadFastembedOnce(): Promise { - // Dynamic imports: both packages are optional peers that eagerly load - // 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 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"); + const requireDirect = createRequire(import.meta.url); + const manifestPath = requireDirect.resolve("fastembed/package.json"); + const manifest: { version?: unknown } = requireDirect(manifestPath); + if (manifest.version !== FASTEMBED_SPEC) { + throw new Error(`Cannot find package fastembed@${FASTEMBED_SPEC}; resolved ${String(manifest.version)}`); } - return await import("fastembed"); + return loadResolvedFastembed(requireDirect.resolve("fastembed"), path.dirname(manifestPath)); } catch (error) { if (!isRecoverableFastembedLoadError(error)) throw error; logger.debug("mnemopi: fastembed not loadable, using on-demand runtime install", { @@ -76,6 +135,16 @@ async function loadFastembedOnce(): Promise { } } +async function loadResolvedFastembed(entry: string, fastembedPackageDir: string): Promise { + const requireFastembed = createRequire(entry); + if (process.platform === "win32") { + const { ortEntry } = await prepareWindowsFastembedRuntime({ fastembedEntry: entry, fastembedPackageDir }); + requireFastembed(ortEntry); + } + const loaded: FastembedModule = requireFastembed(entry); + return loaded; +} + async function loadFromRuntimeInstall(): Promise { const plan = fastembedRuntimeInstallPlan(); const runtimeDir = await ensureRuntimeInstalled({ @@ -89,14 +158,9 @@ async function loadFromRuntimeInstall(): Promise { // onnxruntime-node, @anush008/tokenizers → platform binding, …) through // the runtime cache. installRuntimeModuleResolver({ runtimeNodeModules: nodeModules }); - if (process.platform === "win32") { - const ortEntry = resolveRuntimeModule(nodeModules, "onnxruntime-node"); - if (ortEntry) createRequire(ortEntry)(ortEntry); - } const entry = resolveRuntimeModule(nodeModules, "fastembed"); if (!entry) throw new Error(`fastembed runtime install at ${runtimeDir} has no loadable entry`); - const requireRuntime = createRequire(entry); - return requireRuntime(entry) as FastembedModule; + return loadResolvedFastembed(entry, path.join(nodeModules, "fastembed")); } function isRecoverableFastembedLoadError(error: unknown): boolean { diff --git a/packages/mnemopi/test/fastembed-runtime.test.ts b/packages/mnemopi/test/fastembed-runtime.test.ts index 7b123cb72..1e2206329 100644 --- a/packages/mnemopi/test/fastembed-runtime.test.ts +++ b/packages/mnemopi/test/fastembed-runtime.test.ts @@ -1,7 +1,9 @@ import { describe, expect, test } from "bun:test"; +import { createRequire } from "node:module"; +import * as path from "node:path"; import rootManifest from "../../../package.json" with { type: "json" }; import packageManifest from "../package.json" with { type: "json" }; -import { fastembedRuntimeInstallPlan } from "../src/core/fastembed-runtime"; +import { fastembedRuntimeInstallPlan, prepareWindowsFastembedRuntime } from "../src/core/fastembed-runtime"; // The fastembed peer is pinned as an exact version (not `catalog:`) because // `core/fastembed-runtime.ts` reads it to `bun install` the on-demand embedding @@ -35,4 +37,24 @@ describe("fastembed runtime version pins", () => { expect(plan.versionKey).toContain("transitive-ort"); expect(plan.versionKey).not.toContain("forced-ort"); }); + + test("Windows preload selects fastembed's ORT DLL before inherited paths", async () => { + const requireTest = createRequire(import.meta.url); + const fastembedManifest = requireTest.resolve("fastembed/package.json"); + const fastembedEntry = requireTest.resolve("fastembed"); + const inheritedPath = ["/stale-ort", "/system"].join(path.delimiter); + const env: NodeJS.ProcessEnv = { PATH: inheritedPath }; + const { ortEntry, ortPackageDir, dllDir } = await prepareWindowsFastembedRuntime({ + fastembedEntry, + fastembedPackageDir: path.dirname(fastembedManifest), + arch: "x64", + env, + }); + const ortManifest: { version?: unknown } = requireTest(path.join(ortPackageDir, "package.json")); + + expect(ortManifest.version).toBe(packageManifest.peerDependencies["onnxruntime-node"]); + expect(ortEntry.startsWith(`${ortPackageDir}${path.sep}`)).toBe(true); + expect(await Bun.file(path.join(dllDir, "onnxruntime.dll")).exists()).toBe(true); + expect(env.PATH).toBe(`${dllDir}${path.delimiter}${inheritedPath}`); + }); });