diff --git a/cli.py b/cli.py index 598e6d3b5..62f4be1e3 100644 --- a/cli.py +++ b/cli.py @@ -5108,6 +5108,7 @@ class HermesCLI: f"[bold {_accent_hex()}]{_escape(title_part)}[/] " f"({msg_count} user message{'s' if msg_count != 1 else ''}, {len(restored)} total messages)" ) + self._restore_session_cwd(session_meta, quiet=_quiet_mode) else: if _quiet_mode: print( @@ -5327,6 +5328,59 @@ class HermesCLI: self._console_print() + def _restore_session_cwd(self, session_meta: dict, *, quiet: bool = False) -> None: + """Relaunch a resumed session in the directory it was started from. + + Idempotent and safe to call from every resume path. When the stored + ``cwd`` differs from the current process directory, we both + ``os.chdir()`` (so the process and any ``os.getcwd()`` fallback agree) + and retarget ``TERMINAL_CWD`` (so the terminal tool, code-exec tool, + and relative-path resolution all land in the same place — the local + terminal backend snapshots cwd on first use, which happens after this). + + No-ops when: the session recorded no cwd (gateway/remote/older + sessions), the directory no longer exists, or we're already there. + A missing directory degrades to a single dim warning rather than a + crash — repos get moved and deleted. + """ + recorded = (session_meta or {}).get("cwd") + if not recorded: + return + recorded = os.path.expanduser(str(recorded)) + try: + current = os.getcwd() + except OSError: + current = None + if current and os.path.realpath(recorded) == os.path.realpath(current): + return # Already where the session lived — nothing to announce. + + if not os.path.isdir(recorded): + msg = f"⚠ Session's working directory is gone: {recorded} — staying in {current or '.'}" + if quiet: + print(msg, file=sys.stderr) + else: + self._console_print(f"[{_DIM}]{_escape(msg)}[/]") + return + + try: + os.chdir(recorded) + except OSError as e: + msg = f"⚠ Could not enter session's working directory {recorded}: {e}" + if quiet: + print(msg, file=sys.stderr) + else: + self._console_print(f"[{_DIM}]{_escape(msg)}[/]") + return + + # Retarget the terminal/code-exec tools to match the process cwd. + os.environ["TERMINAL_CWD"] = recorded + + msg = f"↻ Working directory: {recorded}" + if quiet: + print(msg, file=sys.stderr) + else: + self._console_print(f"[{_DIM}]{_escape(msg)}[/]") + def _preload_resumed_session(self) -> bool: """Load a resumed session's history from the DB early (before first chat). @@ -5383,6 +5437,7 @@ class HermesCLI: f"({msg_count} user message{'s' if msg_count != 1 else ''}, " f"{len(restored)} total messages)[/]" ) + self._restore_session_cwd(session_meta) else: accent_color = _accent_hex() self._console_print( diff --git a/run_agent.py b/run_agent.py index 6d0b370e6..dadae7a56 100644 --- a/run_agent.py +++ b/run_agent.py @@ -63,6 +63,31 @@ from pathlib import Path from hermes_constants import get_hermes_home + +def _launch_cwd_for_session(source: str) -> Optional[str]: + """Working directory to stamp on a new session row, or None. + + Only local CLI sessions get a recorded cwd: the directory the process was + launched from is meaningful for ``hermes -c`` / ``--resume`` (relaunch + where you left off). Gateway/cron/remote-backend sessions have no stable + host cwd to restore, so they record nothing. + + ``TERMINAL_ENV`` is set by the CLI's config bridge (``load_cli_config``); + a non-"local" backend (docker/ssh/modal/...) means the host cwd is + irrelevant to the agent's tools, so we skip it there too. + """ + if source != "cli": + return None + backend = (os.environ.get("TERMINAL_ENV") or "local").strip().lower() + if backend and backend != "local": + return None + try: + return os.getcwd() + except OSError: + # cwd was unlinked out from under us — nothing meaningful to record. + return None + + # OpenAI lazy proxy + safe stdio + proxy URL helpers — see agent/process_bootstrap.py. # `OpenAI` is re-exported here so `patch("run_agent.OpenAI", ...)` in tests works. # The other `# noqa: F401` re-exports below cover names accessed via @@ -476,15 +501,17 @@ class AIAgent: """Create session DB row on first use. Disables _session_db on failure.""" if self._session_db_created or not self._session_db: return + source = self.platform or os.environ.get("HERMES_SESSION_SOURCE", "cli") try: self._session_db.create_session( session_id=self.session_id, - source=self.platform or os.environ.get("HERMES_SESSION_SOURCE", "cli"), + source=source, model=self.model, model_config=self._session_init_model_config, system_prompt=self._cached_system_prompt, user_id=None, parent_session_id=self._parent_session_id, + cwd=_launch_cwd_for_session(source), ) self._session_db_created = True except Exception as e: