Syn with main (enabled web ui) - #1
Merged
Merged
Conversation
…#8572) Add agent.gateway_notify_interval config option (default 600s). Set to 0 to disable periodic 'still working' notifications. Bridged to HERMES_AGENT_NOTIFY_INTERVAL env var (same pattern as gateway_timeout and gateway_timeout_warning). The inactivity warning (gateway_timeout_warning) was already configurable; this makes the wall-clock ping configurable too.
…8609) Three fixes for the (empty) response bug affecting open reasoning models: 1. Allow retries after prefill exhaustion — models like mimo-v2-pro always populate reasoning fields via OpenRouter, so the old 'not _has_structured' guard on the retry path blocked retries for EVERY reasoning model after the 2 prefill attempts. Now: 2 prefills + 3 retries = 6 total attempts before (empty). 2. Reset prefill/retry counters on tool-call recovery — the counters accumulated across the entire conversation, never resetting during tool-calling turns. A model cycling empty→prefill→tools→empty burned both prefill attempts and the third empty got zero recovery. Now counters reset when prefill succeeds with tool calls. 3. Strip think blocks before _truly_empty check — inline <think> content made the string non-empty, skipping both retry paths. Reported by users on Telegram with xiaomi/mimo-v2-pro and qwen3.5 models. Reproduced: qwen3.5-9b emits tool calls as XML in reasoning field instead of proper function calls, causing content=None + tool_calls=None + reasoning with embedded <tool_call> XML. Prefill recovery works but counter accumulation caused permanent (empty) in long sessions.
The backup validation checked for 'hermes_state.db' and 'memory_store.db' as telltale markers of a valid Hermes backup zip. Neither name exists in a real Hermes installation — the actual database file is 'state.db' (hermes_state.py: DEFAULT_DB_PATH = get_hermes_home() / 'state.db'). A fresh Hermes installation produces: ~/.hermes/state.db (actual name) ~/.hermes/config.yaml ~/.hermes/.env Because the marker set never matched 'state.db', a backup zip containing only 'state.db' plus 'config.yaml' would fail validation with: 'zip does not appear to be a Hermes backup' and the import would exit with sys.exit(1), silently rejecting a valid backup. Fix: replace the wrong marker names with the correct filename. Adds TestValidateBackupZip with three cases: - state.db is accepted as a valid marker - old wrong names (hermes_state.db, memory_store.db) alone are rejected - config.yaml continues to pass (existing behaviour preserved)
Centralize container detection in hermes_constants.is_container() with process-lifetime caching, matching existing is_wsl()/is_termux() patterns. Dedup _is_inside_container() in config.py to delegate to the new function. Add _run_systemctl() wrapper that converts FileNotFoundError to RuntimeError for defense-in-depth — all 10 bare subprocess.run(_systemctl_cmd(...)) call sites now route through it. Make supports_systemd_services() return False in containers and when systemctl binary is absent (shutil.which check). Add Docker-specific guidance in gateway_command() for install/uninstall/start subcommands — exit 0 with helpful instructions instead of crashing. Make 'hermes status' show 'Manager: docker (foreground)' and 'hermes dump' show 'running (docker, pid N)' inside containers. Fix setup_gateway() to use supports_systemd instead of _is_linux for all systemd-related branches, and show Docker restart policy instructions in containers. Replace inline /.dockerenv check in voice_mode.py with is_container(). Fixes #7420 Co-authored-by: teknium1 <teknium1@users.noreply.github.com>
These were already declared in pyproject.toml but missing from the lockfile.
…n install
Skills.sh installs hit the GitHub API 45 times per install because the
same repo tree was fetched 6 times redundantly. Combined with search
(23 API calls), this totals 68 — exceeding the unauthenticated rate
limit of 60 req/hr, causing 'Could not fetch' errors for users without
a GITHUB_TOKEN.
Changes:
- Add _get_repo_tree() cache to GitHubSource — repo info + recursive
tree fetched once per repo per source instance, eliminating 10
redundant API calls (6 tree + 4 candidate 404s)
- _download_directory_via_tree returns {} (not None) when cached tree
shows path doesn't exist, skipping unnecessary Contents API fallback
- _check_rate_limit_response() detects exhausted quota and sets
is_rate_limited flag
- do_install() shows actionable hint when rate limited: set
GITHUB_TOKEN or install gh CLI
Before: 45 API calls per install (68 total with search)
After: 31 API calls per install (54 total with search — under 60/hr)
Reported by community user from Vietnam (no GitHub auth configured).
…or search/install Add a CI-built skills index served from the docs site. The index is crawled daily by GitHub Actions, resolves all GitHub paths upfront, and is cached locally by the client. When the index is available: - Search uses the cached index (0 GitHub API calls, was 23+) - Install uses resolved paths from index (6 API calls for file downloads only, was 31-45 for discovery + downloads) Total: 68 → 6 GitHub API calls for a typical search + install flow. Unauthenticated users (60 req/hr) can now search and install without hitting rate limits. Components: - scripts/build_skills_index.py: Crawl all sources (skills.sh, GitHub taps, official, clawhub, lobehub), batch-resolve GitHub paths via tree API, output JSON index - tools/skills_hub.py: HermesIndexSource class — search/fetch/inspect backed by the index, with lazy GitHubSource for file downloads - parallel_search_sources() skips external API sources when index is available (0 GitHub calls for search) - .github/workflows/skills-index.yml: twice-daily CI build + deploy - .github/workflows/deploy-site.yml: also builds index during docs deploy Graceful degradation: when the index is unavailable (first run, network down, stale), all methods return empty/None and downstream sources handle the request via direct API as before.
Add _is_openclaw_running() and _warn_if_openclaw_running() to detect OpenClaw processes (via pgrep/tasklist) before hermes claw migrate. Warns the user that messaging platforms only allow one active session per bot token, and lets them cancel or continue. Fixes #7907
…ctive prompt - Use PowerShell to inspect node.exe command lines on Windows, since tasklist output does not include them. - Also check for dedicated openclaw.exe/clawd.exe processes. - Skip the interactive prompt in non-interactive sessions so the preview-only behavior is preserved. - Update tests accordingly. Relates to #7907
…port Combines detection from both PRs into _detect_openclaw_processes(): - Cross-platform process scan (pgrep/tasklist/PowerShell) from PR #8102 - systemd service check from PR #8555 - Returns list[str] with details about what's found Fixes in cleanup warning (from PR #8555): - print_warning -> print_error/print_info (print_warning not in import chain) - Added isatty() guard for non-interactive sessions - Removed duplicate _check_openclaw_running() in favor of shared function Updated all tests to match new API.
…essages (#8665) Four fixes for the Weixin/WeChat adapter, synthesized from the best aspects of community PRs #8407, #8521, #8360, #7695, #8308, #8525, #7531, #8144, #8251. 1. Streaming cursor (▉) stuck permanently — WeChat doesn't support message editing, so the cursor appended during streaming can never be removed. Add SUPPORTS_MESSAGE_EDITING = False to WeixinAdapter and check it in gateway/run.py to use an empty cursor for non-edit platforms. (Fixes #8307, #8326) 2. Media upload failures — two bugs in _send_file(): a) upload_full_url path used PUT (404 on WeChat CDN); now uses POST. b) aes_key was base64(raw_bytes) but the iLink API expects base64(hex_string); images showed as grey boxes. (Fixes #8352, #7529) Also: unified both upload paths into _upload_ciphertext(), preferring upload_full_url. Added send_video/send_voice methods and voice_item media builder for audio/.silk files. Added video_md5 field. 3. Markdown links stripped — WeChat can't render [text](url), so format_message() now converts them to 'text (url)' plaintext. Code blocks are preserved. (Fixes #7617) 4. Blank message prevention — three guards: a) _split_text_for_weixin_delivery('') returns [] not [''] b) send() filters empty/whitespace chunks before _send_text_chunk c) _send_message() raises ValueError for empty text as safety net Community credit: joei4cm (#8407), lyonDan (#8521), SKFDJKLDG (#8360), tomqiaozc (#7695), joshleeeeee (#8308), luoxiao6645(#8525), longsizhuo (#7531), Astral-Yang (#8144), QingWei-Li (#8251).
Users who set up Nous auth without explicitly selecting a model via `hermes model` were silently falling back to anthropic/claude-opus-4.6 (the first entry in _PROVIDER_MODELS['nous']), causing unexpected charges on their Nous plan. Move xiaomi/mimo-v2-pro to the first position so unconfigured users default to a free model instead.
Read tts.speed (global) or tts.<provider>.speed (provider-specific) from config. Provider-specific takes precedence over global. - Edge TTS: converts speed float to SSML prosody rate string - OpenAI TTS: passes speed param clamped to 0.25-4.0 - MiniMax: wired into global tts.speed fallback for consistency Co-authored-by: 0xbyt4 <0xbyt4@users.noreply.github.com>
12 tests covering: - Provider-specific speed overrides global speed - Global speed used as fallback - Default (no speed) preserves existing behavior - Edge SSML rate string conversion (positive/negative) - OpenAI speed clamping to 0.25-4.0 range
_check_compression_model_feasibility() called get_model_context_length() without passing config_context_length, so custom endpoints that do not support /models API queries always fell through to the 128K default, ignoring auxiliary.compression.context_length in config.yaml. Fix: read auxiliary.compression.context_length from config and pass it as config_context_length (highest-priority hint) so the user-configured value is always respected regardless of API availability. Fixes #8499
- Test that auxiliary.compression.context_length from config is forwarded to get_model_context_length (positive case) - Test that invalid/non-integer config values are silently ignored - Fix _make_agent() to set config=None (cherry-picked code reads self.config)
aiohttp.ClientSession defaults to trust_env=False, ignoring HTTP_PROXY/ HTTPS_PROXY env vars. This causes QR login and all API calls to fail for users behind a proxy (e.g. Clash in fake-ip mode), which is common in China where Weixin and WeCom are primarily used. Added trust_env=True to all aiohttp.ClientSession instantiations that connect to external hosts (weixin: 3 places, wecom: 1, matrix: 1). WhatsApp sessions are excluded as they only connect to localhost. httpx-based adapters (dingtalk, signal, wecom_callback) are unaffected as httpx defaults to trust_env=True. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a one-line entry for HermesClaw (community WeChat bridge) to the Community section. It lets users run Hermes Agent and OpenClaw on the same WeChat account.
Some OpenAI-compatible clients (Open WebUI, LobeChat, etc.) send
message content as an array of typed parts instead of a plain string:
[{"type": "text", "text": "hello"}]
The agent pipeline expects strings, so these array payloads caused
silent failures or empty messages.
Add _normalize_chat_content() with defensive limits (recursion depth,
list size, output length) and apply it to both the Chat Completions
and Responses API endpoints. The Responses path had inline
normalization that only handled input_text/output_text — the shared
function also handles the standard 'text' type.
Salvaged from PR #7980 (ikelvingo) — only the content normalization;
the SSE and Weixin changes in that PR were regressions and are not
included.
Co-authored-by: ikelvingo <ikelvingo@users.noreply.github.com>
* feat: add `hermes debug share` — upload debug report to pastebin Adds a new `hermes debug share` command that collects system info (via hermes dump), recent logs (agent.log, errors.log, gateway.log), and uploads the combined report to a paste service (paste.rs primary, dpaste.com fallback). Returns a shareable URL for support. Options: --lines N Number of log lines per file (default: 200) --expire N Paste expiry in days (default: 7, dpaste.com only) --local Print report locally without uploading Files: hermes_cli/debug.py - New module: paste upload + report collection hermes_cli/main.py - Wire cmd_debug + argparse subparser tests/hermes_cli/test_debug.py - 19 tests covering upload, collection, CLI * feat: upload full agent.log and gateway.log as separate pastes hermes debug share now uploads up to 3 pastes: 1. Summary report (system info + log tails) — always 2. Full agent.log (last ~500KB) — if file exists 3. Full gateway.log (last ~500KB) — if file exists Each paste uploads independently; log upload failures are noted but don't block the main report. Output shows all links aligned: Report https://paste.rs/abc agent.log https://paste.rs/def gateway.log https://paste.rs/ghi Also adds _read_full_log() with size-capped tail reading to stay within paste service limits (~512KB per file). * feat: prepend hermes dump to each log paste for self-contained context Each paste (agent.log, gateway.log) now starts with the hermes dump output so clicking any single link gives full system context without needing to cross-reference the summary report. Refactored dump capture into _capture_dump() — called once and reused across the summary report and each log paste. * fix: fall back to .1 rotated log when primary log is missing or empty When gateway.log (or agent.log) doesn't exist or is empty, the debug share now checks for the .1 rotation file. This is common — the gateway rotates logs and the primary file may not exist yet. Extracted _resolve_log_path() to centralize the fallback logic for both _read_log_tail() and _read_full_log(). * chore: remove unused display_hermes_home import
Port from openclaw/openclaw#64796: Per MSC3952 / Matrix v1.7, the m.mentions.user_ids field is the authoritative mention signal. Clients that populate m.mentions but don't duplicate @bot in the body text were being silently dropped when MATRIX_REQUIRE_MENTION=true. Cherry-picked from PR #8673.
Port from openclaw/openclaw#64586: users who copy .env.example without changing placeholder values now get a clear error at startup instead of a confusing auth failure from the platform API. Also rejects placeholder API_SERVER_KEY when binding to a network-accessible address. Cherry-picked from PR #8677.
Adds /debug as a slash command available in CLI, Telegram, Discord, Slack, and all other gateway platforms. Uploads debug report + full logs to paste services and returns shareable URLs. - commands.py: CommandDef in Info category (no cli_only/gateway_only) - gateway/run.py: async handler with run_in_executor for blocking I/O - cli.py: dispatch in process_command to run_debug_share
) Port from nearai/ironclaw#2304: Telegram's 4096 character limit is measured in UTF-16 code units, not Unicode codepoints. Characters outside the Basic Multilingual Plane (emoji like 😀, CJK Extension B, musical symbols) are surrogate pairs: 1 Python char but 2 UTF-16 units. Previously, truncate_message() used Python's len() which counts codepoints. This could produce chunks exceeding Telegram's actual limit when messages contain many astral-plane characters. Changes: - Add utf16_len() helper and _prefix_within_utf16_limit() for UTF-16-aware string measurement and truncation - Add _custom_unit_to_cp() binary-search helper that maps a custom-unit budget to the largest safe codepoint slice position - Update truncate_message() to accept optional len_fn parameter - Telegram adapter now passes len_fn=utf16_len when splitting messages - Fix fallback truncation in Telegram error handler to use _prefix_within_utf16_limit instead of codepoint slicing - Update send_message_tool.py to use utf16_len for Telegram platform - Add comprehensive tests: utf16_len, _prefix_within_utf16_limit, truncate_message with len_fn (emoji splitting, content preservation, code block handling) - Update mock lambdas in reply_mode tests to accept **kw for len_fn
When resuming a session with --resume or -c, the last assistant response was truncated to 200 chars / 3 lines just like older messages in the recap. This forced users to waste tokens re-asking for the response. Now the last assistant message in the recap is shown in full with non-dim styling, so users can see exactly where they left off. Earlier messages remain truncated for compact display. Changes: - Track un-truncated text for the last assistant entry during collection - Replace last entry with full text after history trimming - Render last assistant entry with bold (non-dim) styling - Update existing truncation tests to use multi-message histories - Add new tests for full last response display (char + multiline)
Three changes that address the poor WhatsApp experience reported by users: 1. Reclassify WhatsApp from TIER_LOW to TIER_MEDIUM in display_config.py — enables streaming and tool progress via the existing Baileys /edit bridge endpoint. Users now see progressive responses instead of minutes of silence followed by a wall of text. 2. Lower MAX_MESSAGE_LENGTH from 65536 to 4096 and add proper chunking — send() now calls format_message() and truncate_message() before sending, then loops through chunks with a small delay between them. The base class truncate_message() already handles code block boundary detection (closes/reopens fences at chunk boundaries). reply_to is only set on the first chunk. 3. Override format_message() with WhatsApp-specific markdown conversion — converts **bold** to *bold*, ~~strike~~ to ~strike~, headers to bold text, and [links](url) to text (url). Code blocks and inline code are protected from conversion via placeholder substitution. Together these fix the two user complaints: - 'sends the whole code all the time' → now chunked at 4K with proper formatting - 'terminal gets interrupted and gets cooked' → streaming + tool progress give visual feedback so users don't accidentally interrupt with follow-up messages
…bridge (#8745) * fix(telegram): use UTF-16 code units for message length splitting Port from nearai/ironclaw#2304: Telegram's 4096 character limit is measured in UTF-16 code units, not Unicode codepoints. Characters outside the Basic Multilingual Plane (emoji like 😀, CJK Extension B, musical symbols) are surrogate pairs: 1 Python char but 2 UTF-16 units. Previously, truncate_message() used Python's len() which counts codepoints. This could produce chunks exceeding Telegram's actual limit when messages contain many astral-plane characters. Changes: - Add utf16_len() helper and _prefix_within_utf16_limit() for UTF-16-aware string measurement and truncation - Add _custom_unit_to_cp() binary-search helper that maps a custom-unit budget to the largest safe codepoint slice position - Update truncate_message() to accept optional len_fn parameter - Telegram adapter now passes len_fn=utf16_len when splitting messages - Fix fallback truncation in Telegram error handler to use _prefix_within_utf16_limit instead of codepoint slicing - Update send_message_tool.py to use utf16_len for Telegram platform - Add comprehensive tests: utf16_len, _prefix_within_utf16_limit, truncate_message with len_fn (emoji splitting, content preservation, code block handling) - Update mock lambdas in reply_mode tests to accept **kw for len_fn * fix: resolve npm audit vulnerabilities in browser tools and whatsapp bridge Browser tools (agent-browser): - Override lodash to 4.18.1 (fixes prototype pollution CVEs in transitive dep via node-simctl → @appium/logger). Not reachable in Hermes's code path but cleans the audit report. - basic-ftp and brace-expansion updated via npm audit fix. WhatsApp bridge: - file-type updated (fixes infinite loop in ASF parser + ZIP bomb DoS) - music-metadata updated (fixes infinite loop in ASF parser) - path-to-regexp updated (fixes ReDoS, mitigated by localhost binding) Both components now report 0 npm vulnerabilities. Ref: https://gist.github.com/jacklevin74/b41b710d3e20ba78fb7e2d42e2b83819
…l_preview_length is 0 (#8735) When tool_preview_length is 0 (default for platforms without a tier default, like Session), verbose mode was truncating args JSON to 200 characters. Since the user explicitly opted into verbose mode, they expect full tool call detail — the 200-char cap defeated the purpose. Now: tool_preview_length=0 means no truncation in verbose mode. Positive values still cap as before. Platform message-length limits handle overflow naturally.
…8794) OpenCode Zen was in _DOT_TO_HYPHEN_PROVIDERS, causing all dotted model names (minimax-m2.5-free, gpt-5.4, glm-5.1) to be mangled. The fix: Layer 1 (model_normalize.py): Remove opencode-zen from the blanket dot-to-hyphen set. Add an explicit block that preserves dots for non-Claude models while keeping Claude hyphenated (Zen's Claude endpoint uses anthropic_messages mode which expects hyphens). Layer 2 (run_agent.py _anthropic_preserve_dots): Add opencode-zen and zai to the provider allowlist. Broaden URL check from opencode.ai/zen/go to opencode.ai/zen/ to cover both Go and Zen endpoints. Add bigmodel.cn for ZAI URL detection. Also adds glm-5.1 to ZAI model lists in models.py and setup.py. Closes #7710 Salvaged from contributions by: - konsisumer (PR #7739, #7719) - DomGrieco (PR #8708) - Esashiero (PR #7296) - sharziki (PR #7497) - XiaoYingGee (PR #8750) - APTX4869-maker (PR #8752) - kagura-agent (PR #7157)
…et session (#9893) When compression fails after max attempts, the agent returns {completed: False, partial: True} but was missing the 'failed' flag. The gateway's agent_failed_early guard checked for 'failed' AND 'not final_response', but _run_agent_blocking always converts errors to final_response — making the guard dead code. This caused the oversized session to persist, creating an infinite fail loop where every subsequent message hits the same compression failure. Changes: - run_agent.py: add 'failed: True' and 'compression_exhausted: True' to all 5 compression-exhaustion return paths - gateway/run.py (_run_agent_blocking): forward 'failed' and 'compression_exhausted' flags through to the caller - gateway/run.py (_handle_message_with_agent): fix agent_failed_early to check bool(failed) without the broken 'not final_response' clause; auto-reset the session when compression is exhausted so the next message starts fresh - Update tests to match new guard logic and add TestCompressionExhaustedFlag test class Closes #9893
#10066) - find_docker() now checks HERMES_DOCKER_BINARY env var first, then docker on PATH, then podman on PATH, then macOS known locations - Entrypoint respects HERMES_HOME env var (was hardcoded to /opt/data) - Entrypoint uses groupmod -o to tolerate non-unique GIDs (fixes macOS GID 20 conflict with Debian's dialout group) - Entrypoint makes chown best-effort so rootless Podman continues instead of failing with 'Operation not permitted' - 5 new tests covering env var override, podman fallback, precedence Based on work by alanjds (PR #3996) and malaiwah (PR #8115). Closes #4084.
…#10068) When a user sends a message while the agent is executing a task on the gateway, the agent is now interrupted immediately — not silently queued. Previously, messages were stored in _pending_messages with zero feedback to the user, potentially leaving them waiting 1+ hours. Root cause: Level 1 guard (base.py) intercepted all messages for active sessions and returned with no response. Level 2 (gateway/run.py) which calls agent.interrupt() was never reached. Fix: Expand _handle_active_session_busy_message to handle the normal (non-draining) case: 1. Call running_agent.interrupt(text) to abort in-flight tool calls and exit the agent loop at the next check point 2. Store the message as pending so it becomes the next turn once the interrupted run returns 3. Send a brief ack: 'Interrupting current task (10 min elapsed, iteration 21/60, running: terminal). I'll respond shortly.' 4. Debounce acks to once per 30s to avoid spam on rapid messages Reported by @Lonely__MH.
Previously, non-integer context_length values (e.g. '256K') in config.yaml were silently ignored, causing the agent to fall back to 128K auto-detection with no user feedback. This was confusing for users with custom LiteLLM endpoints expecting larger context. Now prints a clear stderr warning and logs at WARNING level when model.context_length or custom_providers[].models.<model>.context_length cannot be parsed as an integer, telling users to use plain integers (e.g. 256000 instead of '256K'). Reported by community user ChFarhan via Discord.
…ls (#9051, #8620, #9400) Three independent fixes: 1. Reset activity timestamp on cached agent reuse (#9051) When the gateway reuses a cached AIAgent for a new turn, the _last_activity_ts from the previous turn (possibly hours ago) carried over. The inactivity timeout handler immediately saw the agent as idle for hours and killed it. Fix: reset _last_activity_ts, _last_activity_desc, and _api_call_count when retrieving an agent from the cache. 2. Detect uv-managed virtual environments (#8620 sub-issue 1) The systemd unit generator fell back to sys.executable (uv's standalone Python) when running under 'uv run', because sys.prefix == sys.base_prefix (uv doesn't set up traditional venv activation). The generated ExecStart pointed to a Python binary without site-packages, crashing the service on startup. Fix: check VIRTUAL_ENV env var before falling back to sys.executable. uv sets VIRTUAL_ENV even when sys.prefix doesn't reflect the venv. 3. Nudge model to continue after empty post-tool response (#9400) Weaker models (GLM-5, mimo-v2-pro) sometimes return empty responses after tool calls instead of continuing to the next step. The agent silently abandoned the remaining work with '(empty)' or used prior-turn fallback text. Fix: when the model returns empty after tool calls AND there's no prior-turn content to fall back on, inject a one-time user nudge message telling the model to process the tool results and continue. The flag resets after each successful tool round so it can fire again on later rounds. Test plan: 97 gateway + CLI tests pass, 9 venv detection tests pass
…rade, hardening Combined salvage of PRs #9661, #9663, #9674, #9677, #9678 by kshitijk4poor. - Smart tool output collapse: informative 1-line summaries replace generic placeholder - Dedup identical tool results via MD5 hash, truncate large tool_call arguments - Anti-thrashing: skip compression after 2 consecutive <10% savings passes - Structured action-log summary template with numbered actions and Active State - Hardening: max_tokens 1.3x cap, multimodal safety, note idempotency, adaptive cooldown Follow-up fixes applied during salvage: - web_extract: reads 'urls' (list) not 'url' (original PR bug) - Multimodal list content guards in dedup and prune passes - Kept 'Relevant Files' section in template (original PR removed it) Skipped PRs #9665 (user msg preservation — duplication risk) and #9675 (dead code).
…0090) The existing recovery block sanitized self.api_key and self._client_kwargs['api_key'] but did not update self.client.api_key. The OpenAI SDK stores its own copy of api_key and reads it dynamically via the auth_headers property on every request. Without this fix, the retry after sanitization would still send the corrupted key in the Authorization header, causing the same UnicodeEncodeError. The bug manifests when an API key contains Unicode lookalike characters (e.g. ʋ U+028B instead of v) from copy-pasting out of PDFs, rich-text editors, or web pages with decorative fonts. httpx hard-encodes all HTTP headers as ASCII, so the non-ASCII char in the Authorization header triggers the error. Adds TestApiKeyClientSync with two tests verifying: - All three key locations are synced after sanitization - Recovery handles client=None (pre-init) without crashing
…ls, compression model fallback (#9051, #8620, #9400) (#10093) Four independent fixes: 1. Reset activity timestamp on cached agent reuse (#9051) When the gateway reuses a cached AIAgent for a new turn, the _last_activity_ts from the previous turn (possibly hours ago) carried over. The inactivity timeout handler immediately saw the agent as idle for hours and killed it. Fix: reset _last_activity_ts, _last_activity_desc, and _api_call_count when retrieving an agent from the cache. 2. Detect uv-managed virtual environments (#8620 sub-issue 1) The systemd unit generator fell back to sys.executable (uv's standalone Python) when running under 'uv run', because sys.prefix == sys.base_prefix. The generated ExecStart pointed to a Python binary without site-packages. Fix: check VIRTUAL_ENV env var before falling back to sys.executable. uv sets VIRTUAL_ENV even when sys.prefix doesn't reflect the venv. 3. Nudge model to continue after empty post-tool response (#9400) Weaker models sometimes return empty after tool calls. The agent silently abandoned the remaining work. Fix: append assistant('(empty)') + user nudge message and retry once. Resets after each successful tool round. 4. Compression model fallback on permanent errors (#8620 sub-issue 4) When the default summary model (gemini-3-flash) returns 503 'model_not_found' on custom proxies, the compressor entered a 600s cooldown, leaving context growing unbounded. Fix: detect permanent model-not-found errors (503, 404, 'model_not_found', 'no available channel') and fall back to using the main model for compression instead of entering cooldown. One-time fallback with immediate retry. Test plan: 40 compressor tests + 97 gateway/CLI tests + 9 venv tests pass
On some Python versions, argparse fails to route subcommand tokens when the parent parser has nargs='?' optional arguments (--continue). The symptom: 'hermes model' produces 'unrecognized arguments: model' even though 'model' is a registered subcommand. Fix: when argv contains a token matching a known subcommand, set subparsers.required=True to force deterministic routing. If that fails (e.g. 'hermes -c model' where 'model' is consumed as the session name for --continue), fall back to the default optional-subparsers behaviour. Adds 13 tests covering all key argument combinations. Reported via user screenshot showing the exact error on an installed version with the model subcommand listed in usage but rejected at parse time.
hermes doctor now checks whether the ~/.local/bin/hermes symlink exists and points to the correct venv entry point. With --fix, it creates or repairs the symlink automatically. Covers: - Missing symlink at ~/.local/bin/hermes (or $PREFIX/bin on Termux) - Symlink pointing to wrong target - Missing venv entry point (venv/bin/hermes or .venv/bin/hermes) - PATH warning when ~/.local/bin is not on PATH - Skipped on Windows (different mechanism) Addresses user report: 'python -m hermes_cli.main doesn't have an option to fix the local bin/install' 10 new tests covering all scenarios.
…10146) The /model picker called provider_model_ids() which fetches the FULL live API catalog (hundreds of models for Anthropic, Copilot, etc.) and only fell back to the curated list when the live fetch failed. This flips the priority: use the curated model list from list_authenticated_providers() (same lists as `hermes model` and gateway pickers), falling back to provider_model_ids() only when the curated list is empty (e.g. user-defined endpoints).
…targets `_parse_target_ref` has explicit-reference branches for Telegram, Feishu, and numeric IDs, but none for Matrix. As a result, callers of `send_message(target="matrix:!roomid:server")` or `send_message(target="matrix:@user:server")` fall through to `(None, None, False)` and the tool errors out with a resolution failure — even though a raw Matrix room ID or MXID is the most unambiguous possible target. Three-line fix: recognize `!…` as a room ID and `@…` as a user MXID when platform is `matrix`, and return them as explicit targets. Alias-based targets (`#…`) continue to go through the normal resolve path.
…a examples (#10151) Matrix room IDs contain ! and : which must be percent-encoded in URI path segments per the Matrix C-S spec. Without encoding, some homeservers reject the PUT request. Also adds 'matrix:!roomid:server.org' and 'matrix:@user:server.org' to the tool schema examples so models know the correct target format.
…se (#10217) With store=False (our default for the Responses API), the API does not persist response items. When reasoning items with 'id' fields were replayed on subsequent turns, the API attempted a server-side lookup for those IDs and returned 404: Item with id 'rs_...' not found. Items are not persisted when store is set to false. The encrypted_content blob is self-contained for reasoning chain continuity — the id field is unnecessary and triggers the failed lookup. Fix: strip 'id' from reasoning items in both _chat_messages_to_responses_input (message conversion) and _preflight_codex_input_items (normalization layer). The id is still used for local deduplication but never sent to the API. Reported by @zuogl448 on GPT-5.4.
) /browser connect set BROWSER_CDP_URL to http://localhost:9222, but Chrome's --remote-debugging-port only binds to 127.0.0.1 (IPv4). On macOS, 'localhost' can resolve to ::1 (IPv6) first, causing both _resolve_cdp_override's /json/version fetch and agent-browser's --cdp connection to fail when Chrome isn't listening on IPv6. The socket check in the connect handler already used 127.0.0.1 explicitly and succeeded, masking the mismatch. Use 127.0.0.1 in the default CDP URL to match what Chrome actually binds to.
…lood control Three fixes for the duplicate reply bug affecting all gateway platforms: 1. base.py: Suppress stale response when the session was interrupted by a new message that hasn't been consumed yet. Checks both interrupt_event and _pending_messages to avoid false positives. (#8221, #2483) 2. run.py (return path): Remove response_previewed guard from already_sent check. Stream consumer's already_sent alone is authoritative — if content was delivered via streaming, the duplicate send must be suppressed regardless of the agent's response_previewed flag. (#8375) 3. run.py (queued-message path): Same fix — already_sent without response_previewed now correctly marks the first response as already streamed, preventing re-send before processing the queued message. The response_previewed field is still produced by the agent (run_agent.py) but is no longer required as a gate for duplicate suppression. The stream consumer's already_sent flag is the delivery-level truth about what the user actually saw. Concepts from PR #8380 (konsisumer). Closes #8375, #8221, #2483.
…oid dropping valid 0.0 values
…cefully instead of crashing with KeyError
…kflow (#10242) * fix(gateway): suppress duplicate replies on interrupt and streaming flood control Three fixes for the duplicate reply bug affecting all gateway platforms: 1. base.py: Suppress stale response when the session was interrupted by a new message that hasn't been consumed yet. Checks both interrupt_event and _pending_messages to avoid false positives. (#8221, #2483) 2. run.py (return path): Remove response_previewed guard from already_sent check. Stream consumer's already_sent alone is authoritative — if content was delivered via streaming, the duplicate send must be suppressed regardless of the agent's response_previewed flag. (#8375) 3. run.py (queued-message path): Same fix — already_sent without response_previewed now correctly marks the first response as already streamed, preventing re-send before processing the queued message. The response_previewed field is still produced by the agent (run_agent.py) but is no longer required as a gate for duplicate suppression. The stream consumer's already_sent flag is the delivery-level truth about what the user actually saw. Concepts from PR #8380 (konsisumer). Closes #8375, #8221, #2483. * fix(cron): include job_id in delivery and guide models on removal workflow Users reported cron reminders keep firing after asking the agent to stop. Root cause: the conversational agent didn't know the job_id (not in delivery) and models don't reliably do the list→remove two-step without guidance. 1. Include job_id in the cron delivery wrapper so users and agents can reference it when requesting removal. 2. Replace confusing footer ('The agent cannot see this message') with actionable guidance ('To stop or manage this job, send me a new message'). 3. Add explicit list→remove guidance in the cronjob tool schema so models know to list first and never guess job IDs.
Previously send_message only supported media delivery for Telegram. Discord users received a warning that media was omitted. - Add media_files parameter to _send_discord() - Upload media via Discord multipart/form-data API (files[0] field) - Handle Discord in _send_to_platform() same way as Telegram block - Remove Discord from generic chunk loop (now handled above) - Update error/warning strings to mention telegram and discord
- Fix file handle closed before POST: nest session.post() inside the 'with open()' block so aiohttp can read the file during upload - Update warning text to include weixin (also supports media delivery) - Add 8 unit tests covering: text+media, media-only, missing files, upload failures, multiple files, and _send_to_platform routing
…0276) Users are confused about the difference between `hermes model` (terminal command for full provider setup) and `/model` (session command for switching between already-configured providers). This distinction was not documented anywhere. Changes across 4 doc pages: - cli-commands.md: Added warning callout explaining the difference, added --global flag docs, added 'only see OpenRouter models?' info box - slash-commands.md: Added notes on both TUI and messaging /model entries that /model only switches between configured providers - providers.md: Added 'Two Commands for Model Management' comparison table near top of page, added warning callout in switching section - faq.md: Added new FAQ entry '/model only shows one provider' with quick reference table Prompted by user feedback in Discord — new users consistently hit this confusion when trying to add providers from inside a session.
…facing text (#10285) Tool schema descriptions and tool return values contained hardcoded ~/.hermes paths that the model sees and uses. When HERMES_HOME is set to a custom path (Docker containers, profiles), the agent would still reference ~/.hermes — looking at the wrong directory. Fixes 6 locations across 5 files: - tools/tts_tool.py: output_path schema description - tools/cronjob_tools.py: script path schema description - tools/skill_manager_tool.py: skill_manage schema description - tools/skills_tool.py: two tool return messages - agent/skill_commands.py: skill config injection text All now use display_hermes_home() which resolves to the actual HERMES_HOME path (e.g. /opt/data for Docker, ~/.hermes/profiles/X for profiles, ~/.hermes for default). Reported by: Sandeep Narahari (PrithviDevs)
Re-add 'qqbot' and 'automation-templates' doc indexes to sidebars.ts that were accidentally dropped in #9931.
jianlins
pushed a commit
that referenced
this pull request
Apr 20, 2026
…ts (NousResearch#11745) Move moonshotai/kimi-k2.5 to position #1 in every model picker list: - OPENROUTER_MODELS (with 'recommended' tag) - _PROVIDER_MODELS: nous, kimi-coding, opencode-zen, opencode-go, alibaba, huggingface - _model_flow_kimi() Coding Plan model list in main.py kimi-coding-cn and moonshot lists already had kimi-k2.5 first.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Related Issue
Fixes #
Type of Change
Changes Made
How to Test
Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/AFor New Skills
hermes --toolsets skills -q "Use the X skill to do Y"Screenshots / Logs