feat(cli): resume relaunches in the directory the session was started from (#38562)
hermes -c / --resume now reopen a session in its original working directory. The sessions table already had a cwd column; the classic CLI just never wrote or read it. - run_agent._ensure_db_session stamps cwd for local CLI sessions only (new _launch_cwd_for_session gates out gateway/cron and non-local terminal backends, where a host cwd is meaningless to restore). - cli._restore_session_cwd chdir's the process AND retargets TERMINAL_CWD so the terminal tool, code-exec tool, and relative-path resolution all land in the restored dir. Called from both resume paths (interactive run() and the -q single-query path). - Robust degradation: no-op when no cwd recorded, when already there, or when the dir is gone (single dim warning, stays put — no crash).
This commit is contained in:
55
cli.py
55
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(
|
||||
|
||||
29
run_agent.py
29
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:
|
||||
|
||||
Reference in New Issue
Block a user