From 228d8de19cf9b9de5ef007f2c1b72f078d90e40e Mon Sep 17 00:00:00 2001 From: Dusk1e Date: Thu, 4 Jun 2026 04:34:24 +0300 Subject: [PATCH 1/2] fix(cli): restore session cwd on mid-chat /resume and /sessions --- tests/cli/test_cli_resume_command.py | 73 ++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/cli/test_cli_resume_command.py b/tests/cli/test_cli_resume_command.py index 299d5fad273c..de2c0fe639be 100644 --- a/tests/cli/test_cli_resume_command.py +++ b/tests/cli/test_cli_resume_command.py @@ -1,3 +1,4 @@ +import os from types import SimpleNamespace from unittest.mock import MagicMock, patch @@ -153,6 +154,78 @@ class TestCliResumeCommand: assert "``, which delegates here) left + the process + ``TERMINAL_CWD`` pointing at whatever directory the user had + cd'd into — so the terminal/code-exec tools and relative paths ran in the + wrong repo. + """ + + def _resumable_cli(self, session_meta): + cli_obj = _make_cli() + cli_obj._session_db.get_session.return_value = session_meta + cli_obj._session_db.get_messages_as_conversation.return_value = [ + {"role": "user", "content": "hello"}, + ] + cli_obj._session_db.resolve_resume_session_id.return_value = session_meta["id"] + return cli_obj + + def test_handle_resume_restores_recorded_cwd(self, tmp_path): + recorded = str(tmp_path) + cli_obj = self._resumable_cli({"id": "sess_dir", "title": "Dir", "cwd": recorded}) + + with ( + patch("hermes_cli.main._resolve_session_by_name_or_id", return_value="sess_dir"), + patch("cli._cprint"), + patch.object(cli_obj, "_console_print"), + patch("os.chdir") as mock_chdir, + patch.dict(os.environ, {}, clear=False), + ): + cli_obj._handle_resume_command("/resume Dir") + # Assert inside the patch.dict scope — it restores os.environ on exit. + assert os.environ.get("TERMINAL_CWD") == recorded + + mock_chdir.assert_called_once_with(recorded) + + def test_handle_resume_without_recorded_cwd_does_not_chdir(self): + # Gateway/remote/older sessions record no cwd — restore must no-op. + cli_obj = self._resumable_cli({"id": "sess_dir", "title": "Dir"}) + + with ( + patch("hermes_cli.main._resolve_session_by_name_or_id", return_value="sess_dir"), + patch("cli._cprint"), + patch.object(cli_obj, "_console_print"), + patch("os.chdir") as mock_chdir, + ): + cli_obj._handle_resume_command("/resume Dir") + + mock_chdir.assert_not_called() + + def test_sessions_command_restores_recorded_cwd(self, tmp_path): + # /sessions delegates to the resume flow, so it restores cwd too. + recorded = str(tmp_path) + cli_obj = self._resumable_cli({"id": "sess_dir", "title": "Dir", "cwd": recorded}) + + with ( + patch("hermes_cli.main._resolve_session_by_name_or_id", return_value="sess_dir"), + patch("cli._cprint"), + patch.object(cli_obj, "_console_print"), + patch("os.chdir") as mock_chdir, + patch.dict(os.environ, {}, clear=False), + ): + cli_obj._handle_sessions_command("/sessions Dir") + # Assert inside the patch.dict scope — it restores os.environ on exit. + assert os.environ.get("TERMINAL_CWD") == recorded + + mock_chdir.assert_called_once_with(recorded) + + class TestPendingResumeNumberedSelection: """Bare `/resume` arms a one-shot prompt so the next bare number resumes. From 3700ca4a549381f54dc138572f027e2946e9d896 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sat, 18 Jul 2026 23:20:32 -0400 Subject: [PATCH 2/2] fix(cli): restore session cwd on mid-chat /resume (transplant to mixin) Dusk1e's fix wired _restore_session_cwd into _handle_resume_command, but that handler was extracted from cli.py into hermes_cli/cli_commands_mixin.py (094aa85c3) after the PR's base, so the original cli.py hunk no longer applied (a naive cherry-pick fuzzily misplaced it inside new_session(), where session_meta is undefined). Transplanted the call to the end of the handler in its current home; /sessions delegates here so both command forms are covered. Dusk1e's regression tests carry over unchanged. Co-authored-by: Dusk1e --- hermes_cli/cli_commands_mixin.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/hermes_cli/cli_commands_mixin.py b/hermes_cli/cli_commands_mixin.py index 4e65155b6022..8f9e6b8e886e 100644 --- a/hermes_cli/cli_commands_mixin.py +++ b/hermes_cli/cli_commands_mixin.py @@ -834,6 +834,14 @@ class CLICommandsMixin: else: _cprint(f" ↻ Resumed session {target_id}{title_part} — no messages, starting fresh.") + # Retarget the process + tool cwd to where the session was started, so a + # mid-chat /resume (and /sessions , which delegates here) lands in the + # same directory as a startup `hermes -c`/`--resume`. The startup resume + # paths already call this; without it, the terminal/code-exec tools and + # relative-path resolution keep operating in the wrong repo. Idempotent + # and a no-op when the session recorded no cwd. See #38562. + self._restore_session_cwd(session_meta) + def _handle_sessions_command(self, cmd_original: str) -> None: """Handle /sessions [list|] — browse or resume previous sessions.