From c8c44c8beb7917388e9208b2bf18d3720b2ae65f Mon Sep 17 00:00:00 2001 From: AEON Core Date: Wed, 29 Jul 2026 23:22:32 +0200 Subject: [PATCH 1/8] fix(oneshot): honor resume state --- hermes_cli/main.py | 12 ++ hermes_cli/oneshot.py | 82 ++++++++++++- tests/hermes_cli/test_tui_resume_flow.py | 149 +++++++++++++++++++++++ 3 files changed, 242 insertions(+), 1 deletion(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 4db17c3e872d..a7b1df3fd08e 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -171,6 +171,9 @@ def _run_and_exit_oneshot( provider: object = None, toolsets: object = None, usage_file: object = None, + resume_session_id: object = None, + continue_last: object = None, + restore_resume_cwd: bool = True, ) -> None: try: from hermes_cli.oneshot import run_oneshot @@ -181,6 +184,9 @@ def _run_and_exit_oneshot( provider=provider, toolsets=toolsets, usage_file=usage_file, + resume_session_id=resume_session_id, + continue_last=continue_last, + restore_resume_cwd=restore_resume_cwd, ) except KeyboardInterrupt: rc = 130 @@ -10528,6 +10534,9 @@ def _try_termux_fast_cli_launch() -> bool: provider=getattr(args, "provider", None), toolsets=getattr(args, "toolsets", None), usage_file=getattr(args, "usage_file", None), + resume_session_id=getattr(args, "resume", None), + continue_last=getattr(args, "continue_last", None), + restore_resume_cwd=not getattr(args, "no_restore_cwd", False), ) if (args.resume or args.continue_last) and args.command is None: @@ -12131,6 +12140,9 @@ def _add_session_filter_args(p, default_older_help): provider=getattr(args, "provider", None), toolsets=getattr(args, "toolsets", None), usage_file=getattr(args, "usage_file", None), + resume_session_id=getattr(args, "resume", None), + continue_last=getattr(args, "continue_last", None), + restore_resume_cwd=not getattr(args, "no_restore_cwd", False), ) # Handle top-level --resume / --continue as shortcut to chat diff --git a/hermes_cli/oneshot.py b/hermes_cli/oneshot.py index 1f04173f0a6b..7a969fe242f3 100644 --- a/hermes_cli/oneshot.py +++ b/hermes_cli/oneshot.py @@ -173,6 +173,9 @@ def run_oneshot( provider: Optional[str] = None, toolsets: object = None, usage_file: Optional[str] = None, + resume_session_id: Optional[str] = None, + continue_last: object = None, + restore_resume_cwd: bool = True, ) -> int: """Execute a single prompt and print only the final content block. @@ -187,6 +190,11 @@ def run_oneshot( cost, token counts, model, api_calls) is written there after the run — even when the run fails — so pipelines can account for spend per invocation. + resume_session_id: Existing session ID or title whose history and + durable row should receive this turn. + continue_last: ``True`` for the latest CLI session, or a session title. + restore_resume_cwd: Restore the resumed session's recorded workspace + before constructing the agent. Returns the exit code. The caller owns process termination. """ @@ -248,6 +256,9 @@ def run_oneshot( provider=provider, toolsets=explicit_toolsets, use_config_toolsets=use_config_toolsets, + resume_session_id=resume_session_id, + continue_last=continue_last, + restore_resume_cwd=restore_resume_cwd, ) except BaseException as exc: # noqa: BLE001 # Capture anything that escapes the agent (including OSError @@ -310,12 +321,70 @@ def _create_session_db_for_oneshot(): return None +def _load_oneshot_resume( + session_db, + *, + resume_session_id: Optional[str], + continue_last: object, + restore_resume_cwd: bool, +) -> tuple[Optional[str], Optional[list[dict]]]: + """Resolve and load one persisted session for a one-shot continuation.""" + if not (resume_session_id or continue_last): + return None, None + if session_db is None: + raise RuntimeError("Session database unavailable; cannot resume in one-shot mode.") + + target = str(resume_session_id or "").strip() + if not target and isinstance(continue_last, str): + target = continue_last.strip() + if not target and continue_last: + recent = session_db.search_sessions(source="cli", limit=1) + if not recent: + raise ValueError("No previous CLI session found to continue.") + target = recent[0]["id"] + + session_meta = session_db.get_session(target) + if not session_meta: + title_match = session_db.resolve_session_by_title(target) + if title_match: + target = title_match + session_meta = session_db.get_session(target) + if not session_meta: + raise ValueError(f"Session not found: {target}") + + resolved_session_id = session_db.resolve_resume_session_id(target) or target + if resolved_session_id != target: + session_meta = session_db.get_session(resolved_session_id) + if not session_meta: + raise ValueError(f"Session not found: {resolved_session_id}") + + conversation_history, _display_history = session_db.get_resume_conversations( + resolved_session_id + ) + conversation_history = [ + message + for message in conversation_history + if message.get("role") != "session_meta" + ] + session_db.reopen_session(resolved_session_id) + + if restore_resume_cwd: + saved_cwd = str(session_meta.get("cwd") or "").strip() + if saved_cwd and os.path.isdir(saved_cwd): + os.chdir(saved_cwd) + + return resolved_session_id, conversation_history + + def _run_agent( prompt: str, model: Optional[str] = None, provider: Optional[str] = None, toolsets: object = None, use_config_toolsets: bool = True, + resume_session_id: Optional[str] = None, + continue_last: object = None, + restore_resume_cwd: bool = True, ) -> tuple[str, dict]: """Build an AIAgent exactly like a normal CLI chat turn would, then run a single conversation. Returns ``(final_response, run_result)``.""" @@ -402,6 +471,13 @@ def _run_agent( # os._exit and skips finalizers, so an un-closed connection here would leak. agent = None try: + resolved_session_id, conversation_history = _load_oneshot_resume( + session_db, + resume_session_id=resume_session_id, + continue_last=continue_last, + restore_resume_cwd=restore_resume_cwd, + ) + # Read the effective fallback chain from profile config so oneshot # workers honour the same merge semantics as interactive CLI and # gateway sessions. @@ -418,6 +494,7 @@ def _run_agent( quiet_mode=True, platform="cli", session_db=session_db, + session_id=resolved_session_id, credential_pool=runtime.get("credential_pool"), fallback_model=_fb or None, # Interactive callbacks are intentionally NOT wired beyond this @@ -440,7 +517,10 @@ def _run_agent( agent.stream_delta_callback = None agent.tool_gen_callback = None - result = agent.run_conversation(prompt) + result = agent.run_conversation( + prompt, + conversation_history=conversation_history, + ) return (result.get("final_response") or "", result) finally: # Ordering deliberately mirrors gateway/run.py:_cleanup_agent_resources, diff --git a/tests/hermes_cli/test_tui_resume_flow.py b/tests/hermes_cli/test_tui_resume_flow.py index 4bcd3a6119fd..33a0d45f974f 100644 --- a/tests/hermes_cli/test_tui_resume_flow.py +++ b/tests/hermes_cli/test_tui_resume_flow.py @@ -403,6 +403,9 @@ def test_termux_fast_cli_launch_oneshot_uses_light_parser(monkeypatch, main_mod) "provider": "openai", "toolsets": None, "usage_file": "usage.json", + "resume_session_id": None, + "continue_last": None, + "restore_resume_cwd": True, } @@ -657,6 +660,9 @@ def test_main_top_level_oneshot_accepts_toolsets(monkeypatch, main_mod): "provider": None, "toolsets": "web,terminal", "usage_file": "usage.json", + "resume_session_id": None, + "continue_last": None, + "restore_resume_cwd": True, } @@ -1126,6 +1132,46 @@ def test_run_and_exit_oneshot_passes_through_nonzero_return(monkeypatch, main_mo assert exits == [2] +def test_run_and_exit_oneshot_forwards_resume_options(monkeypatch, main_mod): + calls = [] + exits = [] + + def fake_run_oneshot(prompt, **kwargs): + calls.append((prompt, kwargs)) + return 0 + + monkeypatch.setitem( + sys.modules, + "hermes_cli.oneshot", + types.SimpleNamespace(run_oneshot=fake_run_oneshot), + ) + monkeypatch.setattr(main_mod, "_cleanup_oneshot_runtime", lambda: None) + monkeypatch.setattr(main_mod, "_exit_after_oneshot", lambda rc: exits.append(rc)) + + main_mod._run_and_exit_oneshot( + "continue", + resume_session_id="session-1", + continue_last=False, + restore_resume_cwd=False, + ) + + assert calls == [ + ( + "continue", + { + "model": None, + "provider": None, + "toolsets": None, + "usage_file": None, + "resume_session_id": "session-1", + "continue_last": False, + "restore_resume_cwd": False, + }, + ) + ] + assert exits == [0] + + def test_main_oneshot_path_bypasses_late_atexit_abort(): # End-to-end through the real top-level ``main()`` ``-z`` path: a valid # response prints, then a late atexit handler that would abort is bypassed @@ -1219,6 +1265,109 @@ def close(self): assert shutdown_messages == [[{"role": "user", "content": "hello"}]] +def test_oneshot_run_agent_resumes_existing_session(monkeypatch): + import hermes_cli.oneshot as oneshot_mod + + initialized = [] + run_calls = [] + reopened = [] + db_closed = [] + + history = [ + {"role": "session_meta", "content": "internal"}, + {"role": "user", "content": "before"}, + {"role": "assistant", "content": "context"}, + ] + + class FakeAgent: + def __init__(self, **kwargs): + initialized.append(kwargs) + self.suppress_status_output = False + self.stream_delta_callback = object() + self.tool_gen_callback = object() + self._session_messages = [] + + def run_conversation(self, prompt, **kwargs): + run_calls.append((prompt, kwargs)) + return {"final_response": "continued"} + + def shutdown_memory_provider(self, messages=None): + pass + + def close(self): + pass + + class FakeSessionDB: + def get_session(self, session_id): + if session_id == "root": + return {"id": "root", "cwd": "/missing"} + if session_id == "tip": + return {"id": "tip", "cwd": "/missing"} + return None + + def resolve_session_by_title(self, _title): + return None + + def resolve_resume_session_id(self, session_id): + assert session_id == "root" + return "tip" + + def get_resume_conversations(self, session_id): + assert session_id == "tip" + return list(history), list(history) + + def reopen_session(self, session_id): + reopened.append(session_id) + + def close(self): + db_closed.append(True) + + monkeypatch.setitem( + sys.modules, "run_agent", types.SimpleNamespace(AIAgent=FakeAgent) + ) + monkeypatch.setattr( + "hermes_cli.config.load_config", + lambda: {"model": {"default": "gpt-test", "provider": "openai"}}, + ) + monkeypatch.setattr( + "hermes_cli.runtime_provider.resolve_runtime_provider", + lambda **_kwargs: { + "api_key": "key", + "base_url": "https://example.invalid", + "provider": "openai", + "api_mode": "chat_completions", + "credential_pool": None, + }, + ) + monkeypatch.setattr( + oneshot_mod, "_create_session_db_for_oneshot", lambda: FakeSessionDB() + ) + + assert oneshot_mod._run_agent( + "continue", + model="gpt-test", + provider="openai", + use_config_toolsets=False, + resume_session_id="root", + restore_resume_cwd=False, + ) == ("continued", {"final_response": "continued"}) + + assert initialized[0]["session_id"] == "tip" + assert reopened == ["tip"] + assert run_calls == [ + ( + "continue", + { + "conversation_history": [ + {"role": "user", "content": "before"}, + {"role": "assistant", "content": "context"}, + ] + }, + ) + ] + assert db_closed == [True] + + def test_oneshot_run_agent_closes_agent_when_chat_raises(monkeypatch): import hermes_cli.oneshot as oneshot_mod From 9c5274e9bc3bb0e190ac65c997a052e0b41245c8 Mon Sep 17 00:00:00 2001 From: SE87H <297523205+SE87H@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:57:23 +0200 Subject: [PATCH 2/8] fix(oneshot): scope continue to workspace --- hermes_cli/oneshot.py | 33 ++++++++- tests/hermes_cli/test_tui_resume_flow.py | 93 ++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/hermes_cli/oneshot.py b/hermes_cli/oneshot.py index 7a969fe242f3..627f7f3852de 100644 --- a/hermes_cli/oneshot.py +++ b/hermes_cli/oneshot.py @@ -23,6 +23,7 @@ import logging import os +import subprocess import sys from contextlib import redirect_stderr, redirect_stdout from pathlib import Path @@ -338,7 +339,16 @@ def _load_oneshot_resume( if not target and isinstance(continue_last, str): target = continue_last.strip() if not target and continue_last: - recent = session_db.search_sessions(source="cli", limit=1) + recent = [] + workspace_key = _resolve_oneshot_workspace_key() + if workspace_key: + recent = session_db.search_sessions( + source="cli", + limit=1, + workspace_key=workspace_key, + ) + if not recent: + recent = session_db.search_sessions(source="cli", limit=1) if not recent: raise ValueError("No previous CLI session found to continue.") target = recent[0]["id"] @@ -376,6 +386,27 @@ def _load_oneshot_resume( return resolved_session_id, conversation_history +def _resolve_oneshot_workspace_key() -> Optional[str]: + """Return the current repo root, or CWD outside a Git workspace.""" + try: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=5, + ) + if result.returncode == 0 and result.stdout.strip(): + return os.path.abspath(result.stdout.strip()) + except Exception: + pass + try: + return os.getcwd() + except Exception: + return None + + def _run_agent( prompt: str, model: Optional[str] = None, diff --git a/tests/hermes_cli/test_tui_resume_flow.py b/tests/hermes_cli/test_tui_resume_flow.py index 33a0d45f974f..37edb8b63c51 100644 --- a/tests/hermes_cli/test_tui_resume_flow.py +++ b/tests/hermes_cli/test_tui_resume_flow.py @@ -1368,6 +1368,99 @@ def close(self): assert db_closed == [True] +def test_oneshot_continue_prefers_current_workspace(monkeypatch): + import hermes_cli.oneshot as oneshot_mod + + calls = [] + + class FakeSessionDB: + def search_sessions(self, **kwargs): + calls.append(kwargs) + return [{"id": "workspace-session"}] + + def get_session(self, session_id): + return {"id": session_id, "cwd": "/missing"} + + def resolve_session_by_title(self, _title): + return None + + def resolve_resume_session_id(self, session_id): + return session_id + + def get_resume_conversations(self, _session_id): + return ([{"role": "user", "content": "workspace context"}], []) + + def reopen_session(self, _session_id): + pass + + monkeypatch.setattr( + oneshot_mod, + "_resolve_oneshot_workspace_key", + lambda: "/workspace/a", + ) + + session_id, history = oneshot_mod._load_oneshot_resume( + FakeSessionDB(), + resume_session_id=None, + continue_last=True, + restore_resume_cwd=False, + ) + + assert calls == [ + {"source": "cli", "limit": 1, "workspace_key": "/workspace/a"} + ] + assert session_id == "workspace-session" + assert history == [{"role": "user", "content": "workspace context"}] + + +def test_oneshot_continue_falls_back_to_global_mru(monkeypatch): + import hermes_cli.oneshot as oneshot_mod + + calls = [] + + class FakeSessionDB: + def search_sessions(self, **kwargs): + calls.append(kwargs) + if "workspace_key" in kwargs: + return [] + return [{"id": "global-session"}] + + def get_session(self, session_id): + return {"id": session_id, "cwd": "/missing"} + + def resolve_session_by_title(self, _title): + return None + + def resolve_resume_session_id(self, session_id): + return session_id + + def get_resume_conversations(self, _session_id): + return ([{"role": "user", "content": "global context"}], []) + + def reopen_session(self, _session_id): + pass + + monkeypatch.setattr( + oneshot_mod, + "_resolve_oneshot_workspace_key", + lambda: "/workspace/new", + ) + + session_id, history = oneshot_mod._load_oneshot_resume( + FakeSessionDB(), + resume_session_id=None, + continue_last=True, + restore_resume_cwd=False, + ) + + assert calls == [ + {"source": "cli", "limit": 1, "workspace_key": "/workspace/new"}, + {"source": "cli", "limit": 1}, + ] + assert session_id == "global-session" + assert history == [{"role": "user", "content": "global context"}] + + def test_oneshot_run_agent_closes_agent_when_chat_raises(monkeypatch): import hermes_cli.oneshot as oneshot_mod From 591ca367c76f2a0c5c41038628c49899f54013bb Mon Sep 17 00:00:00 2001 From: SE87H Date: Thu, 30 Jul 2026 09:54:19 +0200 Subject: [PATCH 3/8] fix(oneshot): fail when resumed workspace is unavailable --- hermes_cli/oneshot.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/hermes_cli/oneshot.py b/hermes_cli/oneshot.py index 627f7f3852de..11b5e45857fa 100644 --- a/hermes_cli/oneshot.py +++ b/hermes_cli/oneshot.py @@ -376,13 +376,24 @@ def _load_oneshot_resume( for message in conversation_history if message.get("role") != "session_meta" ] - session_db.reopen_session(resolved_session_id) if restore_resume_cwd: saved_cwd = str(session_meta.get("cwd") or "").strip() - if saved_cwd and os.path.isdir(saved_cwd): - os.chdir(saved_cwd) + if saved_cwd: + if not os.path.isdir(saved_cwd): + raise FileNotFoundError( + "Recorded session working directory is unavailable: " + f"{saved_cwd}" + ) + try: + os.chdir(saved_cwd) + except OSError as exc: + raise RuntimeError( + "Failed to restore recorded session working directory: " + f"{saved_cwd}" + ) from exc + session_db.reopen_session(resolved_session_id) return resolved_session_id, conversation_history From 284e6bafd7d9e9671db297d83876c7dfe1461da9 Mon Sep 17 00:00:00 2001 From: SE87H Date: Thu, 30 Jul 2026 09:54:34 +0200 Subject: [PATCH 4/8] test(oneshot): cover unavailable resumed workspace --- tests/hermes_cli/test_oneshot_resume_cwd.py | 73 +++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 tests/hermes_cli/test_oneshot_resume_cwd.py diff --git a/tests/hermes_cli/test_oneshot_resume_cwd.py b/tests/hermes_cli/test_oneshot_resume_cwd.py new file mode 100644 index 000000000000..761f212f57a6 --- /dev/null +++ b/tests/hermes_cli/test_oneshot_resume_cwd.py @@ -0,0 +1,73 @@ +import pytest + + +def test_oneshot_resume_fails_before_reopen_when_recorded_cwd_is_missing(monkeypatch): + import hermes_cli.oneshot as oneshot_mod + + reopened = [] + + class FakeSessionDB: + def get_session(self, session_id): + return {"id": session_id, "cwd": "/recorded/workspace"} + + def resolve_session_by_title(self, _title): + return None + + def resolve_resume_session_id(self, session_id): + return session_id + + def get_resume_conversations(self, _session_id): + return ([{"role": "user", "content": "prior context"}], []) + + def reopen_session(self, session_id): + reopened.append(session_id) + + monkeypatch.setattr(oneshot_mod.os.path, "isdir", lambda _path: False) + + with pytest.raises( + FileNotFoundError, + match="Recorded session working directory is unavailable", + ): + oneshot_mod._load_oneshot_resume( + FakeSessionDB(), + resume_session_id="session-1", + continue_last=False, + restore_resume_cwd=True, + ) + + assert reopened == [] + + +def test_oneshot_resume_allows_explicit_cwd_restore_opt_out(monkeypatch): + import hermes_cli.oneshot as oneshot_mod + + reopened = [] + + class FakeSessionDB: + def get_session(self, session_id): + return {"id": session_id, "cwd": "/recorded/workspace"} + + def resolve_session_by_title(self, _title): + return None + + def resolve_resume_session_id(self, session_id): + return session_id + + def get_resume_conversations(self, _session_id): + return ([{"role": "user", "content": "prior context"}], []) + + def reopen_session(self, session_id): + reopened.append(session_id) + + monkeypatch.setattr(oneshot_mod.os.path, "isdir", lambda _path: False) + + session_id, history = oneshot_mod._load_oneshot_resume( + FakeSessionDB(), + resume_session_id="session-1", + continue_last=False, + restore_resume_cwd=False, + ) + + assert session_id == "session-1" + assert history == [{"role": "user", "content": "prior context"}] + assert reopened == ["session-1"] From e9db5835a95f419dac52c1889337ff5743c902ff Mon Sep 17 00:00:00 2001 From: SE87H Date: Fri, 31 Jul 2026 11:19:08 +0200 Subject: [PATCH 5/8] fix(oneshot): synchronize terminal workspace on resume --- hermes_cli/oneshot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hermes_cli/oneshot.py b/hermes_cli/oneshot.py index 11b5e45857fa..1035908d51ea 100644 --- a/hermes_cli/oneshot.py +++ b/hermes_cli/oneshot.py @@ -392,6 +392,10 @@ def _load_oneshot_resume( "Failed to restore recorded session working directory: " f"{saved_cwd}" ) from exc + # Prompt construction and file/terminal tools prefer this value. + # Publish it only after chdir succeeds so a failed resume leaves + # the caller's runtime context untouched. + os.environ["TERMINAL_CWD"] = saved_cwd session_db.reopen_session(resolved_session_id) return resolved_session_id, conversation_history From b5123936cb13005ba446835ee46b5da5d24564f7 Mon Sep 17 00:00:00 2001 From: SE87H Date: Fri, 31 Jul 2026 11:19:46 +0200 Subject: [PATCH 6/8] test(oneshot): cover terminal workspace synchronization --- tests/hermes_cli/test_oneshot_terminal_cwd.py | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 tests/hermes_cli/test_oneshot_terminal_cwd.py diff --git a/tests/hermes_cli/test_oneshot_terminal_cwd.py b/tests/hermes_cli/test_oneshot_terminal_cwd.py new file mode 100644 index 000000000000..9ad09ec7d538 --- /dev/null +++ b/tests/hermes_cli/test_oneshot_terminal_cwd.py @@ -0,0 +1,195 @@ +from __future__ import annotations + +import os +import sys +import types +from pathlib import Path + +import pytest + + +def _session_db(workspace: Path, *, events: list[object]): + class FakeSessionDB: + def get_session(self, session_id): + if session_id == "session-1": + return {"id": session_id, "cwd": str(workspace)} + return None + + def resolve_session_by_title(self, _title): + return None + + def resolve_resume_session_id(self, session_id): + return session_id + + def get_resume_conversations(self, _session_id): + return ([{"role": "user", "content": "prior context"}], []) + + def reopen_session(self, session_id): + events.append(("reopen", session_id, os.environ.get("TERMINAL_CWD"))) + + def close(self): + events.append("close") + + return FakeSessionDB() + + +def test_resume_publishes_terminal_cwd_before_session_reopen( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + import hermes_cli.oneshot as oneshot + + workspace = tmp_path / "recorded-workspace" + workspace.mkdir() + events: list[object] = [] + monkeypatch.setenv("TERMINAL_CWD", str(tmp_path / "stale-workspace")) + + session_id, history = oneshot._load_oneshot_resume( + _session_db(workspace, events=events), + resume_session_id="session-1", + continue_last=None, + restore_resume_cwd=True, + ) + + assert session_id == "session-1" + assert history == [{"role": "user", "content": "prior context"}] + assert Path.cwd() == workspace.resolve() + assert os.environ["TERMINAL_CWD"] == str(workspace) + assert events == [("reopen", "session-1", str(workspace))] + + +def test_missing_recorded_workspace_does_not_mutate_terminal_context_or_session( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + import hermes_cli.oneshot as oneshot + + events: list[object] = [] + previous = str(tmp_path / "previous-workspace") + monkeypatch.setenv("TERMINAL_CWD", previous) + + with pytest.raises(FileNotFoundError, match="working directory is unavailable"): + oneshot._load_oneshot_resume( + _session_db(tmp_path / "missing", events=events), + resume_session_id="session-1", + continue_last=None, + restore_resume_cwd=True, + ) + + assert os.environ["TERMINAL_CWD"] == previous + assert events == [] + + +def test_failed_chdir_does_not_publish_terminal_context_or_reopen_session( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + import hermes_cli.oneshot as oneshot + + workspace = tmp_path / "recorded-workspace" + workspace.mkdir() + events: list[object] = [] + monkeypatch.setenv("TERMINAL_CWD", "/previous") + + def fail_chdir(_path: object) -> None: + raise OSError("blocked") + + monkeypatch.setattr(os, "chdir", fail_chdir) + + with pytest.raises(RuntimeError, match="Failed to restore"): + oneshot._load_oneshot_resume( + _session_db(workspace, events=events), + resume_session_id="session-1", + continue_last=None, + restore_resume_cwd=True, + ) + + assert os.environ["TERMINAL_CWD"] == "/previous" + assert events == [] + + +def test_no_restore_cwd_preserves_explicit_caller_workspace( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + import hermes_cli.oneshot as oneshot + + workspace = tmp_path / "recorded-workspace" + workspace.mkdir() + events: list[object] = [] + previous = str(tmp_path / "intentional-caller-workspace") + monkeypatch.setenv("TERMINAL_CWD", previous) + original_cwd = Path.cwd() + + oneshot._load_oneshot_resume( + _session_db(workspace, events=events), + resume_session_id="session-1", + continue_last=None, + restore_resume_cwd=False, + ) + + assert Path.cwd() == original_cwd + assert os.environ["TERMINAL_CWD"] == previous + assert events == [("reopen", "session-1", previous)] + + +def test_agent_construction_observes_restored_terminal_workspace( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + import hermes_cli.oneshot as oneshot + + workspace = tmp_path / "recorded-workspace" + workspace.mkdir() + events: list[object] = [] + observed: list[str | None] = [] + db = _session_db(workspace, events=events) + monkeypatch.setenv("TERMINAL_CWD", str(tmp_path / "stale")) + + class FakeAgent: + def __init__(self, **_kwargs): + observed.append(os.environ.get("TERMINAL_CWD")) + self.suppress_status_output = False + self.stream_delta_callback = object() + self.tool_gen_callback = object() + self._session_messages = [] + + def run_conversation(self, _prompt, **_kwargs): + return {"final_response": "ok"} + + def shutdown_memory_provider(self, _messages=None): + pass + + def close(self): + pass + + monkeypatch.setitem(sys.modules, "run_agent", types.SimpleNamespace(AIAgent=FakeAgent)) + monkeypatch.setattr( + "hermes_cli.config.load_config", + lambda: {"model": {"default": "test-model", "provider": "openai"}}, + ) + monkeypatch.setattr( + "hermes_cli.runtime_provider.resolve_runtime_provider", + lambda **_kwargs: { + "api_key": "test", + "base_url": "https://example.invalid", + "provider": "openai", + "requested_provider": "openai", + "api_mode": "chat_completions", + "credential_pool": None, + }, + ) + monkeypatch.setattr(oneshot, "_create_session_db_for_oneshot", lambda: db) + monkeypatch.setattr(oneshot, "get_fallback_chain", lambda _cfg: []) + + result = oneshot._run_agent( + "continue", + model="test-model", + provider="openai", + use_config_toolsets=False, + resume_session_id="session-1", + restore_resume_cwd=True, + ) + + assert result == ("ok", {"final_response": "ok"}) + assert observed == [str(workspace)] From 393abdd05dea9bb049dd4d0aac8367e9e9625e15 Mon Sep 17 00:00:00 2001 From: SE87H Date: Fri, 31 Jul 2026 11:38:15 +0200 Subject: [PATCH 7/8] chore(validation): map SE87H commit attribution --- contributors/emails/julienyezniguian@gmail.com | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 contributors/emails/julienyezniguian@gmail.com diff --git a/contributors/emails/julienyezniguian@gmail.com b/contributors/emails/julienyezniguian@gmail.com new file mode 100644 index 000000000000..353dbcea5182 --- /dev/null +++ b/contributors/emails/julienyezniguian@gmail.com @@ -0,0 +1,2 @@ +SE87H +# Internal validation of upstream PR #74397 From d2ee69de4a5702e991b4a87b520e93e6ac79fe58 Mon Sep 17 00:00:00 2001 From: SE87H Date: Fri, 31 Jul 2026 11:40:54 +0200 Subject: [PATCH 8/8] chore(validation): map internal Codex attribution --- contributors/emails/aeon-core@avaeon.local | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 contributors/emails/aeon-core@avaeon.local diff --git a/contributors/emails/aeon-core@avaeon.local b/contributors/emails/aeon-core@avaeon.local new file mode 100644 index 000000000000..353dbcea5182 --- /dev/null +++ b/contributors/emails/aeon-core@avaeon.local @@ -0,0 +1,2 @@ +SE87H +# Internal validation of upstream PR #74397