From 2ea7cf287e8b8d9590cc5be901396aaa2704bc92 Mon Sep 17 00:00:00 2001 From: YarrowQiao Date: Fri, 8 May 2026 13:14:52 +0800 Subject: [PATCH] fix(tui): pass --expose-gc as node argv instead of NODE_OPTIONS Node refuses to start when NODE_OPTIONS contains --expose-gc: node: --expose-gc is not allowed in NODE_OPTIONS NODE_OPTIONS is restricted to a small allowlist of flags that are safe to inject via env (since any process able to set env vars on a node child could otherwise enable arbitrary capabilities). --expose-gc is not on that list and never has been -- it must be passed as a direct CLI flag. _launch_tui() was appending --expose-gc to NODE_OPTIONS before spawning the TUI's node process, which made `hermes --tui` fail to start on every modern node release. The intent (manual GC for long sessions to avoid fatal-OOM) is preserved by inserting --expose-gc directly into the node argv in _make_tui_argv() -- same effect, but actually allowed. --max-old-space-size=8192 stays in NODE_OPTIONS: it *is* allowlisted, and keeping it there means downstream node spawns inherit the same heap cap without having to re-thread the flag through every spawn site. The dev paths (`tsx src/entry.tsx` and `npm start` fallback) are left alone -- they don't accept node flags directly, and the production dist path is the one users actually hit via `hermes --tui`. Repro before fix: $ hermes --tui /usr/bin/node: --expose-gc is not allowed in NODE_OPTIONS --- hermes_cli/main.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 318e55d3efe..a178daf581e 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -1146,7 +1146,7 @@ def _make_tui_argv(tui_dir: Path, tui_dev: bool) -> tuple[list[str], Path]: p = Path(ext_dir) if (p / "dist" / "entry.js").is_file(): node = _node_bin("node") - return [node, str(p / "dist" / "entry.js")], p + return [node, "--expose-gc", str(p / "dist" / "entry.js")], p # 1b. Bundled in wheel (pip install) bundled = _find_bundled_tui() @@ -1229,7 +1229,7 @@ def _make_tui_argv(tui_dir: Path, tui_dev: bool) -> tuple[list[str], Path]: sys.exit(1) node = _node_bin("node") - return [node, str(tui_dir / "dist" / "entry.js")], tui_dir + return [node, "--expose-gc", str(tui_dir / "dist" / "entry.js")], tui_dir def _normalize_tui_toolsets(toolsets: object) -> list[str]: @@ -1351,16 +1351,16 @@ def _launch_tui( env["HERMES_TUI_TOOL_PROGRESS"] = "off" if accept_hooks: env["HERMES_ACCEPT_HOOKS"] = "1" - # Guarantee an 8GB V8 heap + exposed GC for the TUI. Default node cap is - # ~1.5–4GB depending on version and can fatal-OOM on long sessions with - # large transcripts / reasoning blobs. Token-level merge: respect any - # user-supplied --max-old-space-size (they may have set it higher) and - # avoid duplicating --expose-gc. + # Guarantee an 8GB V8 heap for the TUI. Default node cap is ~1.5–4GB + # depending on version and can fatal-OOM on long sessions with large + # transcripts / reasoning blobs. Token-level merge: respect any + # user-supplied --max-old-space-size (they may have set it higher). + # --expose-gc is *not* added here: Node rejects it in NODE_OPTIONS + # ("--expose-gc is not allowed in NODE_OPTIONS") and refuses to start. + # It is passed as a direct argv flag in _make_tui_argv() instead. _tokens = env.get("NODE_OPTIONS", "").split() if not any(t.startswith("--max-old-space-size=") for t in _tokens): _tokens.append("--max-old-space-size=8192") - if "--expose-gc" not in _tokens: - _tokens.append("--expose-gc") env["NODE_OPTIONS"] = " ".join(_tokens) # HERMES_TUI_RESUME is an internal hand-off from the Python wrapper to the # Ink app. Because we start from os.environ.copy(), an exported/stale value