fix(gateway): hide models without tool support from list#9068
Merged
Conversation
Contributor
Author
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (2 files)
Reviewed by gpt-5.4-20260305 · 411,784 tokens |
imanolmzd-svg
approved these changes
Apr 16, 2026
teknium1
added a commit
to NousResearch/hermes-agent
that referenced
this pull request
Apr 21, 2026
…enticate() (#13468) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * refactor(acp): validate method_id against advertised provider in authenticate() Previously authenticate() accepted any method_id whenever the server had provider credentials configured. This was not a vulnerability under the personal-assistant trust model (ACP is stdio-only, local-trust — anything that can reach the transport is already code-execution-equivalent to the user), but it was sloppy API hygiene: the advertised auth_methods list from initialize() was effectively ignored. Now authenticate() only returns AuthenticateResponse when method_id matches the currently-advertised provider (case-insensitive). Mismatched or missing method_id returns None, consistent with the no-credentials case. Raised by xeloxa via GHSA-g5pf-8w9m-h72x. Declined as a CVE (ACP transport is stdio, local-trust model), but the correctness fix is worth having on its own.
pokoni
pushed a commit
to pokoni/hermes-agent
that referenced
this pull request
Apr 21, 2026
…enticate() (NousResearch#13468) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * refactor(acp): validate method_id against advertised provider in authenticate() Previously authenticate() accepted any method_id whenever the server had provider credentials configured. This was not a vulnerability under the personal-assistant trust model (ACP is stdio-only, local-trust — anything that can reach the transport is already code-execution-equivalent to the user), but it was sloppy API hygiene: the advertised auth_methods list from initialize() was effectively ignored. Now authenticate() only returns AuthenticateResponse when method_id matches the currently-advertised provider (case-insensitive). Mismatched or missing method_id returns None, consistent with the no-credentials case. Raised by xeloxa via GHSA-g5pf-8w9m-h72x. Declined as a CVE (ACP transport is stdio, local-trust model), but the correctness fix is worth having on its own.
teknium1
added a commit
to NousResearch/hermes-agent
that referenced
this pull request
Apr 21, 2026
…bagents (#13718) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * feat(delegate): cross-agent file state coordination for concurrent subagents Prevents mangled edits when concurrent subagents touch the same file (same process, same filesystem — the mangle scenario from #11215). Three layers, all opt-out via HERMES_DISABLE_FILE_STATE_GUARD=1: 1. FileStateRegistry (tools/file_state.py) — process-wide singleton tracking per-agent read stamps and the last writer globally. check_stale() names the sibling subagent in the warning when a non-owning agent wrote after this agent's last read. 2. Per-path threading.Lock wrapped around the read-modify-write region in write_file_tool and patch_tool. Concurrent siblings on the same path serialize; different paths stay fully parallel. V4A multi-file patches lock in sorted path order (deadlock-free). 3. Delegate-completion reminder in tools/delegate_tool.py: after a subagent returns, writes_since(parent, child_start, parent_reads) appends '[NOTE: subagent modified files the parent previously read — re-read before editing: ...]' to entry.summary when the child touched anything the parent had already seen. Complements (does not replace) the existing path-overlap check in run_agent._should_parallelize_tool_batch — batch check prevents same-file parallel dispatch within one agent's turn (cheap prevention, zero API cost), registry catches cross-subagent and cross-turn staleness at write time (detection). Behavior is warning-only, not hard-failing — matches existing project style. Errors surface naturally: sibling writes often invalidate the old_string in patch operations, which already errors cleanly. Tests: tests/tools/test_file_state_registry.py — 16 tests covering registry state transitions, per-path locking, per-path-not-global locking, writes_since filtering, kill switch, and end-to-end integration through the real read_file/write_file/patch handlers.
yong2bba
added a commit
to yong2bba/hermes-agent
that referenced
this pull request
Apr 22, 2026
* fix(tools/delegate): propagate resolved ACP runtime settings to child agents
* fix(whatsapp): kill bridge process tree on Windows disconnect
* fix: forward auth when probing local model metadata
Pass the user's configured api_key through local-server detection and
context-length probes (detect_local_server_type, _query_local_context_length,
query_ollama_num_ctx) and use LM Studio's native /api/v1/models endpoint in
fetch_endpoint_model_metadata when a loaded instance is present — so the
probed context length is the actual runtime value the user loaded the model
at, not just the model's theoretical max.
Helps local-LLM users whose auto-detected context length was wrong, causing
compression failures and context-overrun crashes.
* fix: thread api_key through ollama num_ctx probe + author map
Follow-up for salvaged PR #3185:
- run_agent.py: pass self.api_key to query_ollama_num_ctx() so Ollama
behind an auth proxy (same issue class as the LM Studio fix) can be
probed successfully.
- scripts/release.py AUTHOR_MAP: map @tannerfokkens-maker's local-hostname
commit email.
* fix(tools): keep default-off toolsets disabled
* chore(release): map mavrickdeveloper email for attribution
* chore: map Es1la contributor email for AUTHOR_MAP (#13294)
Credit preserved for PR #13270 (WhatsApp Windows disconnect fix).
* feat: shell hooks — wire shell scripts as Hermes hook callbacks
Users can declare shell scripts in config.yaml under a hooks: block that
fire on plugin-hook events (pre_tool_call, post_tool_call, pre_llm_call,
subagent_stop, etc). Scripts receive JSON on stdin, can return JSON on
stdout to block tool calls or inject context pre-LLM.
Key design:
- Registers closures on existing PluginManager._hooks dict — zero changes
to invoke_hook() call sites
- subprocess.run(shell=False) via shlex.split — no shell injection
- First-use consent per (event, command) pair, persisted to allowlist JSON
- Bypass via --accept-hooks, HERMES_ACCEPT_HOOKS=1, or hooks_auto_accept
- hermes hooks list/test/revoke/doctor CLI subcommands
- Adds subagent_stop hook event fired after delegate_task children exit
- Claude Code compatible response shapes accepted
Cherry-picked from PR #13143 by @pefontana.
* feat: attribution default_headers for ai-gateway provider
Requests through Vercel AI Gateway now carry referrerUrl / appName /
User-Agent attribution so traffic shows up in the gateway's analytics.
Adds _AI_GATEWAY_HEADERS in auxiliary_client and a new
ai-gateway.vercel.sh branch in _apply_client_headers_for_base_url.
* feat: curated picker with live pricing for ai-gateway provider
- Curated AI_GATEWAY_MODELS list in hermes_cli/models.py (OSS first,
kimi-k2.5 as recommended default).
- fetch_ai_gateway_models() filters the curated list against the live
/v1/models catalog; falls back to the snapshot on network failure.
- fetch_ai_gateway_pricing() translates Vercel's input/output field
names to the prompt/completion shape the shared picker expects;
carries input_cache_read / input_cache_write through unchanged.
- get_pricing_for_provider() now handles ai-gateway.
- _model_flow_ai_gateway() provides a guided URL prompt when no key
is set and a pricing-column picker; routes ai-gateway to it instead
of the generic api-key flow.
* feat: promote ai-gateway in provider picker ordering
Moves Vercel AI Gateway from the bottom of the list to near the top,
adjacent to other multi-model aggregators. The existing bottom
position was a result of the list growing by appending new providers
over time — the new position makes it more discoverable.
* feat: auto-promote free Moonshot models to top of ai-gateway picker
When the live Vercel AI Gateway catalog exposes a Moonshot model with
zero input AND output pricing, it's promoted to position #1 as the
recommended default — even if the exact ID isn't in the curated
AI_GATEWAY_MODELS list. This enables dynamic discovery of new free
Moonshot variants without requiring a PR to update curation.
Paid Moonshot models are unaffected; falls back to the normal curated
recommended tag when no free Moonshot is live.
* feat: use Vercel's deep-link for ai-gateway API key creation prompt
Vercel provides a d?to= redirect URL that routes users through their
team picker to the AI Gateway API keys management page. Using this
specific URL lands users directly on the "Create key" page instead of
the generic AI Gateway dashboard.
* chore: register contributor in AUTHOR_MAP for release-note attribution
Adds zheng.jerilyn@gmail.com → jerilynzheng to scripts/release.py so
the check-attribution CI workflow passes.
* fix: correct AI_GATEWAY_MODELS slugs to match Vercel's catalog
The original list was copied from OpenRouter conventions and didn't
match what Vercel actually hosts. Verified against the live
/v1/models endpoint (266 models):
- qwen/qwen3.6-plus → alibaba/qwen3.6-plus (Vercel hosts Qwen under alibaba/)
- z-ai/glm-5.1 → zai/glm-5.1 (no hyphen)
- x-ai/grok-4.20 → xai/grok-4.20-reasoning (no hyphen, picks reasoning variant)
- google/gemini-3-flash-preview → google/gemini-3-flash (no -preview suffix)
- moonshotai/kimi-k2.5 → moonshotai/kimi-k2.6 (newest available)
* fix(gateway/api_server): deduplicate concurrent idempotent requests
* chore(release): map yukipukikedy@gmail.com to Yukipukii1
* fix(env_loader): warn when non-ASCII stripped from credential env vars (#13300)
Load-time sanitizer silently removed non-ASCII codepoints from any
env var ending in _API_KEY / _TOKEN / _SECRET / _KEY, turning
copy-paste artifacts (Unicode lookalikes, ZWSP, NBSP) into opaque
provider-side API_KEY_INVALID errors.
Warn once per key to stderr with the offending codepoints (U+XXXX)
and guidance to re-copy from the provider dashboard.
* fix: restrict provider URL detection to exact hostname matches
* fix: extend hostname-match provider detection across remaining call sites
Aslaaen's fix in the original PR covered _detect_api_mode_for_url and the
two openai/xai sites in run_agent.py. This finishes the sweep: the same
substring-match false-positive class (e.g. https://api.openai.com.evil/v1,
https://proxy/api.openai.com/v1, https://api.anthropic.com.example/v1)
existed in eight more call sites, and the hostname helper was duplicated
in two modules.
- utils: add shared base_url_hostname() (single source of truth).
- hermes_cli/runtime_provider, run_agent: drop local duplicates, import
from utils. Reuse the cached AIAgent._base_url_hostname attribute
everywhere it's already populated.
- agent/auxiliary_client: switch codex-wrap auto-detect, max_completion_tokens
gate (auxiliary_max_tokens_param), and custom-endpoint max_tokens kwarg
selection to hostname equality.
- run_agent: native-anthropic check in the Claude-style model branch
and in the AIAgent init provider-auto-detect branch.
- agent/model_metadata: Anthropic /v1/models context-length lookup.
- hermes_cli/providers.determine_api_mode: anthropic / openai URL
heuristics for custom/unknown providers (the /anthropic path-suffix
convention for third-party gateways is preserved).
- tools/delegate_tool: anthropic detection for delegated subagent
runtimes.
- hermes_cli/setup, hermes_cli/tools_config: setup-wizard vision-endpoint
native-OpenAI detection (paired with deduping the repeated check into
a single is_native_openai boolean per branch).
Tests:
- tests/test_base_url_hostname.py covers the helper directly
(path-containing-host, host-suffix, trailing dot, port, case).
- tests/hermes_cli/test_determine_api_mode_hostname.py adds the same
regression class for determine_api_mode, plus a test that the
/anthropic third-party gateway convention still wins.
Also: add asslaenn5@gmail.com → Aslaaen to scripts/release.py AUTHOR_MAP.
* fix: sweep remaining provider-URL substring checks across codebase
Completes the hostname-hardening sweep — every substring check against a
provider host in live-routing code is now hostname-based. This closes the
same false-positive class for OpenRouter, GitHub Copilot, Kimi, Qwen,
ChatGPT/Codex, Bedrock, GitHub Models, Vercel AI Gateway, Nous, Z.AI,
Moonshot, Arcee, and MiniMax that the original PR closed for OpenAI, xAI,
and Anthropic.
New helper:
- utils.base_url_host_matches(base_url, domain) — safe counterpart to
'domain in base_url'. Accepts hostname equality and subdomain matches;
rejects path segments, host suffixes, and prefix collisions.
Call sites converted (real-code only; tests, optional-skills, red-teaming
scripts untouched):
run_agent.py (10 sites):
- AIAgent.__init__ Bedrock branch, ChatGPT/Codex branch (also path check)
- header cascade for openrouter / copilot / kimi / qwen / chatgpt
- interleaved-thinking trigger (openrouter + claude)
- _is_openrouter_url(), _is_qwen_portal()
- is_native_anthropic check
- github-models-vs-copilot detection (3 sites)
- reasoning-capable route gate (nousresearch, vercel, github)
- codex-backend detection in API kwargs build
- fallback api_mode Bedrock detection
agent/auxiliary_client.py (7 sites):
- extra-headers cascades in 4 distinct client-construction paths
(resolve custom, resolve auto, OpenRouter-fallback-to-custom,
_async_client_from_sync, resolve_provider_client explicit-custom,
resolve_auto_with_codex)
- _is_openrouter_client() base_url sniff
agent/usage_pricing.py:
- resolve_billing_route openrouter branch
agent/model_metadata.py:
- _is_openrouter_base_url(), Bedrock context-length lookup
hermes_cli/providers.py:
- determine_api_mode Bedrock heuristic
hermes_cli/runtime_provider.py:
- _is_openrouter_url flag for API-key preference (issues #420, #560)
hermes_cli/doctor.py:
- Kimi User-Agent header for /models probes
tools/delegate_tool.py:
- subagent Codex endpoint detection
trajectory_compressor.py:
- _detect_provider() cascade (8 providers: openrouter, nous, codex, zai,
kimi-coding, arcee, minimax-cn, minimax)
cli.py, gateway/run.py:
- /model-switch cache-enabled hint (openrouter + claude)
Bedrock detection tightened from 'bedrock-runtime in url' to
'hostname starts with bedrock-runtime. AND host is under amazonaws.com'.
ChatGPT/Codex detection tightened from 'chatgpt.com/backend-api/codex in
url' to 'hostname is chatgpt.com AND path contains /backend-api/codex'.
Tests:
- tests/test_base_url_hostname.py extended with a base_url_host_matches
suite (exact match, subdomain, path-segment rejection, host-suffix
rejection, host-prefix rejection, empty-input, case-insensitivity,
trailing dot).
Validation: 651 targeted tests pass (runtime_provider, minimax, bedrock,
gemini, auxiliary, codex_cloudflare, usage_pricing, compressor_fallback,
fallback_model, openai_client_lifecycle, provider_parity, cli_provider_resolution,
delegate, credential_pool, context_compressor, plus the 4 hostname test
modules). 26-assertion E2E call-site verification across 6 modules passes.
* refactor(steer): simplify injection marker to 'User guidance:' prefix (#13340)
The mid-run steer marker was '[USER STEER (injected mid-run, not tool
output): <text>]'. Replaced with a plain two-newline-prefixed
'User guidance: <text>' suffix.
Rationale: the marker lives inside the tool result's content string
regardless of whether the tool returned JSON, plain text, an MCP
result, or a plugin result. The bracketed tag read like structured
metadata that some tools (terminal, execute_code) could confuse with
their own output formatting. A plain labelled suffix works uniformly
across every content shape we produce.
Behavior unchanged:
- Still injected into the last tool-role message's content.
- Still preserves multimodal (Anthropic) content-block lists by
appending a text block.
- Still drained at both sites added in #12959 and #13205 — per-tool
drain between individual calls, and pre-API-call drain at the top
of each main-loop iteration.
Checked Codex's equivalent (pending_input / inject_user_message_without_turn
in codex-rs/core): they record mid-turn user input as a real role:user
message via record_user_prompt_and_emit_turn_item(). That's cleaner for
their Responses-API model but not portable to Chat Completions where
role alternation after tool_calls is strict. Embedding the guidance in
the last tool result remains the correct placement for us.
Validation: all 21 tests in tests/run_agent/test_steer.py pass.
* refactor(ai-gateway): single source of truth for model catalog (#13304)
Delete the stale literal `_PROVIDER_MODELS["ai-gateway"]` (gpt-5,
gemini-2.5-pro, claude-4.5 — outdated the moment PR #13223 landed with
its curated `AI_GATEWAY_MODELS` snapshot) and derive it from
`AI_GATEWAY_MODELS` instead, so the picker tuples and the bare-id
fallback catalog stay in sync automatically. Also fixes
`get_default_model_for_provider('ai-gateway')` to return kimi-k2.6
(the curated recommendation) instead of claude-opus-4.6.
* fix(whatsapp): remove 120s timeout on bridge npm install (#13339)
The WhatsApp bridge depends on @whiskeysockets/baileys pulled directly
from a GitHub commit tarball, which on slower connections or when
GitHub is sluggish routinely exceeds 120s. The hardcoded timeout
surfaced as a raw TimeoutExpired traceback during 'hermes whatsapp'
setup.
Switch to the same pattern used by the TUI npm install at line
~945: no timeout, --no-fund/--no-audit/--progress=false to keep
output clean, stderr captured and tailed on failure. Also resolve
npm via shutil.which so missing Node.js gives a clean error instead
of FileNotFoundError, and handle Ctrl+C cleanly.
Co-authored-by: teknium1 <teknium@nousresearch.com>
* fix(cli): dispatch /steer inline while agent is running (#13354)
Classic-CLI /steer typed during an active agent run was queued through
self._pending_input alongside ordinary user input. process_loop, which
drains that queue, is blocked inside self.chat() for the entire run,
so the queued command was not pulled until AFTER _agent_running had
flipped back to False — at which point process_command() took the idle
fallback ("No agent running; queued as next turn") and delivered the
steer as an ordinary next-turn user message.
From Utku's bug report on PR #13205: mid-run /steer arrived minutes
later at the end of the turn as a /queue-style message, completely
defeating its purpose.
Fix: add _should_handle_steer_command_inline() gating — when
_agent_running is True and the user typed /steer, dispatch
process_command(text) directly from the prompt_toolkit Enter handler
on the UI thread instead of queueing. This mirrors the existing
_should_handle_model_command_inline() pattern for /model and is
safe because agent.steer() is thread-safe (uses _pending_steer_lock,
no prompt_toolkit state mutation, instant return).
No changes to the idle-path behavior: /steer typed with no active
agent still takes the normal queue-and-drain route so the fallback
"No agent running; queued as next turn" message is preserved.
Validation:
- 7 new unit tests in tests/cli/test_cli_steer_busy_path.py covering
the detector, dispatch path, and idle-path control behavior.
- All 21 existing tests in tests/run_agent/test_steer.py still pass.
- Live PTY end-to-end test with real agent + real openrouter model:
22:36:22 API call #1 (model requested execute_code)
22:36:26 ENTER FIRED: agent_running=True, text='/steer ...'
22:36:26 INLINE STEER DISPATCH fired
22:36:43 agent.log: 'Delivered /steer to agent after tool batch'
22:36:44 API call #2 included the steer; response contained marker
Same test on the tip of main without this fix shows the steer
landing as a new user turn ~20s after the run ended.
* feat: add transport types + migrate Anthropic normalize path
Add agent/transports/types.py with three shared dataclasses:
- NormalizedResponse: content, tool_calls, finish_reason, reasoning, usage, provider_data
- ToolCall: id, name, arguments, provider_data (per-tool-call protocol metadata)
- Usage: prompt_tokens, completion_tokens, total_tokens, cached_tokens
Add normalize_anthropic_response_v2() to anthropic_adapter.py — wraps the
existing v1 function and maps its output to NormalizedResponse. One call site
in run_agent.py (the main normalize branch) uses v2 with a back-compat shim
to SimpleNamespace for downstream code.
No ABC, no registry, no streaming, no client lifecycle. Those land in PR 3
with the first concrete transport (AnthropicTransport).
46 new tests:
- test_types.py: dataclass construction, build_tool_call, map_finish_reason
- test_anthropic_normalize_v2.py: v1-vs-v2 regression tests (text, tools,
thinking, mixed, stop reasons, mcp prefix stripping, edge cases)
Part of the provider transport refactor (PR 2 of 9).
* test: stop testing mutable data — convert change-detectors to invariants (#13363)
Catalog snapshots, config version literals, and enumeration counts are data
that changes as designed. Tests that assert on those values add no
behavioral coverage — they just break CI on every routine update and cost
engineering time to 'fix.'
Replace with invariants where one exists, delete where none does.
Deleted (pure snapshots):
- TestMinimaxModelCatalog (3 tests): 'MiniMax-M2.7 in models' et al
- TestGeminiModelCatalog: 'gemini-2.5-pro in models', 'gemini-3.x in models'
- test_browser_camofox_state::test_config_version_matches_current_schema
(docstring literally said it would break on unrelated bumps)
Relaxed (keep plumbing check, drop snapshot):
- Xiaomi / Arcee / Kimi moonshot / Kimi coding / HuggingFace static lists:
now assert 'provider exists and has >= 1 entry' instead of specific names
- HuggingFace main/models.py consistency test: drop 'len >= 6' floor
Dynamicized (follow source, not a literal):
- 3x test_config.py migration tests: raw['_config_version'] ==
DEFAULT_CONFIG['_config_version'] instead of hardcoded 21
Fixed stale tests against intentional behavior changes:
- test_insights::test_gateway_format_hides_cost: name matches new behavior
(no dollar figures); remove contradicting '$' in text assertion
- test_config::prefers_api_then_url_then_base_url: flipped per PR #9332;
rename + update to base_url > url > api
- test_anthropic_adapter: relax assert_called_once() (xdist-flaky) to
assert called — contract is 'credential flowed through'
- test_interrupt_propagation: add provider/model/_base_url to bare-agent
fixture so the stale-timeout code path resolves
Fixed stale integration tests against opt-in plugin gate:
- transform_tool_result + transform_terminal_output: write plugins.enabled
allow-list to config.yaml and reset the plugin manager singleton
Source fix (real consistency invariant):
- agent/model_metadata.py: add moonshotai/Kimi-K2.6 context length
(262144, same as K2.5). test_model_metadata_has_context_lengths was
correctly catching the gap.
Policy:
- AGENTS.md Testing section: new subsection 'Don't write change-detector
tests' with do/don't examples. Reviewers should reject catalog-snapshot
assertions in new tests.
Covers every test that failed on the last completed main CI run
(24703345583) except test_modal_sandbox_fixes::test_terminal_tool_present
+ test_terminal_and_file_toolsets_resolve_all_tools, which now pass both
alone and with the full tests/tools/ directory (xdist ordering flake that
resolved itself).
* fix(whatsapp): remove shadowing shutil import in cmd_whatsapp (#13364)
The re-pair branch had a redundant 'import shutil' inside cmd_whatsapp,
which made shutil a function-local throughout the whole scope. The
earlier 'shutil.which("npm")' call at the dependency-install step then
crashed with UnboundLocalError before control ever reached the local
import.
shutil is already imported at module level (line 48), so the local
import was dead code anyway. Drop it.
* feat(voice): add cli beep toggle
* feat(skills+terminal): make bundled skill scripts runnable out of the box (#13384)
* feat(skills): inject absolute skill dir and expand ${HERMES_SKILL_DIR} templates
When a skill loads, the activation message now exposes the absolute
skill directory and substitutes ${HERMES_SKILL_DIR} /
${HERMES_SESSION_ID} tokens in the SKILL.md body, so skills with
bundled scripts can instruct the agent to run them by absolute path
without an extra skill_view round-trip.
Also adds opt-in inline-shell expansion: !`cmd` snippets in SKILL.md
are pre-executed (with the skill directory as CWD) and their stdout is
inlined into the message before the agent reads it. Off by default —
enable via skills.inline_shell in config.yaml — because any snippet
runs on the host without approval.
Changes:
- agent/skill_commands.py: template substitution, inline-shell
expansion, absolute skill-dir header, supporting-files list now
shows both relative and absolute forms.
- hermes_cli/config.py: new skills.template_vars,
skills.inline_shell, skills.inline_shell_timeout knobs.
- tests/agent/test_skill_commands.py: coverage for header, both
template tokens (present and missing session id), template_vars
disable, inline-shell default-off, enabled, CWD, and timeout.
- website/docs/developer-guide/creating-skills.md: documents the
template tokens, the absolute-path header, and the opt-in inline
shell with its security caveat.
Validation: tests/agent/ 1591 passed (includes 9 new tests).
E2E: loaded a real skill in an isolated HERMES_HOME; confirmed
${HERMES_SKILL_DIR} resolves to the absolute path, ${HERMES_SESSION_ID}
resolves to the passed task_id, !`date` runs when opt-in is set, and
stays literal when it isn't.
* feat(terminal): source ~/.bashrc (and user-listed init files) into session snapshot
bash login shells don't source ~/.bashrc, so tools that install themselves
there — nvm, asdf, pyenv, cargo, custom PATH exports — stay invisible to
the environment snapshot Hermes builds once per session. Under systemd
or any context with a minimal parent env, that surfaces as
'node: command not found' in the terminal tool even though the binary
is reachable from every interactive shell on the machine.
Changes:
- tools/environments/local.py: before the login-shell snapshot bootstrap
runs, prepend guarded 'source <file>' lines for each resolved init
file. Missing files are skipped, each source is wrapped with a
'[ -r ... ] && . ... || true' guard so a broken rc can't abort the
bootstrap.
- hermes_cli/config.py: new terminal.shell_init_files (explicit list,
supports ~ and ${VAR}) and terminal.auto_source_bashrc (default on)
knobs. When shell_init_files is set it takes precedence; when it's
empty and auto_source_bashrc is on, ~/.bashrc gets auto-sourced.
- tests/tools/test_local_shell_init.py: 10 tests covering the resolver
(auto-bashrc, missing file, explicit override, ~/${VAR} expansion,
opt-out) and the prelude builder (quoting, guarded sourcing), plus
a real-LocalEnvironment snapshot test that confirms exports in the
init file land in subsequent commands' environment.
- website/docs/reference/faq.md: documents the fix in Troubleshooting,
including the zsh-user pattern of sourcing ~/.zshrc or nvm.sh
directly via shell_init_files.
Validation: 10/10 new tests pass; tests/tools/test_local_*.py 40/40
pass; tests/agent/ 1591/1591 pass; tests/hermes_cli/test_config.py
50/50 pass. E2E in an isolated HERMES_HOME: confirmed that a fake
~/.bashrc setting a marker var and PATH addition shows up in a real
LocalEnvironment().execute() call, that auto_source_bashrc=false
suppresses it, that an explicit shell_init_files entry wins over the
auto default, and that a missing bashrc is silently skipped.
* fix(gateway): prevent --replace race condition causing multiple instances
When starting the gateway with --replace, concurrent invocations could
leave multiple instances running simultaneously. This happened because
write_pid_file() used a plain overwrite, so the second racer would
silently replace the first process's PID record.
Changes:
- gateway/status.py: write_pid_file() now uses atomic O_CREAT|O_EXCL
creation. If the file already exists, it raises FileExistsError,
allowing exactly one process to win the race.
- gateway/run.py: before writing the PID file, re-check get_running_pid()
and catch FileExistsError from write_pid_file(). In both cases, stop
the runner and return False so the process exits cleanly.
Fixes #11718
* fix(gateway): force-unlink stale PID file after --replace takeover
If the old process crashed without firing its atexit handler,
remove_pid_file() is a no-op. Force-unlink the stale gateway.pid
so write_pid_file() (O_CREAT|O_EXCL) does not hit FileExistsError.
* fix(gateway): close --replace race completely by claiming PID before adapter startup
Follow-up on top of opriz's atomic PID file fix. The prior change caught
the race AFTER runner.start(), so the loser still opened Telegram polling
and Discord gateway sockets before detecting the conflict and exiting.
Hoist the PID-claim block to BEFORE runner.start(). Now the loser of the
O_CREAT|O_EXCL race returns from start_gateway() without ever bringing up
any platform adapter — no Telegram conflict, no Discord duplicate session.
Also add regression tests:
- test_write_pid_file_is_atomic_against_concurrent_writers: second
write_pid_file() raises FileExistsError rather than clobbering.
- Two existing replace-path tests updated to stateful mocks since the
real post-kill state (get_running_pid None after remove_pid_file)
is now exercised by the hoisted re-check.
* refactor: remove redundant local imports already available at module level
Sweep ~74 redundant local imports across 21 files where the same module
was already imported at the top level. Also includes type fixes and lint
cleanups on the same branch.
* refactor: remove remaining redundant local imports (comprehensive sweep)
Full AST-based scan of all .py files to find every case where a module
or name is imported locally inside a function body but is already
available at module level. This is the second pass — the first commit
handled the known cases from the lint report; this one catches
everything else.
Files changed (19):
cli.py — 16 removals: time as _time/_t/_tmod (×10),
re / re as _re (×2), os as _os, sys,
partial os from combo import,
from model_tools import get_tool_definitions
gateway/run.py — 8 removals: MessageEvent as _ME /
MessageType as _MT (×3), os as _os2,
MessageEvent+MessageType (×2), Platform,
BasePlatformAdapter as _BaseAdapter
run_agent.py — 6 removals: get_hermes_home as _ghh,
partial (contextlib, os as _os),
cleanup_vm, cleanup_browser,
set_interrupt as _sif (×2),
partial get_toolset_for_tool
hermes_cli/main.py — 4 removals: get_hermes_home, time as _time,
logging as _log, shutil
hermes_cli/config.py — 1 removal: get_hermes_home as _ghome
hermes_cli/runtime_provider.py
— 1 removal: load_config as _load_bedrock_config
hermes_cli/setup.py — 2 removals: importlib.util (×2)
hermes_cli/nous_subscription.py
— 1 removal: from hermes_cli.config import load_config
hermes_cli/tools_config.py
— 1 removal: from hermes_cli.config import load_config, save_config
cron/scheduler.py — 3 removals: concurrent.futures, json as _json,
from hermes_cli.config import load_config
batch_runner.py — 1 removal: list_distributions as get_all_dists
(kept print_distribution_info, not at top level)
tools/send_message_tool.py
— 2 removals: import os (×2)
tools/skills_tool.py — 1 removal: logging as _logging
tools/browser_camofox.py
— 1 removal: from hermes_cli.config import load_config
tools/image_generation_tool.py
— 1 removal: import fal_client
environments/tool_context.py
— 1 removal: concurrent.futures
gateway/platforms/bluebubbles.py
— 1 removal: httpx as _httpx
gateway/platforms/whatsapp.py
— 1 removal: import asyncio
tui_gateway/server.py — 2 removals: from datetime import datetime,
import time
All alias references (_time, _t, _tmod, _re, _os, _os2, _json, _ghh,
_ghome, _sif, _ME, _MT, _BaseAdapter, _load_bedrock_config, _httpx,
_logging, _log, get_all_dists) updated to use the top-level names.
* fix(update): keep get_hermes_home late-bound in _install_hangup_protection
Follow-up to the redundant-imports sweep. _install_hangup_protection
used to import get_hermes_home locally; the sweep hoisted it to the
module-level binding already present at line 164.
test_non_fatal_if_log_setup_fails monkeypatches
hermes_cli.config.get_hermes_home to raise, which only works when the
function late-binds its lookup. The hoisted version captures the
reference at import time and bypasses the monkeypatch.
Restore the local import (with a distinct local alias) so the test
seam works and the stdio-untouched-on-setup-failure invariant is
actually exercised.
* feat(maps): add guest_house, camp_site, and dual-key bakery lookup (#13398)
Small follow-up inspired by stale PR #2421 (@poojandpatel).
- bakery now searches both shop=bakery AND amenity=bakery in one Overpass
query so indie bakeries tagged either way are returned. Reproduces #2421's
Lawrenceville, NJ test case (The Gingered Peach, WildFlour Bakery).
- Adds tourism=guest_house and tourism=camp_site as first-class categories.
- CATEGORY_TAGS entries can now be a list of (key, value) tuples; new
_tags_for() normaliser + tag_pairs= kwarg on build_overpass_nearby/bbox
union the results in one query. Old single-tuple call sites unchanged
(back-compat preserved).
- SKILL.md: 44 → 46 categories, list updated.
* fix(gateway): preserve sender attribution in shared group sessions
Generalize shared multi-user session handling so non-thread group sessions
(group_sessions_per_user=False) get the same treatment as shared threads:
inbound messages are prefixed with [sender name], and the session prompt
shows a multi-user note instead of pinning a single **User:** line into
the cached system prompt.
Before: build_session_key already treated these as shared sessions, but
_prepare_inbound_message_text and build_session_context_prompt only
recognized shared threads — creating cross-user attribution drift and
prompt-cache contamination in shared groups.
- Add is_shared_multi_user_session() helper alongside build_session_key()
so both the session key and the multi-user branches are driven by the
same rules (DMs never shared, threads shared unless
thread_sessions_per_user, groups shared unless group_sessions_per_user).
- Add shared_multi_user_session field to SessionContext, populated by
build_session_context() from config.
- Use context.shared_multi_user_session in the prompt builder (label is
'Multi-user thread' when a thread is present, 'Multi-user session'
otherwise).
- Use the helper in _prepare_inbound_message_text so non-thread shared
groups also get [sender] prefixes.
Default behavior unchanged: DMs stay single-user, groups with
group_sessions_per_user=True still show the user normally, shared threads
keep their existing multi-user behavior.
Tests (65 passed):
- tests/gateway/test_session.py: new shared non-thread group prompt case.
- tests/gateway/test_shared_group_sender_prefix.py: inbound preprocessing
for shared non-thread groups and default groups.
* feat: add transport ABC + AnthropicTransport wired to all paths
Add ProviderTransport ABC (4 abstract methods: convert_messages,
convert_tools, build_kwargs, normalize_response) plus optional hooks
(validate_response, extract_cache_stats, map_finish_reason).
Add transport registry with lazy discovery — get_transport() auto-imports
transport modules on first call.
Add AnthropicTransport — delegates to existing anthropic_adapter.py
functions, wired to ALL Anthropic code paths in run_agent.py:
- Main normalize loop (L10775)
- Main build_kwargs (L6673)
- Response validation (L9366)
- Finish reason mapping (L9534)
- Cache stats extraction (L9827)
- Truncation normalize (L9565)
- Memory flush build_kwargs + normalize (L7363, L7395)
- Iteration-limit summary + retry (L8465, L8498)
Zero direct adapter imports remain for transport methods. Client lifecycle,
streaming, auth, and credential management stay on AIAgent.
20 new tests (ABC contract, registry, AnthropicTransport methods).
359 anthropic-related tests pass (0 failures).
PR 3 of the provider transport refactor.
* feat: Add KittenTTS provider for local TTS synthesis
Add support for KittenTTS - a lightweight, local TTS engine with models
ranging from 25-80MB that runs on CPU without requiring a GPU or API key.
Features:
- Support for 8 built-in voices (Jasper, Bella, Luna, etc.)
- Configurable model size (nano 25MB, micro 41MB, mini 80MB)
- Adjustable speech speed
- Model caching for performance
- Automatic WAV to Opus conversion for Telegram voice messages
Configuration example (config.yaml):
tts:
provider: kittentts
kittentts:
model: KittenML/kitten-tts-nano-0.8-int8
voice: Jasper
speed: 1.0
clean_text: true
Installation:
pip install https://github.com/KittenML/KittenTTS/releases/download/0.8.1/kittentts-0.8.1-py3-none-any.whl
* feat(tts): complete KittenTTS integration (tools/setup/docs/tests)
Builds on @AxDSan's PR #2109 to finish the KittenTTS wiring so the
provider behaves like every other TTS backend end to end.
- tools/tts_tool.py: `_check_kittentts_available()` helper and wire
into `check_tts_requirements()`; extend Opus-conversion list to
include kittentts (WAV → Opus for Telegram voice bubbles); point the
missing-package error at `hermes setup tts`.
- hermes_cli/tools_config.py: add KittenTTS entry to the "Text-to-Speech"
toolset picker, with a `kittentts` post_setup hook that auto-installs
the wheel + soundfile via pip.
- hermes_cli/setup.py: `_install_kittentts_deps()`, new choice + install
flow in `_setup_tts_provider()`, provider_labels entry, and status row
in the `hermes setup` summary.
- website/docs/user-guide/features/tts.md: add KittenTTS to the provider
table, config example, ffmpeg note, and the zero-config voice-bubble tip.
- tests/tools/test_tts_kittentts.py: 10 unit tests covering generation,
model caching, config passthrough, ffmpeg conversion, availability
detection, and the missing-package dispatcher branch.
E2E verified against the real `kittentts` wheel:
- WAV direct output (pcm_s16le, 24kHz mono)
- MP3 conversion via ffmpeg (from WAV)
- Telegram flow (provider in Opus-conversion list) produces
`codec_name=opus`, 48kHz mono, `voice_compatible=True`, and the
`[[audio_as_voice]]` marker
- check_tts_requirements() returns True when kittentts is installed
* chore(release): map abdi.moya@gmail.com -> AxDSan for release notes
* fix(security): apply file safety to copilot acp fs
* chore(release): map fr@tecompanytea.com → ifrederico
* test(copilot-acp): patch HERMES_HOME alongside HOME in hub-block test
file_safety now uses profile-aware get_hermes_home(), so the test
fixture must override HERMES_HOME too — otherwise it resolves to the
conftest's isolated tempdir and the hub-cache path doesn't match.
* test(conftest): reset module-level state + unset platform allowlists (#13400)
Three fixes that close the remaining structural sources of CI flakes
after PR #13363.
## 1. Per-test reset of module-level singletons and ContextVars
Python modules are singletons per process, and pytest-xdist workers are
long-lived. Module-level dicts/sets and ContextVars persist across tests
on the same worker. A test that sets state in `tools.approval._session_approved`
and doesn't explicitly clear it leaks that state to every subsequent test
on the same worker.
New `_reset_module_state` autouse fixture in `tests/conftest.py` clears:
- tools.approval: _session_approved, _session_yolo, _permanent_approved,
_pending, _gateway_queues, _gateway_notify_cbs, _approval_session_key
- tools.interrupt: _interrupted_threads
- gateway.session_context: 10 session/cron ContextVars (reset to _UNSET)
- tools.env_passthrough: _allowed_env_vars_var (reset to empty set)
- tools.credential_files: _registered_files_var (reset to empty dict)
- tools.file_tools: _read_tracker, _file_ops_cache
This was the single biggest remaining class of CI flakes.
`test_command_guards::test_warn_session_approved` and
`test_combined_cli_session_approves_both` were failing 12/15 recent main
runs specifically because `_session_approved` carried approvals from a
prior test's session into these tests' `"default"` session lookup.
## 2. Unset platform allowlist env vars in hermetic fixture
`TELEGRAM_ALLOWED_USERS`, `DISCORD_ALLOWED_USERS`, and 20 other
`*_ALLOWED_USERS` / `*_ALLOW_ALL_USERS` vars are now unset per-test in
the same place credential env vars already are. These aren't credentials
but they change gateway auth behavior; if set from any source (user
shell, leaky test, CI env) they flake button-authorization tests.
Fixes three `test_telegram_approval_buttons` tests that were failing
across recent runs of the full gateway directory.
## 3. Two specific tests with module-level captured state
- `test_signal::TestSignalPhoneRedaction`: `agent.redact._REDACT_ENABLED`
is captured at module import from `HERMES_REDACT_SECRETS`, not read
per-call. `monkeypatch.delenv` at test time is too late. Added
`monkeypatch.setattr("agent.redact._REDACT_ENABLED", True)` per
skill xdist-cross-test-pollution Pattern 5.
- `test_internal_event_bypass_pairing::test_non_internal_event_without_user_triggers_pairing`:
`gateway.pairing.PAIRING_DIR` is captured at module import from
HERMES_HOME, so per-test HERMES_HOME redirection in conftest doesn't
retroactively move it. Test now monkeypatches PAIRING_DIR directly to
its tmp_path, preventing rate-limit state from prior xdist workers
from letting the pairing send-call be suppressed.
## Validation
- tests/tools/: 3494 pass (0 fail) including test_command_guards
- tests/gateway/: 3504 pass (0 fail) across repeat runs
- tests/agent/ + tests/hermes_cli/ + tests/run_agent/ + tests/tools/:
8371 pass, 37 skipped, 0 fail — full suite across directories
No production code changed.
* fix(auth): hermes auth remove sticks for shell-exported env vars (#13418)
Removing an env-seeded credential only cleared ~/.hermes/.env and the
current process's os.environ, leaving shell-exported vars (shell profile,
systemd EnvironmentFile, launchd plist) to resurrect the entry on the
next load_pool() call. This matched the pre-#11485 codex behaviour.
Now we suppress env:<VAR> in auth.json on remove, gate _seed_from_env()
behind is_source_suppressed(), clear env:* suppressions on auth add,
and print a diagnostic pointing at the shell when the var lives there.
Applies to every env:* seeded credential (xai, deepseek, moonshot, zai,
nvidia, openrouter, anthropic, etc.), not just xai.
Reported by @teknium1 from community user 'Artificial Brain' — couldn't
remove their xAI key via hermes auth remove.
* feat(cli): add numbered keyboard shortcuts to approval and clarify prompts
* chore(release): add francip to AUTHOR_MAP
* feat(skills): add adversarial-ux-test optional skill
Adds a structured adversarial UX testing skill that roleplays the
worst-case user for any product. Uses a 6-step workflow:
1. Define a specific grumpy persona (age 50+, tech-resistant)
2. Browse the app in-character attempting real tasks
3. Write visceral in-character feedback (the Rant)
4. Apply a pragmatism filter (RED/YELLOW/WHITE/GREEN classification)
5. Create tickets only for real issues (RED + GREEN)
6. Deliver a structured report with screenshots
The pragmatism filter is the key differentiator - it prevents raw
persona complaints from becoming tickets, separating genuine UX
problems from "I hate computers" noise.
Includes example personas for 8 industry verticals and practical
tips from real-world testing sessions.
Ref: https://x.com/Teknium/status/2035708510034641202
* chore: attribution + catalog rows for adversarial-ux-test
- AUTHOR_MAP: omni@comelse.com -> omnissiah-comelse
- skills-catalog.md: add adversarial-ux-test row under dogfood
- optional-skills-catalog.md: add new Dogfood section
* fix(auth): unify credential source removal — every source sticks (#13427)
Every credential source Hermes reads from now behaves identically on
`hermes auth remove`: the pool entry stays gone across fresh load_pool()
calls, even when the underlying external state (env var, OAuth file,
auth.json block, config entry) is still present.
Before this, auth_remove_command was a 110-line if/elif with five
special cases, and three more sources (qwen-cli, copilot, custom
config) had no removal handler at all — their pool entries silently
resurrected on the next invocation. Even the handled cases diverged:
codex suppressed, anthropic deleted-without-suppressing, nous cleared
without suppressing. Each new provider added a new gap.
What's new:
agent/credential_sources.py — RemovalStep registry, one entry per
source (env, claude_code, hermes_pkce, nous device_code, codex
device_code, qwen-cli, copilot gh_cli + env vars, custom config).
auth_remove_command dispatches uniformly via find_removal_step().
Changes elsewhere:
agent/credential_pool.py — every upsert in _seed_from_env,
_seed_from_singletons, and _seed_custom_pool now gates on
is_source_suppressed(provider, source) via a shared helper.
hermes_cli/auth_commands.py — auth_remove_command reduced to 25
lines of dispatch; auth_add_command now clears ALL suppressions for
the provider on re-add (was env:* only).
Copilot is special: the same token is seeded twice (gh_cli via
_seed_from_singletons + env:<VAR> via _seed_from_env), so removing one
entry without suppressing the other variants lets the duplicate
resurrect. The copilot RemovalStep suppresses gh_cli + all three env
variants (COPILOT_GITHUB_TOKEN, GH_TOKEN, GITHUB_TOKEN) at once.
Tests: 11 new unit tests + 4059 existing pass. 12 E2E scenarios cover
every source in isolated HERMES_HOME with simulated fresh processes.
* feat(account-usage): add per-provider account limits module
Ports agent/account_usage.py and its tests from the original PR #2486
branch. Defines AccountUsageSnapshot / AccountUsageWindow dataclasses,
a shared renderer, and provider-specific fetchers for OpenAI Codex
(wham/usage), Anthropic OAuth (oauth/usage), and OpenRouter (/credits
and /key). Wiring into /usage lands in a follow-up salvage commit.
Authored-by: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com>
* feat(/usage): append account limits section in CLI and gateway
Wires the agent/account_usage module from the preceding commit into
/usage so users see provider-side quota/credit info alongside the
existing session token report.
CLI:
- `_show_usage` appends account lines under the token table. Fetch
runs in a 1-worker ThreadPoolExecutor with a 10s timeout so a slow
provider API can never hang the prompt.
Gateway:
- `_handle_usage_command` resolves provider from the live agent when
available, else from the persisted billing_provider/billing_base_url
on the SessionDB row, so /usage still returns account info between
turns when no agent is resident. Fetch runs via asyncio.to_thread.
- Account section is appended to all three return branches: running
agent, no-agent-with-history, and the new no-agent-no-history path
(falls back to account-only output instead of "no data").
Tests:
- 2 new tests in tests/gateway/test_usage_command.py cover the live-
agent account section and the persisted-billing fallback path.
Salvaged from PR #2486 by @kshitijk4poor. The original branch had
drifted ~2615 commits behind main and rewrote _show_usage wholesale,
which would have dropped the rate-limit and cached-agent blocks added
in PRs #6541 and #7038. This commit re-adds only the new behavior on
top of current main.
* feat(opencode-go): add Kimi K2.6 and Qwen3.5/3.6 Plus to curated catalog (#13429)
OpenCode Go's published model list (opencode.ai/docs/go) includes kimi-k2.6,
qwen3.5-plus, and qwen3.6-plus, but Hermes' curated lists didn't carry them.
When the live /models probe fails during `hermes model`, users fell back to
the stale curated list and had to type newer models via 'Enter custom model
name'.
Adds kimi-k2.6 (now first in the Go list), qwen3.6-plus, and qwen3.5-plus
to both the model picker (hermes_cli/models.py) and setup defaults
(hermes_cli/setup.py). All routed through the existing opencode-go
chat_completions path — no api_mode changes needed.
* feat(patch): add 'did you mean?' feedback when patch fails to match
When patch_replace() cannot find old_string in a file, the error message
now includes the closest matching lines from the file with line numbers
and context. This helps the LLM self-correct without a separate read_file
call.
Implements Phase 1 of #536: enhanced patch error feedback with no
architectural changes.
- tools/fuzzy_match.py: new find_closest_lines() using SequenceMatcher
- tools/file_operations.py: attach closest-lines hint to patch errors
- tests/tools/test_fuzzy_match.py: 5 new tests for find_closest_lines
* fix(patch): gate 'did you mean?' to no-match + extend to v4a/skill_manage
Follow-ups on top of @teyrebaz33's cherry-picked commit:
1. New shared helper format_no_match_hint() in fuzzy_match.py with a
startswith('Could not find') gate so the snippet only appends to
genuine no-match errors — not to 'Found N matches' (ambiguous),
'Escape-drift detected', or 'identical strings' errors, which would
all mislead the model.
2. file_tools.patch_tool suppresses the legacy generic '[Hint: old_string
not found...]' string when the rich 'Did you mean?' snippet is
already attached — no more double-hint.
3. Wire the same helper into patch_parser.py (V4A patch mode, both
_validate_operations and _apply_update) and skill_manager_tool.py so
all three fuzzy callers surface the hint consistently.
Tests: 7 new gating tests in TestFormatNoMatchHint cover every error
class (ambiguous, drift, identical, non-zero match count, None error,
no similar content, happy path). 34/34 test_fuzzy_match, 96/96
test_file_tools + test_patch_parser + test_skill_manager_tool pass.
E2E verified across all four scenarios: no-match-with-similar,
no-match-no-similar, ambiguous, success. V4A mode confirmed
end-to-end with a non-matching hunk.
* Normalize FAL_KEY env handling (ignore whitespace-only values)
Treat whitespace-only FAL_KEY the same as unset so users who export
FAL_KEY=" " (or CI that leaves a blank token) get the expected
'not set' error path instead of a confusing downstream fal_client
failure.
Applied to the two direct FAL_KEY checks in image_generation_tool.py:
image_generate_tool's upfront credential check and check_fal_api_key().
Both keep the existing managed-gateway fallback intact.
Adapted the original whitespace/valid tests to pin the managed gateway
to None so the whitespace assertion exercises the direct-key path
rather than silently relying on gateway absence.
* fix(fal): extend whitespace-only FAL_KEY handling to all call sites
Follow-up to PR #2504. The original fix covered the two direct FAL_KEY
checks in image_generation_tool but left four other call sites intact,
including the managed-gateway gate where a whitespace-only FAL_KEY
falsely claimed 'user has direct FAL' and *skipped* the Nous managed
gateway fallback entirely.
Introduce fal_key_is_configured() in tools/tool_backend_helpers.py as a
single source of truth (consults os.environ, falls back to .env for
CLI-setup paths) and route every FAL_KEY presence check through it:
- tools/image_generation_tool.py : _resolve_managed_fal_gateway,
image_generate_tool's upfront check, check_fal_api_key
- hermes_cli/nous_subscription.py : direct_fal detection, selected
toolset gating, tools_ready map
- hermes_cli/tools_config.py : image_gen needs-setup check
Verified by extending tests/tools/test_image_generation_env.py and by
E2E exercising whitespace + managed-gateway composition directly.
* fix: slash commands now respect require_mention in Telegram groups
When require_mention is enabled, slash commands no longer bypass
mention checks. Bare /command without @mention is filtered in groups,
while /command@botname (bot menu) and @botname /command still pass.
Commands still pass unconditionally when require_mention is disabled,
preserving backward compatibility.
Closes #6033
* test(telegram): update /cmd@botname assertion for entity-only detection
Current main's _message_mentions_bot() uses MessageEntity-only detection
(commit e330112a), so the test for '/status@hermes_bot' needs to include
a MENTION entity. Real Telegram always emits one for /cmd@botname — the
bot menu and CommandHandler rely on this mechanism.
* chore(release): add pinion05 to AUTHOR_MAP
* docs(xurl skill): document UsernameNotFound workaround (xurl v1.1.0) (#13458)
xurl v1.1.0 added an optional USERNAME positional to `xurl auth oauth2`
that skips the `/2/users/me` lookup, which has been returning 403/UsernameNotFound
for many devs. Documents the workaround in both setup (step 5) and
troubleshooting.
Reported by @itechnologynet.
* refactor(acp): validate method_id against advertised provider in authenticate() (#13468)
* feat(models): hide OpenRouter models that don't advertise tool support
Port from Kilo-Org/kilocode#9068.
hermes-agent is tool-calling-first — every provider path assumes the
model can invoke tools. Models whose OpenRouter supported_parameters
doesn't include 'tools' (e.g. image-only or completion-only models)
cannot be driven by the agent loop and fail at the first tool call.
Filter them out of fetch_openrouter_models() so they never appear in
the model picker (`hermes model`, setup wizard, /model slash command).
Permissive when the field is missing — OpenRouter-compatible gateways
(Nous Portal, private mirrors, older snapshots) don't always populate
supported_parameters. Treat missing as 'unknown → allow' rather than
silently emptying the picker on those gateways. Only hide models
whose supported_parameters is an explicit list that omits tools.
Tests cover: tools present → kept, tools absent → dropped, field
missing → kept, malformed non-list → kept, non-dict item → kept,
empty list → dropped.
* refactor(acp): validate method_id against advertised provider in authenticate()
Previously authenticate() accepted any method_id whenever the server had
provider credentials configured. This was not a vulnerability under the
personal-assistant trust model (ACP is stdio-only, local-trust — anything
that can reach the transport is already code-execution-equivalent to the
user), but it was sloppy API hygiene: the advertised auth_methods list
from initialize() was effectively ignored.
Now authenticate() only returns AuthenticateResponse when method_id
matches the currently-advertised provider (case-insensitive). Mismatched
or missing method_id returns None, consistent with the no-credentials
case.
Raised by xeloxa via GHSA-g5pf-8w9m-h72x. Declined as a CVE
(ACP transport is stdio, local-trust model), but the correctness fix is
worth having on its own.
* test(mcp): add failing tests for circuit-breaker recovery
The MCP circuit breaker in tools/mcp_tool.py has no half-open state and
no reset-on-reconnect behavior, so once it trips after 3 consecutive
failures it stays tripped for the process lifetime. These tests lock
in the intended recovery behavior:
1. test_circuit_breaker_half_opens_after_cooldown — after the cooldown
elapses, the next call must actually probe the session; success
closes the breaker.
2. test_circuit_breaker_reopens_on_probe_failure — a failed probe
re-arms the cooldown instead of letting every subsequent call
through.
3. test_circuit_breaker_cleared_on_reconnect — a successful OAuth
recovery resets the breaker even if the post-reconnect retry
fails (a successful reconnect is sufficient evidence the server
is viable again).
All three currently fail, as expected.
* fix(mcp): add half-open state to circuit breaker
The MCP circuit breaker previously had no path back to the closed
state: once _server_error_counts[srv] reached _CIRCUIT_BREAKER_THRESHOLD
the gate short-circuited every subsequent call, so the only reset
path (on successful call) was unreachable. A single transient
3-failure blip (bad network, server restart, expired token) permanently
disabled every tool on that MCP server for the rest of the agent
session.
Introduce a classic closed/open/half-open state machine:
- Track a per-server breaker-open timestamp in _server_breaker_opened_at
alongside the existing failure count.
- Add _CIRCUIT_BREAKER_COOLDOWN_SEC (60s). Once the count reaches
threshold, calls short-circuit for the cooldown window.
- After the cooldown elapses, the *next* call falls through as a
half-open probe that actually hits the session. Success resets the
breaker via _reset_server_error; failure re-bumps the count via
_bump_server_error, which re-stamps the open timestamp and re-arms
the cooldown.
The error message now includes the live failure count and an
"Auto-retry available in ~Ns" hint so the model knows the breaker
will self-heal rather than giving up on the tool for the whole
session.
Covers tests 1 (half-opens after cooldown) and 2 (reopens on probe
failure); test 3 (cleared on reconnect) still fails pending fix #2.
* fix(mcp): reset circuit breaker on successful OAuth reconnect
Previously the breaker was only cleared when the post-reconnect retry
call itself succeeded (via _reset_server_error at the end of the try
block). If OAuth recovery succeeded but the retry call happened to
fail for a different reason, control fell through to the
needs_reauth path which called _bump_server_error — adding to an
already-tripped count instead of the fresh count the reconnect
justified. With fix #1 in place this would still self-heal on the
next cooldown, but we should not pay a 60s stall when we already
have positive evidence the server is viable.
Move _reset_server_error(server_name) up to immediately after the
reconnect-and-ready-wait block, before the retry_call. The
subsequent retry still goes through _bump_server_error on failure,
so a genuinely broken server re-trips the breaker as normal — but
the retry starts from a clean count (1 after a failure), not a
stale one.
* fix(/model): accept provider switches when /models is unreachable
Gateway /model <name> --provider opencode-go (or any provider whose /models
endpoint is down, 404s, or doesn't exist) silently failed. validate_requested_model
returned accepted=False whenever fetch_api_models returned None, switch_model
returned success=False, and the gateway never wrote _session_model_overrides —
so the switch appeared to succeed in the error message flow but the next turn
kept calling the old provider.
The validator already had static-catalog fallbacks for MiniMax and Codex
(providers without a /models endpoint). Extended the same pattern as the
terminal fallback: when the live probe fails, consult provider_model_ids()
for the curated catalog. Known models → accepted+recognized. Close typos →
auto-corrected. Unknown models → soft-accepted with a 'Not in curated
catalog' warning. Providers with no catalog at all → soft-accepted with a
generic 'Note:' warning, finally honoring the in-code comment ('Accept and
persist, but warn') that had been lying since it was written.
Tests: 7 new tests in test_opencode_go_validation_fallback.py covering the
catalog lookup, case-insensitive match, auto-correct, unknown-with-suggestion,
unknown-without-suggestion, and no-catalog paths. TestValidateApiFallback in
test_model_validation.py updated — its four 'rejected_when_api_down' tests
were encoding exactly the bug being fixed.
* fix(kimi): send max_tokens, reasoning_effort, and thinking for Kimi/Moonshot
Kimi/Moonshot endpoints require explicit parameters that Hermes was not
sending, causing 'Response truncated due to output length limit' errors
and inconsistent reasoning behavior.
Root cause analysis against Kimi CLI source (MoonshotAI/kimi-cli,
packages/kosong/src/kosong/chat_provider/kimi.py):
1. max_tokens: Kimi's API defaults to a very low value when omitted.
Reasoning tokens share the output budget — the model exhausts it on
thinking alone. Send 32000, matching Kimi CLI's generate() default.
2. reasoning_effort: Kimi CLI sends this as a top-level parameter (not
inside extra_body). Hermes was not sending it at all because
_supports_reasoning_extra_body() returns False for non-OpenRouter
endpoints.
3. extra_body.thinking: Kimi CLI uses with_thinking() which sets
extra_body.thinking={"type":"enabled"} alongside reasoning_effort.
This is a separate control from the OpenAI-style reasoning extra_body
that Hermes sends for OpenRouter/GitHub. Without it, the Kimi gateway
may not activate reasoning mode correctly.
Covers api.kimi.com (Kimi Code) and api.moonshot.ai/cn (Moonshot).
Tests: 6 new test cases for max_tokens, reasoning_effort, and
extra_body.thinking under various configs.
* chore(release): add mengjian-github to AUTHOR_MAP
* fix(skills): respect HERMES_SESSION_PLATFORM in _is_skill_disabled
Fixes #13027
Previously, `_is_skill_disabled()` only checked the explicit `platform`
argument and `os.getenv('HERMES_PLATFORM')`, missing the gateway session
context (`HERMES_SESSION_PLATFORM`). This caused `skill_view()` to expose
skills that were platform-disabled for the active gateway session.
Add `_get_session_platform()` helper that resolves the platform from
`gateway.session_context.get_session_env`, mirroring the logic in
`agent.skill_utils.get_disabled_skill_names()`.
Now the platform resolution follows the same precedence as skill_utils:
1. Explicit `platform` argument
2. `HERMES_PLATFORM` environment variable
3. `HERMES_SESSION_PLATFORM` from gateway session context
* chore: register VTRiot in AUTHOR_MAP
* fix(cron): cancel orphan coroutine on delivery timeout before standalone fallback
When the live adapter delivery path (_deliver_result) or media send path
(_send_media_via_adapter) times out at future.result(timeout=N), the
underlying coroutine scheduled via asyncio.run_coroutine_threadsafe can
still complete on the event loop, causing a duplicate send after the
standalone fallback runs.
Cancel the future on TimeoutError before re-raising, so the standalone
fallback is the sole delivery path.
Adds TestDeliverResultTimeoutCancelsFuture and
TestSendMediaTimeoutCancelsFuture.
* test(cron): exercise _deliver_result and _send_media_via_adapter directly for timeout-cancel
The original tests replicated the try/except/cancel/raise pattern inline with
a mocked future, which tested Python's try/except semantics rather than the
scheduler's behavior. Rewrite them to invoke _deliver_result and
_send_media_via_adapter end-to-end with a real concurrent.futures.Future
whose .result() raises TimeoutError.
Mutation-verified: both tests fail when the try/except wrappers are removed
from cron/scheduler.py, pass with them in place.
* chore: remove stale requirements.txt in favor of pyproject.toml (#13515)
The root requirements.txt has drifted from pyproject.toml for years
(unpinned, missing deps like slack-bolt, slack-sdk, exa-py, anthropic)
and no part of the codebase (CI, Dockerfiles, scripts, docs) consumes
it. It exists only for drive-by 'pip install -r requirements.txt'
users and will drift again within weeks of any sync.
Canonical install remains:
pip install -e ".[all]"
Closes #13488 (thanks @hobostay — your sync was correct, we're just
deleting the drift trap instead of patching it).
* fix(agent): normalize socks:// env proxies for httpx/anthropic
WSL2 / Clash-style setups often export ALL_PROXY=socks://127.0.0.1:PORT. httpx and the Anthropic SDK reject that alias and expect socks5://, so agent startup failed early with "Unknown scheme for proxy URL" before any provider request could proceed.
Add shared normalize_proxy_url()/normalize_proxy_env_vars() helpers in utils.py and route all proxy entry points through them:
- run_agent._get_proxy_from_env
- agent.auxiliary_client._validate_proxy_env_urls
- agent.anthropic_adapter.build_anthropic_client
- gateway.platforms.base.resolve_proxy_url
Regression coverage:
- run_agent proxy env resolution
- auxiliary proxy env normalization
- gateway proxy URL resolution
Verified with:
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 /home/nonlinear/.hermes/hermes-agent/venv/bin/pytest -o addopts='' -p pytest_asyncio.plugin tests/run_agent/test_create_openai_client_proxy_env.py tests/agent/test_proxy_and_url_validation.py tests/gateway/test_proxy_mode.py
39 passed.
* chore(release): add UNLINEARITY to AUTHOR_MAP
* fix(permissions): handle None response from ACP request_permission
* fix: support pagination and cwd filtering in list_sessions
* fix(acp): follow-up — named-const page size, alias kwarg, tests
- Replace kwargs.get('limit', 50) with module-level _LIST_SESSIONS_PAGE_SIZE
constant. ListSessionsRequest schema has no 'limit' field, so the kwarg
path was dead. Constant is the single source of truth for the page cap.
- Use next_cursor= (field name) instead of nextCursor= (alias). Both work
under the schema's populate_by_name config, but using the declared
Python field name is the consistent style in this file.
- Add docstring explaining cwd pass-through and cursor semantics.
- Add 4 tests: first-page with next_cursor, single-page no next_cursor,
cursor resumes after match, unknown cursor returns empty page.
* feat: add buttons to update hermes and restart gateway
* security(runtime_provider): close OLLAMA_API_KEY substring-leak sweep miss (#13522)
Two call sites still used a raw substring check to identify ollama.com:
hermes_cli/runtime_provider.py:496:
_is_ollama_url = "ollama.com" in base_url.lower()
run_agent.py:6127:
if fb_base_url_hint and "ollama.com" in fb_base_url_hint.lower() ...
Same bug class as GHSA-xf8p-v2cg-h7h5 (OpenRouter substring leak), which
was fixed in commit dbb7e00e via base_url_host_matches() across the
codebase. The earlier sweep missed these two Ollama sites. Self-discovered
during April 2026 security-advisory triage; filed as GHSA-76xc-57q6-vm5m.
Impact is narrow — requires a user with OLLAMA_API_KEY configured AND a
custom base_url whose path or look-alike host contains 'ollama.com'.
Users on default provider flows are unaffected. Filed as a draft advisory
to use the private-fork flow; not CVE-worthy on its own.
Fix is mechanical: replace substring check with base_url_host_matches
at both sites. Same helper the rest of the codebase uses.
Tests: 67 -> 71 passing. 7 new host-matcher cases in
tests/test_base_url_hostname.py (path injection, lookalike host,
localtest.me subdomain, ollama.ai TLD confusion, localhost, genuine
ollama.com, api.ollama.com subdomain) + 4 call-site tests in
tests/hermes_cli/test_runtime_provider_resolution.py verifying
OLLAMA_API_KEY is selected only when base_url actually targets
ollama.com.
Fixes GHSA-76xc-57q6-vm5m
* fix(env_passthrough): reject Hermes provider credentials from skill passthrough (#13523)
A skill declaring `required_environment_variables: [ANTHROPIC_TOKEN]` in
its SKILL.md frontmatter silently bypassed the `execute_code` sandbox's
credential-scrubbing guarantee. `register_env_passthrough` had no
blocklist, so any name a skill chose flipped `is_env_passthrough(name) =>
True`, which shortcircuits the sandbox's secret filter.
Fix: reject registration when the name appears in
`_HERMES_PROVIDER_ENV_BLOCKLIST` (the canonical list of Hermes-managed
credentials — provider keys, gateway tokens, etc.). Log a warning naming
GHSA-rhgp-j443-p4rf so operators see the rejection in logs.
Non-Hermes third-party API keys (TENOR_API_KEY for gif-search,
NOTION_TOKEN for notion skills, etc.) remain legitimately registerable —
they were never in the sandbox scrub list in the first place.
Tests: 16 -> 17 passing. Two old tests that documented the bypass
(`test_passthrough_allows_blocklisted_var`, `test_make_run_env_passthrough`)
are rewritten to assert the new fail-closed behavior. New
`test_non_hermes_api_key_still_registerable` locks in that legitimate
third-party keys are unaffected.
Reported in GHSA-rhgp-j443-p4rf by @q1uf3ng. Hardening; not CVE-worthy
on its own per the decision matrix (attacker must already have operator
consent to install a malicious skill).
* fix(acp): wire approval callback + make it thread-local (#13525)
Two related ACP approval issues:
GHSA-96vc-wcxf-jjff — ACP's _run_agent never set HERMES_INTERACTIVE
(or any other flag recognized by tools.approval), so check_all_command_guards
took the non-interactive auto-approve path and never consulted the
ACP-supplied ap…
TFITZ57
added a commit
to TFITZ57/hermes-agent
that referenced
this pull request
Apr 23, 2026
* fix(tui): raise picker selection contrast with inverse + bold
Selected rows in the model/session/skills pickers and approval/clarify
prompts only changed from dim gray to cornsilk, which reads as low
contrast on lighter themes and LCDs (reported during TUI v2 blitz).
Switch the selected row to `inverse bold` with the brand accent color
across modelPicker, sessionPicker, skillsHub, and prompts so the
highlight is terminal-portable and unambiguous. Unselected rows stay
dim. Also extends the sessionPicker middle meta column (which was
always dim) to inherit the row's selection state.
* fix(model-switch): drop stale provider from fallback chain and env after /model
Reported during the TUI v2 blitz test: switching from openrouter to
anthropic via `/model <name> --provider anthropic` appeared to succeed,
but the next turn kept hitting openrouter — the provider the user was
deliberately moving away from.
Two gaps caused this:
1. `Agent.switch_model` reset `_fallback_activated` / `_fallback_index`
but left `_fallback_chain` intact. The chain was seeded from
`fallback_providers:` at agent init for the *original* primary, so
when the new primary returned 401 (invalid/expired Anthropic key),
`_try_activate_fallback()` picked the old provider back up without
informing the user. Prune entries matching either the old primary
(user is moving away) or the new primary (redundant) whenever the
primary provider actually changes.
2. `_apply_model_switch` persisted `HERMES_MODEL` but never updated
`HERMES_INFERENCE_PROVIDER`. Any ambient re-resolution of the runtime
(credential pool refresh, compressor rebuild, aux clients) falls
through to that env var in `resolve_requested_provider`, so it kept
reporting the original provider even after an in-memory switch.
Adds three regression tests: fallback-chain prune on primary change,
no-op on same-provider model swap, and env-var sync on explicit switch.
* fix(tui): @folder: only yields directories, @file: only yields files
Reported during TUI v2 blitz testing: typing `@folder:` in the composer
pulled up .dockerignore, .env, .gitignore, and every other file in the
cwd alongside the actual directories. The completion loop yielded every
entry regardless of the explicit prefix and auto-rewrote each completion
to @file: vs @folder: based on is_dir — defeating the user's choice.
Also fixed a pre-existing adjacent bug: a bare `@file:` or `@folder:`
(no path) used expanded=="." as both search_dir AND match_prefix,
filtering the list to dotfiles only. When expanded is empty or ".",
search in cwd with no prefix filter.
- want_dir = prefix == "@folder:" drives an explicit is_dir filter
- preserve the typed prefix in completion text instead of rewriting
- three regression tests cover: folder-only, file-only, and the bare-
prefix case where completions keep the `@folder:` prefix
* fix(tui): truncate long picker rows so the height stays stable
A6 added a fixed-height grid (Array.from({length: VISIBLE})), but the
row <Text> itself had no wrap prop so Ink defaulted to wrap="wrap".
A sufficiently long model or provider name would wrap to a second
visual line and bounce the overall picker height right back — which
is exactly what reappeared during the TUI v2 blitz retest on /model.
Pin every picker row (and the empty-state / padding rows) to
wrap="truncate-end" so each slot is guaranteed one line. Applies
across modelPicker, sessionPicker, and skillsHub.
* fix(tui): stabilize slash-completion dropdown height
The completion popup (e.g. typing `/model`) grew from 8 rows at
compIdx=0 up to 16 rows at compIdx≥8 — the slice end was `compIdx + 8`
so every arrow-down added another rendered row until the window filled.
Reported during TUI v2 retest: "as i scroll and more options appear,
for some reason more options appear and it expands the height".
Fixed viewport (`COMPLETION_WINDOW = 16`) centered on compIdx, clamped
so it never slides past the array bounds. Renders exactly
`min(WINDOW, completions.length)` rows every frame.
* fix(tui): pager supports scrolling (up/down/page/top/bottom)
The pager overlay backing /history, /toolsets, /help and any paged slash
output only advanced with Enter/Space and closed at the end. Could not
scroll back, scroll line-by-line, or jump to endpoints.
Adds Up/Down (↑↓, j/k), PgUp (b), g/G for top/bottom, keeps existing
Enter/Space/PgDn forward-and-auto-close, and clamps offset so
over-scrolling past the last page is a no-op.
* fix(tui): preserve prior segment output on Ctrl+C interrupt
interruptTurn only flushed the in-flight streaming chunk (bufRef) to
the transcript before calling idle(), which wiped segmentMessages and
pendingSegmentTools. Every tool call and commentary line the agent had
already emitted in the current turn disappeared the moment the user
cancelled, even though that output is exactly what they want to keep
when they hit Ctrl+C (quote from the blitz feedback: "everything was
fine up until the point where you wanted to push to main").
Append each flushed segment message to the transcript first, then
render the in-flight partial with the `*[interrupted]*` marker and its
pendingSegmentTools. Sys-level "interrupted" note still fires when
there is nothing to preserve.
* fix(tui): route skills.manage through the long-handler thread pool
`/skills browse` is documented to scan 6 sources and take ~15s, but the
gateway dispatched `skills.manage` on the main RPC thread. While it
ran, every other inbound RPC — completions, new slash commands, even
`approval.respond` — blocked until the HTTP fetches finished, making
the whole TUI feel frozen. Reported during TUI v2 retest:
"/skills browse blocks everything else".
`_LONG_HANDLERS` already exists precisely for this pattern (slash.exec,
shell.exec, session.resume, etc. run on `_pool`). Add `skills.manage`
to that set so browse/search/install run off the dispatcher; the fast
`list` / `inspect` actions pay a negligible thread-pool hop.
* improve llama.cpp skill
* fix(skills/llama-cpp): concise description, restore python bindings, fix curl
- Description truncated to 60 chars in system prompt (extract_skill_description),
so the 500-char HF workflow description never reached the agent; shortened to
'llama.cpp local GGUF inference + HF Hub model discovery.' (56 chars).
- Restore llama-cpp-python section (basic, chat+stream, embeddings,
Llama.from_pretrained) and frontmatter dependencies entry.
- Fix broken 'Authorization: Bearer ***' curl line (missing closing quote;
llama-server doesn't require auth by default).
* fix(gateway): always inject reply-to pointer, not just when quoted text is absent (#13676)
The [Replying to: "..."] prefix is disambiguation, not deduplication. When
a user explicitly replies to a prior message, the agent needs a pointer to
which specific message they're referencing — even when the quoted text
already exists somewhere in history. History can contain the same or
similar text multiple times; without an explicit pointer the agent has to
guess (or answer for both subjects), and the reply signal is silently
dropped.
Example: in a conversation comparing Japan and Italy, replying to the
"Japan is great for culture..." message and asking "What's the best time
to go?" — previously the found_in_history check suppressed the prefix
because the quoted text was already in history, leaving the agent to
guess which destination the user meant. Now the pointer is always present.
Drops the found_in_history guard added in #1594. Token overhead is
minimal (snippet capped at 500 chars on the new user turn; cached prefix
unaffected). Behavior becomes deterministic: reply sent ⇒ pointer present.
Thanks to smartyi for flagging this.
* feat(image-gen): add GPT Image 2 to FAL catalog (#13677)
Adds OpenAI's new GPT Image 2 model via FAL.ai, selectable through
`hermes tools` → Image Generation. SOTA text rendering (including CJK)
and world-aware photorealism.
- FAL_MODELS entry with image_size_preset style
- 4:3 presets on all aspect ratios — 16:9 (1024x576) falls below
GPT-Image-2's 655,360 min-pixel floor and would be rejected
- quality pinned to medium (same rule as gpt-image-1.5) for
predictable Nous Portal billing
- BYOK (openai_api_key) deliberately omitted from supports so all
users stay on shared FAL billing
- 6 new tests covering preset mapping, quality pinning, and
supports-whitelist integrity
- Docs table + aspect-ratio map updated
Live-tested end-to-end: 39.9s cold request, clean 1024x768 PNG
* refactor(delegate): drop dead default_toolsets from CLI default config
delegation.default_toolsets was declared in cli.py's CLI_CONFIG default
dict and documented in cli-config.yaml.example, but never read: none of
tools/delegate_tool.py, _load_config(), or any call site ever looked it
up. The live fallback is the DEFAULT_TOOLSETS module constant at
tools/delegate_tool.py:101, which stays as-is.
hermes_cli/config.py's DEFAULT_CONFIG["delegation"] already omits the
key — this commit aligns cli.py with that.
Adds a regression test in tests/hermes_cli/test_config_drift.py so a
future refactor that re-adds the key without wiring it up to
_load_config() fails loudly.
Part of Initiative 2 / M0.5.
* docs(delegate): remove default_toolsets from example config and docs
Matches the default-config removal in the preceding commit.
default_toolsets was documented for users to set but was never actually
read at runtime, so showing it in the example config and the delegation
user guide was misleading.
No deprecation note is added: the key was always a no-op, so users who
copied it from the example continue to see no behavior change. Their
config.yaml still parses; the key is just silently unused, same as
before.
Part of Initiative 2 / M0.5.
* test(delegate): make default_toolsets regression test robust to user config
The prior form of this test asserted on CLI_CONFIG["delegation"] after
importing cli, which only passed by accident of pytest-xdist worker
scheduling. cli._hermes_home is frozen at module import time (cli.py:76),
before the tests/conftest.py autouse HERMES_HOME-isolation fixture can
fire, so CLI_CONFIG ends up populated by deep-merging the contributor's
actual ~/.hermes/config.yaml over the defaults (cli.py:359-366). Any
contributor (like me) who still has the legacy key set in their own
config causes a false failure the moment another test file in the same
xdist worker imports cli at module level.
Asserting on the source of load_cli_config() instead sidesteps all of
that: the test now checks the defaults literal directly and is
independent of user config, HERMES_HOME, import order, and worker
scheduling.
Demonstrated failure mode before this fix:
pytest tests/hermes_cli/test_config_drift.py \
tests/hermes_cli/test_skills_hub.py -o addopts=""
-> FAILED (CLI_CONFIG["delegation"] contained "default_toolsets"
from the user's ~/.hermes/config.yaml)
Part of Initiative 2 / M0.5.
* feat(gateway): recognize .pdf in MEDIA: tag extraction (#13683)
PDFs emitted by tools (report generators, document exporters, etc.) now
deliver as native attachments when wrapped in MEDIA: — same as images,
audio, and video.
Bare .pdf paths are intentionally NOT added to extract_local_files(), so
the agent can still reference PDFs in text without auto-sending them.
* fix(tui): inject VS16 so text-default emoji render as color glyphs
Models frequently emit bare codepoints like U+26A0 (⚠), U+2139 (ℹ),
U+2764 (❤), U+2714 (✔), U+2600 (☀), U+263A (☺) which, per Unicode, have
Emoji_Presentation=No and render as monochrome text-style glyphs in
terminals unless followed by VS16 (U+FE0F). Agent output leaked through
the TUI like `⚠ careful` instead of `⚠️ careful`.
Added `ensureEmojiPresentation` (lib/emoji.ts): scans for the curated
set of text-default codepoints and appends VS16 when the next char is
not already VS16, ZWJ, or a keycap-enclosing mark. Idempotent and
fast-pathed by a Unicode-range regex so ASCII-heavy text is untouched.
Applied once at the top of `Md`'s line parse. Hermes-ink's stringWidth
already accounts for VS16, so cursor/layout stays correct.
* feat(delegate): orchestrator role and configurable spawn depth (default flat)
Adds role='leaf'|'orchestrator' to delegate_task. With max_spawn_depth>=2,
an orchestrator child retains the 'delegation' toolset and can spawn its
own workers; leaf children cannot delegate further (identical to today).
Default posture is flat — max_spawn_depth=1 means a depth-0 parent's
children land at the depth-1 floor and orchestrator role silently
degrades to leaf. Users opt into nested delegation by raising
max_spawn_depth to 2 or 3 in config.yaml.
Also threads acp_command/acp_args through the main agent loop's delegate
dispatch (previously silently dropped in the schema) via a new
_dispatch_delegate_task helper, and adds a DelegateEvent enum with
legacy-string back-compat for gateway/ACP/CLI progress consumers.
Config (hermes_cli/config.py defaults):
delegation.max_concurrent_children: 3 # floor-only, no upper cap
delegation.max_spawn_depth: 1 # 1=flat (default), 2-3 unlock nested
delegation.orchestrator_enabled: true # global kill switch
Salvaged from @pefontana's PR #11215. Overrides vs. the original PR:
concurrency stays at 3 (PR bumped to 5 + cap 8 — we keep the floor only,
no hard ceiling); max_spawn_depth defaults to 1 (PR defaulted to 2 which
silently enabled one level of orchestration for every user).
Co-authored-by: pefontana <fontana.pedro93@gmail.com>
* fix(auxiliary): refresh Nous runtime credentials after aux 401s
* docs(delegate): clarify that the parent agent, not the user, populates goal/context (#13698)
The 'subagents know nothing' warning and the 'no conversation history'
constraint both said the user provides the goal/context fields. In
practice the LLM parent agent calls delegate_task; the user configures
the feature but doesn't write delegation calls. Rewording to point at
the parent agent matches how the tool actually works.
* fix(vision): resolve Nous vision model correctly in auto-detect path
Two changes:
1. _PROVIDER_VISION_MODELS: add 'nous' -> 'xiaomi/mimo-v2-omni' entry
so the vision auto-detect chain picks the correct multimodal model.
2. resolve_provider_client: detect when the requested model is a vision
model (from _PROVIDER_VISION_MODELS or known vision model names) and
pass vision=True to _try_nous(). Previously, _try_nous() was always
called without vision=True in resolve_provider_client(), causing it to
return the default text model (gemini-3-flash-preview or mimo-v2-pro)
instead of the vision-capable mimo-v2-omni.
The _try_nous() function already handled free-tier vision correctly, but
the resolve_provider_client() path (used by the auto-detect vision chain)
never signaled that a vision task was in progress.
Verified: xiaomi/mimo-v2-omni returns HTTP 200 with image inputs on Nous
inference API. google/gemini-3-flash-preview returns 404 with images.
* chore(release): add Ifkellx to AUTHOR_MAP for PR #12687
* fix(security): TUI approval overlay accepts blind keystrokes, CLI thread-local callback invisible to agent
Two bugs that allow dangerous commands to execute without informed user consent.
TUI (Ink): useInputHandlers consumes the isBlocked return path, but Ink's
EventEmitter delivers keystrokes to ALL registered useInput listeners. The
ApprovalPrompt component receives arrow keys, number keys, and Enter even
though the overlay appears frozen. The user sees no visual feedback, but
keystrokes are processed — allowing blind approval, session-wide auto-approve
(choice "session"), or permanent allowlist writes (choice "always") without
the user knowing.
Discovered while replicating #13618 (TUI approval overlay freezes terminal).
Fix: in useInputHandlers, when overlay.approval/clarify/confirm is active,
only intercept Ctrl+C. All other keys pass through. This makes the overlay
visually responsive so the user can see what they are selecting.
CLI (prompt_toolkit): _callback_tls in terminal_tool.py is threading.local().
set_approval_callback() is called in the main thread during run(), but the
agent executes in a background thread. _get_approval_callback() returns None
in the agent thread, falling back to stdin input() which prompt_toolkit
blocks. The user sees the approval text but cannot respond — the terminal is
unusable until the 60s timeout expires with a default "deny".
Fix: set callbacks inside run_agent() (the thread target), matching the
pattern already used by acp_adapter/server.py. Clear on thread exit to avoid
stale references.
Closes #13618
* test(approval): regression guards for thread-local callback contract
Two unit tests that pin down the threading.local semantics the CLI freeze
fix (#13617 / #13618) relies on:
- main-thread registration must be invisible to child threads (documents
the underlying bug — if this ever starts passing visible, ACP's
GHSA-qg5c-hvr5-hjgr race has returned)
- child-thread registration must be visible from that same thread AND
cleared by the finally block (documents the fix pattern used by
cli.py's run_agent closure and acp_adapter/server.py)
Pairs with the fix in the preceding commit by @Societus.
* fix(vision): route Nous main-provider vision through tier-aware backend
* fix(vision): restore tier-aware Nous vision model selection (#13703)
Revert two overreaches from #13699 that forced paid Nous vision to
xiaomi/mimo-v2-omni instead of the tier-appropriate gemini-3-flash-preview:
1. Remove "nous": "xiaomi/mimo-v2-omni" from _PROVIDER_VISION_MODELS —
#13696 already routes nous main-provider vision through the strict
backend, and this entry caused any direct resolve_provider_client(
"nous", ...) aggregator-lookup path to pick the wrong model for paid.
2. Drop the 'elif vision' paid override in _try_nous() that forced
mimo-v2-omni on every Nous vision call regardless of tier. Paid
accounts now keep gemini-3-flash-preview for vision as well as text.
Free-tier behavior unchanged: still uses mimo-v2-omni for vision,
mimo-v2-pro for text (check_nous_free_tier() branch).
E2E verified:
paid vision → google/gemini-3-flash-preview
free vision → xiaomi/mimo-v2-omni
paid text → google/gemini-3-flash-preview
free text → xiaomi/mimo-v2-pro
* feat(llm-wiki): port provenance markers, source hashing, and quality signals from llm-wiki-compiler (#13700)
Three additive conventions inspired by github.com/atomicmemory/llm-wiki-compiler:
- Paragraph-level provenance: `^[raw/articles/source.md]` markers on pages synthesizing 3+ sources, so readers can trace individual claims without re-reading full source files.
- Raw source content hashing: `sha256:` in raw/ frontmatter enables re-ingest drift detection — skip unchanged sources, flag changed ones.
- Optional `confidence` and `contested` frontmatter fields let lint surface weak or disputed claims without re-reading every page's prose.
Lint gains two new checks (quality signals, source drift) and one expanded check (contradictions now surfaces frontmatter-flagged pages).
Also adds a Related Tools section pointing users who want batch/scheduled compilation at llm-wiki-compiler (Obsidian-compatible, works on the same vault).
All additions are opt-in — existing wikis need no migration. Skill version 2.0.0 -> 2.1.0.
* fix(tui): don't swallow Kimi/Qwen ~! ~? kaomoji as subscript spans
The inline markdown regex had `~([^~\s][^~]*?)~` for Pandoc-style subscript
(H~2~O, CO~2~). On models that decorate prose with kaomoji like `thing ~!`
and `cool ~?` — Kimi especially — the opener `~!` paired with the next
stray `~` on the line and dim-formatted everything between them with a
leading `_` character, mangling markdown output.
Tighten the pattern to short alphanumeric-only content (`~[A-Za-z0-9]{1,8}~`)
since real subscript never contains punctuation, spaces, or long runs.
Same tightening applied to stripInlineMarkup so width measurement stays
consistent. Classic CLI was unaffected because it renders these literally.
* refactor(tui): clean markdown.tsx per KISS/DRY
- Drop the outer no-op capture group from INLINE_RE and restructure the
source as an ordered list of patterns-with-index-comments so each
alternative is individually greppable. Shift group indices in MdInline
down by one accordingly.
- Inline single-use helpers (parseFence, isFenceClose, isMarkdownFence,
trimBareUrl) and intermediate variables (path, lang, raw, prefix, body,
depth, task body, setext match, etc.).
- Hoist block-level regexes used inside MdImpl (FENCE_CLOSE_RE, SETEXT_RE,
BULLET_RE, TASK_RE, NUMBERED_RE, QUOTE_RE) to top-level consts so
they're compiled once instead of per-line.
- Collapse the duplicate compact-vs-normal blank-line branches into one
if/!compact gap call.
- Move Fence and MdProps types to the bottom per house style.
- Shorten splitTableRow → splitRow and use optional chaining in a few
match sites.
No behavior change; 162/162 tests pass. Net -22 LoC.
* fix(tui): /resume picker shows telegram/discord/etc sessions
Reported during TUI v2 blitz retest: /resume modal only surfaced tui/cli
rows, even though `hermes --tui --resume <id>` with a pasted telegram
session id works fine. The handler double-fetched with explicit
`source="tui"` and `source="cli"` filters and dropped everything else on
the floor.
Drop the filter — list_sessions_rich(source=None) already excludes
child sessions (subagents, compression continuations) via its default,
and users want to resume messenger sessions from inside the TUI.
Adds gateway regression coverage.
* fix(tui): up-arrow inside a multi-line buffer moves cursor, not history
Reported during TUI v2 blitz retest: typing a multi-line message with
shift-Enter and then pressing Up to edit an earlier line swapped the
whole buffer for the previous history entry instead of moving the
cursor up a line. Down then restored the draft → the buffer appeared
to "flip" between the draft and a prior prompt.
`useInputHandlers` cycles history on Up/Down, but textInput only
checked `inputBuf.length` — that only counts lines committed with a
trailing backslash, not shift-Enter newlines inside `input` itself.
Fix: detect logical lines inside the input string and move the cursor
one line up/down preserving column offset (clamp to line end when the
destination is shorter, standard editor behavior). Only fall through
to history cycling when the cursor is already on the first line (Up)
or last line (Down).
Adds unit coverage for the new `lineNav` helper.
* fix(tui): /history shows the TUI's own transcript, scrollable
Reported during TUI v2 blitz retest: `/history` in the TUI only shows
prompts from non-TUI Hermes runs and can't scroll the window. Root
cause is the slash-worker subprocess: it's a detached HermesCLI that
never sees the TUI's turns, so its `conversation_history` starts empty
and `show_history` surfaces whatever was persisted from earlier CLI
sessions — not what the user just did inside the TUI.
Intercept `/history` as a local slash command so it dumps
`ctx.local.getHistoryItems()` — the TUI's own transcript — routed
through the pager (which scrolls after #13591). Accepts an optional
preview-length argument (default 400 chars per message).
Adds createSlashHandler coverage.
* fix(tui): tool inline_diff renders inline with the active turn
Reported during TUI v2 blitz retest: code-review diffs from tool.complete
appeared at the top of the current interaction thread, out of sequence
with the agent's messages and tool rows below them.
Root cause — `sys(inline_diff)` appends to `historyItems`, which sits
above the `StreamingAssistant` pane that renders the active turn.
Until the turn closed, the diff visually floated above everything
else happening in the same turn.
Route the diff through `turnController.appendSegmentMessage` instead
so it flushes any pending streaming text first, then lands in the
segment stream beside assistant output and tool calls. On
`message.complete` the segment list is committed to history in emit
order (diff → final text), matching what the gateway sent.
Adds a regression test that exercises tool.complete → message.complete
with an inline_diff payload and asserts both the streaming and final
placement.
* feat(delegate): cross-agent file state coordination for concurrent subagents (#13718)
* feat(models): hide OpenRouter models that don't advertise tool support
Port from Kilo-Org/kilocode#9068.
hermes-agent is tool-calling-first — every provider path assumes the
model can invoke tools. Models whose OpenRouter supported_parameters
doesn't include 'tools' (e.g. image-only or completion-only models)
cannot be driven by the agent loop and fail at the first tool call.
Filter them out of fetch_openrouter_models() so they never appear in
the model picker (`hermes model`, setup wizard, /model slash command).
Permissive when the field is missing — OpenRouter-compatible gateways
(Nous Portal, private mirrors, older snapshots) don't always populate
supported_parameters. Treat missing as 'unknown → allow' rather than
silently emptying the picker on those gateways. Only hide models
whose supported_parameters is an explicit list that omits tools.
Tests cover: tools present → kept, tools absent → dropped, field
missing → kept, malformed non-list → kept, non-dict item → kept,
empty list → dropped.
* feat(delegate): cross-agent file state coordination for concurrent subagents
Prevents mangled edits when concurrent subagents touch the same file
(same process, same filesystem — the mangle scenario from #11215).
Three layers, all opt-out via HERMES_DISABLE_FILE_STATE_GUARD=1:
1. FileStateRegistry (tools/file_state.py) — process-wide singleton
tracking per-agent read stamps and the last writer globally.
check_stale() names the sibling subagent in the warning when a
non-owning agent wrote after this agent's last read.
2. Per-path threading.Lock wrapped around the read-modify-write
region in write_file_tool and patch_tool. Concurrent siblings on
the same path serialize; different paths stay fully parallel.
V4A multi-file patches lock in sorted path order (deadlock-free).
3. Delegate-completion reminder in tools/delegate_tool.py: after a
subagent returns, writes_since(parent, child_start, parent_reads)
appends '[NOTE: subagent modified files the parent previously
read — re-read before editing: ...]' to entry.summary when the
child touched anything the parent had already seen.
Complements (does not replace) the existing path-overlap check in
run_agent._should_parallelize_tool_batch — batch check prevents
same-file parallel dispatch within one agent's turn (cheap prevention,
zero API cost), registry catches cross-subagent and cross-turn
staleness at write time (detection).
Behavior is warning-only, not hard-failing — matches existing project
style. Errors surface naturally: sibling writes often invalidate the
old_string in patch operations, which already errors cleanly.
Tests: tests/tools/test_file_state_registry.py — 16 tests covering
registry state transitions, per-path locking, per-path-not-global
locking, writes_since filtering, kill switch, and end-to-end
integration through the real read_file/write_file/patch handlers.
* fix(tui): only cycle history at input boundaries on arrows
Follow-up on #13726 from blitz feedback: Up/Down history cycling should only trigger when the caret is at the start/end boundary (or the input is empty).\n\nPreviously useInputHandlers intercepted arrows whenever inputBuf was empty, which still stole Up/Down from normal multiline editing. textInput now publishes caret position through inputSelectionStore even with no active selection, and useInputHandlers gates history/queue cycling on those boundaries.
* fix(tui): keep inline diffs below tool rows and strip ANSI
Follow-up on #13729 from blitz screenshot feedback.\n\n- When tool.complete carried inline_diff but no buffered assistant text existed, pending tool rows were still in streamPendingTools, so diff rendered above the tool row section. appendSegmentMessage now emits pending tool rows as a trail segment before appending the diff artifact.\n- Strip ANSI color escapes from inline_diff payloads so we don't render loud red/green terminal palettes in the transcript.
* fix(tui): narrow /resume sources to human adapters
Follow-up on #13724: showing literally every source was too noisy.\n\n now fetches a wider window (, larger limit) and then filters to a curated allowlist of human-facing sources (tui/cli plus chat adapters like telegram/discord/slack/whatsapp/etc). This keeps row #7 fixed (telegram sessions visible in /resume) without surfacing internal source kinds such as tool/acp.
* fix(tui): arrow history fallback when no line exists
Follow-up on multiline arrow behavior: Up/Down now fall back to queue/history whenever there is no logical line above/below the caret (not only at absolute start/end character positions). This makes Up from the end of the top line cycle history, matching expected readline-ish behavior.
* fix(tui): render inline diffs inside assistant completion
Follow-up for #13729: segment-level system artifacts still looked detached in real flow.\n\nInstead of appending inline_diff as a standalone segment/system row, queue sanitized diffs during tool.complete and append them as a fenced diff block to the assistant completion text on message.complete. This keeps the diff in the same message flow as the assistant response.
* fix(tui): dedupe inline_diff when assistant already echoes it
Avoid duplicate diff rendering in #13729 flow. We now skip queued inline diffs that are already present in final assistant text and dedupe repeated queued diffs by exact content.
* fix(tui): keep review-diff tool rows terse
When tool.complete already carries inline_diff, the assistant message owns the full diff block. Suppress the tool-row summary/detail in that case so the turn shows one detailed diff surface instead of a rich diff plus a duplicated tool-detail payload.
* fix(tui): dedupe inline diffs, strip CLI review-diff header
After the prior inline-diff fix, the gateway still prepends a literal
" ┊ review diff" line to inline_diff (it's terminal chrome written by
`_emit_inline_diff`). Wrapping that in a ```diff fence left that header
inside the code block. The agent also often narrates its own edit in a
second fenced diff, so the assistant message ended up stacking two
diff blocks for the same change.
- Strip the leading "┊ review diff" header from queued inline diffs
before fencing.
- Skip appending the fenced diff entirely when the assistant already
wrote its own ```diff (or ```patch) fence.
Keeps the single-surface diff UX even when the agent is chatty.
* fix(tts): use per-provider input-character caps instead of global 4000 (#13743)
A single global MAX_TEXT_LENGTH = 4000 truncated every TTS provider at
4000 chars, causing long inputs to be silently chopped even though the
underlying APIs allow much more:
- OpenAI: 4096
- xAI: 15000
- MiniMax: 10000
- ElevenLabs: 5000 / 10000 / 30000 / 40000 (model-aware)
- Gemini: ~5000
- Edge: ~5000
The schema description also told the model 'Keep under 4000 characters',
which encouraged the agent to self-chunk long briefs into multiple TTS
calls (producing 3 separate audio files instead of one).
New behavior:
- PROVIDER_MAX_TEXT_LENGTH table + ELEVENLABS_MODEL_MAX_TEXT_LENGTH
encode the documented per-provider limits.
- _resolve_max_text_length(provider, cfg) resolves:
1. tts.<provider>.max_text_length user override
2. ElevenLabs model_id lookup
3. provider default
4. 4000 fallback
- text_to_speech_tool() and stream_tts_to_speaker() both call the
resolver; old MAX_TEXT_LENGTH alias kept for back-compat.
- Schema description no longer hardcodes 4000.
Tests: 27 new unit + E2E tests; all 53 existing TTS tests and 253
voice-command/voice-cli tests still pass.
* feat(skills): add baoyu-comic skill
* refactor(skills): adapt baoyu-comic for Hermes
Port the upstream baoyu-comic skill to Hermes' tool ecosystem, matching
the earlier baoyu-infographic adaptation:
- metadata namespace openclaw -> hermes (+ tags, homepage)
- drop EXTEND.md preferences system (references/config/ removed,
workflow Step 1.1 removed)
- user prompts via clarify (one question at a time) instead of
AskUserQuestion batches
- image generation via image_generate instead of baoyu-imagine, with
aspect-ratio mapping to landscape/portrait/square
- Windows/PowerShell/WSL shell snippets dropped
- file I/O referenced via Hermes write_file/read_file tools
- CLI-style --flags converted to natural-language options and
user-intent cues (skill matching has no slash command trigger)
Add PORT_NOTES.md documenting the adaptations and a sync procedure.
Art-style/tone/layout reference files are preserved verbatim from
upstream v1.56.1.
* fix(skills): address baoyu-comic PR review
- Remove PDF merge feature and scripts/ directory (no pdf-lib dep)
- Correct image_generate docs: prompt-only, returns URL; add
curl download step after every call
- Downgrade reference images to text-based trait extraction
(style/palette/scene); character sheet is agent-facing reference
- Unify source file naming on source-{slug}.md across SKILL.md
and workflow.md
* fix(skills): clarify baoyu-comic character sheet role
Page prompts are written in Step 5 from the text descriptions in
characters/characters.md — the PNG sheet generated in Step 7.1
cannot be used to write them. Reposition the PNG as a human-facing
review artifact (and reference for later regenerations / manual
edits), and drop the confusing "Character sheet | Strategy" tables
since the embedding rule is uniform.
* docs: document delegation width + depth knobs (#13745)
Fills the three gaps left by the orchestrator/width-depth salvage:
- configuration.md §Delegation: max_concurrent_children, max_spawn_depth,
orchestrator_enabled are now in the canonical config.yaml reference
with a paragraph covering defaults, clamping, role-degradation, and
the 3x3x3=27-leaf cost scaling.
- environment-variables.md: adds DELEGATION_MAX_CONCURRENT_CHILDREN to
the Agent Behavior table.
- features/delegation.md: corrects stale 'default 5, cap 8' wording
(that was from the original PR; the salvage landed on default 3 with
no ceiling and a tool error on excess instead of truncation).
* fix(website): run skill extraction automatically on npm run build/start (#13747)
website/src/pages/skills/index.tsx imports ../../data/skills.json, but
that file is git-ignored and generated at build time by
website/scripts/extract-skills.py. CI workflows (deploy-site.yml,
docs-site-checks.yml) run the script explicitly before 'npm run build',
so production and PR checks always work — but 'npm run build' on a
contributor's machine fails with:
Module not found: Can't resolve '../../data/skills.json'
because the extraction step was never wired into the npm scripts.
Adds a prebuild/prestart hook that runs extract-skills.py automatically.
If python3 or pyyaml aren't installed locally, writes an empty
skills.json instead of hard-failing — the Skills Hub page renders with
an empty state, the rest of the site builds normally, and CI (which
always has the deps) still generates the full catalog for production.
* fix(skills/baoyu-comic): absolute curl paths + clarify-timeout handling (#13775)
* fix(skills/baoyu-comic): require absolute paths for curl -o downloads
When downloading generated images across several batches of image_generate
calls, relying on persistent-shell CWD is unsafe. The terminal tool's shell
can rotate (TERMINAL_LIFETIME_SECONDS expiry, a failed cd that leaves the
shell somewhere else), and 'curl -fsSL <url> -o relative.png' then silently
writes to the wrong directory with no error.
Update the skill's Step 7 Download step to require absolute -o paths (or
workdir= on the terminal tool) and add a matching pitfall entry referencing
the Apr 2026 incident where pages 06-09 of a 10-page comic landed at the
repo root instead of comic/<slug>/. The agent then spent several turns
claiming the files existed where they didn't.
* fix(skills/baoyu-comic): handle clarify timeouts correctly in Step 2
A clarify timeout returning 'Use your best judgement to make the choice
and proceed' is NOT user consent to default the entire Step 2 questionnaire.
It is a per-question default only. Add guidance at both instruction sites
(SKILL.md User Questions section, references/workflow.md Step 2 header)
telling the agent to:
1. Continue asking the remaining questions in the sequence after a
timeout — each question is an independent consent point.
2. Surface every defaulted choice in the next user-visible message
so the user can correct it when they return. An unreported default
is indistinguishable from never having asked.
Reported live Apr 2026: agent asked style question via clarify, got a
timeout response, and silently defaulted style + narrative focus +
audience + review flags in one pass. User only learned style had
defaulted to 'ohmsha' after the comic was fully generated.
* fix(prompt): tell CLI agents not to emit MEDIA:/path tags (#13766)
The CLI has no attachment channel — MEDIA:<path> tags are only
intercepted on messaging gateway platforms (Telegram, Discord,
Slack, WhatsApp, Signal, BlueBubbles, email, etc.). On the CLI
they render as literal text, which is confusing for users.
The CLI platform hint was the one PLATFORM_HINTS entry that said
nothing about file delivery, so models trained on the messaging
hints would default to MEDIA: tags on the CLI too. Tool schemas
(browser_tool, tts_tool, etc.) also recommend MEDIA: generically.
Extend the CLI hint to explicitly discourage MEDIA: tags and tell
the agent to reference files by plain absolute path instead.
Add a regression test asserting the CLI hint carries negative
guidance about MEDIA: while messaging hints keep positive guidance.
* fix: add User-Agent claude-code/0.1.0 for Kimi /coding endpoint
- Add _is_kimi_coding_endpoint() to detect Kimi coding API
- Place Kimi check BEFORE _requires_bearer_auth to ensure User-Agent header is set
- Without this header, Kimi returns 403 on /coding/v1/messages
- Fixes kimi-2.5, kimi-for-coding, kimi-k2.6-code-preview all returning 403
* fix: auto-detect anthropic_messages mode for Kimi /coding/v1 endpoints
* fix(kimi-coding): add KIMI_CODING_API_KEY fallback + api_mode detection for /coding endpoint
* fix(kimi-coding): set anthropic_messages api_mode for /coding endpoint
* fix: Update Kimi Coding API endpoint and User-Agent
* fix: Enhance Kimi Coding API mode detection and User-Agent
* fix(kimi): reconcile sk-kimi- routing with Anthropic SDK URL semantics
Follow-ups after salvaging xiaoqiang243's kimi-for-coding patches:
- KIMI_CODE_BASE_URL: drop trailing /v1 (was /coding/v1).
The /coding endpoint speaks Anthropic Messages, and the Anthropic SDK
appends /v1/messages internally. /coding/v1 + SDK suffix produced
/coding/v1/v1/messages (a 404). /coding + SDK suffix now yields
/coding/v1/messages correctly.
- kimi-coding ProviderConfig: keep legacy default api.moonshot.ai/v1 so
non-sk-kimi- moonshot keys still authenticate. sk-kimi- keys are
already redirected to api.kimi.com/coding via _resolve_kimi_base_url.
- doctor.py: update Kimi UA to claude-code/0.1.0 (was KimiCLI/1.30.0)
and rewrite /coding base URLs to /coding/v1 for the /models health
check (Anthropic surface has no /models).
- test_kimi_env_vars: accept KIMI_CODING_API_KEY as a secondary env var.
E2E verified:
sk-kimi-<key> → https://api.kimi.com/coding/v1/messages (Anthropic)
sk-<legacy> → https://api.moonshot.ai/v1/chat/completions (OpenAI)
UA: claude-code/0.1.0, x-api-key: <sk-kimi-*>
* chore(release): map xiaoqiang243 personal email in AUTHOR_MAP
* feat: add ResponsesApiTransport + wire all Codex transport paths
Add ResponsesApiTransport wrapping codex_responses_adapter.py behind the
ProviderTransport ABC. Auto-registered via _discover_transports().
Wire ALL Codex transport methods to production paths in run_agent.py:
- build_kwargs: main _build_api_kwargs codex branch (50 lines extracted)
- normalize_response: main loop + flush + summary + retry (4 sites)
- convert_tools: memory flush tool override
- convert_messages: called internally via build_kwargs
- validate_response: response validation gate
- preflight_kwargs: request sanitization (2 sites)
Remove 7 dead legacy wrappers from AIAgent (_responses_tools,
_chat_messages_to_responses_input, _normalize_codex_response,
_preflight_codex_api_kwargs, _preflight_codex_input_items,
_extract_responses_message_text, _extract_responses_reasoning_text).
Keep 3 ID manipulation methods still used by _build_assistant_message.
Update 18 test call sites across 3 test files to call adapter functions
directly instead of through deleted AIAgent wrappers.
24 new tests. 343 codex/responses/transport tests pass (0 failures).
PR 4 of the provider transport refactor.
* fix(delegation): add hard timeout and stale detection for subagent execution (#13770)
- Wrap child.run_conversation() in a ThreadPoolExecutor with configurable
timeout (delegation.child_timeout_seconds, default 300s) to prevent
indefinite blocking when a subagent's API call or tool HTTP request hangs.
- Add heartbeat stale detection: if a child's api_call_count doesn't
advance for 5 consecutive heartbeat cycles (~2.5 min), stop touching
the parent's activity timestamp so the gateway inactivity timeout
can fire as a last resort.
- Add 'timeout' as a new exit_reason/status alongside the existing
completed/max_iterations/interrupted states.
- Use shutdown(wait=False) on the timeout executor to avoid the
ThreadPoolExecutor.__exit__ deadlock when a child is stuck on
blocking I/O.
Closes #13768
* remove Nous Portal free-model allowlist
Drop _NOUS_ALLOWED_FREE_MODELS + filter_nous_free_models and its two call
sites. Whatever Nous Portal prices as free now shows up in the picker as-is
— no local allowlist gatekeeping. Free-tier partitioning (paid vs free in
the menu) still runs via partition_nous_models_by_tier.
* feat(aux): use Portal /api/nous/recommended-models for auxiliary models
Wire the auxiliary client (compaction, vision, session search, web extract)
to the Nous Portal's curated recommended-models endpoint when running on
Nous Portal, with a TTL-cached fetch that mirrors how we pull /models for
pricing.
hermes_cli/models.py
- fetch_nous_recommended_models(portal_base_url, force_refresh=False)
10-minute TTL cache, keyed per portal URL (staging vs prod don't
collide). Public endpoint, no auth required. Returns {} on any
failure so callers always get a dict.
- get_nous_recommended_aux_model(vision, free_tier=None, ...)
Tier-aware pick from the payload:
- Paid tier → paidRecommended{Vision,Compaction}Model, falling back
to freeRecommended* when the paid field is null (common during
staged rollouts of new paid models).
- Free tier → freeRecommended* only, never leaks paid models.
When free_tier is None, auto-detects via the existing
check_nous_free_tier() helper (already cached 3 min against
/api/oauth/account). Detection errors default to paid so we never
silently downgrade a paying user.
agent/auxiliary_client.py — _try_nous()
- Replaces the hardcoded xiaomi/mimo free-tier branch with a single call
to get_nous_recommended_aux_model(vision=vision).
- Falls back to _NOUS_MODEL (google/gemini-3-flash-preview) when the
Portal is unreachable or returns a null recommendation.
- The Portal is now the source of truth for aux model selection; the
xiaomi allowlist we used to carry is effectively dead.
Tests (15 new)
- tests/hermes_cli/test_models.py::TestNousRecommendedModels
Fetch caching, per-portal keying, network failure, force_refresh;
paid-prefers-paid, paid-falls-to-free, free-never-leaks-paid,
auto-detect, detection-error → paid default, null/blank modelName
handling.
- tests/agent/test_auxiliary_client.py::TestNousAuxiliaryRefresh
_try_nous honors Portal recommendation for text + vision, falls
back to google/gemini-3-flash-preview on None or exception.
Behavior won't visibly change today — both tier recommendations currently
point at google/gemini-3-flash-preview — but the moment the Portal ships
a better paid recommendation, subscribers pick it up within 10 minutes
without a Hermes release.
* feat: add ChatCompletionsTransport + wire all default paths
Third concrete transport — handles the default 'chat_completions' api_mode used
by ~16 OpenAI-compatible providers (OpenRouter, Nous, NVIDIA, Qwen, Ollama,
DeepSeek, xAI, Kimi, custom, etc.). Wires build_kwargs + validate_response to
production paths.
Based on PR #13447 by @kshitijk4poor, with fixes:
- Preserve tool_call.extra_content (Gemini thought_signature) via
ToolCall.provider_data — the original shim stripped it, causing 400 errors
on multi-turn Gemini 3 thinking requests.
- Preserve reasoning_content distinctly from reasoning (DeepSeek/Moonshot) so
the thinking-prefill retry check (_has_structured) still triggers.
- Port Kimi/Moonshot quirks (32000 max_tokens, top-level reasoning_effort,
extra_body.thinking) that landed on main after the original PR was opened.
- Keep _qwen_prepare_chat_messages_inplace alive and call it through the
transport when sanitization already deepcopied (avoids a second deepcopy).
- Skip the back-compat SimpleNamespace shim in the main normalize loop — for
chat_completions, response.choices[0].message is already the right shape
with .content/.tool_calls/.reasoning/.reasoning_content/.reasoning_details
and per-tool-call .extra_content from the OpenAI SDK.
run_agent.py: -239 lines in _build_api_kwargs default branch extracted to the
transport. build_kwargs now owns: codex-field sanitization, Qwen portal prep,
developer role swap, provider preferences, max_tokens resolution (ephemeral >
user > NVIDIA 16384 > Qwen 65536 > Kimi 32000 > anthropic_max_output), Kimi
reasoning_effort + extra_body.thinking, OpenRouter/Nous/GitHub reasoning,
Nous product attribution tags, Ollama num_ctx, custom-provider think=false,
Qwen vl_high_resolution_images, request_overrides.
39 new transport tests (8 build_kwargs, 5 Kimi, 4 validate, 4 normalize
including extra_content regression, 3 cache stats, 3 basic). Tests/run_agent/
targeted suite passes (885/885 + 15 skipped; the 1 remaining failure is the
test_concurrent_interrupt flake present on origin/main).
* fix(tui): don't force-open Activity on every error
Reverts the auto-expand-on-new-error effect added in 93b47d96. The
effect overrode the user's chosen detailsMode and visually interrupted
every turn. Red/yellow chevron tint remains as the passive signal —
click to read, just like Thinking and Tool calls.
* fix(tui): demote gateway log-noise from Activity to info tone
Restore the old-CLI contract where only complete failures tint Activity
red. Everything else is still visible for debugging but no longer
commandeers attention.
- gateway.stderr: always tone='info' (drops the ERRLIKE_RE regex)
- gateway.protocol_error: both pushes demoted to 'info'
- commands.catalog cold-start failure: demoted to 'info'
- approval.request: no longer duplicates the overlay into Activity
Kept as 'error': terminal `error` event, gateway.start_timeout,
gateway-exited, explicit status.update kinds.
* feat: add BedrockTransport + wire all Bedrock transport paths
Fourth and final transport — completes the transport layer with all four
api_modes covered. Wraps agent/bedrock_adapter.py behind the ProviderTransport
ABC, handles both raw boto3 dicts and already-normalized SimpleNamespace.
Wires all transport methods to production paths in run_agent.py:
- build_kwargs: _build_api_kwargs bedrock branch
- validate_response: response validation, new bedrock_converse branch
- finish_reason: new bedrock_converse branch in finish_reason extraction
Based on PR #13467 by @kshitijk4poor, with one adjustment: the main normalize
loop does NOT add a bedrock_converse branch to invoke normalize_response on
the already-normalized response. Bedrock's normalize_converse_response runs
at the dispatch site (run_agent.py:5189), so the response already has the
OpenAI-compatible .choices[0].message shape by the time the main loop sees
it. Falling through to the chat_completions else branch is correct and
sidesteps a redundant NormalizedResponse rebuild.
Transport coverage — complete:
| api_mode | Transport | build_kwargs | normalize | validate |
|--------------------|--------------------------|:------------:|:---------:|:--------:|
| anthropic_messages | AnthropicTransport | ✅ | ✅ | ✅ |
| codex_responses | ResponsesApiTransport | ✅ | ✅ | ✅ |
| chat_completions | ChatCompletionsTransport | ✅ | ✅ | ✅ |
| bedrock_converse | BedrockTransport | ✅ | ✅ | ✅ |
17 new BedrockTransport tests pass. 117 transport tests total pass.
160 bedrock/converse tests across tests/agent/ pass. Full tests/run_agent/
targeted suite passes (885/885 + 15 skipped; the 1 remaining failure is the
pre-existing test_concurrent_interrupt flake on origin/main).
* chore(models): drop 3 models from nous portal recommended list (#13822)
Remove nvidia/nemotron-3-super-120b-a12b:free, arcee-ai/trinity-large-preview:free,
and openrouter/elephant-alpha from _PROVIDER_MODELS['nous']. The paid nemotron and
arcee-thinking variants remain.
* fix(kimi): don't send Anthropic thinking to api.kimi.com/coding (#13826)
Kimi's /coding endpoint speaks the Anthropic Messages protocol but has
its own thinking semantics: when thinking.enabled is sent, Kimi validates
the history and requires every prior assistant tool-call message to carry
OpenAI-style reasoning_content. The Anthropic path never populates that
field, and convert_messages_to_anthropic strips Anthropic thinking blocks
on third-party endpoints — so after one tool-calling turn the next request
fails with:
HTTP 400: thinking is enabled but reasoning_content is missing in
assistant tool call message at index N
Kimi on chat_completions handles thinking via extra_body in
ChatCompletionsTransport (#13503). On the Anthropic route, drop the
parameter entirely and let Kimi drive reasoning server-side.
build_anthropic_kwargs now gates the reasoning_config -> thinking block
on not _is_kimi_coding_endpoint(base_url).
Tests: 8 new parametric tests cover /coding, /coding/v1, /coding/anthropic,
/coding/ (trailing slash), explicit disabled, other third-party endpoints
still getting thinking (MiniMax), native Anthropic unaffected, and the
non-/coding Kimi root route.
* feat(models): add minimax/minimax-m2.5:free to OpenRouter catalog (#13836)
Surfaces the free variant alongside the paid minimax-m2.5 entry in
both the OPENROUTER_MODELS fallback snapshot and the nous/openrouter
provider model list.
* feat(plugins): pluggable image_gen backends + OpenAI provider (#13799)
* feat(plugins): pluggable image_gen backends + OpenAI provider
Adds a ImageGenProvider ABC so image generation backends register as
bundled plugins under `plugins/image_gen/<name>/`. The plugin scanner
gains three primitives to make this work generically:
- `kind:` manifest field (`standalone` | `backend` | `exclusive`).
Bundled `kind: backend` plugins auto-load — no `plugins.enabled`
incantation. User-installed backends stay opt-in.
- Path-derived keys: `plugins/image_gen/openai/` gets key
`image_gen/openai`, so a future `tts/openai` cannot collide.
- Depth-2 recursion into category namespaces (parent dirs without a
`plugin.yaml` of their own).
Includes `OpenAIImageGenProvider` as the first consumer (gpt-image-1.5
default, plus gpt-image-1, gpt-image-1-mini, DALL-E 3/2). Base64
responses save to `$HERMES_HOME/cache/images/`; URL responses pass
through.
FAL stays in-tree for this PR — a follow-up ports it into
`plugins/image_gen/fal/` so the in-tree `image_generation_tool.py`
slims down. The dispatch shim in `_handle_image_generate` only fires
when `image_gen.provider` is explicitly set to a non-FAL value, so
existing FAL setups are untouched.
- 41 unit tests (scanner recursion, kind parsing, gate logic,
registry, OpenAI payload shapes)
- E2E smoke verified: bundled plugin autoloads, registers, and
`_handle_image_generate` routes to OpenAI when configured
* fix(image_gen/openai): don't send response_format to gpt-image-*
The live API rejects it: 'Unknown parameter: response_format'
(verified 2026-04-21 with gpt-image-1.5). gpt-image-* models return
b64_json unconditionally, so the parameter was both unnecessary and
actively broken.
* feat(image_gen/openai): gpt-image-2 only, drop legacy catalog
gpt-image-2 is the latest/best OpenAI image model (released 2026-04-21)
and there's no reason to expose the older gpt-image-1.5 / gpt-image-1 /
dall-e-3 / dall-e-2 alongside it — slower, lower quality, or awkward
(dall-e-2 squares only). Trim the catalog down to a single model.
Live-verified end-to-end: landscape 1536x1024 render of a Moog-style
synth matches prompt exactly, 2.4MB PNG saved to cache.
* feat(image_gen/openai): expose gpt-image-2 as three quality tiers
Users pick speed/fidelity via the normal model picker instead of a
hidden quality knob. All three tier IDs resolve to the single underlying
gpt-image-2 API model with a different quality parameter:
gpt-image-2-low ~15s fast iteration
gpt-image-2-medium ~40s default
gpt-image-2-high ~2min highest fidelity
Live-measured on OpenAI's API today: 15.4s / 40.8s / 116.9s for the
same 1024x1024 prompt.
Config:
image_gen.openai.model: gpt-image-2-high
# or
image_gen.model: gpt-image-2-low
# or env var for scripts/tests
OPENAI_IMAGE_MODEL=gpt-image-2-medium
Live-verified end-to-end with the low tier: 18.8s landscape render of a
golden retriever in wildflowers, vision-confirmed exact match.
* feat(tools_config): plugin image_gen providers inject themselves into picker
'hermes tools' → Image Generation now shows plugin-registered backends
alongside Nous Subscription and FAL.ai without tools_config.py needing
to know about them. OpenAI appears as a third option today; future
backends appear automatically as they're added.
Mechanism:
- ImageGenProvider gains an optional get_setup_schema() hook
(name, badge, tag, env_vars). Default derived from display_name.
- tools_config._plugin_image_gen_providers() pulls the schemas from
every registered non-FAL plugin provider.
- _visible_providers() appends those rows when rendering the Image
Generation category.
- _configure_provider() handles the new image_gen_plugin_name marker:
writes image_gen.provider and routes to the plugin's list_models()
catalog for the model picker.
- _toolset_needs_configuration_prompt('image_gen') stops demanding a
FAL key when any plugin provider reports is_available().
FAL is skipped in the plugin path because it already has hardcoded
TOOL_CATEGORIES rows — when it gets ported to a plugin in a follow-up
PR the hardcoded rows go away and it surfaces through the same path
as OpenAI.
Verified live: picker shows Nous Subscription / FAL.ai / OpenAI.
Picking OpenAI prompts for OPENAI_API_KEY, then shows the
gpt-image-2-low/medium/high model picker sourced from the plugin.
397 tests pass across plugins/, tools_config, registry, and picker.
* fix(image_gen): close final gaps for plugin-backend parity with FAL
Two small places that still hardcoded FAL:
- hermes_cli/setup.py status line: an OpenAI-only setup showed
'Image Generation: missing FAL_KEY'. Now probes plugin providers
and reports '(OpenAI)' when one is_available() — or falls back to
'missing FAL_KEY or OPENAI_API_KEY' if nothing is configured.
- image_generate tool schema description: said 'using FAL.ai, default
FLUX 2 Klein 9B'. Rewrote provider-neutral — 'backend and model are
user-configured' — and notes the 'image' field can be a URL or an
absolute path, which the gateway delivers either way via
extract_local_files().
* feat: add Step Plan provider support (salvage #6005)
Adds a first-class 'stepfun' API-key provider surfaced as Step Plan:
- Support Step Plan setup for both International and China regions
- Discover Step Plan models live from /step_plan/v1/models, with a
small coding-focused fallback catalog when discovery is unavailable
- Thread StepFun through provider metadata, setup persistence, status
and doctor output, auxiliary routing, and model normalization
- Add tests for provider resolution, model validation, metadata
mapping, and StepFun region/model persistence
Based on #6005 by @hengm3467.
Co-authored-by: hengm3467 <100685635+hengm3467@users.noreply.github.com>
* fix(packaging): include agent.* sub-packages in pyproject.toml
The transport refactor (PRs #13862 ff.) added agent/transports/ as a
sub-package but the setuptools packages.find include list only had
"agent" (top-level files), not "agent.*" (sub-packages).
pip install / Nix builds therefore ship run_agent.py (which now imports
from agent.transports on every API call) but omit the transports
directory entirely, causing:
ModuleNotFoundError: No module named 'agent.transports'
on every LLM call for packaged installs.
Adds "agent.*" to match the existing pattern used by tools, gateway,
tui_gateway, and plugins.
* fix: preserve reasoning_content on Kimi replay
* feat(optional-skills): add page-agent skill under new web-development category (#13976)
Adds an optional skill that walks users through installing and using
alibaba/page-agent — a pure-JS in-page GUI agent that web developers
embed into their own webapps so end users can drive the UI with
natural language.
Three install paths: CDN demo (30s, no install), npm install into an
existing app with provider config table (Qwen/OpenAI/Ollama/OpenRouter),
and clone-from-source for dev/contributor workflow.
Clear use-case framing up front (embed AI copilot in SaaS/admin/B2B,
modernize legacy UIs, accessibility via natural language) and an
explicit NOT-for list that points users wanting server-side browser
automation back to Hermes' built-in browser tool.
Live-verified: repo builds on Node 22.22 + npm 10.9, dev:demo serves
at localhost:5174, API surface (new PageAgent{...}, panel.show(),
execute(task)) matches what the skill documents. Also verified
discovery end-to-end via OptionalSkillSource with isolated
HERMES_HOME — search/inspect/fetch all resolve
official/web-development/page-agent correctly.
New category directory: optional-skills/web-development/ with a
DESCRIPTION.md explaining the distinction from Hermes' own browser
automation (outside-in vs inside-out).
* feat(wecom): add QR scan flow and interactive setup wizard for bot credentials
* docs(wecom): document QR scan-to-create setup flow
* fix(wecom): visible poll progress + clearer no-bot-info failure + docstring note
Follow-ups on top of salvaged #13923 (@keifergu):
- Print QR poll dot every 3s instead of every 18s so "Fetching
configuration results..." doesn't look hung.
- On "status=success but no bot_info" from the WeCom query endpoint,
log the full payload at WARNING and tell the user we're falling
back to manual entry (was previously a single opaque line).
- Document in the qr_scan_for_bot_info() docstring that the
work.weixin.qq.com/ai/qc/* endpoints are the admin-console web-UI
flow, not the public developer API, and may change without notice.
Also add keifergu@tencent.com to scripts/release.py AUTHOR_MAP so
release notes attribute the feature correctly.
* feat(state): auto-prune old sessions + VACUUM state.db at startup (#13861)
* feat(state): auto-prune old sessions + VACUUM state.db at startup
state.db accumulates every session, message, and FTS5 index entry forever.
A heavy user (gateway + cron) reported 384MB with 982 sessions / 68K messages
causing slowdown; manual 'hermes sessions prune --older-than 7' + VACUUM
brought it to 43MB. The prune command and VACUUM are not wired to run
automatically anywhere — sessions grew unbounded until users noticed.
Changes:
- hermes_state.py: new state_meta key/value table, vacuum() method, and
maybe_auto_prune_and_vacuum() — idempotent via last-run timestamp in
state_meta so it only actually executes once per min_interval_hours
across all Hermes processes for a given HERMES_HOME. Never raises.
- hermes_cli/config.py: new 'sessions:' block in DEFAULT_CONFIG
(auto_prune=True, retention_days=90, vacuum_after_prune=True,
min_interval_hours=24). Added to _KNOWN_ROOT_KEYS.
- cli.py: call maintenance once at HermesCLI init (shared helper
_run_state_db_auto_maintenance reads config and delegates to DB).
- gateway/run.py: call maintenance once at GatewayRunner init.
- Docs: user-guide/sessions.md rewrites 'Automatic Cleanup' section.
Why VACUUM matters: SQLite does NOT shrink the file on DELETE — freed
pages get reused on next INSERT. Without VACUUM, a delete-heavy DB stays
bloated forever. VACUUM only runs when the prune actually removed rows,
so tight DBs don't pay the I/O cost.
Tests: 10 new tests in tests/test_hermes_state.py covering state_meta,
vacuum, idempotency, interval skipping, VACUUM-only-when-needed,
corrupt-marker recovery. All 246 existing state/config/gateway tests
still pass.
Verified E2E with real imports + isolated HERMES_HOME: DEFAULT_CONFIG
exposes the new block, load_config() returns it for fresh installs,
first call prunes+vacuums, second call within min_interval_hours skips,
and the state_meta marker persists across connection close/reopen.
* sessions.auto_prune defaults to false (opt-in)
Session history powers session_search recall across past conversations,
so silently pruning on startup could surprise users. Ship the machinery
disabled and let users opt in when they notice state.db is hurting
performance.
- DEFAULT_CONFIG.sessions.auto_prune: True → False
- Call-site fallbacks in cli.py and gateway/run.py match the new default
(so unmigrated configs still see off)
- Docs: flip 'Enable in config.yaml' framing + tip explains the tradeoff
* feat(hindsight): richer session-scoped retain metadata
- Add configurable retain_tags / retain_source / retain_user_prefix /
retain_assistant_prefix knobs for native Hindsight.
- Thread gateway session identity (user_name, chat_id, chat_name,
chat_type, thread_id) through AIAgent and MemoryManager into
MemoryProvider.initialize kwargs so providers can scope and tag
retained memories.
- Hindsight attaches the new identity fields as retain metadata,
merges per-call tool tags with configured default tags, and uses
the configurable transcript labels for auto-retained turns.
Co-authored-by: Abner <abner.the.foreman@agentmail.to>
* chore(release): map Abner email to Abnertheforeman
* refactor(qqbot): migrate qr onboard flow to sync + consolidate into onboard.py
- Replace async create_bind_task/poll_bind_result with synchronous
httpx.Client equivalents, eliminating manual event loop management
- Move _render_qr and full qr_register() entry-point into onboard.py,
mirroring the Feishu onboarding pattern
- Remove _qqbot_render_qr and _qqbot_qr_flow from gateway.py (~90 lines);
call site becomes a single qr_register() import
- Fix potential segfault: previous code called loop.close() in the EXPIRED
branch and again in the finally block (double-close crashed under uvloop)
* fix(cli): ensure project .env is sanitized before loading
* chore(release): map hharry11 email to GitHub handle
* feat(dashboard): track real API call count per session
Adds schema v7 'api_call_count' column. run_agent.py increments it by 1
per LLM API call, web_server analytics SQL aggregates it, frontend uses
the real counter instead of summing sessions.
The 'API Calls' card on the analytics dashboard previously displayed
COUNT(*) from the sessions table — the number of conversations, not
LLM requests. Each session makes 10-90 API calls through the tool loop,
so the reported number was ~30x lower than real.
Salvaged from PR #10140 (@kshitijk4poor). The cache-token accuracy
portions of the original PR were deferred — per-provider analytics is
the better path there, since cache_write_tokens and actual_cost_usd
are only reliably available from a subset of providers (Anthropic
native, Codex Responses, OpenRouter with usage.include).
Tests:
- schema_version v7 assertion
- migration v2 -> v7 adds api_call_count column with default 0
- update_token_counts increments api_call_count by provided delta
- absolute=True sets api_call_count directly
- /api/analytics/usage exposes total_api_calls in totals
* fix(plugins+nous): auto-coerce memory plugins; actionable Nous 401 diagnostic (#14005)
* fix(plugins): auto-coerce user-installed memory plugins to kind=exclusive
User-installed memory provider plugins…
ulasbilgen
pushed a commit
to ulasbilgen/hermes-adhd-agent
that referenced
this pull request
May 1, 2026
…enticate() (NousResearch#13468) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * refactor(acp): validate method_id against advertised provider in authenticate() Previously authenticate() accepted any method_id whenever the server had provider credentials configured. This was not a vulnerability under the personal-assistant trust model (ACP is stdio-only, local-trust — anything that can reach the transport is already code-execution-equivalent to the user), but it was sloppy API hygiene: the advertised auth_methods list from initialize() was effectively ignored. Now authenticate() only returns AuthenticateResponse when method_id matches the currently-advertised provider (case-insensitive). Mismatched or missing method_id returns None, consistent with the no-credentials case. Raised by xeloxa via GHSA-g5pf-8w9m-h72x. Declined as a CVE (ACP transport is stdio, local-trust model), but the correctness fix is worth having on its own.
ulasbilgen
pushed a commit
to ulasbilgen/hermes-adhd-agent
that referenced
this pull request
May 1, 2026
…bagents (NousResearch#13718) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * feat(delegate): cross-agent file state coordination for concurrent subagents Prevents mangled edits when concurrent subagents touch the same file (same process, same filesystem — the mangle scenario from NousResearch#11215). Three layers, all opt-out via HERMES_DISABLE_FILE_STATE_GUARD=1: 1. FileStateRegistry (tools/file_state.py) — process-wide singleton tracking per-agent read stamps and the last writer globally. check_stale() names the sibling subagent in the warning when a non-owning agent wrote after this agent's last read. 2. Per-path threading.Lock wrapped around the read-modify-write region in write_file_tool and patch_tool. Concurrent siblings on the same path serialize; different paths stay fully parallel. V4A multi-file patches lock in sorted path order (deadlock-free). 3. Delegate-completion reminder in tools/delegate_tool.py: after a subagent returns, writes_since(parent, child_start, parent_reads) appends '[NOTE: subagent modified files the parent previously read — re-read before editing: ...]' to entry.summary when the child touched anything the parent had already seen. Complements (does not replace) the existing path-overlap check in run_agent._should_parallelize_tool_batch — batch check prevents same-file parallel dispatch within one agent's turn (cheap prevention, zero API cost), registry catches cross-subagent and cross-turn staleness at write time (detection). Behavior is warning-only, not hard-failing — matches existing project style. Errors surface naturally: sibling writes often invalidate the old_string in patch operations, which already errors cleanly. Tests: tests/tools/test_file_state_registry.py — 16 tests covering registry state transitions, per-path locking, per-path-not-global locking, writes_since filtering, kill switch, and end-to-end integration through the real read_file/write_file/patch handlers.
aj-nt
pushed a commit
to aj-nt/hermes-agent
that referenced
this pull request
May 1, 2026
…enticate() (NousResearch#13468) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * refactor(acp): validate method_id against advertised provider in authenticate() Previously authenticate() accepted any method_id whenever the server had provider credentials configured. This was not a vulnerability under the personal-assistant trust model (ACP is stdio-only, local-trust — anything that can reach the transport is already code-execution-equivalent to the user), but it was sloppy API hygiene: the advertised auth_methods list from initialize() was effectively ignored. Now authenticate() only returns AuthenticateResponse when method_id matches the currently-advertised provider (case-insensitive). Mismatched or missing method_id returns None, consistent with the no-credentials case. Raised by xeloxa via GHSA-g5pf-8w9m-h72x. Declined as a CVE (ACP transport is stdio, local-trust model), but the correctness fix is worth having on its own.
aj-nt
pushed a commit
to aj-nt/hermes-agent
that referenced
this pull request
May 1, 2026
…bagents (NousResearch#13718) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * feat(delegate): cross-agent file state coordination for concurrent subagents Prevents mangled edits when concurrent subagents touch the same file (same process, same filesystem — the mangle scenario from NousResearch#11215). Three layers, all opt-out via HERMES_DISABLE_FILE_STATE_GUARD=1: 1. FileStateRegistry (tools/file_state.py) — process-wide singleton tracking per-agent read stamps and the last writer globally. check_stale() names the sibling subagent in the warning when a non-owning agent wrote after this agent's last read. 2. Per-path threading.Lock wrapped around the read-modify-write region in write_file_tool and patch_tool. Concurrent siblings on the same path serialize; different paths stay fully parallel. V4A multi-file patches lock in sorted path order (deadlock-free). 3. Delegate-completion reminder in tools/delegate_tool.py: after a subagent returns, writes_since(parent, child_start, parent_reads) appends '[NOTE: subagent modified files the parent previously read — re-read before editing: ...]' to entry.summary when the child touched anything the parent had already seen. Complements (does not replace) the existing path-overlap check in run_agent._should_parallelize_tool_batch — batch check prevents same-file parallel dispatch within one agent's turn (cheap prevention, zero API cost), registry catches cross-subagent and cross-turn staleness at write time (detection). Behavior is warning-only, not hard-failing — matches existing project style. Errors surface naturally: sibling writes often invalidate the old_string in patch operations, which already errors cleanly. Tests: tests/tools/test_file_state_registry.py — 16 tests covering registry state transitions, per-path locking, per-path-not-global locking, writes_since filtering, kill switch, and end-to-end integration through the real read_file/write_file/patch handlers.
Luminet2023
pushed a commit
to Luminet2023/hermes-agent
that referenced
this pull request
May 1, 2026
…enticate() (NousResearch#13468) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * refactor(acp): validate method_id against advertised provider in authenticate() Previously authenticate() accepted any method_id whenever the server had provider credentials configured. This was not a vulnerability under the personal-assistant trust model (ACP is stdio-only, local-trust — anything that can reach the transport is already code-execution-equivalent to the user), but it was sloppy API hygiene: the advertised auth_methods list from initialize() was effectively ignored. Now authenticate() only returns AuthenticateResponse when method_id matches the currently-advertised provider (case-insensitive). Mismatched or missing method_id returns None, consistent with the no-credentials case. Raised by xeloxa via GHSA-g5pf-8w9m-h72x. Declined as a CVE (ACP transport is stdio, local-trust model), but the correctness fix is worth having on its own.
pull Bot
pushed a commit
to soitun/hermes-webui
that referenced
this pull request
May 3, 2026
…+ augment fallback (nesquena#1426) Augments @bergeouss's PR nesquena#1548 v2 with the structural fix the issue actually requested. The original PR added 5 hardcoded entries to _FALLBACK_MODELS which would rot fast as OpenRouter's free-tier roster turns over monthly. Adds proper live-fetch logic to the OpenRouter group population so the free-tier list stays fresh without requiring a code release every time a new free model lands. api/config.py:2120 — replaces the static _FALLBACK_MODELS slice with: 1. Live curated catalog via hermes_cli.models.fetch_openrouter_models() — applies the tool-support filter (Kilo-Org/kilocode#9068). 2. Free-tier live fetch — direct call to https://openrouter.ai/api/v1/models, filtered to free-tier-only (pricing.prompt == 0 AND pricing.completion == 0, OR :free suffix), bypasses the tool-support filter so newly-added free variants appear even before OpenRouter annotates them with tools. Capped at 30 entries to keep the picker usable. 3. Defense-in-depth fallback to _FALLBACK_MODELS (which retains @bergeouss's hardcoded list for offline / test envs). 4. Deduplication via seen_ids — model in both surfaces appears once. 5 new tests + 1 fixed test in tests/test_minimax_provider.py (scoped the provider='MiniMax' assertion to direct-MiniMax routes by filtering for 'minimax/' prefix and excluding ':free' since the OpenRouter free-tier variant minimax/minimax-m2.5:free correctly carries provider='OpenRouter'). Co-authored-by: bergeouss <[email protected]>
02356abc
pushed a commit
to 02356abc/hermes-agent
that referenced
this pull request
May 14, 2026
…enticate() (NousResearch#13468) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * refactor(acp): validate method_id against advertised provider in authenticate() Previously authenticate() accepted any method_id whenever the server had provider credentials configured. This was not a vulnerability under the personal-assistant trust model (ACP is stdio-only, local-trust — anything that can reach the transport is already code-execution-equivalent to the user), but it was sloppy API hygiene: the advertised auth_methods list from initialize() was effectively ignored. Now authenticate() only returns AuthenticateResponse when method_id matches the currently-advertised provider (case-insensitive). Mismatched or missing method_id returns None, consistent with the no-credentials case. Raised by xeloxa via GHSA-g5pf-8w9m-h72x. Declined as a CVE (ACP transport is stdio, local-trust model), but the correctness fix is worth having on its own.
02356abc
pushed a commit
to 02356abc/hermes-agent
that referenced
this pull request
May 14, 2026
…bagents (NousResearch#13718) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * feat(delegate): cross-agent file state coordination for concurrent subagents Prevents mangled edits when concurrent subagents touch the same file (same process, same filesystem — the mangle scenario from NousResearch#11215). Three layers, all opt-out via HERMES_DISABLE_FILE_STATE_GUARD=1: 1. FileStateRegistry (tools/file_state.py) — process-wide singleton tracking per-agent read stamps and the last writer globally. check_stale() names the sibling subagent in the warning when a non-owning agent wrote after this agent's last read. 2. Per-path threading.Lock wrapped around the read-modify-write region in write_file_tool and patch_tool. Concurrent siblings on the same path serialize; different paths stay fully parallel. V4A multi-file patches lock in sorted path order (deadlock-free). 3. Delegate-completion reminder in tools/delegate_tool.py: after a subagent returns, writes_since(parent, child_start, parent_reads) appends '[NOTE: subagent modified files the parent previously read — re-read before editing: ...]' to entry.summary when the child touched anything the parent had already seen. Complements (does not replace) the existing path-overlap check in run_agent._should_parallelize_tool_batch — batch check prevents same-file parallel dispatch within one agent's turn (cheap prevention, zero API cost), registry catches cross-subagent and cross-turn staleness at write time (detection). Behavior is warning-only, not hard-failing — matches existing project style. Errors surface naturally: sibling writes often invalidate the old_string in patch operations, which already errors cleanly. Tests: tests/tools/test_file_state_registry.py — 16 tests covering registry state transitions, per-path locking, per-path-not-global locking, writes_since filtering, kill switch, and end-to-end integration through the real read_file/write_file/patch handlers.
jliounis
pushed a commit
to jliounis/kilocode
that referenced
this pull request
May 18, 2026
…ut-tools fix(gateway): hide models without tool support from list
gweeteve
pushed a commit
to gweeteve/hermes-agent
that referenced
this pull request
Jun 2, 2026
…enticate() (NousResearch#13468) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * refactor(acp): validate method_id against advertised provider in authenticate() Previously authenticate() accepted any method_id whenever the server had provider credentials configured. This was not a vulnerability under the personal-assistant trust model (ACP is stdio-only, local-trust — anything that can reach the transport is already code-execution-equivalent to the user), but it was sloppy API hygiene: the advertised auth_methods list from initialize() was effectively ignored. Now authenticate() only returns AuthenticateResponse when method_id matches the currently-advertised provider (case-insensitive). Mismatched or missing method_id returns None, consistent with the no-credentials case. Raised by xeloxa via GHSA-g5pf-8w9m-h72x. Declined as a CVE (ACP transport is stdio, local-trust model), but the correctness fix is worth having on its own.
gweeteve
pushed a commit
to gweeteve/hermes-agent
that referenced
this pull request
Jun 2, 2026
…bagents (NousResearch#13718) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * feat(delegate): cross-agent file state coordination for concurrent subagents Prevents mangled edits when concurrent subagents touch the same file (same process, same filesystem — the mangle scenario from NousResearch#11215). Three layers, all opt-out via HERMES_DISABLE_FILE_STATE_GUARD=1: 1. FileStateRegistry (tools/file_state.py) — process-wide singleton tracking per-agent read stamps and the last writer globally. check_stale() names the sibling subagent in the warning when a non-owning agent wrote after this agent's last read. 2. Per-path threading.Lock wrapped around the read-modify-write region in write_file_tool and patch_tool. Concurrent siblings on the same path serialize; different paths stay fully parallel. V4A multi-file patches lock in sorted path order (deadlock-free). 3. Delegate-completion reminder in tools/delegate_tool.py: after a subagent returns, writes_since(parent, child_start, parent_reads) appends '[NOTE: subagent modified files the parent previously read — re-read before editing: ...]' to entry.summary when the child touched anything the parent had already seen. Complements (does not replace) the existing path-overlap check in run_agent._should_parallelize_tool_batch — batch check prevents same-file parallel dispatch within one agent's turn (cheap prevention, zero API cost), registry catches cross-subagent and cross-turn staleness at write time (detection). Behavior is warning-only, not hard-failing — matches existing project style. Errors surface naturally: sibling writes often invalidate the old_string in patch operations, which already errors cleanly. Tests: tests/tools/test_file_state_registry.py — 16 tests covering registry state transitions, per-path locking, per-path-not-global locking, writes_since filtering, kill switch, and end-to-end integration through the real read_file/write_file/patch handlers.
the-grove-ai
pushed a commit
to the-grove-ai/hermes-autonomaton-refactor
that referenced
this pull request
Jun 2, 2026
…enticate() (#13468) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * refactor(acp): validate method_id against advertised provider in authenticate() Previously authenticate() accepted any method_id whenever the server had provider credentials configured. This was not a vulnerability under the personal-assistant trust model (ACP is stdio-only, local-trust — anything that can reach the transport is already code-execution-equivalent to the user), but it was sloppy API hygiene: the advertised auth_methods list from initialize() was effectively ignored. Now authenticate() only returns AuthenticateResponse when method_id matches the currently-advertised provider (case-insensitive). Mismatched or missing method_id returns None, consistent with the no-credentials case. Raised by xeloxa via GHSA-g5pf-8w9m-h72x. Declined as a CVE (ACP transport is stdio, local-trust model), but the correctness fix is worth having on its own.
the-grove-ai
pushed a commit
to the-grove-ai/hermes-autonomaton-refactor
that referenced
this pull request
Jun 2, 2026
…bagents (#13718) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * feat(delegate): cross-agent file state coordination for concurrent subagents Prevents mangled edits when concurrent subagents touch the same file (same process, same filesystem — the mangle scenario from #11215). Three layers, all opt-out via HERMES_DISABLE_FILE_STATE_GUARD=1: 1. FileStateRegistry (tools/file_state.py) — process-wide singleton tracking per-agent read stamps and the last writer globally. check_stale() names the sibling subagent in the warning when a non-owning agent wrote after this agent's last read. 2. Per-path threading.Lock wrapped around the read-modify-write region in write_file_tool and patch_tool. Concurrent siblings on the same path serialize; different paths stay fully parallel. V4A multi-file patches lock in sorted path order (deadlock-free). 3. Delegate-completion reminder in tools/delegate_tool.py: after a subagent returns, writes_since(parent, child_start, parent_reads) appends '[NOTE: subagent modified files the parent previously read — re-read before editing: ...]' to entry.summary when the child touched anything the parent had already seen. Complements (does not replace) the existing path-overlap check in run_agent._should_parallelize_tool_batch — batch check prevents same-file parallel dispatch within one agent's turn (cheap prevention, zero API cost), registry catches cross-subagent and cross-turn staleness at write time (detection). Behavior is warning-only, not hard-failing — matches existing project style. Errors surface naturally: sibling writes often invalidate the old_string in patch operations, which already errors cleanly. Tests: tests/tools/test_file_state_registry.py — 16 tests covering registry state transitions, per-path locking, per-path-not-global locking, writes_since filtering, kill switch, and end-to-end integration through the real read_file/write_file/patch handlers.
Egavasyug
pushed a commit
to Egavasyug/hermes-agent
that referenced
this pull request
Jun 10, 2026
…enticate() (NousResearch#13468) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * refactor(acp): validate method_id against advertised provider in authenticate() Previously authenticate() accepted any method_id whenever the server had provider credentials configured. This was not a vulnerability under the personal-assistant trust model (ACP is stdio-only, local-trust — anything that can reach the transport is already code-execution-equivalent to the user), but it was sloppy API hygiene: the advertised auth_methods list from initialize() was effectively ignored. Now authenticate() only returns AuthenticateResponse when method_id matches the currently-advertised provider (case-insensitive). Mismatched or missing method_id returns None, consistent with the no-credentials case. Raised by xeloxa via GHSA-g5pf-8w9m-h72x. Declined as a CVE (ACP transport is stdio, local-trust model), but the correctness fix is worth having on its own.
Egavasyug
pushed a commit
to Egavasyug/hermes-agent
that referenced
this pull request
Jun 10, 2026
…bagents (NousResearch#13718) * feat(models): hide OpenRouter models that don't advertise tool support Port from Kilo-Org/kilocode#9068. hermes-agent is tool-calling-first — every provider path assumes the model can invoke tools. Models whose OpenRouter supported_parameters doesn't include 'tools' (e.g. image-only or completion-only models) cannot be driven by the agent loop and fail at the first tool call. Filter them out of fetch_openrouter_models() so they never appear in the model picker (`hermes model`, setup wizard, /model slash command). Permissive when the field is missing — OpenRouter-compatible gateways (Nous Portal, private mirrors, older snapshots) don't always populate supported_parameters. Treat missing as 'unknown → allow' rather than silently emptying the picker on those gateways. Only hide models whose supported_parameters is an explicit list that omits tools. Tests cover: tools present → kept, tools absent → dropped, field missing → kept, malformed non-list → kept, non-dict item → kept, empty list → dropped. * feat(delegate): cross-agent file state coordination for concurrent subagents Prevents mangled edits when concurrent subagents touch the same file (same process, same filesystem — the mangle scenario from NousResearch#11215). Three layers, all opt-out via HERMES_DISABLE_FILE_STATE_GUARD=1: 1. FileStateRegistry (tools/file_state.py) — process-wide singleton tracking per-agent read stamps and the last writer globally. check_stale() names the sibling subagent in the warning when a non-owning agent wrote after this agent's last read. 2. Per-path threading.Lock wrapped around the read-modify-write region in write_file_tool and patch_tool. Concurrent siblings on the same path serialize; different paths stay fully parallel. V4A multi-file patches lock in sorted path order (deadlock-free). 3. Delegate-completion reminder in tools/delegate_tool.py: after a subagent returns, writes_since(parent, child_start, parent_reads) appends '[NOTE: subagent modified files the parent previously read — re-read before editing: ...]' to entry.summary when the child touched anything the parent had already seen. Complements (does not replace) the existing path-overlap check in run_agent._should_parallelize_tool_batch — batch check prevents same-file parallel dispatch within one agent's turn (cheap prevention, zero API cost), registry catches cross-subagent and cross-turn staleness at write time (detection). Behavior is warning-only, not hard-failing — matches existing project style. Errors surface naturally: sibling writes often invalidate the old_string in patch operations, which already errors cleanly. Tests: tests/tools/test_file_state_registry.py — 16 tests covering registry state transitions, per-path locking, per-path-not-global locking, writes_since filtering, kill switch, and end-to-end integration through the real read_file/write_file/patch handlers.
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.
Summary
Kilo Gateway models that don't advertise
toolsin their supported parameters are now filtered out infetchKiloModels, so they no longer appear in the model picker. Kilo requires tool calling, so showing these models leads to immediate runtime failures when a user selects them.