merged PR #5432: fix(mnemopi): pin Windows ORT DLL path
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -48,6 +48,67 @@ export function fastembedRuntimeInstallPlan(): FastembedRuntimeInstallPlan {
|
||||
}
|
||||
let fastembedLoad: Promise<FastembedModule> | 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<WindowsFastembedRuntime> {
|
||||
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<FastembedModule> {
|
||||
fastembedLoad ??= loadFastembedOnce().catch(error => {
|
||||
fastembedLoad = null;
|
||||
@@ -57,16 +118,14 @@ export function loadFastembed(): Promise<FastembedModule> {
|
||||
}
|
||||
|
||||
async function loadFastembedOnce(): Promise<FastembedModule> {
|
||||
// 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<FastembedModule> {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadResolvedFastembed(entry: string, fastembedPackageDir: string): Promise<FastembedModule> {
|
||||
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<FastembedModule> {
|
||||
const plan = fastembedRuntimeInstallPlan();
|
||||
const runtimeDir = await ensureRuntimeInstalled({
|
||||
@@ -89,14 +158,9 @@ async function loadFromRuntimeInstall(): Promise<FastembedModule> {
|
||||
// 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 {
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user