Merge branch 'simp/r3-25-B' into simp/integration3
This commit is contained in:
@@ -33,11 +33,7 @@ def _git_ok(git_cmd, args, cwd, **kw) -> bool:
|
||||
|
||||
|
||||
def _prune_orphan_rescue_refs(
|
||||
git_cmd,
|
||||
cwd,
|
||||
branch,
|
||||
keep=_ORPHAN_RESCUE_REFS_TO_KEEP,
|
||||
max_age_days=_ORPHAN_RESCUE_REF_MAX_AGE_DAYS,
|
||||
git_cmd, cwd, branch, keep=_ORPHAN_RESCUE_REFS_TO_KEEP, max_age_days=_ORPHAN_RESCUE_REF_MAX_AGE_DAYS
|
||||
) -> None:
|
||||
"""Expire old orphan rescue refs (``refs/hermes-update-backups/orphan-<branch>-<ts>-<sha>``).
|
||||
|
||||
@@ -49,9 +45,7 @@ def _prune_orphan_rescue_refs(
|
||||
from hermes_cli.update_cmd import _git_run
|
||||
with suppress(OSError):
|
||||
prefix = f"refs/hermes-update-backups/orphan-{branch}-"
|
||||
list_result = _git_run(
|
||||
git_cmd, ["for-each-ref", "--format=%(refname)", "--sort=refname", f"{prefix}*"], cwd,
|
||||
)
|
||||
list_result = _git_run(git_cmd, ["for-each-ref", "--format=%(refname)", "--sort=refname", f"{prefix}*"], cwd)
|
||||
if list_result.returncode != 0:
|
||||
return
|
||||
refs = [line.strip() for line in list_result.stdout.splitlines() if line.strip()]
|
||||
@@ -277,27 +271,21 @@ def _sync_with_upstream_if_needed(
|
||||
the check never happened, so the caller never reports "up to date" on an origin-only compare.
|
||||
"""
|
||||
from hermes_cli.update_cmd import (
|
||||
_add_upstream_remote,
|
||||
_count_commits_between,
|
||||
_has_upstream_remote,
|
||||
_mark_skip_upstream_prompt,
|
||||
_no_prompt_git_kwargs,
|
||||
_should_skip_upstream_prompt,
|
||||
_add_upstream_remote, _count_commits_between, _has_upstream_remote,
|
||||
_mark_skip_upstream_prompt, _no_prompt_git_kwargs, _should_skip_upstream_prompt,
|
||||
)
|
||||
if not _has_upstream_remote(git_cmd, cwd):
|
||||
if _should_skip_upstream_prompt():
|
||||
return False
|
||||
|
||||
print(
|
||||
"\nℹ Your fork is not tracking the official Hermes repository.\n"
|
||||
" This means you may miss updates from NousResearch/hermes-agent.\n"
|
||||
)
|
||||
if assume_yes or (input_fn is None and not (sys.stdin.isatty() and sys.stdout.isatty())):
|
||||
# --yes means "don't block", not "mutate my remotes"; don't persist the decline.
|
||||
print(" Skipping upstream setup (non-interactive run).")
|
||||
print(" Add it later with: git remote add upstream https://github.com/NousResearch/hermes-agent.git")
|
||||
print(" Skipping upstream setup (non-interactive run).\n"
|
||||
" Add it later with: git remote add upstream https://github.com/NousResearch/hermes-agent.git")
|
||||
return False
|
||||
|
||||
if input_fn is not None:
|
||||
response = input_fn("Add official repo as 'upstream' remote? [y/N]", "n").strip().lower()
|
||||
else:
|
||||
@@ -306,7 +294,6 @@ def _sync_with_upstream_if_needed(
|
||||
except (EOFError, KeyboardInterrupt, UnicodeDecodeError):
|
||||
print()
|
||||
response = "n"
|
||||
|
||||
if response not in {"", "y", "yes"}:
|
||||
print(" Skipped. Run 'git remote add upstream https://github.com/NousResearch/hermes-agent.git' to add later.")
|
||||
_mark_skip_upstream_prompt()
|
||||
@@ -320,21 +307,16 @@ def _sync_with_upstream_if_needed(
|
||||
# Only upstream/main: a bare fetch drags in thousands of auto-generated branches.
|
||||
print("\n→ Fetching upstream...")
|
||||
try:
|
||||
subprocess.run(
|
||||
git_cmd + ["fetch", "upstream", "main", "--quiet"],
|
||||
cwd=cwd, capture_output=True, check=True, **_no_prompt_git_kwargs(),
|
||||
)
|
||||
subprocess.run(git_cmd + ["fetch", "upstream", "main", "--quiet"], cwd=cwd, capture_output=True, check=True, **_no_prompt_git_kwargs())
|
||||
except subprocess.CalledProcessError:
|
||||
print(" ✗ Failed to fetch upstream. Skipping upstream sync.")
|
||||
return False
|
||||
|
||||
origin_ahead = _count_commits_between(git_cmd, cwd, "upstream/main", "origin/main")
|
||||
upstream_ahead = _count_commits_between(git_cmd, cwd, "origin/main", "upstream/main")
|
||||
|
||||
if origin_ahead < 0 or upstream_ahead < 0:
|
||||
print(" ✗ Could not compare branches. Skipping upstream sync.")
|
||||
return False
|
||||
|
||||
if origin_ahead > 0:
|
||||
print(
|
||||
f"\nℹ Your fork has {origin_ahead} commit(s) not on upstream.\n"
|
||||
@@ -342,17 +324,13 @@ def _sync_with_upstream_if_needed(
|
||||
" If you want to merge upstream changes, run:\n git pull upstream main"
|
||||
)
|
||||
return True
|
||||
|
||||
if upstream_ahead == 0:
|
||||
print(" ✓ Fork is up to date with upstream")
|
||||
return True
|
||||
|
||||
print(f"\n→ Fork is {upstream_ahead} commit(s) behind upstream\n→ Pulling from upstream...")
|
||||
try:
|
||||
subprocess.run(
|
||||
git_cmd + ["pull", "--ff-only", "upstream", "main"],
|
||||
cwd=cwd, check=True, **_no_prompt_git_kwargs(),
|
||||
)
|
||||
subprocess.run(git_cmd + ["pull", "--ff-only", "upstream", "main"], cwd=cwd, check=True, **_no_prompt_git_kwargs())
|
||||
except subprocess.CalledProcessError:
|
||||
print(" ✗ Failed to pull from upstream. You may need to resolve conflicts manually.")
|
||||
return False
|
||||
@@ -430,8 +408,7 @@ def _portable_git_candidates() -> list:
|
||||
from hermes_cli.update_cmd import get_default_hermes_root, get_hermes_home
|
||||
candidates = []
|
||||
with suppress(Exception):
|
||||
for root in (get_default_hermes_root(), Path(get_hermes_home())):
|
||||
candidates.append(root / "git" / "mingw64" / "libexec" / "git-core" / "git.exe")
|
||||
candidates += [root / "git" / "mingw64" / "libexec" / "git-core" / "git.exe" for root in (get_default_hermes_root(), Path(get_hermes_home()))]
|
||||
return candidates
|
||||
|
||||
|
||||
@@ -459,10 +436,7 @@ def _ensure_non_trampoline_git(git_cmd: list) -> list:
|
||||
return git_cmd
|
||||
real_git = _locate_real_git()
|
||||
if real_git is None:
|
||||
print(
|
||||
"⚠ Detected a broken git trampoline and could not locate a real "
|
||||
"git binary — the update will fall back to the ZIP path."
|
||||
)
|
||||
print("⚠ Detected a broken git trampoline and could not locate a real git binary — the update will fall back to the ZIP path.")
|
||||
return git_cmd
|
||||
print(f"⚠ Detected a broken git trampoline; switching to real git at {real_git}")
|
||||
return [str(real_git)] + list(git_cmd[1:])
|
||||
@@ -479,10 +453,7 @@ def _discard_lockfile_churn(git_cmd, repo_root):
|
||||
return
|
||||
changed = [line.strip() for line in diff.stdout.splitlines()]
|
||||
dirty_package_dirs = {Path(p).parent for p in changed if p.endswith("package.json")}
|
||||
dirty = [
|
||||
p for p in changed
|
||||
if p.endswith("package-lock.json") and Path(p).parent not in dirty_package_dirs
|
||||
]
|
||||
dirty = [p for p in changed if p.endswith("package-lock.json") and Path(p).parent not in dirty_package_dirs]
|
||||
if not dirty:
|
||||
return
|
||||
_git_run(git_cmd, ["checkout", "--", *dirty], repo_root)
|
||||
|
||||
@@ -7,10 +7,12 @@ Origin helpers are imported lazily per function (no cycle; test patches on the o
|
||||
import logging
|
||||
from contextlib import contextmanager, suppress
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import time as _time
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from hermes_cli.update_cmd_common import _best_effort
|
||||
@@ -31,8 +33,6 @@ def _abort_on_error(prefix: str):
|
||||
def _write_update_planned_stop_marker(profile_path: Path, pid: int) -> bool:
|
||||
"""Write a planned-stop marker into a specific profile home."""
|
||||
try:
|
||||
from datetime import timezone
|
||||
|
||||
from gateway.status import _get_process_start_time
|
||||
from utils import atomic_json_write
|
||||
|
||||
@@ -218,8 +218,6 @@ def _hermes_holder_subcommand(cmdline: str) -> str | None:
|
||||
``hermes(.exe)`` entry token, return the first following token that isn't a flag or a flag's value.
|
||||
"""
|
||||
try:
|
||||
import shlex
|
||||
|
||||
tokens = shlex.split(cmdline, posix=False)
|
||||
except Exception:
|
||||
tokens = cmdline.split()
|
||||
@@ -352,9 +350,7 @@ def _refuse_gateway_ancestor_tree_kill(pids: list[int], *, gateway_mode: bool) -
|
||||
return True
|
||||
|
||||
|
||||
def _ledger_manual_serve_holders(
|
||||
matches: list[tuple[int, str, str]],
|
||||
) -> list[dict]:
|
||||
def _ledger_manual_serve_holders(matches: list[tuple[int, str, str]]) -> list[dict]:
|
||||
"""Full ledger entries for venv holders that are MANUAL serve/dashboard backends.
|
||||
|
||||
Positive identity only: self-registered purpose serve/dashboard, live (pid, create_time), recorded spawner
|
||||
@@ -559,9 +555,7 @@ def _handoff_reapable_backend_pids(matches: list[tuple[int, str, str]]) -> list[
|
||||
return roots or None
|
||||
|
||||
|
||||
def _stop_process_trees(
|
||||
pids: list[int] | list[tuple[int, int]],
|
||||
) -> None:
|
||||
def _stop_process_trees(pids: list[int] | list[tuple[int, int]]) -> None:
|
||||
"""Force-stop each PID with its full child tree (Windows); best effort, never raises.
|
||||
|
||||
``taskkill /T /F``: stopping only the parent can leave a ``.hermes-runtime`` child holding the install open.
|
||||
@@ -570,11 +564,7 @@ def _stop_process_trees(
|
||||
from hermes_cli._subprocess_compat import pid_is_hermes, windows_hide_flags
|
||||
|
||||
for entry in pids:
|
||||
if isinstance(entry, tuple):
|
||||
pid, expected_start_time = entry
|
||||
else:
|
||||
pid = int(entry)
|
||||
expected_start_time = get_process_start_time(pid)
|
||||
pid, expected_start_time = entry if isinstance(entry, tuple) else (int(entry), get_process_start_time(int(entry)))
|
||||
try:
|
||||
if expected_start_time is None:
|
||||
logger.debug("Skipping taskkill of PID %s: process identity unavailable", pid)
|
||||
@@ -799,13 +789,9 @@ def _pause_windows_gateway_services(service_gateways, token: dict, profiles: dic
|
||||
paused_services.append(current_service_name)
|
||||
current_service_name = None
|
||||
if paused_services:
|
||||
token["services"] = paused_services
|
||||
token["expected_services"] = list(paused_services)
|
||||
token["restarted_services"] = []
|
||||
token.update(services=paused_services, expected_services=list(paused_services), restarted_services=[])
|
||||
token["service_profiles"] = {
|
||||
str(service.name): str(service.profile)
|
||||
for service in service_gateways
|
||||
if str(service.name) in paused_services
|
||||
str(s.name): str(s.profile) for s in service_gateways if str(s.name) in paused_services
|
||||
}
|
||||
print(" ✓ Paused Windows gateway service(s): " + ", ".join(paused_services))
|
||||
return token
|
||||
@@ -917,34 +903,32 @@ def _pause_windows_gateways_for_update() -> dict | None:
|
||||
survivors = _m()._wait_for_windows_update_gateway_exit(mapped_pids, timeout=drain_timeout)
|
||||
unmapped_pids = [pid for pid in running_pids if pid not in profile_processes and pid not in service_gateway_pids]
|
||||
|
||||
# Snapshot unmapped gateways' argv *before* force-killing so resume can replay it.
|
||||
# Unmapped = no profile->PID-file mapping (e.g. Scheduled Task ``pythonw.exe -m ...``).
|
||||
unmapped: list[dict] = []
|
||||
for pid in unmapped_pids:
|
||||
argv = None
|
||||
def _argv_or_none(pid: int):
|
||||
try:
|
||||
argv = _capture_gateway_argv(int(pid))
|
||||
return _capture_gateway_argv(pid)
|
||||
except Exception as exc:
|
||||
logger.debug("Could not capture argv for unmapped gateway %s: %s", pid, exc)
|
||||
unmapped.append({"pid": int(pid), "argv": argv})
|
||||
return None
|
||||
|
||||
# Snapshot unmapped gateways' argv *before* force-killing so resume can replay it.
|
||||
# Unmapped = no profile->PID-file mapping (e.g. Scheduled Task ``pythonw.exe -m ...``).
|
||||
unmapped = [{"pid": int(pid), "argv": _argv_or_none(int(pid))} for pid in unmapped_pids]
|
||||
|
||||
# Tree-kill survivors, unmapped gateways, and pre-drain launchers; a launcher
|
||||
# already gone with its worker raises ProcessLookupError and is skipped.
|
||||
force_killed = []
|
||||
for pid in sorted(set(survivors).union(unmapped_pids).union(launcher_pids)):
|
||||
with suppress(ProcessLookupError, PermissionError, OSError):
|
||||
pid_int = int(pid)
|
||||
terminate_pid(pid_int, force=True, expected_start_time=get_process_start_time(pid_int))
|
||||
force_killed.append(pid_int)
|
||||
terminate_pid(int(pid), force=True, expected_start_time=get_process_start_time(int(pid)))
|
||||
force_killed.append(int(pid))
|
||||
|
||||
if profiles:
|
||||
print(f" ✓ Paused gateway profile(s): {', '.join(sorted(profiles))}")
|
||||
if force_killed:
|
||||
print(f" → Force-stopped {len(force_killed)} gateway process(es)")
|
||||
|
||||
if unmapped_pids:
|
||||
print(f" → Stopped {len(unmapped_pids)} gateway process(es) without profile mapping")
|
||||
if sum(1 for u in unmapped if u.get("argv")) < len(unmapped_pids):
|
||||
if any(not u.get("argv") for u in unmapped):
|
||||
# No recoverable cmdline (psutil missing, access denied, gone): manual restart.
|
||||
print(" Restart manually after update: hermes gateway run")
|
||||
|
||||
@@ -1011,15 +995,13 @@ def _refresh_bootstrap_cache_scripts(branch: str = "main") -> None:
|
||||
"""
|
||||
from hermes_cli.update_cmd import _m
|
||||
with _best_effort('Could not refresh bootstrap-cache scripts after update: %s'):
|
||||
import re as _re
|
||||
|
||||
cache_dir = Path(_m().get_hermes_home()) / "bootstrap-cache"
|
||||
if not cache_dir.is_dir():
|
||||
return
|
||||
# Mirror install_script.rs::sanitize_ref().
|
||||
safe_ref = _re.sub(r"[^A-Za-z0-9._-]", "_", str(branch or "main"))
|
||||
safe_ref = re.sub(r"[^A-Za-z0-9._-]", "_", str(branch or "main"))
|
||||
# Mirror install_script.rs::is_valid_commit(): immutable commit pin, never rewrite.
|
||||
if _re.fullmatch(r"[0-9a-fA-F]{7,40}", safe_ref):
|
||||
if re.fullmatch(r"[0-9a-fA-F]{7,40}", safe_ref):
|
||||
return
|
||||
refreshed = []
|
||||
for kind, src_name in (("ps1", "install.ps1"), ("sh", "install.sh")):
|
||||
@@ -1276,24 +1258,17 @@ def _clear_windows_venv_holders_or_exit(args, gateway_mode: bool, _windows_gatew
|
||||
f" ⚠ {len(_gateway_holders)} gateway process(es) still hold the venv after the pause; stopping them",
|
||||
_gateway_holders, stop=_terminate_leftover_gateways,
|
||||
)
|
||||
if _venv_holders:
|
||||
# Positive-identity rung (any context): spawn ledger proves the holder is an
|
||||
# orphaned backend (self-registered, spawner provably dead). No PPID archaeology.
|
||||
_ledger_backends = _m()._ledger_reapable_backend_pids(_venv_holders)
|
||||
if _ledger_backends:
|
||||
_venv_holders = _reap_and_rescan(
|
||||
f" ⚠ {len(_ledger_backends)} ledger-identified orphaned "
|
||||
"Hermes backend process(es) hold the venv; stopping their trees", _ledger_backends,
|
||||
)
|
||||
if _venv_holders:
|
||||
# Desktop `serve` backends whose app is GONE: nothing respawns an orphan, so
|
||||
# reap the tree. Live-Desktop backends return None and keep the refusal.
|
||||
_orphan_backends = _m()._orphaned_desktop_backend_pids(_venv_holders)
|
||||
if _orphan_backends:
|
||||
_venv_holders = _reap_and_rescan(
|
||||
f" ⚠ {len(_orphan_backends)} orphaned Desktop backend "
|
||||
"process(es) still hold the venv; stopping their trees", _orphan_backends,
|
||||
)
|
||||
# Tree-reap rungs: (classifier, message). Ledger rung = positive identity in any context (self-registered
|
||||
# backend, spawner provably dead; no PPID archaeology). Orphan rung = Desktop `serve` whose app is GONE
|
||||
# (nothing respawns an orphan); live-Desktop backends return None and keep the refusal.
|
||||
for classifier, message in (
|
||||
(_m()._ledger_reapable_backend_pids, "ledger-identified orphaned Hermes backend process(es) hold the venv"),
|
||||
(_m()._orphaned_desktop_backend_pids, "orphaned Desktop backend process(es) still hold the venv"),
|
||||
):
|
||||
if _venv_holders:
|
||||
backends = classifier(_venv_holders)
|
||||
if backends:
|
||||
_venv_holders = _reap_and_rescan(f" ⚠ {len(backends)} {message}; stopping their trees", backends)
|
||||
if _venv_holders:
|
||||
# Manual serve/dashboard rung (e.g. `hermes serve --host <ip>` for a REMOTE Desktop):
|
||||
# ledger identity only (spawner dead; Desktop-owned keep the refusal). Stop and
|
||||
|
||||
Reference in New Issue
Block a user