From 425aba766bf8b509cd8a2021ce00635e5248bb8f Mon Sep 17 00:00:00 2001 From: noctilust <1663402+noctilust@users.noreply.github.com> Date: Tue, 19 May 2026 00:10:09 -0700 Subject: [PATCH] fix(cli): ignore stale HERMES_TUI_RESUME env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HERMES_TUI_RESUME is an internal env var the Python wrapper exports to hand a session ID off to the Ink TUI. Because _launch_tui started from os.environ.copy(), any exported/stale value in the user's shell leaked through — so plain `hermes --tui` would try to resume a missing session and leave the UI at 'error: session not found' with no live session. Drop HERMES_TUI_RESUME from the env before conditionally re-setting it from the argparse-resolved resume_session_id. Tests cover both the drop path and the set-from-arg path. Salvage of #28080 by @noctilust. --- hermes_cli/main.py | 8 +++++ tests/hermes_cli/test_tui_resume_flow.py | 42 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 53cef76771c..aad45eb2e37 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -1278,6 +1278,14 @@ def _launch_tui( 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 + # in the user's shell would otherwise make a plain `hermes --tui` try to + # resume a non-existent session and leave the UI at "error: session not + # found" with no live session. Only forward a resume id that argparse + # resolved for this invocation; direct `node ui-tui/dist/entry.js` users can + # still set HERMES_TUI_RESUME themselves. + env.pop("HERMES_TUI_RESUME", None) if resume_session_id: env["HERMES_TUI_RESUME"] = resume_session_id diff --git a/tests/hermes_cli/test_tui_resume_flow.py b/tests/hermes_cli/test_tui_resume_flow.py index bfdea103ae3..0c3cde535cc 100644 --- a/tests/hermes_cli/test_tui_resume_flow.py +++ b/tests/hermes_cli/test_tui_resume_flow.py @@ -541,6 +541,48 @@ def test_launch_tui_exit_code_42_relaunches_update(monkeypatch, main_mod): mock_relaunch.assert_called_once_with(["update"], preserve_inherited=False) +def test_launch_tui_drops_stale_resume_env_without_resume_arg(monkeypatch, main_mod): + captured = {} + + monkeypatch.setenv("HERMES_TUI_RESUME", "stale-missing-session") + monkeypatch.setattr( + main_mod, + "_make_tui_argv", + lambda tui_dir, tui_dev: (["node", "dist/entry.js"], Path(".")), + ) + monkeypatch.setattr( + main_mod.subprocess, + "call", + lambda argv, cwd=None, env=None: captured.update({"env": env}) or 1, + ) + + with pytest.raises(SystemExit): + main_mod._launch_tui() + + assert "HERMES_TUI_RESUME" not in captured["env"] + + +def test_launch_tui_sets_resume_env_from_resume_arg(monkeypatch, main_mod): + captured = {} + + monkeypatch.setenv("HERMES_TUI_RESUME", "stale-missing-session") + monkeypatch.setattr( + main_mod, + "_make_tui_argv", + lambda tui_dir, tui_dev: (["node", "dist/entry.js"], Path(".")), + ) + monkeypatch.setattr( + main_mod.subprocess, + "call", + lambda argv, cwd=None, env=None: captured.update({"env": env}) or 1, + ) + + with pytest.raises(SystemExit): + main_mod._launch_tui(resume_session_id="20260518_000000_goodid") + + assert captured["env"]["HERMES_TUI_RESUME"] == "20260518_000000_goodid" + + def test_make_tui_argv_dev_prebuilds_hermes_ink(monkeypatch, main_mod, tmp_path): tui_dir = tmp_path / "ui-tui" tsx = tui_dir / "node_modules" / ".bin" / "tsx"