Summary
On Windows, the npm CLI wrapper spawns the native mnemon binary without windowsHide: true. When the parent is a GUI process with no console (e.g. DSH Desktop / Electron calling mnemon status), Windows allocates a new console window for every invocation — visible as a Windows Terminal window that flashes open and closed.
The harness polls mnemon status roughly every 10 seconds, so this flashes repeatedly and continuously during normal use.
Affected files
npm/cli/lib/child.js — used by every runChild() call, i.e. the main path from bin/mnemon.js:
export async function runChild(command, args, options = {}) {
const child = spawn(command, args, { stdio: "inherit", ...options });
// ← windowsHide: true is missing
npm/cli/lib/update.js — the mnemon update path:
output(command, args) {
const result = spawnSync(command, args, { encoding: "utf8" });
// ← windowsHide: true is missing
bin/mnemon.js resolves the platform binary and delegates to runChild, so this file covers all ordinary subcommands:
const binary = path.join(platformRoot, target.binary);
settle(await runChild(binary, args));
Environment
|
|
| OS |
Windows 11 Education 10.0.26200 |
| Terminal host |
Windows Terminal 1.24.11911.0 (default on Win11) |
| @mnemon-dev/mnemon |
0.2.8 |
| Node |
via the DSH Desktop 2.0.10 Electron binary |
| Consumer |
dsh-mnemon 0.5.10 (calls mnemon … --store default status) |
Steps to reproduce
- Install
@mnemon-dev/mnemon globally on Windows 11 (Windows Terminal as default host).
- From a GUI process that owns no console (Electron app,
pythonw.exe, or a harness like DSH Desktop), spawn:
node <npm-root>/@mnemon-dev/mnemon/bin/mnemon.js --data-dir %USERPROFILE%\.mnemon --store default status
- Observed: a terminal window appears and closes instantly, on every invocation. An agent harness that polls status every ~10 s flashes continuously.
- Expected: no window; the CLI is a background tool.
Root-cause proof
windowsHide: true is what makes Node pass CREATE_NO_WINDOW to CreateProcess. Without it, a console-subsystem child of a console-less parent gets a brand-new visible console.
Process-creation trace of the real call path (harness → wrapper → native binary):
DSH Desktop.exe
→ node .../@mnemon-dev/mnemon/bin/mnemon.js --data-dir C:\Users\...\.mnemon --store default status
→ mnemon.exe --data-dir C:\Users\...\.mnemon --store default status
→ conhost.exe 0x4
→ OpenConsole.exe -Embedding
→ WindowsTerminal.exe -Embedding ← the visible flash
Repeated at 16:18:31 → 16:18:41 → 16:18:53 → 16:19:02 (≈ every 10 s).
Controlled A/B from a console-less parent (pythonw.exe), running git --version and enumerating windows with EnumWindows (same spawn semantics as child.js):
Without windowsHide (current code):
NEW terminal windows: 2
class=CASCADIA_HOSTING_WINDOW_CLASS visible=True pid=2772 title='D:\\Git\\cmd\\git.exe'
class=PseudoConsoleWindow visible=True pid=19800 title=''
With windowsHide: true:
NEW terminal windows: 0
(no window: child ran without a visible console)
A foreground-window poll (GetForegroundWindow every 80 ms) also caught the flash directly:
FOCUS -> WindowsTerminal [...|CASCADIA_HOSTING_WINDOW_CLASS|...\@mnemon-dev\mnemon-win32-x64\bin\mnemon.exe]
Suggested fix
- const child = spawn(command, args, { stdio: "inherit", ...options });
+ const child = spawn(command, args, { stdio: "inherit", windowsHide: true, ...options });
- const result = spawnSync(command, args, { encoding: "utf8" });
+ const result = spawnSync(command, args, { encoding: "utf8", windowsHide: true });
Note on ordering in child.js: windowsHide is placed before ...options so a caller may still override it, matching how stdio is already arranged.
windowsHide is a documented no-op on POSIX, so this is safe cross-platform.
Verification after patching the installed package
node --check passes on both patched files; ESM import of child.js resolves.
- End-to-end: ran the exact
mnemon status command chain 4× → 0 new terminal windows (previously 1 per run).
Notes
Summary
On Windows, the npm CLI wrapper spawns the native
mnemonbinary withoutwindowsHide: true. When the parent is a GUI process with no console (e.g. DSH Desktop / Electron callingmnemon status), Windows allocates a new console window for every invocation — visible as a Windows Terminal window that flashes open and closed.The harness polls
mnemon statusroughly every 10 seconds, so this flashes repeatedly and continuously during normal use.Affected files
npm/cli/lib/child.js— used by everyrunChild()call, i.e. the main path frombin/mnemon.js:npm/cli/lib/update.js— themnemon updatepath:bin/mnemon.jsresolves the platform binary and delegates torunChild, so this file covers all ordinary subcommands:Environment
mnemon … --store default status)Steps to reproduce
@mnemon-dev/mnemonglobally on Windows 11 (Windows Terminal as default host).pythonw.exe, or a harness like DSH Desktop), spawn:Root-cause proof
windowsHide: trueis what makes Node passCREATE_NO_WINDOWtoCreateProcess. Without it, a console-subsystem child of a console-less parent gets a brand-new visible console.Process-creation trace of the real call path (harness → wrapper → native binary):
Repeated at
16:18:31 → 16:18:41 → 16:18:53 → 16:19:02(≈ every 10 s).Controlled A/B from a console-less parent (
pythonw.exe), runninggit --versionand enumerating windows withEnumWindows(same spawn semantics aschild.js):Without
windowsHide(current code):With
windowsHide: true:A foreground-window poll (
GetForegroundWindowevery 80 ms) also caught the flash directly:Suggested fix
Note on ordering in
child.js:windowsHideis placed before...optionsso a caller may still override it, matching howstdiois already arranged.windowsHideis a documented no-op on POSIX, so this is safe cross-platform.Verification after patching the installed package
node --checkpasses on both patched files; ESM import ofchild.jsresolves.mnemon statuscommand chain 4× → 0 new terminal windows (previously 1 per run).Notes
dsh-mnemon-source-runtime'sresolveGitBranch()); I am reporting that separately toomdsh-dev/dsh-mnemonsince it is a different repository.spawn/execFile/spawnSynccall site on Windows passeswindowsHide.