Merge remote-tracking branch 'origin/main' into hermes/hermes-6b48295e

This commit is contained in:
Teknium
2026-06-11 07:38:25 -07:00
239 changed files with 18356 additions and 2494 deletions
+98 -2
View File
@@ -32,6 +32,7 @@ import logging
import os
import re
import shlex
import site
import sys
import signal
import tempfile
@@ -135,6 +136,60 @@ _GATEWAY_SECRET_PATTERNS = (
)
def _ensure_windows_gateway_venv_imports() -> None:
"""Make detached Windows gateway runs see the Hermes venv packages.
Some Windows restart paths run the gateway under uv's base ``pythonw.exe``
to avoid the venv launcher respawning a visible console interpreter. That
mode can import the source tree via cwd/PYTHONPATH but still miss optional
packages installed only in ``venv/Lib/site-packages`` (notably the MCP SDK).
Patch the live process before MCP discovery so tool injection does not
depend on every launcher preserving PYTHONPATH perfectly.
"""
if sys.platform != "win32":
return
project_root = Path(__file__).resolve().parent.parent
candidates: list[Path] = []
if os.environ.get("VIRTUAL_ENV"):
candidates.append(Path(os.environ["VIRTUAL_ENV"]))
candidates.append(project_root / "venv")
seen: set[str] = set()
for venv_dir in candidates:
try:
resolved_venv = venv_dir.resolve()
except OSError:
resolved_venv = venv_dir
venv_key = str(resolved_venv).lower()
if venv_key in seen:
continue
seen.add(venv_key)
site_packages = resolved_venv / "Lib" / "site-packages"
if not site_packages.exists():
continue
project_entry = str(project_root)
site_entry = str(site_packages)
if project_entry not in sys.path:
sys.path.insert(0, project_entry)
# addsitepackages() semantics matter here: pywin32, used by the MCP
# SDK on Windows, relies on .pth processing to expose pywintypes.
site.addsitedir(site_entry)
if site_entry in sys.path:
sys.path.remove(site_entry)
insert_at = 1 if sys.path and sys.path[0] == project_entry else 0
sys.path.insert(insert_at, site_entry)
os.environ["VIRTUAL_ENV"] = str(resolved_venv)
pythonpath = [project_entry, site_entry]
if os.environ.get("PYTHONPATH"):
pythonpath.append(os.environ["PYTHONPATH"])
os.environ["PYTHONPATH"] = os.pathsep.join(dict.fromkeys(pythonpath))
return
def _gateway_platform_value(platform: Any) -> str:
"""Return a normalized gateway platform value for enums or raw strings."""
return str(getattr(platform, "value", platform) or "").strip().lower()
@@ -4255,10 +4310,25 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
)
"""
).strip()
watcher_env = os.environ.copy()
# This watcher is intentionally outside the running gateway. If it
# inherits the gateway marker, `hermes gateway restart` refuses to
# run as a self-restart loop guard and the gateway stays stopped.
watcher_env.pop("_HERMES_GATEWAY", None)
project_root = Path(__file__).resolve().parent.parent
venv_dir = Path(watcher_env.get("VIRTUAL_ENV") or project_root / "venv")
site_packages = venv_dir / "Lib" / "site-packages"
if site_packages.exists():
watcher_env["VIRTUAL_ENV"] = str(venv_dir)
pythonpath = [str(project_root), str(site_packages)]
if watcher_env.get("PYTHONPATH"):
pythonpath.append(watcher_env["PYTHONPATH"])
watcher_env["PYTHONPATH"] = os.pathsep.join(dict.fromkeys(pythonpath))
subprocess.Popen(
[sys.executable, "-c", watcher, str(current_pid), *cmd_argv],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=watcher_env,
**windows_detach_popen_kwargs(),
)
return
@@ -4268,12 +4338,20 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
f"while kill -0 {current_pid} 2>/dev/null; do sleep 0.2; done; "
f"{cmd} gateway restart"
)
# Same marker scrub as the Windows watcher above: this watcher runs
# `hermes gateway restart` from outside the gateway, but it inherits
# _HERMES_GATEWAY=1 from us, and the CLI's self-restart loop guard
# refuses to run when that marker is set — silently (DEVNULL), so the
# gateway stops and never comes back.
watcher_env = os.environ.copy()
watcher_env.pop("_HERMES_GATEWAY", None)
setsid_bin = shutil.which("setsid")
if setsid_bin:
subprocess.Popen(
[setsid_bin, "bash", "-lc", shell_cmd],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=watcher_env,
start_new_session=True,
)
else:
@@ -4281,6 +4359,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
["bash", "-lc", shell_cmd],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=watcher_env,
start_new_session=True,
)
@@ -12946,6 +13025,10 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
last_tool = [None] # Mutable container for tracking in closure
last_progress_msg = [None] # Track last message for dedup
repeat_count = [0] # How many times the same message repeated
# True when the previously enqueued progress line was a terminal
# fenced code block — consecutive terminal calls then drop the
# repeated "💻 terminal" header and render back-to-back blocks.
last_was_terminal_block = [False]
# ── Discord voice "verbal ack before tool calls" ────────────────
# When the bot is in a voice channel with the continuous mixer
@@ -13102,7 +13185,13 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
):
from agent.display import get_tool_preview_max_len
_cmd_full = args["command"].rstrip()
_code_block_full = f"{emoji} {tool_name}\n```\n{_cmd_full}\n```"
# Consecutive terminal calls: drop the repeated
# "💻 terminal" header so back-to-back commands render as
# adjacent code blocks under a single header.
_block_header = (
"" if last_was_terminal_block[0] else f"{emoji} {tool_name}\n"
)
_code_block_full = f"{_block_header}```\n{_cmd_full}\n```"
# Single-line, capped preview for non-verbose modes.
_pl = get_tool_preview_max_len()
_cap = _pl if _pl > 0 else 40
@@ -13113,13 +13202,15 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
_cmd_short = _cmd_short[:_cap - 3] + "..."
elif _multiline:
_cmd_short = _cmd_short + " ..."
_code_block_short = f"{emoji} {tool_name}\n```\n{_cmd_short}\n```"
_code_block_short = f"{_block_header}```\n{_cmd_short}\n```"
# Verbose mode: show detailed arguments, respects tool_preview_length
if progress_mode == "verbose":
if _code_block_full is not None:
last_was_terminal_block[0] = True
progress_queue.put(_code_block_full)
return
last_was_terminal_block[0] = False
if args:
from agent.display import get_tool_preview_max_len
_pl = get_tool_preview_max_len()
@@ -13144,6 +13235,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
# fenced block (built above) instead of the truncated preview.
if _code_block_short is not None:
msg = _code_block_short
last_was_terminal_block[0] = True
elif preview:
from agent.display import get_tool_preview_max_len
_pl = get_tool_preview_max_len()
@@ -13151,8 +13243,10 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
if len(preview) > _cap:
preview = preview[:_cap - 3] + "..."
msg = f"{emoji} {tool_name}: \"{preview}\""
last_was_terminal_block[0] = False
else:
msg = f"{emoji} {tool_name}..."
last_was_terminal_block[0] = False
# Dedup: collapse consecutive identical progress messages.
# Common with execute_code where models iterate with the same
@@ -15909,6 +16003,8 @@ async def start_gateway(config: Optional[GatewayConfig] = None, replace: bool =
atexit.register(remove_pid_file)
atexit.register(release_gateway_runtime_lock)
_ensure_windows_gateway_venv_imports()
# MCP tool discovery — run in an executor so the asyncio event loop
# stays responsive even when a configured MCP server is slow or
# unreachable. discover_mcp_tools() uses a blocking 120s wait