OmniRoute Version
3.8.52 (contains #15855)
Installation Method
npm (global)
Operating System
Windows
OS Version
Windows 11 Pro 25H2 26200.9550
Node.js Version
24.14.1
Provider(s) Involved
No response
Model(s) Involved
No response
Client Tool
No response
Description
Follow-up to #15464. The #15855 deadline fix (60s → 240s, configurable) works as shipped, but on my machine autostart still never converged, because the worker can never reach notifyTrayReady at all: the tray loader (bin/cli/runtime/trayRuntime.ts + bin/cli/tray/traySystray.mjs) has two independent bugs that both fail silently. The result is the exact same invisible-kill signature as in #15464, just at +240s instead of +60s — the launcher taskkill /T /Fs a healthy, fully-listening worker at the deadline (detachedTray.mjs:176).
Bug 1 — systray2 is imported as a directory URL (trayRuntime.ts:26-28, :74)
export function systrayModuleSpecifier(runtimeDir: string): string {
return pathToFileURL(join(runtimeDir, "node_modules", SYSTRAY_PACKAGE)).href;
}A URL/import path that resolves to a directory is never treated as a package root by the ESM loader — Node does not consult main/exports for URL-path imports, so the import throws regardless of how correct the file:// scheme construction is (this is separate from and unaffected by the #11238 fix, which correctly addressed the scheme error):
Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import '<HOME>\.omniroute\runtime\node_modules\systray2' is not supported resolving ES modules
Did you mean to import "file:///<HOME>/.omniroute/runtime/node_modules/systray2/index.js"?loadSystray catches this, warns to stderr, and returns null → initSystrayUnix returns null → maybeStartTray does not succeed → notifyTrayReady never fires → parent kills the (listening) worker at 240s.
Bug 2 — the default-export unwrap produces a non-constructor (trayRuntime.ts:74-75)
return (mod.default ?? mod.SysTray ?? mod) as (new (...args: unknown[]) => unknown) | null;systray2@2.1.4 compiles to CJS exporting { __esModule: true, default: SysTray }. Under the CJS→ESM interop, mod.default is that exports object, not the class (module namespace keys: __esModule, default, module.exports). So even with Bug 1 fixed and the package cached, new SysTray(...) throws:
TypeError: SysTray is not a constructorwhich initSystrayUnix's bare catch {} (traySystray.mjs:74-75) swallows → null again, with no trace. The correct unwrap is the classic interop pattern: mod.default.default when typeof mod.default?.default === "function".
Why the install premise of #15855 never holds
Across all my boots (including pre-#15855 ones), the runtime dir never gained systray2 — only better-sqlite3 (from nativeDeps.mjs). The lazy install never converged anywhere, so "install (~120s) + boot (~90s) < 240s" was never the operative question here: readiness was structurally unreachable. After I cached systray2@2.1.4 manually and fixed the loader locally, the tray came up on the first try — which also confirms tray_windows_release.exe spawning and AV behavior are fine.
Secondary (same class of problem): the failure remains invisible by construction
Everything about this bug chain logs only to stderr, and on the autostart path stderr is discarded twice over: the VBS runs hidden (window style 0), and the worker is spawned detached: true, stdio: "ignore", windowsHide: true (detachedTray.mjs:38). None of it lands in app.log — my last pre-fix boots show a worker that reaches EmbedWsProxy listening ~77s in and then stops mid-normal-operation with no shutdown line, no error, and autostart status still reporting enabled: true / vbs-startup. isEnabledWin() checks only the VBS file's existence.
The kill-timing also matches the readiness deadline rather than any boot failure: the first post-#15855 boot writes its .pid seconds after the worker spawn, and its final log line lands ~240s after the spawn — right at the deadline, while the server had been listening and fully operational for the previous ~2.5 minutes.
Steps to Reproduce
Works from a fresh install (systray2 uncached) and after manually caching it, so it reproduces on warm and cold states:
npm install -g omniroute(3.8.52) +omniroute autostart enable, then run the generated%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\OmniRoute.vbs(oromniroute serve --no-open --trayin a visible terminal).- Wait ~4 minutes →
✖ Tray worker did not become ready, exit code 1, after the full 240s deadline; the worker was actually listening the entire time.
Direct probes of the loader mechanism (outside the hidden worker; <HOME> = the user's home dir):
node -e "import('file:///<HOME>/.omniroute/runtime/node_modules/systray2/index.js').then(() => console.log('ok'), e => console.error(e.code, e.message))"
# -> with systray2 cached:
# ERR_UNSUPPORTED_DIR_IMPORT: Directory import '<HOME>\.omniroute\runtime\node_modules\systray2' is not supported resolving ES modulesAfter pointing the import at systray2/index.js:
const mod = await import(systray2IndexFileUrl);
const SysTray = mod.default ?? mod.SysTray ?? mod;
new SysTray({ menu: { ... } });
// -> TypeError: SysTray is not a constructor (mod.default is the exports object; the class is mod.default.default)Expected Behavior
After omniroute autostart enable and a reboot (or running the VBS directly), OmniRoute serves and the tray is ready within the (now 240s) deadline — and when the tray fails for any reason, the reason is obtainable somewhere a user can find it.
Actual Behavior
The worker boots fully, listens, and is taskkilled at the deadline. systray2 is never cached (the lazy install runs to completion but its result is never reachable by the loader), so every boot repeats the cycle. app.log shows a silent mid-operation stop; autostart status reports enabled: true; no error anywhere visible.
Test Impact
Both failure layers are unit-testable without a GUI:
- importing the resolved module spec must not throw
ERR_UNSUPPORTED_DIR_IMPORT(assertsystrayModuleSpecifier()returns a file URL ending in the package entry, and thatimport()succeeds against a temp-installed systray2); loadSystray()must return atypeof === "function"value for the real systray2 CJS shape{ __esModule, default: SysTray }(and still for ESM shapes), instead of returning the exports object;initSystrayUnixshould surface (not swallow) constructor/import failures per its PR #1080 comment intent.
Error Logs / Output
(ongoing boot on 3.8.52, relative to worker start:)
[T+77s] [EmbedWsProxy] Listening on 127.0.0.1:20131
…(normal operation for ~2.5 more minutes)…
[T+~198s] [ConnectionRecovery] proactively restored 1 connection(s)…
(end of log — no shutdown, no error; process tree gone, killed at the 240s readiness deadline)
loader probe 1: Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import '<HOME>\.omniroute\runtime\node_modules\systray2' is not supported resolving ES modules
loader probe 2 (after pointing at index.js): TypeError: SysTray is not a constructorScreenshots
No response
Additional Context
Both fixes are in bin/cli/runtime/trayRuntime.ts (patch applied locally, tray + autostart verified working end-to-end afterwards: ✔ OmniRoute tray started in background, tray binary healthy from the runtime cache):
// 1) resolve the package entry instead of the directory
export function systrayModuleSpecifier(runtimeDir: string): string {
const pkgPath = join(runtimeDir, "node_modules", SYSTRAY_PACKAGE, "package.json");
let entry = "index.js";
try {
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { main?: string };
if (pkg.main) entry = pkg.main;
} catch {}
return pathToFileURL(join(runtimeDir, "node_modules", SYSTRAY_PACKAGE, entry)).href;
}
// 2) unwrap the CJS interop layer, only accept a constructor
let exported: unknown = (mod as { default?: unknown }).default ?? mod;
if (exported && typeof exported === "object" && typeof (exported as { default?: unknown }).default === "function") {
exported = (exported as { default: unknown }).default;
}
if (typeof exported !== "function") return null;
return exported as new (...args: unknown[]) => unknown;Bug 1 would hit macOS/Linux identically (both resolve a directory file-URL for the same code path); I only have Windows here to confirm, but the ESM resolution rule is platform-independent. Filed as a follow-up since #15855's author note said "if the problem is still there on 3.8.52, comment here" — it is, via a mechanism the deadline change can't reach.
Activity
0No activity yet.