Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5006,6 +5006,7 @@ def _init_agent(self, *, model_override: str = None, runtime_override: dict = No
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(
Expand Down Expand Up @@ -5225,6 +5226,59 @@ def show_banner(self):

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).

Expand Down Expand Up @@ -5281,6 +5335,7 @@ def _preload_resumed_session(self) -> bool:
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(
Expand Down
29 changes: 28 additions & 1 deletion run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,31 @@

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
Expand Down Expand Up @@ -476,15 +501,17 @@ def _ensure_db_session(self) -> None:
"""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:
Expand Down
Loading