feat: per-terminal --continue via terminal breadcrumbs
This commit is contained in:
@@ -5056,6 +5056,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
||||
timestamp_str = self.session_start.strftime("%Y%m%d_%H%M%S")
|
||||
short_uuid = uuid.uuid4().hex[:6]
|
||||
self.session_id = f"{timestamp_str}_{short_uuid}"
|
||||
self._write_terminal_breadcrumb()
|
||||
|
||||
# History file for persistent input recall across sessions
|
||||
self._history_file = _hermes_home / ".hermes_history"
|
||||
@@ -9251,6 +9252,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
||||
timestamp_str = self.session_start.strftime("%Y%m%d_%H%M%S")
|
||||
short_uuid = uuid.uuid4().hex[:6]
|
||||
self.session_id = f"{timestamp_str}_{short_uuid}"
|
||||
self._write_terminal_breadcrumb()
|
||||
self.conversation_history = []
|
||||
self._pending_title = None
|
||||
self._resumed = False
|
||||
@@ -12263,6 +12265,22 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
||||
}
|
||||
_cprint(labels.get(self.tool_progress_mode, ""))
|
||||
|
||||
def _write_terminal_breadcrumb(self) -> None:
|
||||
"""Record this terminal's live session for bare ``hermes -c``.
|
||||
|
||||
Called at session start and whenever ``self.session_id`` is
|
||||
reassigned mid-run (/new, /branch, auto-compression rotation) so a
|
||||
later bare ``-c`` in THIS terminal resumes THIS conversation's live
|
||||
tip. Best-effort — never raises, no-op without a terminal identity
|
||||
or when session.terminal_continue is false.
|
||||
"""
|
||||
try:
|
||||
from hermes_cli.terminal_breadcrumbs import write_breadcrumb
|
||||
|
||||
write_breadcrumb(self.session_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _transfer_session_yolo(self, old_session_id: str, new_session_id: str) -> None:
|
||||
"""Move YOLO bypass state from an old session key to a new one.
|
||||
|
||||
@@ -12586,6 +12604,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
||||
and self.agent.session_id != self.session_id
|
||||
):
|
||||
self.session_id = self.agent.session_id
|
||||
self._write_terminal_breadcrumb()
|
||||
self._pending_title = None
|
||||
# Manual /compress replaces conversation_history with a new
|
||||
# compressed handoff for the child session. Persist it from
|
||||
@@ -15660,6 +15679,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
||||
):
|
||||
self._transfer_session_yolo(self.session_id, self.agent.session_id)
|
||||
self.session_id = self.agent.session_id
|
||||
self._write_terminal_breadcrumb()
|
||||
self._pending_title = None
|
||||
|
||||
# Get the final response
|
||||
@@ -16044,6 +16064,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
||||
agent._persist_session(messages, conversation_history)
|
||||
if getattr(agent, "session_id", None):
|
||||
self.session_id = agent.session_id
|
||||
self._write_terminal_breadcrumb()
|
||||
|
||||
try:
|
||||
if persist_lock is None:
|
||||
|
||||
@@ -33,6 +33,15 @@ DEFAULT_CONFIG = {
|
||||
# sessions (no live client) so accumulated agents don't pile up under memory
|
||||
# pressure. Reopening one re-resumes it from disk. 0/null disables.
|
||||
"max_live_sessions": 16,
|
||||
"session": {
|
||||
# Per-terminal `hermes -c`: each CLI session drops a breadcrumb file
|
||||
# under $HERMES_HOME/terminal-sessions/<terminal-id>, and a bare
|
||||
# -c/--continue resumes THIS terminal's session (tmux pane, kitty
|
||||
# window, wezterm pane, plain tty, ...) instead of the globally
|
||||
# most-recent one. Set false to restore the old latest-session
|
||||
# behavior everywhere.
|
||||
"terminal_continue": True,
|
||||
},
|
||||
"agent": {
|
||||
"max_turns": 500,
|
||||
# Inactivity timeout for gateway agent execution (seconds).
|
||||
|
||||
+24
-10
@@ -2672,17 +2672,31 @@ def cmd_chat(args):
|
||||
print("Use 'hermes sessions list' to see available sessions.")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# -c with no argument — continue the most recent session
|
||||
source = "tui" if use_tui else "cli"
|
||||
last_id = _resolve_last_session(source=source)
|
||||
if not last_id and source == "tui":
|
||||
last_id = _resolve_last_session(source="cli")
|
||||
if last_id:
|
||||
args.resume = last_id
|
||||
# -c with no argument — prefer this terminal's own breadcrumb
|
||||
# (written at session start / rotation) so side-by-side terminals
|
||||
# each continue their own conversation. Falls back to the
|
||||
# most-recent session when there is no valid breadcrumb, or when
|
||||
# session.terminal_continue is false in config.yaml.
|
||||
try:
|
||||
from hermes_cli.terminal_breadcrumbs import resolve_breadcrumb_session
|
||||
|
||||
_crumb_id = resolve_breadcrumb_session()
|
||||
except Exception:
|
||||
_crumb_id = None
|
||||
if _crumb_id:
|
||||
args.resume = _crumb_id
|
||||
else:
|
||||
kind = "TUI" if use_tui else "CLI"
|
||||
print(f"No previous {kind} session found to continue.")
|
||||
sys.exit(1)
|
||||
# No valid breadcrumb — continue the most recent session
|
||||
source = "tui" if use_tui else "cli"
|
||||
last_id = _resolve_last_session(source=source)
|
||||
if not last_id and source == "tui":
|
||||
last_id = _resolve_last_session(source="cli")
|
||||
if last_id:
|
||||
args.resume = last_id
|
||||
else:
|
||||
kind = "TUI" if use_tui else "CLI"
|
||||
print(f"No previous {kind} session found to continue.")
|
||||
sys.exit(1)
|
||||
|
||||
# --resume @claude / --resume @codex: import a foreign session (Claude
|
||||
# Code / Codex CLI) and resume the newly created Hermes session.
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
"""Per-terminal session breadcrumbs for ``hermes -c`` / ``--continue``.
|
||||
|
||||
Each CLI session writes a tiny breadcrumb file
|
||||
``$HERMES_HOME/terminal-sessions/<terminal-id>`` containing
|
||||
``{"session_id": ..., "cwd": ..., "ts": ...}``. A bare ``hermes -c`` then
|
||||
resumes the session that belongs to THIS terminal (tty / tmux pane / kitty
|
||||
window / wezterm pane / ...) instead of the globally most-recent session —
|
||||
so two terminals side by side each continue their own conversation.
|
||||
|
||||
Everything here is strictly best-effort: no function raises, and when no
|
||||
stable terminal identity can be derived (no tty and no known multiplexer
|
||||
env var) breadcrumbs are skipped entirely and ``-c`` falls back to the
|
||||
existing latest-session behavior. Gated by ``session.terminal_continue``
|
||||
in config.yaml (default true).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# Multiplexer / terminal-emulator identity env vars, checked in order when
|
||||
# no real tty path is available (e.g. stdin piped but stdout still a pty
|
||||
# owned by a known terminal).
|
||||
_TERMINAL_ENV_VARS = (
|
||||
"ZELLIJ_PANE_ID",
|
||||
"TMUX_PANE",
|
||||
"KITTY_WINDOW_ID",
|
||||
"WEZTERM_PANE",
|
||||
"TERM_SESSION_ID",
|
||||
"WT_SESSION",
|
||||
)
|
||||
|
||||
# Breadcrumbs older than this are pruned opportunistically on each write —
|
||||
# a pane id from a tmux server restarted last month means nothing today.
|
||||
_STALE_AFTER_SECONDS = 30 * 24 * 60 * 60
|
||||
|
||||
_SANITIZE_RE = re.compile(r"[^A-Za-z0-9._-]")
|
||||
|
||||
|
||||
def _breadcrumbs_dir() -> Path:
|
||||
from hermes_constants import get_hermes_home
|
||||
|
||||
return get_hermes_home() / "terminal-sessions"
|
||||
|
||||
|
||||
def _sanitize(raw: str) -> str:
|
||||
"""Make an id safe to use as a filename (``/dev/pts/3`` -> ``dev-pts-3``)."""
|
||||
return _SANITIZE_RE.sub("-", raw.strip().strip("/"))[:120]
|
||||
|
||||
|
||||
def get_terminal_id() -> Optional[str]:
|
||||
"""Derive a stable identity for the terminal this process runs in.
|
||||
|
||||
Prefers the real tty device path (stdin, then stdout), else the first
|
||||
present multiplexer/emulator env var. Returns ``None`` when neither is
|
||||
available — callers must then skip breadcrumbs entirely.
|
||||
"""
|
||||
for fd in (sys.stdin, sys.stdout):
|
||||
try:
|
||||
name = os.ttyname(fd.fileno())
|
||||
except Exception:
|
||||
continue
|
||||
if name:
|
||||
return f"tty-{_sanitize(name)}"
|
||||
for var in _TERMINAL_ENV_VARS:
|
||||
val = os.environ.get(var)
|
||||
if val:
|
||||
return f"{var.lower()}-{_sanitize(val)}"
|
||||
return None
|
||||
|
||||
|
||||
def is_enabled() -> bool:
|
||||
"""Config gate: ``session.terminal_continue`` (default true)."""
|
||||
try:
|
||||
from hermes_cli.config import load_config
|
||||
|
||||
return bool((load_config().get("session") or {}).get("terminal_continue", True))
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
|
||||
def _prune_stale(directory: Path, now: float) -> None:
|
||||
"""Best-effort removal of breadcrumbs older than the staleness window."""
|
||||
try:
|
||||
for entry in directory.iterdir():
|
||||
try:
|
||||
if entry.is_file() and now - entry.stat().st_mtime > _STALE_AFTER_SECONDS:
|
||||
entry.unlink()
|
||||
except OSError:
|
||||
continue
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def write_breadcrumb(session_id: str, cwd: Optional[str] = None) -> None:
|
||||
"""Record that this terminal's live session is ``session_id``.
|
||||
|
||||
Synchronous, best-effort, never raises. No-op when the feature is
|
||||
disabled, the session id is empty, or no terminal identity exists.
|
||||
"""
|
||||
try:
|
||||
if not session_id or not is_enabled():
|
||||
return
|
||||
terminal_id = get_terminal_id()
|
||||
if not terminal_id:
|
||||
return
|
||||
directory = _breadcrumbs_dir()
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
now = time.time()
|
||||
payload = {
|
||||
"session_id": session_id,
|
||||
"cwd": cwd or os.getcwd(),
|
||||
"ts": now,
|
||||
}
|
||||
tmp = directory / f".{terminal_id}.tmp"
|
||||
tmp.write_text(json.dumps(payload), encoding="utf-8")
|
||||
os.replace(tmp, directory / terminal_id)
|
||||
_prune_stale(directory, now)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def read_breadcrumb() -> Optional[dict]:
|
||||
"""Return this terminal's breadcrumb payload, or ``None``.
|
||||
|
||||
Ignores breadcrumbs older than the staleness window. Never raises.
|
||||
"""
|
||||
try:
|
||||
terminal_id = get_terminal_id()
|
||||
if not terminal_id:
|
||||
return None
|
||||
path = _breadcrumbs_dir() / terminal_id
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(data, dict) or not str(data.get("session_id") or "").strip():
|
||||
return None
|
||||
ts = data.get("ts")
|
||||
if isinstance(ts, (int, float)) and time.time() - ts > _STALE_AFTER_SECONDS:
|
||||
return None
|
||||
return data
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def resolve_breadcrumb_session() -> Optional[str]:
|
||||
"""Resolve a bare ``-c`` for this terminal, or ``None`` to fall back.
|
||||
|
||||
Returns the breadcrumb's session id only when it still exists in the
|
||||
session DB, projected forward through the compression chain so the
|
||||
resume lands on the live tip rather than a dead compressed parent
|
||||
(same projection as ``main._resolve_session_by_name_or_id``).
|
||||
"""
|
||||
if not is_enabled():
|
||||
return None
|
||||
crumb = read_breadcrumb()
|
||||
if not crumb:
|
||||
return None
|
||||
session_id = str(crumb.get("session_id") or "").strip()
|
||||
if not session_id:
|
||||
return None
|
||||
db = None
|
||||
try:
|
||||
from hermes_state import SessionDB
|
||||
|
||||
db = SessionDB()
|
||||
if not db.get_session(session_id):
|
||||
return None # session was deleted — fall back to latest
|
||||
try:
|
||||
session_id = db.get_compression_tip(session_id) or session_id
|
||||
except Exception:
|
||||
pass
|
||||
return session_id
|
||||
except Exception:
|
||||
return None
|
||||
finally:
|
||||
if db is not None:
|
||||
try:
|
||||
db.close()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,204 @@
|
||||
"""Tests for hermes_cli/terminal_breadcrumbs.py — per-terminal ``hermes -c``.
|
||||
|
||||
Covers terminal id derivation (tty vs env vars vs none), breadcrumb
|
||||
write/read roundtrip under a temp HERMES_HOME, stale-session fallback
|
||||
(breadcrumb pointing at a deleted session), compression-tip projection,
|
||||
and the session.terminal_continue config gate.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli import terminal_breadcrumbs as tb
|
||||
|
||||
|
||||
TERMINAL_ENV_VARS = (
|
||||
"ZELLIJ_PANE_ID",
|
||||
"TMUX_PANE",
|
||||
"KITTY_WINDOW_ID",
|
||||
"WEZTERM_PANE",
|
||||
"TERM_SESSION_ID",
|
||||
"WT_SESSION",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hermes_home(tmp_path, monkeypatch):
|
||||
home = tmp_path / ".hermes"
|
||||
home.mkdir()
|
||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||
return home
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def no_terminal_env(monkeypatch):
|
||||
"""Strip every terminal-identity env var so tests control identity."""
|
||||
for var in TERMINAL_ENV_VARS:
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
|
||||
|
||||
def _fake_no_tty(monkeypatch):
|
||||
monkeypatch.setattr(tb.os, "ttyname", lambda fd: (_ for _ in ()).throw(OSError()))
|
||||
|
||||
|
||||
def _fake_tty(monkeypatch, name="/dev/pts/7"):
|
||||
monkeypatch.setattr(tb.os, "ttyname", lambda fd: name)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- identity
|
||||
|
||||
def test_terminal_id_prefers_tty(monkeypatch, no_terminal_env):
|
||||
_fake_tty(monkeypatch, "/dev/pts/7")
|
||||
monkeypatch.setenv("TMUX_PANE", "%3")
|
||||
assert tb.get_terminal_id() == "tty-dev-pts-7"
|
||||
|
||||
|
||||
def test_terminal_id_env_var_order(monkeypatch, no_terminal_env):
|
||||
_fake_no_tty(monkeypatch)
|
||||
monkeypatch.setenv("KITTY_WINDOW_ID", "12")
|
||||
monkeypatch.setenv("WT_SESSION", "abc-123")
|
||||
# KITTY_WINDOW_ID comes before WT_SESSION in the preference order
|
||||
assert tb.get_terminal_id() == "kitty_window_id-12"
|
||||
|
||||
|
||||
def test_terminal_id_sanitizes_env_value(monkeypatch, no_terminal_env):
|
||||
_fake_no_tty(monkeypatch)
|
||||
monkeypatch.setenv("TMUX_PANE", "%41")
|
||||
tid = tb.get_terminal_id()
|
||||
assert tid is not None
|
||||
assert "/" not in tid and "%" not in tid
|
||||
|
||||
|
||||
def test_terminal_id_none_when_no_identity(monkeypatch, no_terminal_env):
|
||||
_fake_no_tty(monkeypatch)
|
||||
assert tb.get_terminal_id() is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------- write / read
|
||||
|
||||
def test_breadcrumb_roundtrip(hermes_home, monkeypatch, no_terminal_env):
|
||||
_fake_tty(monkeypatch)
|
||||
tb.write_breadcrumb("20260815_120000_abc123", cwd="/tmp/project")
|
||||
crumb = tb.read_breadcrumb()
|
||||
assert crumb is not None
|
||||
assert crumb["session_id"] == "20260815_120000_abc123"
|
||||
assert crumb["cwd"] == "/tmp/project"
|
||||
assert isinstance(crumb["ts"], float)
|
||||
files = list((hermes_home / "terminal-sessions").iterdir())
|
||||
assert [f.name for f in files] == ["tty-dev-pts-7"]
|
||||
|
||||
|
||||
def test_write_skipped_without_terminal_identity(hermes_home, monkeypatch, no_terminal_env):
|
||||
_fake_no_tty(monkeypatch)
|
||||
tb.write_breadcrumb("20260815_120000_abc123")
|
||||
assert not (hermes_home / "terminal-sessions").exists()
|
||||
assert tb.read_breadcrumb() is None
|
||||
|
||||
|
||||
def test_two_terminals_do_not_clobber(hermes_home, monkeypatch, no_terminal_env):
|
||||
_fake_tty(monkeypatch, "/dev/pts/1")
|
||||
tb.write_breadcrumb("session-one")
|
||||
_fake_tty(monkeypatch, "/dev/pts/2")
|
||||
tb.write_breadcrumb("session-two")
|
||||
assert tb.read_breadcrumb()["session_id"] == "session-two"
|
||||
_fake_tty(monkeypatch, "/dev/pts/1")
|
||||
assert tb.read_breadcrumb()["session_id"] == "session-one"
|
||||
|
||||
|
||||
def test_stale_breadcrumb_ignored_and_pruned(hermes_home, monkeypatch, no_terminal_env):
|
||||
_fake_tty(monkeypatch, "/dev/pts/1")
|
||||
directory = hermes_home / "terminal-sessions"
|
||||
directory.mkdir(parents=True)
|
||||
stale = directory / "tty-dev-pts-1"
|
||||
stale.write_text(
|
||||
json.dumps({"session_id": "old", "cwd": "/", "ts": time.time() - 40 * 86400})
|
||||
)
|
||||
old_mtime = time.time() - 40 * 86400
|
||||
os.utime(stale, (old_mtime, old_mtime))
|
||||
# read: stale payload rejected
|
||||
assert tb.read_breadcrumb() is None
|
||||
# write from another terminal prunes the stale file
|
||||
_fake_tty(monkeypatch, "/dev/pts/2")
|
||||
tb.write_breadcrumb("fresh")
|
||||
assert not stale.exists()
|
||||
|
||||
|
||||
def test_corrupt_breadcrumb_returns_none(hermes_home, monkeypatch, no_terminal_env):
|
||||
_fake_tty(monkeypatch, "/dev/pts/1")
|
||||
directory = hermes_home / "terminal-sessions"
|
||||
directory.mkdir(parents=True)
|
||||
(directory / "tty-dev-pts-1").write_text("not json{")
|
||||
assert tb.read_breadcrumb() is None
|
||||
|
||||
|
||||
# ------------------------------------------------------------- resolution
|
||||
|
||||
def _make_session(home: Path, session_id: str):
|
||||
from hermes_state import SessionDB
|
||||
|
||||
db = SessionDB()
|
||||
db.create_session(session_id, "cli")
|
||||
db.close()
|
||||
|
||||
|
||||
def test_resolve_picks_this_terminals_session(hermes_home, monkeypatch, no_terminal_env):
|
||||
_fake_tty(monkeypatch, "/dev/pts/5")
|
||||
_make_session(hermes_home, "20260815_100000_aaaaaa")
|
||||
_make_session(hermes_home, "20260815_110000_bbbbbb") # newer, other terminal
|
||||
tb.write_breadcrumb("20260815_100000_aaaaaa")
|
||||
assert tb.resolve_breadcrumb_session() == "20260815_100000_aaaaaa"
|
||||
|
||||
|
||||
def test_resolve_falls_back_when_session_deleted(hermes_home, monkeypatch, no_terminal_env):
|
||||
_fake_tty(monkeypatch, "/dev/pts/5")
|
||||
_make_session(hermes_home, "20260815_110000_bbbbbb")
|
||||
tb.write_breadcrumb("20260815_100000_deleted") # never existed / deleted
|
||||
assert tb.resolve_breadcrumb_session() is None
|
||||
|
||||
|
||||
def test_resolve_projects_through_compression_chain(hermes_home, monkeypatch, no_terminal_env):
|
||||
_fake_tty(monkeypatch, "/dev/pts/5")
|
||||
_make_session(hermes_home, "20260815_100000_parent")
|
||||
tb.write_breadcrumb("20260815_100000_parent")
|
||||
|
||||
from hermes_state import SessionDB
|
||||
|
||||
monkeypatch.setattr(
|
||||
SessionDB,
|
||||
"get_compression_tip",
|
||||
lambda self, sid: "20260815_110000_child",
|
||||
)
|
||||
assert tb.resolve_breadcrumb_session() == "20260815_110000_child"
|
||||
|
||||
|
||||
# ------------------------------------------------------------ config gate
|
||||
|
||||
def test_config_gate_off_disables_writes_and_resolution(
|
||||
hermes_home, monkeypatch, no_terminal_env
|
||||
):
|
||||
_fake_tty(monkeypatch, "/dev/pts/9")
|
||||
import hermes_cli.config as config_mod
|
||||
|
||||
monkeypatch.setattr(
|
||||
config_mod, "load_config", lambda: {"session": {"terminal_continue": False}}
|
||||
)
|
||||
tb.write_breadcrumb("20260815_120000_abc123")
|
||||
assert not (hermes_home / "terminal-sessions").exists()
|
||||
# Even with a pre-existing breadcrumb, resolution must decline
|
||||
monkeypatch.setattr(config_mod, "load_config", lambda: {})
|
||||
tb.write_breadcrumb("20260815_120000_abc123")
|
||||
monkeypatch.setattr(
|
||||
config_mod, "load_config", lambda: {"session": {"terminal_continue": False}}
|
||||
)
|
||||
assert tb.resolve_breadcrumb_session() is None
|
||||
|
||||
|
||||
def test_config_gate_default_is_enabled(monkeypatch):
|
||||
import hermes_cli.config as config_mod
|
||||
|
||||
monkeypatch.setattr(config_mod, "load_config", lambda: {})
|
||||
assert tb.is_enabled() is True
|
||||
@@ -115,6 +115,10 @@ hermes chat -c
|
||||
|
||||
This looks up the most recent `cli` session from the SQLite database and loads its full conversation history.
|
||||
|
||||
#### Per-Terminal Continue
|
||||
|
||||
A bare `-c` is terminal-aware: each CLI session drops a small breadcrumb file under `~/.hermes/terminal-sessions/` keyed by the terminal it runs in (tty device, tmux pane, kitty window, wezterm pane, Zellij pane, Windows Terminal session, ...). When you run `hermes -c` again in the *same* terminal, Hermes resumes that terminal's own session — so two panes side by side each continue their own conversation instead of both grabbing the globally most-recent one. If there's no breadcrumb for the terminal (first use, deleted session, or a stale breadcrumb older than 30 days), `-c` falls back to the most-recent-session behavior. `-c "name"` and `--resume` are unaffected. Disable with `session.terminal_continue: false` in `config.yaml`.
|
||||
|
||||
### Resume by Name
|
||||
|
||||
If you've given a session a title (see [Session Naming](#session-naming) below), you can resume it by name:
|
||||
|
||||
Reference in New Issue
Block a user