chore: uptick
This commit is contained in:
+149
-11
@@ -800,6 +800,8 @@ def _print_tui_exit_summary(session_id: Optional[str], active_session_file: Opti
|
||||
|
||||
title = db.get_session_title(target)
|
||||
message_count = int(session.get("message_count") or 0)
|
||||
if message_count == 0:
|
||||
return # No real conversation — don't show resume info
|
||||
input_tokens = int(session.get("input_tokens") or 0)
|
||||
output_tokens = int(session.get("output_tokens") or 0)
|
||||
cache_read_tokens = int(session.get("cache_read_tokens") or 0)
|
||||
@@ -5431,6 +5433,45 @@ def _find_stale_dashboard_pids() -> list[int]:
|
||||
return dashboard_pids
|
||||
|
||||
|
||||
def _print_curator_first_run_notice() -> None:
|
||||
"""Print a short heads-up about the skill curator after `hermes update`.
|
||||
|
||||
Only fires when the curator is enabled AND has no recorded run yet, which
|
||||
is exactly the window where the gateway ticker used to fire Curator
|
||||
against a fresh skill library immediately after an update. We defer the
|
||||
first real pass by one ``interval_hours``; this notice tells the user how
|
||||
to preview or disable before then. Silent on steady state.
|
||||
"""
|
||||
try:
|
||||
from agent import curator
|
||||
except Exception:
|
||||
return
|
||||
try:
|
||||
if not curator.is_enabled():
|
||||
return
|
||||
state = curator.load_state()
|
||||
except Exception:
|
||||
return
|
||||
if state.get("last_run_at"):
|
||||
# Curator has run before (real or already seeded) — no notice needed.
|
||||
return
|
||||
try:
|
||||
hours = curator.get_interval_hours()
|
||||
except Exception:
|
||||
hours = 24 * 7
|
||||
days = max(1, hours // 24)
|
||||
print()
|
||||
print("ℹ Skill curator")
|
||||
print(
|
||||
f" Background skill maintenance is enabled. First pass is deferred "
|
||||
f"~{days}d after installation; only agent-created skills are in "
|
||||
f"scope and nothing is ever auto-deleted (archive is recoverable)."
|
||||
)
|
||||
print(" Preview now: hermes curator run --dry-run")
|
||||
print(" Pause it: hermes curator pause")
|
||||
print(" Docs: https://hermes-agent.nousresearch.com/docs/user-guide/features/curator")
|
||||
|
||||
|
||||
def _kill_stale_dashboard_processes(
|
||||
reason: str = "the running backend no longer matches the updated frontend",
|
||||
) -> None:
|
||||
@@ -5668,6 +5709,10 @@ def _update_via_zip(args):
|
||||
|
||||
print()
|
||||
print("✓ Update complete!")
|
||||
try:
|
||||
_print_curator_first_run_notice()
|
||||
except Exception as e:
|
||||
logger.debug("Curator first-run notice failed: %s", e)
|
||||
_kill_stale_dashboard_processes()
|
||||
|
||||
|
||||
@@ -6673,6 +6718,7 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
||||
if gateway_mode
|
||||
else None
|
||||
)
|
||||
assume_yes = bool(getattr(args, "yes", False))
|
||||
|
||||
print("⚕ Updating Hermes Agent...")
|
||||
print()
|
||||
@@ -6792,8 +6838,10 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
||||
else:
|
||||
auto_stash_ref = _stash_local_changes_if_needed(git_cmd, PROJECT_ROOT)
|
||||
|
||||
prompt_for_restore = auto_stash_ref is not None and (
|
||||
gateway_mode or (sys.stdin.isatty() and sys.stdout.isatty())
|
||||
prompt_for_restore = (
|
||||
auto_stash_ref is not None
|
||||
and not assume_yes
|
||||
and (gateway_mode or (sys.stdin.isatty() and sys.stdout.isatty()))
|
||||
)
|
||||
|
||||
# Check if there are updates
|
||||
@@ -7054,7 +7102,10 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
||||
print(f" ℹ️ {len(missing_config)} new config option(s) available")
|
||||
|
||||
print()
|
||||
if gateway_mode:
|
||||
if assume_yes:
|
||||
print(" ℹ --yes: auto-applying config migration (skipping API-key prompts).")
|
||||
response = "y"
|
||||
elif gateway_mode:
|
||||
response = (
|
||||
_gateway_prompt(
|
||||
"Would you like to configure new options now? [Y/n]", "n"
|
||||
@@ -7080,14 +7131,17 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
||||
|
||||
if response in ("", "y", "yes"):
|
||||
print()
|
||||
# In gateway mode, run auto-migrations only (no input() prompts
|
||||
# for API keys which would hang the detached process).
|
||||
results = migrate_config(interactive=not gateway_mode, quiet=False)
|
||||
# In gateway mode OR under --yes, run auto-migrations only (no
|
||||
# input() prompts for API keys which would hang the detached
|
||||
# process / defeat the point of --yes).
|
||||
results = migrate_config(
|
||||
interactive=not (gateway_mode or assume_yes), quiet=False
|
||||
)
|
||||
|
||||
if results["env_added"] or results["config_added"]:
|
||||
print()
|
||||
print("✓ Configuration updated!")
|
||||
if gateway_mode and missing_env:
|
||||
if (gateway_mode or assume_yes) and missing_env:
|
||||
print(" ℹ API keys require manual entry: hermes config migrate")
|
||||
else:
|
||||
print()
|
||||
@@ -7098,6 +7152,15 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
||||
print()
|
||||
print("✓ Update complete!")
|
||||
|
||||
# Curator first-run heads-up. Only prints when curator is enabled AND
|
||||
# has never run — i.e. the window where the ticker would otherwise
|
||||
# have fired against a fresh skill library. Kept silent on steady
|
||||
# state so we don't nag.
|
||||
try:
|
||||
_print_curator_first_run_notice()
|
||||
except Exception as e:
|
||||
logger.debug("Curator first-run notice failed: %s", e)
|
||||
|
||||
# Repair RHEL-family root installs where /usr/local/bin isn't on PATH
|
||||
# for non-login interactive shells. No-op on every other platform.
|
||||
try:
|
||||
@@ -7137,6 +7200,8 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
||||
supports_systemd_services,
|
||||
_ensure_user_systemd_env,
|
||||
find_gateway_pids,
|
||||
find_profile_gateway_processes,
|
||||
launch_detached_profile_gateway_restart,
|
||||
_get_service_pids,
|
||||
_graceful_restart_via_sigusr1,
|
||||
)
|
||||
@@ -7240,6 +7305,7 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
||||
|
||||
restarted_services = []
|
||||
killed_pids = set()
|
||||
relaunched_profiles = []
|
||||
|
||||
# --- Systemd services (Linux) ---
|
||||
# Discover all hermes-gateway* units (default + profiles)
|
||||
@@ -7429,7 +7495,33 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
||||
manual_pids = find_gateway_pids(
|
||||
exclude_pids=service_pids, all_profiles=True
|
||||
)
|
||||
profile_processes = {
|
||||
proc.pid: proc
|
||||
for proc in find_profile_gateway_processes(exclude_pids=service_pids)
|
||||
if proc.pid in manual_pids
|
||||
}
|
||||
for pid, proc in profile_processes.items():
|
||||
if not launch_detached_profile_gateway_restart(proc.profile, pid):
|
||||
continue
|
||||
# Prefer a graceful SIGUSR1 drain so in-flight agent runs
|
||||
# finish before the watcher respawns the gateway. If the
|
||||
# gateway doesn't support SIGUSR1 or doesn't exit within
|
||||
# the drain budget, fall back to SIGTERM — the watcher
|
||||
# still sees the exit and relaunches either way.
|
||||
drained = _graceful_restart_via_sigusr1(
|
||||
pid, drain_timeout=_drain_budget,
|
||||
)
|
||||
if not drained:
|
||||
try:
|
||||
os.kill(pid, _signal.SIGTERM)
|
||||
except (ProcessLookupError, PermissionError):
|
||||
pass
|
||||
killed_pids.add(pid)
|
||||
relaunched_profiles.append(proc.profile)
|
||||
|
||||
for pid in manual_pids:
|
||||
if pid in profile_processes:
|
||||
continue
|
||||
try:
|
||||
os.kill(pid, _signal.SIGTERM)
|
||||
killed_pids.add(pid)
|
||||
@@ -7440,11 +7532,14 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
||||
print()
|
||||
for svc in restarted_services:
|
||||
print(f" ✓ Restarted {svc}")
|
||||
if killed_pids:
|
||||
print(f" → Stopped {len(killed_pids)} manual gateway process(es)")
|
||||
if relaunched_profiles:
|
||||
names = ", ".join(relaunched_profiles)
|
||||
print(f" ✓ Restarting manual gateway profile(s): {names}")
|
||||
unmapped_count = len(killed_pids) - len(relaunched_profiles)
|
||||
if unmapped_count:
|
||||
print(f" → Stopped {unmapped_count} manual gateway process(es)")
|
||||
print(" Restart manually: hermes gateway run")
|
||||
# Also restart for each profile if needed
|
||||
if len(killed_pids) > 1:
|
||||
if unmapped_count > 1:
|
||||
print(
|
||||
" (or: hermes -p <profile> gateway run for each profile)"
|
||||
)
|
||||
@@ -7453,6 +7548,42 @@ def _cmd_update_impl(args, gateway_mode: bool):
|
||||
# No gateways were running — nothing to do
|
||||
pass
|
||||
|
||||
# --- Post-restart survivor sweep -----------------------------
|
||||
# Issue #17648: some gateways ignore SIGTERM (stuck drain,
|
||||
# blocked I/O, PID dead but zombie). The detached profile
|
||||
# watchers wait 120s for the old PID to exit — if it never
|
||||
# does, no respawn happens and the user keeps hitting
|
||||
# ImportError against a stale sys.modules. Give the
|
||||
# graceful paths a brief window to complete, then SIGKILL
|
||||
# any remaining pre-update PIDs so the watcher / service
|
||||
# manager can relaunch with fresh code.
|
||||
try:
|
||||
_time.sleep(3.0)
|
||||
_service_pids_after = _get_service_pids()
|
||||
_surviving = find_gateway_pids(
|
||||
exclude_pids=_service_pids_after, all_profiles=True,
|
||||
)
|
||||
# Scope to PIDs we already tried to kill during this
|
||||
# update (killed_pids). Anything new is a gateway that
|
||||
# started AFTER our restart attempt — respecting user
|
||||
# intent, we don't kill those.
|
||||
_stuck = [pid for pid in _surviving if pid in killed_pids]
|
||||
if _stuck:
|
||||
print()
|
||||
print(
|
||||
f" ⚠ {len(_stuck)} gateway process(es) ignored SIGTERM — force-killing"
|
||||
)
|
||||
for pid in _stuck:
|
||||
try:
|
||||
os.kill(pid, _signal.SIGKILL)
|
||||
except (ProcessLookupError, PermissionError):
|
||||
pass
|
||||
# Give the OS a beat to reap the processes so the
|
||||
# watchers see them exit and respawn.
|
||||
_time.sleep(1.5)
|
||||
except Exception as _sweep_exc:
|
||||
logger.debug("Post-restart survivor sweep failed: %s", _sweep_exc)
|
||||
|
||||
except Exception as e:
|
||||
logger.debug("Gateway restart during update failed: %s", e)
|
||||
|
||||
@@ -9861,6 +9992,13 @@ Examples:
|
||||
default=False,
|
||||
help="Force a pre-update backup for this run (off by default; overrides updates.pre_update_backup)",
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"--yes",
|
||||
"-y",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Assume yes for interactive prompts (config migration, stash restore). API-key entry is skipped; run 'hermes config migrate' separately for those.",
|
||||
)
|
||||
update_parser.set_defaults(func=cmd_update)
|
||||
|
||||
# =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user