Merge branch 'main' of github.com:NousResearch/hermes-agent into bb/gui

# Conflicts:
#	cli.py
#	hermes_cli/main.py
#	run_agent.py
#	tests/hermes_cli/test_cmd_update.py
#	tools/mcp_tool.py
#	web/src/lib/gatewayClient.ts
This commit is contained in:
Brooklyn Nicholson
2026-05-18 01:26:56 -05:00
260 changed files with 24547 additions and 13573 deletions
+115 -8
View File
@@ -4763,11 +4763,106 @@ class GatewayRunner:
pass
return False
# Auto-decompose: turn fresh triage tasks into ready workgraphs
# before the dispatcher fans out workers. Gated by
# ``kanban.auto_decompose`` (default True). Capped by
# ``kanban.auto_decompose_per_tick`` (default 3) so a bulk-load
# of triage tasks doesn't burst-spend the aux LLM in one tick;
# remainder defers to subsequent ticks.
auto_decompose_enabled = bool(kanban_cfg.get("auto_decompose", True))
try:
auto_decompose_per_tick = int(
kanban_cfg.get("auto_decompose_per_tick", 3) or 3
)
except (TypeError, ValueError):
auto_decompose_per_tick = 3
if auto_decompose_per_tick < 1:
auto_decompose_per_tick = 1
def _auto_decompose_tick() -> int:
"""Run the auto-decomposer for up to N triage tasks across all
boards. Returns the number of triage tasks that were
successfully decomposed or specified this tick.
"""
try:
from hermes_cli import kanban_decompose as _decomp
except Exception as exc: # pragma: no cover
logger.warning(
"kanban auto-decompose: import failed (%s); skipping", exc,
)
return 0
try:
boards = _kb.list_boards(include_archived=False)
except Exception:
boards = [_kb.read_board_metadata(_kb.DEFAULT_BOARD)]
attempted = 0
successes = 0
for b in boards:
slug = b.get("slug") or _kb.DEFAULT_BOARD
if attempted >= auto_decompose_per_tick:
break
# Pin this board for the duration of the call — same
# pattern as the dashboard specify endpoint. The
# decomposer module connects with no board kwarg and
# relies on the env var.
prev_env = os.environ.get("HERMES_KANBAN_BOARD")
try:
os.environ["HERMES_KANBAN_BOARD"] = slug
try:
triage_ids = _decomp.list_triage_ids()
except Exception as exc:
logger.debug(
"kanban auto-decompose: list_triage_ids failed on board %s (%s)",
slug, exc,
)
triage_ids = []
for tid in triage_ids:
if attempted >= auto_decompose_per_tick:
break
attempted += 1
try:
outcome = _decomp.decompose_task(
tid, author="auto-decomposer",
)
except Exception:
logger.exception(
"kanban auto-decompose: decompose_task crashed on %s",
tid,
)
continue
if outcome.ok:
successes += 1
if outcome.fanout and outcome.child_ids:
logger.info(
"kanban auto-decompose [%s]: %s%d children",
slug, tid, len(outcome.child_ids),
)
else:
logger.info(
"kanban auto-decompose [%s]: %s → single task (no fanout)",
slug, tid,
)
else:
# Common no-op reasons (no aux client configured) shouldn't
# spam logs every tick. Log at debug.
logger.debug(
"kanban auto-decompose [%s]: %s skipped: %s",
slug, tid, outcome.reason,
)
finally:
if prev_env is None:
os.environ.pop("HERMES_KANBAN_BOARD", None)
else:
os.environ["HERMES_KANBAN_BOARD"] = prev_env
return successes
logger.info(
"kanban dispatcher: embedded in gateway (interval=%.1fs)", interval
)
while self._running:
try:
if auto_decompose_enabled:
await asyncio.to_thread(_auto_decompose_tick)
results = await asyncio.to_thread(_tick_once)
any_spawned = False
for slug, res in (results or []):
@@ -8845,7 +8940,7 @@ class GatewayRunner:
lines.append("Failed/paused: (none)")
return "\n".join(lines)
if action in ("pause", "resume"):
if action in {"pause", "resume"}:
if not target:
return f"Usage: /platform {action} <name>"
platform = _resolve_platform(target)
@@ -8953,13 +9048,15 @@ class GatewayRunner:
logger.debug("Failed to write restart dedup marker: %s", e)
active_agents = self._running_agent_count()
# When running under a service manager (systemd/launchd), use the
# service restart path: exit with code 75 so the service manager
# restarts us. The detached subprocess approach (setsid + bash)
# doesn't work under systemd because KillMode=mixed kills all
# processes in the cgroup, including the detached helper.
# When running under a service manager (systemd/launchd) or inside a
# Docker/Podman container, use the service restart path: exit with
# code 75 so the service manager / container restart policy restarts
# us. The detached subprocess approach (setsid + bash) doesn't work
# under systemd (KillMode=mixed kills the cgroup) or Docker (tini
# exits when the gateway dies, taking the detached helper with it).
_under_service = bool(os.environ.get("INVOCATION_ID")) # systemd sets this
if _under_service:
_in_container = os.path.exists("/.dockerenv") or os.path.exists("/run/.containerenv")
if _under_service or _in_container:
self.request_restart(detached=False, via_service=True)
else:
self.request_restart(detached=True, via_service=False)
@@ -12528,6 +12625,12 @@ class GatewayRunner:
and getattr(source, "chat_type", None) == "dm"
):
metadata["telegram_dm_topic_reply_fallback"] = True
# Telegram DM topic lanes need direct_messages_topic_id in metadata
# so synthetic/queued messages (goal continuations, status notices)
# route to the correct topic even when reply anchor is unavailable.
tid = str(thread_id)
if tid and tid not in {"", "1"}:
metadata["direct_messages_topic_id"] = tid
anchor = reply_to_message_id or getattr(source, "message_id", None)
if anchor is not None:
metadata["telegram_reply_to_message_id"] = str(anchor)
@@ -12813,7 +12916,11 @@ class GatewayRunner:
update_cmd = (
f"PYTHONUNBUFFERED=1 {hermes_cmd_str} update --gateway"
f" > {shlex.quote(str(output_path))} 2>&1; "
f"status=$?; printf '%s' \"$status\" > {shlex.quote(str(exit_code_path))}"
# Avoid `status=$?`: `status` is a read-only special parameter
# in zsh, and this command string is copied/reused in macOS/zsh
# operator wrappers. Keep the template zsh-safe even though this
# specific subprocess currently runs under bash.
f"rc=$?; printf '%s' \"$rc\" > {shlex.quote(str(exit_code_path))}"
)
setsid_bin = shutil.which("setsid")
if setsid_bin: