From b219f9716d3cc619f168cef1f72093430b82d89a Mon Sep 17 00:00:00 2001 From: darvsum Date: Sat, 16 May 2026 13:18:01 +0800 Subject: [PATCH 01/10] fix: preserve discover_models in _normalize_custom_provider_entry The _normalize_custom_provider_entry() function was dropping the discover_models field from custom_provider entries because: 1. It was not listed in _KNOWN_KEYS, so it was logged as an unknown key and ignored. 2. The function builds the normalized dict by explicitly copying known fields, so even if the warning was suppressed, the value was not carried through. This caused downstream model_switch.py to default discover_models to True, triggering /models HTTP probes on unreachable endpoints. With 4 unreachable internal endpoints at ~6s timeout each, the /api/model/options endpoint took ~24s instead of <1s. --- hermes_cli/config.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hermes_cli/config.py b/hermes_cli/config.py index c41158e42ae6..e4447183746b 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -2914,6 +2914,7 @@ def _normalize_custom_provider_entry( "api_mode", "transport", "model", "default_model", "models", "context_length", "rate_limit_delay", "request_timeout_seconds", "stale_timeout_seconds", + "discover_models", } for camel, snake in _CAMEL_ALIASES.items(): if camel in entry and snake not in entry: @@ -3004,6 +3005,10 @@ def _normalize_custom_provider_entry( if isinstance(rate_limit_delay, (int, float)) and rate_limit_delay >= 0: normalized["rate_limit_delay"] = rate_limit_delay + discover_models = entry.get("discover_models") + if isinstance(discover_models, bool): + normalized["discover_models"] = discover_models + return normalized From 3a4dd750baf3b918a3f3c8e2abc1223c978e6378 Mon Sep 17 00:00:00 2001 From: hueilau <33933019+hueilau@users.noreply.github.com> Date: Sat, 16 May 2026 23:02:46 -0700 Subject: [PATCH 02/10] fix: strip image parts for non-vision models with provider profiles _propare_messages_for_non_vision_model() was only called in the legacy flag path (no provider profile). Providers with registered profiles (e.g. DeepSeek, Kimi) bypassed the strip, causing HTTP 400 errors when image_url content blocks reached their non-vision APIs. This mirrors the existing behavior in the legacy path, ensuring all non-vision models get image stripping regardless of profile status. Vision-capable models are unaffected (the function is a no-op for them). --- run_agent.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/run_agent.py b/run_agent.py index b239f2aeb609..5e0a9ec06aca 100644 --- a/run_agent.py +++ b/run_agent.py @@ -10033,6 +10033,11 @@ def _build_api_kwargs(self, api_messages: list) -> dict: if _ephemeral_out is not None: self._ephemeral_max_output_tokens = None + # Strip image parts for non-vision models that have provider profiles + # (e.g. DeepSeek, Kimi). The legacy path below already does this, but + # registered providers with profiles were bypassing the strip. + api_messages = self._prepare_messages_for_non_vision_model(api_messages) + return _ct.build_kwargs( model=self.model, messages=api_messages, From 92c2c1dce72b2c387707ee908f13bed07469a494 Mon Sep 17 00:00:00 2001 From: Timur00Kh <32297275+Timur00Kh@users.noreply.github.com> Date: Sun, 17 May 2026 00:28:24 +0400 Subject: [PATCH 03/10] fix(gateway): add direct_messages_topic_id for synthetic Telegram DM events When /goal loop generates synthetic MessageEvents (goal continuations, status notices), the reply anchor is unavailable (message_id=None). For Telegram DM topic lanes, the Telegram adapter requires direct_messages_topic_id to route messages correctly; without it, the adapter falls back to message_thread_id=None, sending messages to the root 'All Messages' thread instead of the active topic lane. The fix includes direct_messages_topic_id in thread metadata for all non-General Telegram DM topics, ensuring queued/synthetic messages are delivered to the correct thread even when no reply anchor exists. --- gateway/run.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gateway/run.py b/gateway/run.py index 458603c3115b..56185190e26b 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -12546,6 +12546,12 @@ def _thread_metadata_for_source( 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) From b780df3423efe440978234de2b12d5b02b1b8a64 Mon Sep 17 00:00:00 2001 From: Grogger Date: Sat, 16 May 2026 12:06:09 -0400 Subject: [PATCH 04/10] fix(windows): suppress console window flash on subprocess spawns Add creationflags=CREATE_NO_WINDOW to every Windows Popen call across the terminal, process registry, code execution, and kanban worker subsystems. Prevents visible CMD windows from flashing on the user's desktop during agent operation. Also adds the _IS_WINDOWS module constant to kanban_db.py where it was missing, for consistency with the other patched files. 5 Popen sites across 4 files: - tools/environments/local.py (terminal foreground spawn) - tools/process_registry.py (background process spawn) - tools/code_execution_tool.py (sandbox + interpreter probe) - hermes_cli/kanban_db.py (kanban worker spawn) --- hermes_cli/kanban_db.py | 2 ++ tools/code_execution_tool.py | 2 ++ tools/environments/local.py | 1 + tools/process_registry.py | 1 + 4 files changed, 6 insertions(+) diff --git a/hermes_cli/kanban_db.py b/hermes_cli/kanban_db.py index 0db694ff5b1b..9d5ddad6ed0e 100644 --- a/hermes_cli/kanban_db.py +++ b/hermes_cli/kanban_db.py @@ -93,6 +93,7 @@ VALID_STATUSES = {"triage", "todo", "ready", "running", "blocked", "done", "archived"} VALID_WORKSPACE_KINDS = {"scratch", "worktree", "dir"} KNOWN_TOOLSET_NAMES = frozenset(name.casefold() for name in get_toolset_names()) +_IS_WINDOWS = sys.platform == "win32" # A running task's claim is valid for 15 minutes; after that the next # dispatcher tick reclaims it. Workers that outlive this window should call @@ -4024,6 +4025,7 @@ def _default_spawn( stderr=subprocess.STDOUT, env=env, start_new_session=True, + creationflags=subprocess.CREATE_NO_WINDOW if _IS_WINDOWS else 0, ) except FileNotFoundError: log_f.close() diff --git a/tools/code_execution_tool.py b/tools/code_execution_tool.py index 3822ce539f23..bdbc4bfbe1bf 100644 --- a/tools/code_execution_tool.py +++ b/tools/code_execution_tool.py @@ -1238,6 +1238,7 @@ def execute_code( stderr=subprocess.PIPE, stdin=subprocess.DEVNULL, preexec_fn=None if _IS_WINDOWS else os.setsid, + creationflags=subprocess.CREATE_NO_WINDOW if _IS_WINDOWS else 0, ) # --- Poll loop: watch for exit, timeout, and interrupt --- @@ -1568,6 +1569,7 @@ def _is_usable_python(python_path: str) -> bool: "import sys; sys.exit(0 if sys.version_info >= (3, 8) else 1)"], timeout=5, capture_output=True, + creationflags=subprocess.CREATE_NO_WINDOW if _IS_WINDOWS else 0, ) return result.returncode == 0 except (OSError, subprocess.TimeoutExpired, subprocess.SubprocessError): diff --git a/tools/environments/local.py b/tools/environments/local.py index 3b9d65449faa..177e5efab15d 100644 --- a/tools/environments/local.py +++ b/tools/environments/local.py @@ -513,6 +513,7 @@ def _run_bash(self, cmd_string: str, *, login: bool = False, stderr=subprocess.STDOUT, stdin=subprocess.PIPE if stdin_data is not None else subprocess.DEVNULL, preexec_fn=None if _IS_WINDOWS else os.setsid, + creationflags=subprocess.CREATE_NO_WINDOW if _IS_WINDOWS else 0, cwd=_popen_cwd, ) if not _IS_WINDOWS: diff --git a/tools/process_registry.py b/tools/process_registry.py index 184939adf755..8429a71e0872 100644 --- a/tools/process_registry.py +++ b/tools/process_registry.py @@ -557,6 +557,7 @@ def spawn_local( stderr=subprocess.STDOUT, stdin=subprocess.PIPE, preexec_fn=None if _IS_WINDOWS else os.setsid, + creationflags=subprocess.CREATE_NO_WINDOW if _IS_WINDOWS else 0, ) session.process = proc From 98a129c4f92af4bdf70db7eabc99c14ec4a43ac2 Mon Sep 17 00:00:00 2001 From: lemassykoi <16377344+lemassykoi@users.noreply.github.com> Date: Sat, 16 May 2026 23:02:46 -0700 Subject: [PATCH 05/10] fix(model-switch): probe /models for custom providers without api_key The Telegram/Discord model picker skipped live model discovery for custom providers (llama.cpp, Ollama) unless an api_key was configured. Local providers typically don't require auth on the /models endpoint. The CLI always probes /models, so this brings the gateway picker into parity. Change: `if api_url and api_key:` -> `if api_url:` --- hermes_cli/model_switch.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index fec1f33d0925..a5d299165fcb 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -1688,7 +1688,11 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool: continue # Live model discovery from custom provider endpoints (matches # Section 3 behavior for user ``providers:`` entries). - if api_url and api_key: + # Also probes when no api_key is set (e.g. local llama.cpp / + # Ollama servers) — the /models endpoint often works without + # auth. The CLI's _model_flow_named_custom always probes, so + # the Telegram/Discord picker should do the same for parity. + if api_url: try: from hermes_cli.models import fetch_api_models From 5580f24efac0b09e0ad076088b61d5364662f6d6 Mon Sep 17 00:00:00 2001 From: draplater <6349758+draplater@users.noreply.github.com> Date: Sat, 16 May 2026 23:02:46 -0700 Subject: [PATCH 06/10] feat: inject current time into goal judge prompt The goal judge only receives the goal text and the agent's last response. It has no concept of the current time, making it impossible to evaluate time-sensitive goals like 'keep working until 5pm'. This commit adds 'Current time' to both JUDGE_USER_PROMPT_TEMPLATE and JUDGE_USER_PROMPT_WITH_SUBGOALS_TEMPLATE, computed from datetime.now().astimezone() at judge call time. --- hermes_cli/goals.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/hermes_cli/goals.py b/hermes_cli/goals.py index 62ee00547c16..d6a139419a71 100644 --- a/hermes_cli/goals.py +++ b/hermes_cli/goals.py @@ -34,6 +34,7 @@ import re import time from dataclasses import dataclass, field, asdict +from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Tuple logger = logging.getLogger(__name__) @@ -110,6 +111,7 @@ JUDGE_USER_PROMPT_TEMPLATE = ( "Goal:\n{goal}\n\n" "Agent's most recent response:\n{response}\n\n" + "Current time: {current_time}\n\n" "Is the goal satisfied?" ) @@ -120,6 +122,7 @@ "Additional criteria the user added mid-loop (all must also be " "satisfied for the goal to be DONE):\n{subgoals_block}\n\n" "Agent's most recent response:\n{response}\n\n" + "Current time: {current_time}\n\n" "Decision: For each numbered criterion above, find concrete " "evidence in the agent's response that the criterion is " "satisfied. Do not accept generic phrases like 'all requirements " @@ -415,6 +418,7 @@ def judge_goal( # Build the prompt — pick the with-subgoals variant when applicable. clean_subgoals = [s.strip() for s in (subgoals or []) if s and s.strip()] + current_time = datetime.now(tz=timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S %Z") if clean_subgoals: subgoals_block = "\n".join( f"- {i}. {text}" for i, text in enumerate(clean_subgoals, start=1) @@ -423,11 +427,13 @@ def judge_goal( goal=_truncate(goal, 2000), subgoals_block=_truncate(subgoals_block, 2000), response=_truncate(last_response, _JUDGE_RESPONSE_SNIPPET_CHARS), + current_time=current_time, ) else: prompt = JUDGE_USER_PROMPT_TEMPLATE.format( goal=_truncate(goal, 2000), response=_truncate(last_response, _JUDGE_RESPONSE_SNIPPET_CHARS), + current_time=current_time, ) try: From bcb11c072eafb7c9126d25eaca85961d53ec3aea Mon Sep 17 00:00:00 2001 From: pr7426 Date: Sun, 17 May 2026 02:15:45 +0800 Subject: [PATCH 07/10] fix(cron): prevent parallel job result loss on exception Replace generator-based result collection with explicit per-future handling. Each future is now processed independently with a 600s timeout. Before: _results.extend(f.result() for f in _futures) - One exception stops the generator, remaining results are lost - No timeout: one hung job blocks the entire tick After: as_completed() + per-future try/except - Each future handled independently - 600s timeout prevents indefinite blocking - Failed futures are logged and counted as failures --- cron/scheduler.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cron/scheduler.py b/cron/scheduler.py index d470e8c2c746..322fa64906fe 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -1802,7 +1802,12 @@ def _process_job(job: dict) -> bool: for job in parallel_jobs: _ctx = contextvars.copy_context() _futures.append(_tick_pool.submit(_ctx.run, _process_job, job)) - _results.extend(f.result() for f in _futures) + for f in concurrent.futures.as_completed(_futures, timeout=600): + try: + _results.append(f.result()) + except Exception as exc: + logger.error("Parallel cron job future failed: %s", exc) + _results.append(False) # Best-effort sweep of MCP stdio subprocesses that survived their # session teardown during this tick. Runs AFTER every job has From 12c8a36db2c22fde2bcfeb20217a19fafad6e4a3 Mon Sep 17 00:00:00 2001 From: Rahul Date: Fri, 15 May 2026 13:45:07 +0530 Subject: [PATCH 08/10] fix(tests): mock keychain in TestReadClaudeCodeCredentials to prevent credential leakage Tests in TestReadClaudeCodeCredentials were not mocking _read_claude_code_credentials_from_keychain, which was added after the tests were written. On macOS machines with real Claude Code credentials stored in the Keychain, the function returns live credentials instead of the test fixtures, causing assertions to fail and leaking real tokens in test output. Add an autouse fixture that stubs the keychain reader to None so all tests in the class exercise only the file-based credential path. Co-Authored-By: Claude Sonnet 4.6 --- tests/agent/test_anthropic_adapter.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/agent/test_anthropic_adapter.py b/tests/agent/test_anthropic_adapter.py index 0ba2ba29f51b..259e9c1c5237 100644 --- a/tests/agent/test_anthropic_adapter.py +++ b/tests/agent/test_anthropic_adapter.py @@ -157,6 +157,13 @@ def test_minimax_cn_anthropic_endpoint_omits_tool_streaming_beta(self): class TestReadClaudeCodeCredentials: + @pytest.fixture(autouse=True) + def no_keychain(self, monkeypatch): + monkeypatch.setattr( + "agent.anthropic_adapter._read_claude_code_credentials_from_keychain", + lambda: None, + ) + def test_reads_valid_credentials(self, tmp_path, monkeypatch): cred_file = tmp_path / ".claude" / ".credentials.json" cred_file.parent.mkdir(parents=True) From d81bc94cb2499a5afd7d5a11729f9a125deeed04 Mon Sep 17 00:00:00 2001 From: flamiinngo Date: Sun, 17 May 2026 02:10:50 +0100 Subject: [PATCH 09/10] fix(scripts): fix UnicodeEncodeError in footgun checker on Windows The check-windows-footguns.py script outputs a checkmark (U+2713) and cross (U+2717) to report results. Windows terminals default to cp1252, which cannot encode these characters, so running the script on Windows threw a UnicodeEncodeError before any results were printed. This made the tool completely unusable on the exact platform it exists to help -- a developer on Windows trying to check their code for Windows-safety issues would just get a crash instead. Fix: reconfigure stdout and stderr to UTF-8 at the start of main(), before any output is produced. Verified on Windows 11 Home with Python 3.13 (terminal defaulting to cp1252). --- scripts/check-windows-footguns.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/check-windows-footguns.py b/scripts/check-windows-footguns.py index f424be90710e..7ae7ca50c4e7 100644 --- a/scripts/check-windows-footguns.py +++ b/scripts/check-windows-footguns.py @@ -551,6 +551,14 @@ def print_rules() -> None: def main(argv: list[str]) -> int: + # Windows terminals default to cp1252, which can't encode the ✓/✗ + # characters used in the output. Reconfigure streams to UTF-8 so the + # script works correctly on the very platform it is designed to help. + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8") + args = parse_args(argv) if args.list: From 2f67beaf10d7d766aea90347c8484f41c87d9573 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sat, 16 May 2026 23:04:09 -0700 Subject: [PATCH 10/10] chore(release): AUTHOR_MAP entries for batch salvage group 3 contributors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds release-note attribution mappings for 9 contributors from group 3: - @darvsum (PR #26766) - @hueilau (PR #26498) - @Timur00Kh (PR #27114) - @Grogger (PR #27061) - @lemassykoi (PR #27042) - @draplater (PR #26707) - @pr7426 (PR #27048) - @therahul-yo (PR #26215) - @flamiinngo (PR #27205) #27154 dropped from this batch — already landed on main as 4e9cedcd4. --- scripts/release.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scripts/release.py b/scripts/release.py index 6bbc2ad4ae37..52da4c2f4b7c 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -1113,6 +1113,19 @@ "hermesagent26@gmail.com": "hermesagent26", # PR #26438 (kimi model-name reasoning pad) "276067471+hermesagent26@users.noreply.github.com": "hermesagent26", "71590782+kriscolab@users.noreply.github.com": "kriscolab", # PR #26926 (deepseek default_aux_model) + # batch salvage (May 2026 LHF run, group 3) + "darvsum@users.noreply.github.com": "darvsum", # PR #26766 (preserve discover_models in normalize) + "peter@Peters-Mac-mini.local": "hueilau", # PR #26498 (strip image parts for non-vision) + "33933019+hueilau@users.noreply.github.com": "hueilau", + "32297275+Timur00Kh@users.noreply.github.com": "Timur00Kh", # PR #27114 (telegram DM topic for synthetic events) + "al.bellemare@gmail.com": "Grogger", # PR #27061 (windows console flash suppress) + "clement@nousresearch.com": "lemassykoi", # PR #27042 (model-switch probe keyless providers) + "16377344+lemassykoi@users.noreply.github.com": "lemassykoi", + "draplater@icloud.com": "draplater", # PR #26707 (goal judge current time) + "6349758+draplater@users.noreply.github.com": "draplater", + "pr7426@users.noreply.github.com": "pr7426", # PR #27048 (cron parallel job loss) + "rahulnilvan43@gmail.com": "therahul-yo", # PR #26215 (mock keychain in tests) + "kingsleyemeka117@gmail.com": "flamiinngo", # PR #27205 (UnicodeEncodeError footgun checker) }