Commit Graph

1677 Commits

Author SHA1 Message Date
alt-glitch
aad40f6d0c fix(tests): update mocks for file sync changes
- Modal snapshot tests: accept **kw in iter_skills_files/iter_cache_files
  mock lambdas to match new container_base kwarg
- SSH preflight test: mock _detect_remote_home, _ensure_remote_dirs,
  init_session, and FileSyncManager added in file sync PR
2026-04-10 03:01:46 -07:00
alt-glitch
41c233cb99 test: add reproducible perf benchmark for file sync overhead
Direct env.execute() timing — no LLM in the loop.
Measures per-command wall-clock including sync check.

Results on SSH:
- echo median: 617ms (pure SSH round-trip + spawn overhead)
- sync-triggered after 6s wait: 621ms (mtime skip adds ~0ms)
- within-interval (no sync): 618ms

Confirms mtime skip makes sync overhead unmeasurable.
2026-04-10 03:01:46 -07:00
alt-glitch
1f1f297528 feat(environments): unified file sync with change tracking and deletion
Replace per-backend ad-hoc file sync with a shared FileSyncManager
that handles mtime-based change detection, remote deletion of
locally-removed files, and transactional state updates.

- New FileSyncManager class (tools/environments/file_sync.py)
  with callbacks for upload/delete, rate limiting, and rollback
- Shared iter_sync_files() eliminates 3 duplicate implementations
- SSH: replace unconditional rsync with scp + mtime skip
- Modal/Daytona: replace inline _synced_files dict with manager
- All 3 backends now sync credentials + skills + cache uniformly
- Remote deletion: files removed locally are cleaned from remote
- HERMES_FORCE_FILE_SYNC=1 env var for debugging
- Base class _before_execute() simplified to empty hook
- 12 unit tests covering mtime skip, deletion, rollback, rate limiting
2026-04-10 03:01:46 -07:00
Yuhan Lei
f92298fe95 fix(acp): populate usage from top-level result fields 2026-04-10 03:00:12 -07:00
Teknium
a420235b66 fix: reject foreground timeout above cap instead of clamping
Change behavior from silent clamping to returning an error when the
model requests a foreground timeout exceeding FOREGROUND_MAX_TIMEOUT.
This forces the model to use background=true for long-running commands
rather than silently changing its intent.

- Config default timeouts above the cap are NOT rejected (user's choice)
- Only explicit model-requested timeouts trigger rejection
- Added boundary test for timeout exactly at the limit
2026-04-10 02:58:54 -07:00
kshitijk4poor
6c3565df57 fix(terminal): cap foreground timeout to prevent session deadlocks
When the model calls terminal() in foreground mode without background=true
(e.g. to start a server), the tool call blocks until the command exits or
the timeout expires. Without an upper bound the model can request arbitrarily
high timeouts (the schema had minimum=1 but no maximum), blocking the entire
agent session for hours until the gateway idle watchdog kills it.

Changes:
- Add FOREGROUND_MAX_TIMEOUT (600s, configurable via
  TERMINAL_MAX_FOREGROUND_TIMEOUT env var) that caps foreground timeout
- Clamp effective_timeout to the cap when background=false and timeout
  exceeds the limit
- Include a timeout_note in the tool result when clamped, nudging the
  model to use background=true for long-running processes
- Update schema description to show the max timeout value
- Remove dead clamping code in the background branch that could never
  fire (max_timeout was set to effective_timeout, so timeout > max_timeout
  was always false)
- Add 7 tests covering clamping, no-clamping, config-default-exceeds-cap
  edge case, background bypass, default timeout, constant value, and
  schema content

Self-review fixes:
- Fixed bug where timeout_note said 'Requested timeout Nones' when
  clamping fired from config default exceeding cap (timeout param is
  None). Now uses unclamped_timeout instead of the raw timeout param.
- Removed unused pytest import from test file
- Extracted test config dict into _make_env_config() helper
- Fixed tautological test_default_value assertion
- Added missing test for config default > cap with no model timeout
2026-04-10 02:58:54 -07:00
kshitijk4poor
51d826f889 fix(gateway): apply /model session overrides so switch persists across messages
The gateway /model command stored session overrides in
_session_model_overrides but run_sync() never consulted them when
resolving the model and runtime for the next message.  It always read
from config.yaml, so the switch was lost as soon as a new agent was
created.

Two fixes:

1. In run_sync(), apply _session_model_overrides after resolving from
   config.yaml/env — the override takes precedence for model, provider,
   api_key, base_url, and api_mode.

2. In post-run fallback detection, check whether the model mismatch
   (agent.model != config_model) is due to an intentional /model switch
   before evicting the cached agent.  Without this, the first message
   after /model would work (cached agent reused) but the fallback
   detector would evict it, causing the next message to revert.

Affects all gateway platforms (Telegram, Discord, Slack, WhatsApp,
Signal, Matrix, BlueBubbles, HomeAssistant) since they all share
GatewayRunner._run_agent().

Fixes #6213
2026-04-10 02:58:42 -07:00
Young
940237c6fd fix(cli): prevent stale image attachment on text paste and voice input
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 02:58:18 -07:00
Carlos
7368854398 Refresh OpenRouter model catalog
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 02:57:39 -07:00
Carlos
38ccd9eb95 Harden setup provider flows
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 02:57:39 -07:00
Teknium
6da952bc50
fix(gateway): /usage now shows rate limits, cost, and token details between turns (#7038)
The gateway /usage handler only looked in _running_agents for the agent
object, which is only populated while the agent is actively processing a
message. Between turns (when users actually type /usage), the dict is
empty and the handler fell through to a rough message-count estimate.

The agent object actually lives in _agent_cache between turns (kept for
prompt caching). This fix checks both dicts, with _running_agents taking
priority (mid-turn) and _agent_cache as the between-turns fallback.

Also brings the gateway output to parity with the CLI /usage:
- Model name
- Detailed token breakdown (input, output, cache read, cache write)
- Cost estimation (estimated amount or 'included' for subscriptions)
- Cache token lines hidden when zero (cleaner output)

This fixes Nous Portal rate limit headers not showing up for gateway
users — the data was being captured correctly but the handler could
never see it.
2026-04-10 02:33:01 -07:00
Teknium
8779a268a7
feat: add Anthropic Fast Mode support to /fast command (#7037)
Extends the /fast command to support Anthropic's Fast Mode beta in addition
to OpenAI Priority Processing. When enabled on Claude Opus 4.6, adds
speed:"fast" and the fast-mode-2026-02-01 beta header to API requests for
~2.5x faster output token throughput.

Changes:
- hermes_cli/models.py: Add _ANTHROPIC_FAST_MODE_MODELS registry,
  model_supports_fast_mode() now recognizes Claude Opus 4.6,
  resolve_fast_mode_overrides() returns {speed: fast} for Anthropic
  vs {service_tier: priority} for OpenAI
- agent/anthropic_adapter.py: Add _FAST_MODE_BETA constant,
  build_anthropic_kwargs() accepts fast_mode=True which injects
  speed:fast + beta header via extra_headers (skipped for third-party
  Anthropic-compatible endpoints like MiniMax)
- run_agent.py: Pass fast_mode to build_anthropic_kwargs in the
  anthropic_messages path of _build_api_kwargs()
- cli.py: Update _handle_fast_command with provider-aware messaging
  (shows 'Anthropic Fast Mode' vs 'Priority Processing')
- hermes_cli/commands.py: Update /fast description to mention both
  providers
- tests: 13 new tests covering Anthropic model detection, override
  resolution, CLI availability, routing, adapter kwargs, and
  third-party endpoint safety
2026-04-10 02:32:15 -07:00
Teknium
0848a79476
fix(update): always reset on stash conflict — never leave conflict markers (#7010)
When `hermes update` stashes local changes and the restore hits merge
conflicts, the old code prompted the user to reset or keep conflict
markers.  If the user declined the reset, git conflict markers
(<<<<<<< Updated upstream) were left in source files, making hermes
completely unrunnable with a SyntaxError on the next invocation.

Additionally, the interactive path called sys.exit(1), which killed
the entire update process before pip dependency install, skill sync,
and gateway restart could finish — even though the code pull itself
had succeeded.

Changes:
- Always auto-reset to clean state when stash restore conflicts
- Remove the "Reset working tree?" prompt (footgun)
- Remove sys.exit(1) — return False so cmd_update continues normally
- User's changes remain safely in the stash for manual recovery

Also fixes a secondary bug where the conflict handling prompt used
bare input() instead of the input_fn parameter, which would hang
in gateway mode.

Tests updated: replaced prompt/sys.exit assertions with auto-reset
behavior checks; removed the "user declines reset" test (path no
longer exists).
2026-04-10 00:32:20 -07:00
Teknium
871313ae2d
fix: clear conversation_history after mid-loop compression to prevent empty sessions (#7001)
After mid-loop compression (triggered by 413, context_overflow, or Anthropic
long-context tier errors), _compress_context() creates a new session in SQLite
and resets _last_flushed_db_idx=0. However, conversation_history was not cleared,
so _flush_messages_to_session_db() computed:

    flush_from = max(len(conversation_history=200), _last_flushed_db_idx=0) = 200
    messages[200:] → empty (compressed messages < 200)

This resulted in zero messages being written to the new session's SQLite store.
On resume, the user would see 'Session found but has no messages.'

The preflight compression path (line 7311) already had the fix:
    conversation_history = None

This commit adds the same clearing to the three mid-loop compression sites:
- Anthropic long-context tier overflow
- HTTP 413 payload too large
- Generic context_overflow error

Reported by Aaryan (Nous community).
2026-04-10 00:14:59 -07:00
Teknium
8104f400f8 test: disable text batching in existing adapter tests
Set _text_batch_delay_seconds = 0 on test adapter fixtures so messages
dispatch immediately (bypassing async batching). This preserves the
existing synchronous assertion patterns while the batching logic is
tested separately in test_text_batching.py.
2026-04-09 23:25:27 -07:00
Teknium
1ed00496f2 test: add text batching tests for Discord, Matrix, WeCom, Telegram, Feishu
22 tests covering:
- Single message dispatch after delay
- Split message aggregation (2-way and 3-way)
- Different chats/rooms not merged
- Adaptive delay for near-limit chunks
- State cleanup after flush
- Split continuation merging

All 5 platform adapters tested.
2026-04-09 23:25:27 -07:00
Teknium
f783986f5a
fix: increase stream read timeout default to 120s, auto-raise for local LLMs (#6967)
Raise the default httpx stream read timeout from 60s to 120s for all
providers. Additionally, auto-detect local LLM endpoints (Ollama,
llama.cpp, vLLM) and raise the read timeout to HERMES_API_TIMEOUT
(1800s) since local models can take minutes for prefill on large
contexts before producing the first token.

The stale stream timeout already had this local auto-detection pattern;
the httpx read timeout was missing it — causing a hard 60s wall that
users couldn't find (HERMES_STREAM_READ_TIMEOUT was undocumented).

Changes:
- Default HERMES_STREAM_READ_TIMEOUT: 60s -> 120s
- Auto-detect local endpoints -> raise to 1800s (user override respected)
- Document HERMES_STREAM_READ_TIMEOUT and HERMES_STREAM_STALE_TIMEOUT
- Add 10 parametrized tests

Reported-by: Pavan Srinivas (@pavanandums)
2026-04-09 22:35:30 -07:00
emozilla
bda9aa17cb fix(streaming): prevent <think> in prose from suppressing response output
When the model mentions <think> as literal text in its response (e.g.
"(/think not producing <think> tags)"), the streaming display treated it
as a reasoning block opener and suppressed everything after it. The
response box would close with truncated content and no error — the API
response was complete but the display ate it.

Root cause: _stream_delta() matched <think> anywhere in the text stream
regardless of position. Real reasoning blocks always start at the
beginning of a line; mentions in prose appear mid-sentence.

Fix: track line position across streaming deltas with a
_stream_last_was_newline flag. Only enter reasoning suppression when
the tag appears at a block boundary (start of stream, after a newline,
or after only whitespace on the current line). Add a _flush_stream()
safety net that recovers buffered content if no closing tag is found
by end-of-stream.

Also fixes three related issues discovered during investigation:

- anthropic_adapter: _get_anthropic_max_output() now normalizes dots to
  hyphens so 'claude-opus-4.6' matches the 'claude-opus-4-6' table key
  (was returning 32K instead of 128K)

- run_agent: send explicit max_tokens for Claude models on Nous Portal,
  same as OpenRouter — both proxy to Anthropic's API which requires it.
  Without it the backend defaults to a low limit that truncates responses.

- run_agent: reset truncated_tool_call_retries after successful tool
  execution so a single truncation doesn't poison the entire conversation.
2026-04-09 22:16:36 -07:00
Teknium
8394b5ddd2
feat: expand /fast to all OpenAI Priority Processing models (#6960)
Previously /fast only supported gpt-5.4 and forced a provider switch to
openai-codex. Now supports all 13 models from OpenAI's Priority Processing
pricing table (gpt-5.4, gpt-5.4-mini, gpt-5.2, gpt-5.1, gpt-5, gpt-5-mini,
gpt-4.1, gpt-4.1-mini, gpt-4.1-nano, gpt-4o, gpt-4o-mini, o3, o4-mini).

Key changes:
- Replaced _FAST_MODE_BACKEND_CONFIG with _PRIORITY_PROCESSING_MODELS frozenset
- Removed provider-forcing logic — service_tier is now injected into whatever
  API path the user is already on (Codex Responses, Chat Completions, or
  OpenRouter passthrough)
- Added request_overrides support to chat_completions path in run_agent.py
- Updated messaging from 'Codex inference tier' to 'Priority Processing'
- Expanded test coverage for all supported models
2026-04-09 22:06:30 -07:00
g-guthrie
d416a69288 feat: add Codex fast mode toggle (/fast command)
Add /fast slash command to toggle OpenAI Codex service_tier between
normal and priority ('fast') inference. Only exposed for models
registered in _FAST_MODE_BACKEND_CONFIG (currently gpt-5.4).

- Registry-based backend config for extensibility
- Dynamic command visibility (hidden from help/autocomplete for
  non-supported models) via command_filter on SlashCommandCompleter
- service_tier flows through request_overrides from route resolution
- Omit max_output_tokens for Codex backend (rejects it)
- Persists to config.yaml under agent.service_tier

Salvage cleanup: removed simple_term_menu/input() menu (banned),
bare /fast now shows status like /reasoning. Removed redundant
override resolution in _build_api_kwargs — single source of truth
via request_overrides from route.

Co-authored-by: Hermes Agent <hermes@nousresearch.com>
2026-04-09 21:54:32 -07:00
kshitijk4poor
08e2a1a51e fix(anthropic): omit tool-streaming beta on MiniMax endpoints
MiniMax's Anthropic-compatible endpoints reject requests that include
the fine-grained-tool-streaming beta header — every tool-use message
triggers a connection error (~18s timeout). Regular chat works fine.

Add _common_betas_for_base_url() that filters out the tool-streaming
beta for Bearer-auth (MiniMax) endpoints while keeping all other betas.
All four client-construction branches now use the filtered list.

Based on #6528 by @HiddenPuppy.
Original cherry-picked from PR #6688 by kshitijk4poor.
Fixes #6510, fixes #6555.
2026-04-09 17:53:52 -07:00
Teknium
9634e20e15
feat: API server model name derived from profile name (#6857)
* feat: API server model name derived from profile name

For multi-user setups (e.g. OpenWebUI), each profile's API server now
advertises a distinct model name on /v1/models:

- Profile 'lucas' -> model ID 'lucas'
- Profile 'admin' -> model ID 'admin'
- Default profile -> 'hermes-agent' (unchanged)

Explicit override via API_SERVER_MODEL_NAME env var or
platforms.api_server.model_name config for custom names.

Resolves friction where OpenWebUI couldn't distinguish multiple
hermes-agent connections all advertising the same model name.

* docs: multi-user setup with profiles for API server + Open WebUI

- api-server.md: added Multi-User Setup section, API_SERVER_MODEL_NAME
  to config table, updated /v1/models description
- open-webui.md: added Multi-User Setup with Profiles section with
  step-by-step guide, updated model name references
- environment-variables.md: added API_SERVER_MODEL_NAME entry
2026-04-09 17:07:29 -07:00
AIandI0x1
2d0d05a337 fix(agent): detect truncated streaming tool calls before execution
When a streaming response is cut mid-tool-call (connection drop, timeout),
the accumulated function.arguments is invalid JSON. The mock response
builder defaulted finish_reason to 'stop', so the agent loop treated it
as a valid completed turn and tried to execute tools with broken args.

Fix: validate tool call arguments with json.loads() during mock response
reconstruction. If any are invalid JSON, override finish_reason to
'length'. In the main loop's length handler, if tool calls are present,
refuse to execute and return partial=True with a clear error instead of
silently failing or wasting retries.

Also fixes _thinking_exhausted to not short-circuit when tool calls are
present — truncated tool calls are not thinking exhaustion.

Original cherry-picked from PR #6776 by AIandI0x1.
Closes #6638.
2026-04-09 17:03:54 -07:00
Teknium
3b554bf839 fix: test for suppress_status_output should capture stdout, not mock _vprint
The test was mocking _vprint entirely, bypassing the suppress guard.
Switch to capturing _print_fn output so the real _vprint runs and
the guard suppresses retry noise as intended.
2026-04-09 16:24:53 -07:00
adybag14-cyber
c3141429b7 fix(termux): tighten voice setup and mobile chat UX 2026-04-09 16:24:53 -07:00
adybag14-cyber
769ec1ee1a fix(termux): deepen browser, voice, and tui support 2026-04-09 16:24:53 -07:00
adybag14-cyber
3237733ca5 fix(termux): harden execute_code and mobile browser/audio UX 2026-04-09 16:24:53 -07:00
adybag14-cyber
54d5138a54 fix(termux): harden env-backed background jobs 2026-04-09 16:24:53 -07:00
adybag14-cyber
6dcb3c4774 fix(termux): compact narrow-screen tui chrome 2026-04-09 16:24:53 -07:00
adybag14-cyber
096b3f9f12 fix(termux): add local image chat route 2026-04-09 16:24:53 -07:00
adybag14-cyber
a3aed1bd26 fix(termux): keep quiet chat output parseable 2026-04-09 16:24:53 -07:00
adybag14-cyber
4970705ed3 fix(termux): silence quiet chat tool previews 2026-04-09 16:24:53 -07:00
adybag14-cyber
2194425918 fix(termux): make setup-hermes use android path 2026-04-09 16:24:53 -07:00
adybag14-cyber
3878495972 fix(termux): disable gateway service flows on android 2026-04-09 16:24:53 -07:00
adybag14-cyber
4e40e93b98 fix(termux): improve status and install UX 2026-04-09 16:24:53 -07:00
adybag14-cyber
122925a6f2 fix(termux): honor temp dirs for local temp artifacts 2026-04-09 16:24:53 -07:00
adybag14-cyber
e79cc88985 feat: add tested Termux install path and EOF-aware gh auth 2026-04-09 16:24:53 -07:00
sprmn24
e053433c84 fix(error_classifier): disambiguate usage-limit patterns in _classify_by_message
_classify_by_message had no handling for _USAGE_LIMIT_PATTERNS, so
messages like 'usage limit exceeded, try again in 5 minutes' arriving
without an HTTP status code fell through to FailoverReason.unknown
instead of rate_limit.

Apply the same billing/rate-limit disambiguation that _classify_402
already uses: USAGE_LIMIT_PATTERNS + transient signal → rate_limit,
USAGE_LIMIT_PATTERNS alone → billing.

Add 4 tests covering the no-status-code usage-limit path.
2026-04-09 16:24:13 -07:00
Siddharth Balyan
1789c2699a
feat(nix): shared-state permission model for interactive CLI users (#6796)
* feat(nix): shared-state permission model for interactive CLI users

Enable interactive CLI users in the hermes group to share full
read-write state (sessions, memories, logs, cron) with the gateway
service via a setgid + group-writable permission model.

Changes:

nix/nixosModules.nix:
- Directories use setgid 2770 (was 0750) so new files inherit the
  hermes group. home/ stays 0750 (no interactive write needed).
- Activation script creates HERMES_HOME subdirs (cron, sessions, logs,
  memories) — previously Python created them but managed mode now skips
  mkdir.
- Activation migrates existing runtime files to group-writable (chmod
  g+rw). Nix-managed files (config.yaml, .env, .managed) stay 0640/0644.
- Gateway systemd unit gets UMask=0007 so files it creates are 0660.

hermes_cli/config.py:
- ensure_hermes_home() splits into managed/unmanaged paths. Managed mode
  verifies dirs exist (raises RuntimeError if not) instead of creating
  them. Scoped umask(0o007) ensures SOUL.md is created as 0660.

hermes_logging.py:
- _ManagedRotatingFileHandler subclass applies chmod 0660 after log
  rotation in managed mode. RotatingFileHandler.doRollover() creates new
  files via open() which uses the process umask (0022 → 0644), not the
  scoped umask from ensure_hermes_home().

Verified with a 13-subtest NixOS VM integration test covering setgid,
interactive writes, file ownership, migration, and gateway coexistence.

Refs: #6044

* Fix managed log file mode on initial open

Co-authored-by: Siddharth Balyan <alt-glitch@users.noreply.github.com>

* refactor: simplify managed file handler and merge activation loops

- Cache is_managed() result in handler __init__ instead of lazy-importing
  on every _open()/_chmod_if_managed() call. Avoids repeated stat+env
  checks on log rotation.
- Merge two for-loops over the same subdir list in activation script
  into a single loop (mkdir + chown + chmod + find in one pass).

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Siddharth Balyan <alt-glitch@users.noreply.github.com>
2026-04-10 03:48:42 +05:30
Teknium
6b437f7934
fix: /browser connect auto-launch uses dedicated profile dir (#6821)
Chrome auto-launch now passes --user-data-dir, --no-first-run, and
--no-default-browser-check so the debug instance doesn't conflict with
an already-running Chrome using the default profile. The profile dir
lives at {hermes_home}/chrome-debug/.

Also updates the fallback manual instructions to include the same flags
and removes the stale 'close existing Chrome windows' hint.
2026-04-09 14:55:45 -07:00
Teknium
f91fffbe33
Revert "fix: /browser connect auto-launch uses dedicated profile dir"
This reverts commit c3854e0f85.
2026-04-09 14:54:37 -07:00
Teknium
c3854e0f85
fix: /browser connect auto-launch uses dedicated profile dir
Chrome auto-launch now passes --user-data-dir, --no-first-run, and
--no-default-browser-check so the debug instance doesn't conflict with
an already-running Chrome using the default profile. The profile dir
lives at {hermes_home}/chrome-debug/.

Also updates the fallback manual instructions to include the same flags
and removes the stale 'close existing Chrome windows' hint.
2026-04-09 14:52:58 -07:00
Teknium
c8bbd29aae fix: update tests for gws migration
- Rewrite test_google_workspace_api.py: test bridge token handling
  and calendar date range instead of removed get_credentials()
- Update test_google_oauth_setup.py: partial scopes now accepted
  with warning instead of rejected with SystemExit
2026-04-09 14:28:35 -07:00
Greer Guthrie
775a46ce75 fix: normalize reasoning effort ordering in UI 2026-04-09 14:20:16 -07:00
Zheng Li
88dbbfe982 feat(gateway): unified proxy support for Discord and Telegram with macOS auto-detection
- Add resolve_proxy_url() to base.py — shared by all platform adapters
- Check HTTPS_PROXY / HTTP_PROXY / ALL_PROXY env vars first
- Fall back to macOS system proxy via scutil --proxy (zero-config)
- Pass proxy= to discord.py commands.Bot() for gateway connectivity
- Refactor telegram_network.py to use shared resolver
- Update test fixtures to accept proxy kwarg
2026-04-09 14:19:06 -07:00
dashed
7f7b02b764 fix(slack): comprehensive mrkdwn formatting — 6 bug fixes + 52 tests
Fixes blockquote > escaping, edit_message raw markdown, ***bold italic***
handling, HTML entity double-escaping (&amp;amp;), Wikipedia URL parens
truncation, and step numbering format. Also adds format_message to the
tool-layer _send_to_platform for consistent formatting across all
delivery paths.

Changes:
- Protect Slack entities (<@user>, <https://...|label>, <!here>) from
  escaping passes
- Protect blockquote > markers before HTML entity escaping
- Unescape-before-escape for idempotent HTML entity handling
- ***bold italic*** → *_text_* conversion (before **bold** pass)
- URL regex upgraded to handle balanced parentheses
- mrkdwn:True flag on chat_postMessage payloads
- format_message applied in edit_message and send_message_tool
- 52 new tests (format, edit, streaming, splitting, tool chunking)
- Use reversed(dict) idiom for placeholder restoration

Based on PR #3715 by dashed, cherry-picked onto current main.
2026-04-09 14:07:32 -07:00
Doruk Ardahan
7d499c75db feat(slack): add require_mention and free_response_channels config support
Port the mention gating pattern from Telegram, Discord, WhatsApp, and
Matrix adapters to the Slack platform adapter.

- Add _slack_require_mention() with explicit-false parsing and env var
  fallback (SLACK_REQUIRE_MENTION)
- Add _slack_free_response_channels() with env var fallback
  (SLACK_FREE_RESPONSE_CHANNELS)
- Replace hardcoded mention check with configurable gating logic
- Bridge slack config.yaml settings to env vars
- Bridge free_response_channels through the generic platform bridging loop
- Add 26 tests covering config parsing, env fallback, gating logic

Config usage:
  slack:
    require_mention: false
    free_response_channels:
      - "C0AQWDLHY9M"

Default behavior unchanged: channels require @mention (backward compatible).

Based on PR #5885 by dorukardahan, cherry-picked and adapted to current main.
2026-04-09 14:07:32 -07:00
Dylan Socolobsky
c6dba918b3
fix(tests): fix several failing/flaky tests on main (#6777)
* fix(tests): mock is_safe_url in tests that use example.com

Tests using example.com URLs were failing because is_safe_url does a real DNS lookup which fails in environments where example.com doesn't resolve, causing the request to be blocked before reaching the already-mocked HTTP client. This should fix around 17 failing tests.

These tests test logic, caching, etc. so mocking this method should not modify them in any way. TestMattermostSendUrlAsFile was already doing this so we follow the same pattern.

* fix(test): use case-insensitive lookup for model context length check

DEFAULT_CONTEXT_LENGTHS uses inconsistent casing (MiniMax keys are lowercase, Qwen keys are mixed-case) so the test was broken in some cases since it couldn't find the model.

* fix(test): patch is_linux in systemd gateway restart test

The test only patched is_macos to False but didn't patch is_linux to True. On macOS hosts, is_linux() returns False and the systemd restart code path is skipped entirely, making the assertion fail.

* fix(test): use non-blocklisted env var in docker forward_env tests

GITHUB_TOKEN is in api_key_env_vars and thus in _HERMES_PROVIDER_ENV_BLOCKLIST so the env var is silently dropped, we replace it with a non-blocked one like DATABASE_URL so the tests actually work.

* fix(test): fully isolate _has_any_provider_configured from host env

_has_any_provider_configured() checks all env vars from PROVIDER_REGISTRY (not just the 5 the tests were clearing) and also calls get_auth_status() which detects gh auth token for Copilot. On machines with any of these set, the function returns True before reaching the code path under test.

Clear all registry vars and mock get_auth_status so host credentials don't interfere.

* fix(test): correct path to hermes_base_env.py in tool parser tests

Path(__file__).parent.parent resolved to tests/, not the project root.
The file lives at environments/hermes_base_env.py so we need one more parent level.

* fix(test): accept optional HTML fields in Matrix send payload

_send_matrix sometimes adds format and formatted_body when the markdown library is installed. The test was doing an exact dict equality check which broke. Check required fields instead.

* fix(test): add config.yaml to codex vision requirements test

The test only wrote auth.json but not config.yaml, so _read_main_provider() returned empty and vision auto-detect never tried the codex provider. Add a config.yaml pointing at openai-codex so the fallback path actually resolves the client.

* fix(test): clear OPENROUTER_API_KEY in _isolate_hermes_home

run_agent.py calls load_hermes_dotenv() at import time, which injects API keys from ~/.hermes/.env into os.environ before any test fixture runs. This caused test_agent_loop_tool_calling to make real API calls instead of skipping, which ends up making some tests fail.

* fix(test): add get_rate_limit_state to agent mock in usage report tests

_show_usage now calls agent.get_rate_limit_state() for rate limit
  display. The SimpleNamespace mock was missing this method.

* fix(test): update expected Camofox config version from 12 to 13

* fix(test): mock _get_enabled_platforms in nous managed defaults test

Importing gateway.run leaks DISCORD_BOT_TOKEN into os.environ, which makes _get_enabled_platforms() return ["cli", "discord"] instead of just ["cli"]. tools_command loops per platform, so apply_nous_managed_defaults
  runs twice: the first call sets config values, the second sees them as
  already configured and returns an empty set, causing the assertion to
  fail.
2026-04-09 13:17:06 -07:00
Teknium
3eade90b39
fix: OpenClaw migration now shows dry-run preview before executing (#6769)
The setup wizard's OpenClaw migration previously ran immediately with
aggressive defaults (overwrite=True, preset=full) after a single
'Would you like to import?' prompt. This caused several problems:

- Config values with different semantics (e.g. tool_call_execution:
  'auto' in OpenClaw vs 'off' for Hermes yolo mode) were imported
  without translation
- Gateway tokens were hijacked from OpenClaw without warning, taking
  over Telegram/Slack/Discord channels
- Instruction files (.md) containing OpenClaw-specific setup/restart
  procedures were copied, causing Hermes restart failures

Now the migration:
1. Asks 'Would you like to see what can be imported?' (softer framing)
2. Runs a dry-run preview showing everything that would be imported
3. Displays categorized warnings for high-impact items (gateway
   takeover, config value differences, instruction files)
4. Asks for explicit confirmation with default=No
5. Executes with overwrite=False (preserves existing Hermes config)

Also extracts _load_openclaw_migration_module() for reuse and adds
_print_migration_preview() with keyword-based warning detection.

Tests updated for two-phase behavior + new test for decline-after-preview.
2026-04-09 12:15:06 -07:00
KUSH42
34d06a9802 fix(compaction): don't halve context_length on output-cap-too-large errors
When the API returns "max_tokens too large given prompt" (input tokens
are within the context window, but input + requested output > window),
the old code incorrectly routed through the same handler as "prompt too
long" errors, calling get_next_probe_tier() and permanently halving
context_length. This made things worse: the window was fine, only the
requested output size needed trimming for that one call.

Two distinct error classes now handled separately:

  Prompt too long  — input itself exceeds context window.
    Fix: compress history + halve context_length (existing behaviour,
    unchanged).

  Output cap too large — input OK, but input + max_tokens > window.
    Fix: parse available_tokens from the error message, set a one-shot
    _ephemeral_max_output_tokens override for the retry, and leave
    context_length completely untouched.

Changes:
- agent/model_metadata.py: add parse_available_output_tokens_from_error()
  that detects Anthropic's "available_tokens: N" error format and returns
  the available output budget, or None for all other error types.
- run_agent.py: call the new parser first in the is_context_length_error
  block; if it fires, set _ephemeral_max_output_tokens (with a 64-token
  safety margin) and break to retry without touching context_length.
  _build_api_kwargs consumes the ephemeral value exactly once then clears
  it so subsequent calls use self.max_tokens normally.
- agent/anthropic_adapter.py: expand build_anthropic_kwargs docstring to
  clearly document the max_tokens (output cap) vs context_length (total
  window) distinction, which is a persistent source of confusion due to
  the OpenAI-inherited "max_tokens" name.
- cli-config.yaml.example: add inline comments explaining both keys side
  by side where users are most likely to look.
- website/docs/integrations/providers.md: add a callout box at the top
  of "Context Length Detection" and clarify the troubleshooting entry.
- tests/test_ctx_halving_fix.py: 24 tests across four classes covering
  the parser, build_anthropic_kwargs clamping, ephemeral one-shot
  consumption, and the invariant that context_length is never mutated
  on output-cap errors.
2026-04-09 11:27:41 -07:00
Teknium
2772d99085
fix: remove /prompt slash command — footgun via prefix expansion (#6752)
/pr <anything> silently resolved to /prompt via the shortest-match
tiebreaker in prefix expansion, permanently overwriting the system
prompt and persisting to config. The command's functionality (setting
agent.system_prompt) is available via config.yaml and /personality
covers the common use case.

Removes: CommandDef, dispatch branch, _handle_prompt_command handler,
docs references, and updates subcommand extraction test.
2026-04-09 11:27:27 -07:00
Teknium
3007174a61
fix: prevent 400 format errors from triggering compression loop on Codex Responses API (#6751)
The error classifier's generic-400 heuristic only extracted err_body_msg from
the nested body structure (body['error']['message']), missing the flat body
format used by OpenAI's Responses API (body['message']). This caused
descriptive 400 errors like 'Invalid input[index].name: string does not match
pattern' to appear generic when the session was large, misclassifying them as
context overflow and triggering an infinite compression loop.

Added flat-body fallback in _classify_400() consistent with the parent
classify_api_error() function's existing handling at line 297-298.
2026-04-09 11:11:34 -07:00
Teknium
1a3ae6ac6e
feat: structured API error classification for smart failover (#6514)
Add agent/error_classifier.py with a priority-ordered classification
pipeline that replaces scattered inline string-matching in the retry
loop with structured error taxonomy and recovery hints.

FailoverReason enum (14 categories): auth, auth_permanent, billing,
rate_limit, overloaded, server_error, timeout, context_overflow,
payload_too_large, model_not_found, format_error, thinking_signature,
long_context_tier, unknown.

ClassifiedError dataclass carries reason + recovery action hints
(retryable, should_compress, should_rotate_credential, should_fallback).

Key improvements over inline matching:
- 402 disambiguation: 'insufficient credits' = billing (immediate rotate),
  'usage limit, try again' = rate_limit (backoff first)
- OpenRouter 403 'key limit exceeded' correctly classified as billing
- Error cause chain walking (walks __cause__/__context__ up to 5 levels)
- Body message included in pattern matching (SDK str() misses it)
- Server disconnect + large session check ordered before generic transport
  catch so RemoteProtocolError triggers compression when appropriate
- Chinese error message support for context overflow

run_agent.py: replaced 6 inline detection blocks with classifier calls,
net -55 lines. All recovery actions (pool rotation, fallback activation,
compression, transport recovery) unchanged.

65 new unit tests + 10 E2E tests + live tests with real SDK error objects.
Inspired by OpenClaw's failover error classification system.
2026-04-09 04:10:11 -07:00
Teknium
8dfc96dbbb
feat: capture provider rate limit headers and show in /usage (#6541)
Parse x-ratelimit-* headers from inference API responses (Nous Portal,
OpenRouter, OpenAI-compatible) and display them in the /usage command.

- New agent/rate_limit_tracker.py: parse 12 rate limit headers (RPM/RPH/
  TPM/TPH limits, remaining, reset timers), format as progress bars (CLI)
  or compact one-liner (gateway)
- Hook into streaming path in run_agent.py: stream.response.headers is
  available on the OpenAI SDK Stream object before chunks are consumed
- CLI /usage: appends rate limit section with progress bars + warnings
  when any bucket exceeds 80%
- Gateway /usage: appends compact rate limit summary
- 24 unit tests covering parsing, formatting, edge cases

Headers captured per response:
  x-ratelimit-{limit,remaining,reset}-{requests,tokens}{,-1h}

Example CLI display:
  Nous Rate Limits (captured just now):
    Requests/min [░░░░░░░░░░░░░░░░░░░░]  0.1%  1/800 used  (799 left, resets in 59s)
    Tokens/hr    [░░░░░░░░░░░░░░░░░░░░]  0.0%  49/336.0M   (336.0M left, resets in 52m)
2026-04-09 03:43:14 -07:00
konsisumer
3c8ec7037c fix(agent): catch PermissionError in subdirectory hint discovery
Wrap is_dir() in _is_valid_subdir() and is_file() in
_load_hints_for_directory() with OSError handlers so that
inaccessible directories (e.g. /root from a non-root Daytona
host user) are silently skipped instead of crashing the agent.

The existing PermissionError PRs for prompt_builder.py (#6247,
#6321, #6355) do not cover subdirectory_hints.py, which was
identified as a separate crash path in the #6214 comments.

Ref: #6214
2026-04-09 03:10:30 -07:00
Kira
161c2c4da4 fix(skills): archive OpenClaw cron store without config 2026-04-09 03:06:11 -07:00
Lumen Radley
e22416dd9b fix: handle empty sudo password and false prompts 2026-04-09 02:50:07 -07:00
Teknium
a94099908a
fix(state): orphan children instead of cascade-deleting in prune/delete (#6513)
prune_sessions and delete_session only handled direct children when
satisfying the parent_session_id FK constraint. Multi-level chains
(A -> B -> C) caused IntegrityError because deleting B while C still
referenced it was blocked by the FK.

Fix: NULL out parent_session_id for any session whose parent is about
to be deleted. This orphans children instead of cascade-deleting them,
which also respects the prune retention window — newer child sessions
are no longer deleted just because an ancestor is old.

Reported by Aaryan2304 in PR #6463.
2026-04-09 02:41:56 -07:00
Teknium
b408379e9d
fix: reduce credential exhaustion TTL from 24 hours to 1 hour (#6504)
The 24-hour default cooldown for 402-exhausted credentials was far too
aggressive — if a user tops up credits or the 402 was caused by an
oversized max_tokens request rather than true billing exhaustion, they
shouldn't have to wait a full day. Reduce to 1 hour (matching the
existing 429 TTL).

Inspired by PR #6493 (michalkomar).
2026-04-09 02:37:23 -07:00
Kira
e1b0b135cb fix(discord): accept .log attachments and raise document size limit 2026-04-09 02:26:33 -07:00
Teknium
1eabbe905e
fix: retry 3 times when model returns truly empty response (#6488)
When a model returns no content, no structured reasoning, and no tool
calls (common with open models), the agent now silently retries up to
3 times before falling through to (empty).

Silent retry (no synthetic messages) keeps the conversation history
clean, preserves prompt caching, and respects the no-synthetic-user-
injection invariant.  Most empty responses from open models are
transient (provider hiccups, rate limits, sampling flukes) so a
simple retry is sufficient.

This fills the last gap in the empty-response recovery chain:
1. _last_content_with_tools fallback (prior tool turn had content)
2. Thinking-only prefill continuation (#5931 — structured reasoning)
3. Empty response silent retry (NEW — truly empty, no reasoning)
4. (empty) terminal (last resort after all retries exhausted)

Inline <think> blocks are excluded — the model chose to reason, it
just produced no visible text.  That differs from truly empty.

Tests:
- Updated test_truly_empty to expect 4 API calls (1 + 3 retries)
- Added test_truly_empty_response_succeeds_on_nudge
2026-04-09 02:06:12 -07:00
Cherif Yaya
5cf4fac2aa fix: restore codex fallback auth-store lookup 2026-04-09 01:56:10 -07:00
Teknium
d40264d53b test: add coverage for token-budget tail protection
Tests for the new behavior paths:
- Large tool outputs no longer block compaction (motivating scenario)
- Hard minimum of 3 tail messages always protected
- 1.5x soft ceiling for oversized messages
- Small conversations still compress (min 8 messages)
- Token-budget prune path in _prune_old_tool_results
- Fallback to message-count when no token budget
2026-04-08 23:54:23 -07:00
BongSuCHOI
c506126123 fix(tests): update context_compressor tests for min_tail=3
PR #6240 changed tail protection from protect_last_n to min(3, ...)
which increased the minimum compressible message count and shifted
tail boundaries. Three tests broke:

- test_summary_role_avoids_consecutive_user_messages: 6→8 msgs
- test_double_collision_user_head_assistant_tail: 7→8 msgs
- test_no_collision_scenarios_still_work: 6→8 msgs

All tests now exceed the new min_for_compress threshold (6) and
maintain proper role alternation in both head and tail sections.
2026-04-08 23:54:23 -07:00
Nicolò Boschi
25757d631b feat(hindsight): feature parity, setup wizard, and config improvements
Port missing features from the hindsight-hermes external integration
package into the native plugin. Only touches plugin files — no core
changes.

Features:
- Tags on retain/recall (tags, recall_tags, recall_tags_match)
- Recall config (recall_max_tokens, recall_max_input_chars, recall_types,
  recall_prompt_preamble)
- Retain controls (retain_every_n_turns, auto_retain, auto_recall,
  retain_async via aretain_batch, retain_context)
- Bank config via Banks API (bank_mission, bank_retain_mission)
- Structured JSON retain with per-message timestamps
- Full session accumulation with document_id for dedup
- Custom post_setup() wizard with curses picker
- Mode-aware dep install (hindsight-client for cloud, hindsight-all for local)
- local_external mode and openai_compatible LLM provider
- OpenRouter support with auto base URL
- Auto-upgrade of hindsight-client to >=0.4.22 on session start
- Comprehensive debug logging across all operations
- 46 unit tests
- Updated README and website docs
2026-04-08 23:54:15 -07:00
Teknium
d97f6cec7f
feat(gateway): add BlueBubbles iMessage platform adapter (#6437)
Adds Apple iMessage as a gateway platform via BlueBubbles macOS server.

Architecture:
- Webhook-based inbound (event-driven, no polling/dedup needed)
- Email/phone → chat GUID resolution for user-friendly addressing
- Private API safety (checks helper_connected before tapback/typing)
- Inbound attachment downloading (images, audio, documents cached locally)
- Markdown stripping for clean iMessage delivery
- Smart progress suppression for platforms without message editing

Based on PR #5869 by @benjaminsehl (webhook architecture, GUID resolution,
Private API safety, progress suppression) with inbound attachment downloading
from PR #4588 by @1960697431 (attachment cache routing).

Integration points: Platform enum, env config, adapter factory, auth maps,
cron delivery, send_message routing, channel directory, platform hints,
toolset definition, setup wizard, status display.

27 tests covering config, adapter, webhook parsing, GUID resolution,
attachment download routing, toolset consistency, and prompt hints.
2026-04-08 23:54:03 -07:00
Teknium
241bd4fc7e fix: add size cap to assistant thread metadata cache
Prevents unbounded memory growth in _assistant_threads dict.
Evicts oldest entries when exceeding _ASSISTANT_THREADS_MAX (5000),
matching the pattern used by _mentioned_threads and _seen_messages.
2026-04-08 23:53:50 -07:00
helix4u
30a0fcaec8 fix(slack): handle assistant thread lifecycle events 2026-04-08 23:53:50 -07:00
Teknium
5449c01d26 fix: clean env vars in pairing regression test
The test_non_internal_event_without_user_triggers_pairing test relied on
no Discord auth env vars being set, but gateway/run.py loads dotenv at
module level. In environments with DISCORD_ALLOW_ALL_USERS=True in .env,
the auth check passed instead of triggering the pairing flow.

Clear DISCORD_ALLOW_ALL_USERS, DISCORD_ALLOWED_USERS, GATEWAY_ALLOW_ALL_USERS,
and GATEWAY_ALLOWED_USERS via monkeypatch to ensure test isolation.
2026-04-08 23:01:04 -07:00
xingkongliang
1d8d4f28ae fix(gateway): prevent background process notifications from triggering false pairing requests
When a background process with notify_on_complete=True finishes, the
gateway injects a synthetic MessageEvent to notify the session. This
event was constructed without user_id, causing _is_user_authorized()
to reject it and — for DM-origin sessions — trigger the pairing flow,
sending "Hi~ I don't recognize you yet!" with a pairing code to the
chat owner.

Add an `internal` flag to MessageEvent that bypasses authorization
checks for system-generated synthetic events. Only the process watcher
sets this flag; no external/adapter code path can produce it.

Includes 4 regression tests covering the fix and the normal pairing path.
2026-04-08 23:01:04 -07:00
helix4u
e94008c404 fix(terminal): guard invalid command values 2026-04-08 21:37:51 -07:00
Teknium
54db7cbbe1
fix(agent): tiered context pressure warnings + gateway dedup (#6411)
Combines the approaches from PR #6309 (duan78) and PR #5963 (KUSH42):

Tiered warnings (from #5963):
- Replaces boolean _context_pressure_warned with float _context_pressure_warned_at
- Fires at 85% (orange) and re-fires at 95% (red/critical)
- Adds 'compacting context...' status message before compression

Gateway dedup (from #6309):
- Class-level dict _context_pressure_last_warned survives across AIAgent
  instances (gateway creates a new instance per message)
- 5-minute cooldown per session prevents warning spam
- Higher-tier warnings bypass the cooldown (85% → 95% always fires)
- Compression reset clears the dedup entry for the session
- Stale entries evicted (older than 2x cooldown) to prevent memory leak

Does NOT inject into messages — purely user-facing via _safe_print (CLI)
and status_callback (gateway). Zero prompt cache impact.

Fixes #6309. Fixes #5963.
2026-04-08 21:31:44 -07:00
Teknium
989d4ea43d fix: set compression_count on mock to avoid TypeError in test
The new degradation warning reads compression_count as an int,
but the existing test's MagicMock returns a MagicMock object
for that attribute, causing '>=' comparison to fail.
2026-04-08 20:54:23 -07:00
Helmi
092061711e fix(gateway): add staged inactivity warning before timeout escalation
Introduce gateway_timeout_warning (default 900s) as a pre-timeout alert
layer.  When inactivity reaches the warning threshold, a single
notification is sent to the user offering to wait or reset.  If
inactivity continues to the gateway_timeout (default 1800s), the full
timeout fires as before.

This gives users a chance to intervene before work is lost on slow
API providers without disabling the safety timeout entirely.

Config: agent.gateway_timeout_warning in config.yaml, or
HERMES_AGENT_TIMEOUT_WARNING env var (0 = disable warning).
2026-04-08 20:01:06 -07:00
Teknium
980fadfea9
fix(models): preserve OpenRouter variant tags (:free, :extended, :fast) during model switch (#6383)
Step c in switch_model() blindly converted the first colon to a slash for
aggregator providers, even when the model name already contained a slash
(vendor/model format). This mangled variant tags like :free into /free,
causing 400 Bad Request from the API.

Fix: skip the colon→slash conversion when the model already has a slash,
since the colon is a variant tag, not a vendor separator. The module
docstring already documented this intent (line 17-18) but the
implementation didn't enforce it.

Reported via Discord. Related to PR #6088 (which identified the same bug
but placed the fix in model_normalize.py instead of model_switch.py where
the actual mangling occurs).
2026-04-08 19:58:16 -07:00
konsisumer
42e366f27b fix(agent): respect config timeout for flush_memories instead of hardcoded 30s
The _call_llm() and direct OpenAI fallback paths in flush_memories() both
hardcoded timeout=30.0, ignoring the user-configurable value at
auxiliary.flush_memories.timeout in config.yaml.

Remove the explicit timeout from the auxiliary _call_llm() call so that
_get_task_timeout('flush_memories') reads from config. For the direct
OpenAI fallback, import and use _get_task_timeout() instead of the
hardcoded value.

Add two regression tests verifying both code paths respect the config.

Fixes #6154
2026-04-08 18:55:33 -07:00
Teknium
e26393ffc2
fix: Signal duplicate replies with streaming + per-platform tool_progress (#6348)
Fixes #4647 — Signal replies duplicated when gateway streaming is enabled.

Root cause: stream_consumer.py did not handle the case where send() returns
success=True but no message_id (Signal behavior). Every stream delta produced
a separate send() call (7+ messages instead of 2), plus the gateway sent
another full duplicate since already_sent was never set.

Changes:
- stream_consumer.py: Add elif branch for success-without-message_id — enters
  fallback mode (sets already_sent, disables editing, sends only continuation)
- signal.py send(): Extract timestamp from signal-cli RPC result as message_id
  so stream consumer follows normal edit→fallback path
- signal.py: Add public stop_typing() delegating to _stop_typing_indicator()
  so base adapter's _keep_typing finally block can clean up typing tasks
- gateway/run.py: Per-platform tool_progress_overrides (#6164) — lets users
  set e.g. signal: off while keeping telegram: all
- hermes_cli/config.py: Add tool_progress_overrides to DEFAULT_CONFIG

Refs: #4647, #6164
2026-04-08 17:39:45 -07:00
Teknium
e19252afc4 fix: update tests for unified spawn-per-call execution model
- Docker env tests: verify _build_init_env_args() instead of per-execute
  Popen flags (env forwarding is now init-time only)
- Docker: preserve explicit forward_env bypass of blocklist from main
- Daytona tests: adapt to SDK-native timeout, _ThreadedProcessHandle,
  base.py interrupt handling, HERMES_STDIN_ heredoc prefix
- Modal tests: fix _load_module to include _ThreadedProcessHandle stub,
  check ensurepip in _resolve_modal_image instead of __init__
- SSH tests: mock time.sleep on base module instead of removed ssh import
- Add missing BaseEnvironment attributes to __new__()-based test fixtures
2026-04-08 17:23:15 -07:00
alt-glitch
d684d7ee7e feat(environments): unified spawn-per-call execution layer
Replace dual execution model (PersistentShellMixin + per-backend oneshot)
with spawn-per-call + session snapshot for all backends except ManagedModal.

Core changes:
- Every command spawns a fresh bash process; session snapshot (env vars,
  functions, aliases) captured at init and re-sourced before each command
- CWD persists via file-based read (local) or in-band stdout markers (remote)
- ProcessHandle protocol + _ThreadedProcessHandle adapter for SDK backends
- cancel_fn wired for Modal (sandbox.terminate) and Daytona (sandbox.stop)
- Shared utilities extracted: _pipe_stdin, _popen_bash, _load_json_store,
  _save_json_store, _file_mtime_key, _SYNC_INTERVAL_SECONDS
- Rate-limited file sync unified in base _before_execute() with _sync_files() hook
- execute_oneshot() removed; all 11 call sites in code_execution_tool.py
  migrated to execute()
- Daytona timeout wrapper replaced with SDK-native timeout parameter
- persistent_shell.py deleted (291 lines)

Backend-specific:
- Local: process-group kill via os.killpg, file-based CWD read
- Docker: -e env flags only on init_session, not per-command
- SSH: shlex.quote transport, ControlMaster connection reuse
- Singularity: apptainer exec with instance://, no forced --pwd
- Modal: _AsyncWorker + _ThreadedProcessHandle, cancel_fn -> sandbox.terminate
- Daytona: SDK-level timeout (not shell wrapper), cancel_fn -> sandbox.stop
- ManagedModal: unchanged (gateway owns execution); docstring added explaining why
2026-04-08 17:23:15 -07:00
Teknium
7d26feb9a3
feat(discord): add DISCORD_REPLY_TO_MODE setting (#6333)
Add configurable reply-reference behavior for Discord, matching the
existing Telegram (TELEGRAM_REPLY_TO_MODE) and Mattermost
(MATTERMOST_REPLY_MODE) implementations.

Modes:
- 'off': never reply-reference the original message
- 'first': reply-reference on first chunk only (default, current behavior)
- 'all': reply-reference on every chunk

Set DISCORD_REPLY_TO_MODE=off in .env to disable reply-to messages.

Changes:
- gateway/config.py: parse DISCORD_REPLY_TO_MODE env var
- gateway/platforms/discord.py: read reply_to_mode from config, respect
  it in send() — skip fetch_message entirely when 'off'
- hermes_cli/config.py: add to OPTIONAL_ENV_VARS for hermes setup
- 23 tests covering config, send behavior, env var override
- docs: discord.md env var table + environment-variables.md reference

Closes community request from Stuart on Discord.
2026-04-08 17:08:40 -07:00
Teknium
7156f8d866
fix: CI test failures — metadata key, cli console, docker env, vision order (#6294)
Fixes 9 test failures on current main, incorporating ideas from PR stack
#6219-#6222 by xinbenlv with corrections:

- model_metadata: sync HF context length key casing
  (minimaxai/minimax-m2.5 → MiniMaxAI/MiniMax-M2.5)

- cli.py: route quick command error output through self.console
  instead of creating a new ChatConsole() instance

- docker.py: explicit docker_forward_env entries now bypass the
  Hermes secret blocklist (intentional opt-in wins over generic filter)

- auxiliary_client: revert _read_main_provider() to simple
  provider.strip().lower() — the _normalize_aux_provider() call
  introduced in 5c03f2e7 stripped the custom: prefix, breaking
  named custom provider resolution

- auxiliary_client: flip vision auto-detection order to
  active provider → OpenRouter → Nous → stop (was OR → Nous → active)

- test: update vision priority test to match new order

Based on PR #6219-#6222 by xinbenlv.
2026-04-08 16:37:05 -07:00
jjovalle99
d46db0a1b4 fix(tools): use correct import path for mistralai SDK
mistralai v2.x is a namespace package — `Mistral` class lives at
`mistralai.client`, not at the top-level `mistralai` module. The
previous `from mistralai import Mistral` raises ImportError at runtime.

Update both production code and test fixture to use the correct path.
2026-04-08 13:47:08 -07:00
jjovalle99
5f4b93c20f feat(tools): add Voxtral Transcribe STT provider (Mistral AI) 2026-04-08 13:47:08 -07:00
kshitijk4poor
3377017eb4 feat(qwen): add Qwen OAuth provider with portal request support
Based on #6079 by @tunamitom with critical fixes and comprehensive tests.

Changes from #6079:
- Fix: sanitization overwrite bug — Qwen message prep now runs AFTER codex
  field sanitization, not before (was silently discarding Qwen transforms)
- Fix: missing try/except AuthError in runtime_provider.py — stale Qwen
  credentials now fall through to next provider on auto-detect
- Fix: 'qwen' alias conflict — bare 'qwen' stays mapped to 'alibaba'
  (DashScope); use 'qwen-portal' or 'qwen-cli' for the OAuth provider
- Fix: hardcoded ['coder-model'] replaced with live API fetch + curated
  fallback list (qwen3-coder-plus, qwen3-coder)
- Fix: extract _is_qwen_portal() helper + _qwen_portal_headers() to replace
  5 inline 'portal.qwen.ai' string checks and share headers between init
  and credential swap
- Fix: add Qwen branch to _apply_client_headers_for_base_url for mid-session
  credential swaps
- Fix: remove suspicious TypeError catch blocks around _prompt_provider_choice
- Fix: handle bare string items in content lists (were silently dropped)
- Fix: remove redundant dict() copies after deepcopy in message prep
- Revert: unrelated ai-gateway test mock removal and model_switch.py comment deletion

New tests (30 test functions):
- _qwen_cli_auth_path, _read_qwen_cli_tokens (success + 3 error paths)
- _save_qwen_cli_tokens (roundtrip, parent creation, permissions)
- _qwen_access_token_is_expiring (5 edge cases: fresh, expired, within skew,
  None, non-numeric)
- _refresh_qwen_cli_tokens (success, preserve old refresh, 4 error paths,
  default expires_in, disk persistence)
- resolve_qwen_runtime_credentials (fresh, auto-refresh, force-refresh,
  missing token, env override)
- get_qwen_auth_status (logged in, not logged in)
- Runtime provider resolution (direct, pool entry, alias)
- _build_api_kwargs (metadata, vl_high_resolution_images, message formatting,
  max_tokens suppression)
2026-04-08 13:46:30 -07:00
Teknium
4f467700d4
fix(doctor): only check the active memory provider, not all providers unconditionally (#6285)
* fix(tools): skip camofox auto-cleanup when managed persistence is enabled

When managed_persistence is enabled, cleanup_browser() was calling
camofox_close() which destroys the server-side browser context via
DELETE /sessions/{userId}, killing login sessions across cron runs.

Add camofox_soft_cleanup() — a public wrapper that drops only the
in-memory session entry when managed persistence is on, returning True.
When persistence is off it returns False so the caller falls back to
the full camofox_close().  The inactivity reaper still handles idle
resource cleanup.

Also surface a logger.warning() when _managed_persistence_enabled()
fails to load config, replacing a silent except-and-return-False.

Salvaged from #6182 by el-analista (Eduardo Perea Fernandez).
Added public API wrapper to avoid cross-module private imports,
and test coverage for both persistence paths.

Co-authored-by: Eduardo Perea Fernandez <el-analista@users.noreply.github.com>

* fix(doctor): only check the active memory provider, not all providers unconditionally

hermes doctor had hardcoded Honcho Memory and Mem0 Memory sections that
always ran regardless of the user's memory.provider config setting. After
the swappable memory provider update (#4623), users with leftover Honcho
config but no active provider saw false 'broken' errors.

Replaced both sections with a single Memory Provider section that reads
memory.provider from config.yaml and only checks the configured provider.
Users with no external provider see a green 'Built-in memory active' check.

Reported by community user michaelruiz001, confirmed by Eri (Honcho).

---------

Co-authored-by: Eduardo Perea Fernandez <el-analista@users.noreply.github.com>
2026-04-08 13:44:58 -07:00
Teknium
8b0afa0e57
fix: aggressive worktree and branch cleanup to prevent accumulation (#6134)
Problem: hermes -w sessions accumulated 37+ worktrees and 1200+ orphaned
branches because:
- _cleanup_worktree bailed on any dirty working tree, but agent sessions
  almost always leave untracked files/artifacts behind
- _prune_stale_worktrees had the same dirty-check, so stale worktrees
  survived indefinitely
- pr-* and hermes/* branches from PR review had zero cleanup mechanism

Changes:
- _cleanup_worktree: check for unpushed commits instead of dirty state.
  Agent work lives in pushed commits/PRs — dirty working tree without
  unpushed commits is just artifacts, safe to remove.
- _prune_stale_worktrees: three-tier age system:
  - Under 24h: skip (session may be active)
  - 24h-72h: remove if no unpushed commits
  - Over 72h: force remove regardless
- New _prune_orphaned_branches: on each -w startup, deletes local
  hermes/hermes-* and pr-* branches with no corresponding worktree.
  Protects main, checked-out branch, and active worktree branches.

Tests: 42 pass (6 new covering unpushed-commit logic, force-prune
tier, and orphaned branch cleanup).
2026-04-08 04:44:49 -07:00
Teknium
ab21fbfd89 fix: add gateway coverage for session boundary hooks, move test to tests/cli/
- Fire on_session_finalize and on_session_reset in gateway _handle_reset_command()
- Fire on_session_finalize during gateway stop() for each active agent
- Move CLI test from tests/ root to tests/cli/ (matches recent restructure)
- Add 5 gateway tests covering reset hooks, ordering, shutdown, and error handling
- Place on_session_reset after new session is guaranteed to exist (covers
  the get_or_create_session fallback path)
2026-04-08 04:27:34 -07:00
Felipe de Leon
bdc72ec355 feat(cli): add on_session_finalize and on_session_reset plugin hooks
Plugins can now subscribe to session boundary events via
ctx.register_hook('on_session_finalize', ...) and
ctx.register_hook('on_session_reset', ...).

on_session_finalize — fires during CLI exit (/quit, Ctrl-C) and
before /new or /reset, giving plugins a chance to flush or clean up.

on_session_reset — fires after a new session is created via
/new or /reset, so plugins can initialize per-session state.

Closes #5592
2026-04-08 04:27:34 -07:00
Teknium
1368caf66f
fix(anthropic): smart thinking block signature management (#6112)
Anthropic signs thinking blocks against the full turn content. Any
upstream mutation (context compression, session truncation, orphan
stripping, message merging) invalidates the signature, causing HTTP 400
'Invalid signature in thinking block' — especially in long-lived
gateway sessions.

Strategy (following clawdbot/OpenClaw pattern):

1. Strip thinking/redacted_thinking from all assistant messages EXCEPT
   the last one — preserves reasoning continuity on the current
   tool-use chain while avoiding stale signature errors on older turns.

2. Downgrade unsigned thinking blocks to plain text — Anthropic can't
   validate them, but the reasoning content is preserved.

3. Strip cache_control from thinking/redacted_thinking blocks to
   prevent cache markers from interfering with signature validation.

4. Drop thinking blocks from the second message when merging
   consecutive assistant messages (role alternation enforcement).

5. Error recovery: on HTTP 400 mentioning 'signature' and 'thinking',
   strip all reasoning_details from the conversation and retry once.
   This is the safety net for edge cases the proactive stripping
   misses.

Addresses the issue reported in PR #6086 by @mingginwan while
preserving reasoning continuity (their PR stripped ALL thinking
blocks unconditionally).

Files changed:
- agent/anthropic_adapter.py: thinking block management in
  convert_messages_to_anthropic (strip old turns, downgrade unsigned,
  strip cache_control, merge-time strip)
- run_agent.py: one-shot signature error recovery in retry loop
- tests/test_anthropic_adapter.py: 10 new tests covering all cases
2026-04-08 03:38:08 -07:00
Teknium
30ea423ce8
fix: unify reasoning_effort to config.yaml only, remove HERMES_REASONING_EFFORT env var
Gateway and cron had inconsistent reasoning_effort resolution:
- CLI: config.yaml only (correct)
- Gateway: config.yaml first, env var fallback
- Cron: env var first, config.yaml fallback

All three now read exclusively from agent.reasoning_effort in config.yaml.
Removed HERMES_REASONING_EFFORT env var support entirely — .env is for
secrets only, not behavioral config.
2026-04-08 03:36:44 -07:00
mrshu
19b0ddce40 fix(process): correct detached crash recovery state
Previously crash recovery recreated detached sessions as if they were
fully managed, so polls and kills could lie about liveness and the
checkpoint could forget recovered jobs after the next restart.
This commit refreshes recovered host-backed sessions from real PID
state, keeps checkpoint data durable, and preserves notify watcher
metadata while treating sandbox-only PIDs as non-recoverable.

- Persist `pid_scope` in `tools/process_registry.py` and skip
  recovering sandbox-backed entries without a host-visible PID handle
- Refresh detached sessions on access so `get`/`poll`/`wait` and active
  session queries observe exited processes instead of hanging forever
- Allow recovered host PIDs to be terminated honestly and requeue
  `notify_on_complete` watchers during checkpoint recovery
- Add regression tests for durable checkpoints, detached exit/kill
  behavior, sandbox skip logic, and recovered notify watchers
2026-04-08 03:35:43 -07:00
landy
383db35925 fix: improve streaming fallback after edit failures 2026-04-08 03:33:43 -07:00
Vasanthdev2004
085c1c6875 fix(browser): preserve agent-browser paths with spaces 2026-04-08 02:35:48 -07:00
Teknium
3696c74bfb fix: preserve existing thresholds, remove pre-read byte guard
- DEFAULT_RESULT_SIZE_CHARS: 50K -> 100K (match current _LARGE_RESULT_CHARS)
- DEFAULT_PREVIEW_SIZE_CHARS: 2K -> 1.5K (match current _LARGE_RESULT_PREVIEW_CHARS)
- Per-tool overrides all set to 100K (terminal, execute_code, search_files)
- Remove pre-read byte guard (no behavioral regression vs current main)
- Revert limit signature change to int=500 (match current default)
- Restore original read_file schema description
- Update test assertions to match 100K thresholds
2026-04-08 02:24:32 -07:00
alt-glitch
bbcff8dcd0 fix(tools): address PR review — remove _extract_raw_output, BudgetConfig everywhere, read_file hardening
- Remove _extract_raw_output: persist content verbatim (fixes size mismatch bug)
- Drop import aliases: import from budget_config directly, one canonical name
- BudgetConfig param on maybe_persist_tool_result and enforce_turn_budget
- read_file: limit=None signature, pre-read guard fires only when limit omitted (256KB)
- Unify binary extensions: file_operations.py imports from binary_extensions.py
- Exclude .pdf and .svg from binary set (text-based, agents may inspect)
- Remove redundant outer try/except in eval path (internal fallback handles it)
- Fix broken tests: update assertion strings for new persistence format
- Module-level constants: _PRE_READ_MAX_BYTES, _DEFAULT_READ_LIMIT
- Remove redundant pathlib import (Path already at module level)
- Update spec.md with IMPLEMENTED annotations and design decisions
2026-04-08 02:24:32 -07:00
alt-glitch
65e24c942e wip: tool result fixes -- persistence 2026-04-08 02:24:32 -07:00
kshitij
22d1bda185 fix(minimax): correct context lengths, model catalog, thinking guard, aux model, and config base_url
Cherry-picked from PR #6046 by kshitijk4poor with dead code stripped.

- Context lengths: 204800 → 1M (M1) / 1048576 (M2.5/M2.7) per official docs
- Model catalog: add M1 family, remove deprecated M2.1 and highspeed variants
- Thinking guard: skip extended thinking for MiniMax (Anthropic-compat endpoint)
- Aux model: MiniMax-M2.7-highspeed → MiniMax-M2.7 (same model, half price)
- Config base_url: honour model.base_url for API-key providers (fixes China users)
- Stripped unused get_minimax_max_output() / _MINIMAX_MAX_OUTPUT (no consumer)

Fixes #5777, #4082, #6039. Closes #3895.
2026-04-08 02:20:46 -07:00
Mibayy
ab271ebe10 fix(vision): simplify vision auto-detection to openrouter → nous → active provider
Simplify the vision auto-detection chain from 5 backends (openrouter,
nous, codex, anthropic, custom) down to 3:

  1. OpenRouter  (known vision-capable default model)
  2. Nous Portal (known vision-capable default model)
  3. Active provider + model (whatever the user is running)
  4. Stop

This is simpler and more predictable. The active provider step uses
resolve_provider_client() which handles all provider types including
named custom providers (from #5978).

Removed the complex preferred-provider promotion logic and API-level
fallback — the chain is short enough that it doesn't need them.

Based on PR #5376 by Mibay. Closes #5366.
2026-04-08 01:21:54 -07:00
zocomputer
e1befe5077 feat(agent): add jittered retry backoff
Adds agent/retry_utils.py with jittered_backoff() — exponential backoff
with additive jitter to prevent thundering-herd retry spikes when
multiple gateway sessions hit the same rate-limited provider.

Replaces fixed exponential backoff at 4 call sites:
- run_agent.py: None-choices retry path (5s base, 120s cap)
- run_agent.py: API error retry path (2s base, 60s cap)
- trajectory_compressor.py: sync + async summarization retries

Thread-safe jitter counter with overflow guards ensures unique seeds
across concurrent retries.

Trimmed from original PR to keep only wired-in functionality.

Co-authored-by: martinp09 <martinp09@users.noreply.github.com>
2026-04-08 00:41:36 -07:00
Teknium
fff237e111
feat(cron): track delivery failures in job status (#6042)
_deliver_result() now returns Optional[str] — None on success, error
message on failure. All failure paths (unknown platform, platform
disabled, config load error, send failure, unresolvable target)
return descriptive error strings.

mark_job_run() gains delivery_error param, tracked as
last_delivery_error on the job — separate from agent execution errors.
A job where the agent succeeded but delivery failed shows
last_status='ok' + last_delivery_error='...'.

The cronjob list tool now surfaces last_delivery_error so agents and
users can see when cron outputs aren't arriving.

Inspired by PR #5863 (oxngon) — reimplemented with proper wiring.

Tests: 3 new mark_job_run tests + 6 new _deliver_result return tests.
2026-04-07 22:49:01 -07:00
Teknium
598c25d43e
feat(feishu): add interactive card approval buttons (#6043)
Add button-based exec approval to the Feishu adapter, matching the
existing Discord, Telegram, and Slack implementations.

When the agent encounters a dangerous command, Feishu users now see
an interactive card with four buttons instead of text instructions:
- Allow Once (primary)
- Allow Session
- Always Allow
- Deny (danger)

Implementation:
- send_exec_approval() sends an interactive card via the Feishu
  message API with buttons carrying hermes_action in their value dict
- _handle_card_action_event() intercepts approval button clicks
  before routing them as synthetic commands, directly calling
  resolve_gateway_approval() to unblock the agent thread
- _update_approval_card() replaces the orange approval card with a
  green (approved) or red (denied) status card showing who acted
- _approval_state dict tracks pending approval_id → session_key
  mappings; cleaned up on resolution

The gateway's existing routing in _approval_notify_sync already checks
getattr(type(adapter), 'send_exec_approval', None) and will
automatically use the button-based flow for Feishu.

Tests: 16 new tests covering send, callback resolution, state
management, card updates, and non-interference with existing card
actions.
2026-04-07 22:45:14 -07:00
Teknium
5c03f2e7cc
fix: provider/model resolution — salvage 4 PRs + MiniMax aux URL fix (#5983)
Salvaged fixes from community PRs:

- fix(model_switch): _read_auth_store → _load_auth_store + fix auth store
  key lookup (was checking top-level dict instead of store['providers']).
  OAuth providers now correctly detected in /model picker.
  Cherry-picked from PR #5911 by Xule Lin (linxule).

- fix(ollama): pass num_ctx to override 2048 default context window.
  Ollama defaults to 2048 context regardless of model capabilities. Now
  auto-detects from /api/show metadata and injects num_ctx into every
  request. Config override via model.ollama_num_ctx. Fixes #2708.
  Cherry-picked from PR #5929 by kshitij (kshitijk4poor).

- fix(aux): normalize provider aliases for vision/auxiliary routing.
  Adds _normalize_aux_provider() with 17 aliases (google→gemini,
  claude→anthropic, glm→zai, etc). Fixes vision routing failure when
  provider is set to 'google' instead of 'gemini'.
  Cherry-picked from PR #5793 by e11i (Elizabeth1979).

- fix(aux): rewrite MiniMax /anthropic base URLs to /v1 for OpenAI SDK.
  MiniMax's inference_base_url ends in /anthropic (Anthropic Messages API),
  but auxiliary client uses OpenAI SDK which appends /chat/completions →
  404 at /anthropic/chat/completions. Generic _to_openai_base_url() helper
  rewrites terminal /anthropic to /v1 for OpenAI-compatible endpoint.
  Inspired by PR #5786 by Lempkey.

Added debug logging to silent exception blocks across all fixes.

Co-authored-by: Hermes Agent <hermes@nousresearch.com>
2026-04-07 22:23:28 -07:00
Jonathan Barket
7fe6782a25 feat(tools): add "no_mcp" sentinel to exclude MCP servers per platform
Currently, MCP servers are included on all platforms by default. If a
platform's toolset list does not explicitly name any MCP servers, every
globally enabled MCP server is injected. There is no way to opt a
platform out of MCP servers entirely.

This matters for the API server platform when used as an execution
backend — each spawned agent session gets the full MCP tool schema
injected into its system prompt, dramatically inflating token usage
(e.g. 57K tokens vs 9K without MCP tools) and slowing response times.

Add a "no_mcp" sentinel value for platform_toolsets. When present in a
platform's toolset list, all MCP servers are excluded for that platform.
Other platforms are unaffected.

Usage in config.yaml:

    platform_toolsets:
      api_server:
        - terminal
        - file
        - web
        - no_mcp    # exclude all MCP servers

The sentinel is filtered out of the final toolset — it does not appear
as an actual toolset name.
2026-04-07 18:00:01 -07:00
Teknium
b9a5e6e247 fix: use camelCase structuredContent attr, prefer structured over text
- The MCP SDK Pydantic model uses camelCase (structuredContent), not
  snake_case (structured_content). The original getattr was a silent no-op.
- When structuredContent is present, return it AS the result instead of
  alongside text — the structured payload is the machine-readable data.
- Move test file to tests/tools/ and fix fake class to use camelCase.
- Patch _run_on_mcp_loop in tests so the handler actually executes.
2026-04-07 18:00:01 -07:00
r266-tech
363c5bc3c3 test(mcp): add structured_content preservation tests 2026-04-07 18:00:01 -07:00
Teknium
cbf1f15cfe
fix(auxiliary): resolve named custom providers and 'main' alias in auxiliary routing (#5978)
* fix(telegram): replace substring caption check with exact line-by-line match

Captions in photo bursts and media group albums were silently dropped when
a shorter caption happened to be a substring of an existing one (e.g.
"Meeting" lost inside "Meeting agenda"). Extract a shared _merge_caption
static helper that splits on "\n\n" and uses exact match with whitespace
normalisation, then use it in both _enqueue_photo_event and
_queue_media_group_event.

Adds 13 unit tests covering the fixed bug scenarios.

Cherry-picked from PR #2671 by Dilee.

* fix: extend caption substring fix to all platforms

Move _merge_caption helper from TelegramAdapter to BasePlatformAdapter
so all adapters inherit it. Fix the same substring-containment bug in:
- gateway/platforms/base.py (photo burst merging)
- gateway/run.py (priority photo follow-up merging)
- gateway/platforms/feishu.py (media batch merging)

The original fix only covered telegram.py. The same bug existed in base.py
and run.py (pure substring check) and feishu.py (list membership without
whitespace normalization).

* fix(auxiliary): resolve named custom providers and 'main' alias in auxiliary routing

Two bugs caused auxiliary tasks (vision, compression, etc.) to fail when
using named custom providers defined in config.yaml:

1. 'provider: main' was hardcoded to 'custom', which only checks legacy
   OPENAI_BASE_URL env vars. Now reads _read_main_provider() to resolve
   to the actual provider (e.g., 'custom:beans', 'openrouter', 'deepseek').

2. Named custom provider names (e.g., 'beans') fell through to
   PROVIDER_REGISTRY which doesn't know about config.yaml entries.
   Now checks _get_named_custom_provider() before the registry fallback.

Fixes both resolve_provider_client() and _normalize_vision_provider()
so the fix covers all auxiliary tasks (vision, compression, web_extract,
session_search, etc.).

Adds 13 unit tests. Reported by Laura via Discord.

---------

Co-authored-by: Dilee <uzmpsk.dilekakbas@gmail.com>
2026-04-07 17:59:47 -07:00
Teknium
9692b3c28a
fix: CLI/UX batch — ChatConsole errors, curses scroll, skin-aware banner, git state banner (#5974)
* fix(cli): route error messages through ChatConsole inside patch_stdout

Cherry-pick of PR #5798 by @icn5381.

Replace self.console.print() with ChatConsole().print() for 11 error/status
messages reachable during the interactive session. Inside patch_stdout,
self.console (plain Rich Console) writes raw ANSI escapes that StdoutProxy
mangles into garbled text. ChatConsole uses prompt_toolkit's native
print_formatted_text which renders correctly.

Same class of bug as #2262 — that fix covered agent output but missed
these error paths in _ensure_runtime_credentials, _init_agent, quick
commands, skill loading, and plan mode.

* fix(model-picker): add scrolling viewport to curses provider menu

Cherry-pick of PR #5790 by @Lempkey. Fixes #5755.

_curses_prompt_choice rendered items starting unconditionally from index 0
with no scroll offset. The 'More providers' submenu has 13 entries. On
terminals shorter than ~16 rows, items past the fold were never drawn.
When UP-arrow wrapped cursor from 0 to the last item (Cancel, index 12),
the highlight rendered off-screen — appearing as if only Cancel existed.

Adds scroll_offset tracking that adjusts each frame to keep the cursor
inside the visible window.

* feat(cli): skin-aware compact banner + git state in startup banner

Combined salvage of PR #5922 by @ASRagab and PR #5877 by @xinbenlv.

Compact banner changes (from #5922):
- Read active skin colors and branding instead of hardcoding gold/NOUS HERMES
- Default skin preserves backward-compatible legacy branding
- Non-default skins use their own agent_name and colors

Git state in banner (from #5877):
- New format_banner_version_label() shows upstream/local git hashes
- Full banner title now includes git state (upstream hash, carried commits)
- Compact banner line2 shows the version label with git state
- Widen compact banner max width from 64 to 88 to fit version info

Both the full Rich banner and compact fallback are now skin-aware
and show git state.
2026-04-07 17:59:42 -07:00
Teknium
f3c59321af fix: add _profile_arg tests + move STT language to config.yaml
- Add 7 unit tests for _profile_arg: default home, named profile,
  hash path, nested path, invalid name, systemd integration, launchd integration
- Add stt.local.language to config.yaml (empty = auto-detect)
- Both STT code paths now read config.yaml first, env var fallback,
  then default (auto-detect for faster-whisper, 'en' for CLI command)
- HERMES_LOCAL_STT_LANGUAGE env var still works as backward-compat fallback
2026-04-07 17:59:16 -07:00
Teknium
50d1518df6 fix(tests): update tool_progress_callback test calls to new 4-arg signature
Follow-up to sroecker's PR #5918 — test mocks were using the old 3-arg
callback signature (name, preview, args) instead of the new
(event_type, name, preview, args, **kwargs).
2026-04-07 17:56:01 -07:00
Teknium
efbe8d674a docs: add Discord channel controls and Telegram reactions documentation
- Discord: ignored_channels, no_thread_channels config reference + examples
- Telegram: message reactions section with config, behavior notes
- Environment variables reference updated for all new vars
2026-04-07 17:55:55 -07:00
Teknium
a6547f399f test: add tests for Discord channel controls and Telegram reactions
- 14 tests for ignored_channels, no_thread_channels, and config bridging
- 17 tests for reaction enable/disable, API calls, error handling, and config
2026-04-07 17:55:55 -07:00
lesterli
37bf19a29d fix(codex): align validation with normalization for empty stream output
The response validation stage unconditionally marked Codex Responses API
replies as invalid when response.output was empty, triggering unnecessary
retries and fallback chains. However, _normalize_codex_response can
recover from this state by synthesizing output from response.output_text.

Now the validation stage checks for output_text before marking the
response invalid, matching the normalization logic. Also fixes
logging.warning → logger.warning for consistency with the rest of the
file.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:29:41 -07:00
Teknium
469cd16fe0
fix(security): consolidated security hardening — SSRF, timing attack, tar traversal, credential leakage (#5944)
Salvaged from PRs #5800 (memosr), #5806 (memosr), #5915 (Ruzzgar), #5928 (Awsh1).

Changes:
- Use hmac.compare_digest for API key comparison (timing attack prevention)
- Apply provider env var blocklist to Docker containers (credential leakage)
- Replace tar.extractall() with safe extraction in TerminalBench2 (CVE-2007-4559)
- Add SSRF protection via is_safe_url to ALL platform adapters:
  base.py (cache_image_from_url, cache_audio_from_url),
  discord, slack, telegram, matrix, mattermost, feishu, wecom
  (Signal and WhatsApp protected via base.py helpers)
- Update tests: mock is_safe_url in Mattermost download tests
- Add security tests for tar extraction (traversal, symlinks, safe files)
2026-04-07 17:28:37 -07:00
Zainan Victor Zhou
0d41fb0827 fix(gateway): show full session id and title in /status 2026-04-07 17:27:09 -07:00
Jeff Escalante
4aef055805 fix(gateway/webhook): don't pop delivery_info on send
The webhook adapter stored per-request `deliver`/`deliver_extra` config in
`_delivery_info[chat_id]` during POST handling and consumed it via `.pop()`
inside `send()`. That worked for routes whose agent run produced exactly
one outbound message — the final response — but it broke whenever the
agent emitted any interim status message before the final response.

Status messages flow through the same `send(chat_id, ...)` path as the
final response (see `gateway/run.py::_status_callback_sync` →
`adapter.send(...)`). Common triggers include:

  - "🔄 Primary model failed — switching to fallback: ..."
    (run_agent.py::_emit_status when `fallback_providers` activates)
  - context-pressure / compression notices
  - any other lifecycle event routed through `status_callback`

When any of those fired, the first `send()` call popped the entry, so the
subsequent final-response `send()` saw an empty dict and silently
downgraded `deliver_type` from `"telegram"` (or `discord`/`slack`/etc.) to
the default `"log"`. The agent's response was logged to the gateway log
instead of being delivered to the configured cross-platform target — no
warning, no error, just a missing message.

This was easy to hit in practice. Any user with `fallback_providers`
configured saw it the first time their primary provider hiccuped on a
webhook-triggered run. Routes that worked perfectly in dev (where the
primary stays healthy) silently dropped responses in prod.

Fix: read `_delivery_info` with `.get()` so multiple `send()` calls for
the same `chat_id` all see the same delivery config. To keep the dict
bounded without relying on per-send cleanup, add a parallel
`_delivery_info_created` timestamp dict and a `_prune_delivery_info()`
helper that drops entries older than `_idempotency_ttl` (1h, same window
already used by `_seen_deliveries`). Pruning runs on each POST, mirroring
the existing `_seen_deliveries` cleanup pattern.

Worst-case memory footprint is now `rate_limit * TTL = 30/min * 60min =
1800` entries, each ~1KB → under 2 MB. In practice it'll be far smaller
because most webhooks complete in seconds, not the full hour.

Test changes:
  - `test_delivery_info_cleaned_after_send` is replaced with
    `test_delivery_info_survives_multiple_sends`, which is now the
    regression test for this bug — it asserts that two consecutive
    `send()` calls both see the delivery config.
  - A new `test_delivery_info_pruned_via_ttl` covers the TTL cleanup
    behavior.
  - The two integration tests that asserted `chat_id not in
    adapter._delivery_info` after `send()` now assert the opposite, with
    a comment explaining why.

All 40 tests in `tests/gateway/test_webhook_adapter.py` and
`tests/gateway/test_webhook_integration.py` pass. Verified end-to-end
locally against a dynamic `hermes webhook subscribe` route configured
with `--deliver telegram --deliver-chat-id <user>`: with `gpt-5.4` as
the primary (currently flaky) and `claude-opus-4.6` as the fallback,
the fallback notification fires, the agent finishes, and the final
response is delivered to Telegram as expected.
2026-04-07 17:27:09 -07:00
Siddharth Balyan
f3006ebef9
refactor(tests): re-architect tests + fix CI failures (#5946)
* refactor: re-architect tests to mirror the codebase

* Update tests.yml

* fix: add missing tool_error imports after registry refactor

* fix(tests): replace patch.dict with monkeypatch to prevent env var leaks under xdist

patch.dict(os.environ) can leak TERMINAL_ENV across xdist workers,
causing test_code_execution tests to hit the Modal remote path.

* fix(tests): fix update_check and telegram xdist failures

- test_update_check: replace patch("hermes_cli.banner.os.getenv") with
  monkeypatch.setenv("HERMES_HOME") — banner.py no longer imports os
  directly, it uses get_hermes_home() from hermes_constants.

- test_telegram_conflict/approval_buttons: provide real exception classes
  for telegram.error mock (NetworkError, TimedOut, BadRequest) so the
  except clause in connect() doesn't fail with "catching classes that do
  not inherit from BaseException" when xdist pollutes sys.modules.

* fix(tests): accept unavailable_models kwarg in _prompt_model_selection mock
2026-04-07 17:19:07 -07:00
Teknium
99ff375f7a
fix(gateway): respect tool_preview_length in all/new progress modes (#5937)
Previously, all/new tool progress modes always hard-truncated previews
to 40 chars, ignoring the display.tool_preview_length config. This made
it impossible for gateway users to see meaningful command/path info
without switching to verbose mode (which shows too much detail).

Now all/new modes read tool_preview_length from config:
- tool_preview_length: 0 (default/unset) → 40 chars (no regression)
- tool_preview_length: 120 → 120-char previews in all/new mode
- verbose mode: unchanged (already respected the config)

Users who want longer previews can set:
  display:
    tool_preview_length: 120

Reported by demontut_ on Discord.
2026-04-07 14:10:56 -07:00
Dilee
4a630c2071 fix(telegram): replace substring caption check with exact line-by-line match
Captions in photo bursts and media group albums were silently dropped when
a shorter caption happened to be a substring of an existing one (e.g.
"Meeting" lost inside "Meeting agenda"). Extract a shared _merge_caption
static helper that splits on "\n\n" and uses exact match with whitespace
normalisation, then use it in both _enqueue_photo_event and
_queue_media_group_event.

Adds 13 unit tests covering the fixed bug scenarios.

Cherry-picked from PR #2671 by Dilee.
2026-04-07 14:08:59 -07:00
Teknium
7b18eeee9b
feat(supermemory): add multi-container, search_mode, identity template, and env var override (#5933)
Based on PR #5413 spec by MaheshtheDev (Mahesh Sanikommu).

Changes:
- Add search_mode config (hybrid/memories/documents) passed to SDK
- Add {identity} template support in container_tag for profile-scoped containers
- Add SUPERMEMORY_CONTAINER_TAG env var override (priority over config)
- Add multi-container mode: enable_custom_container_tags, custom_containers,
  custom_container_instructions in supermemory.json
- Dynamic tool schemas when multi-container enabled (optional container_tag param)
- Whitelist validation for custom container tags in tool calls
- Simplify get_config_schema() to only prompt for API key during setup
- Defer container_tag sanitization to initialize() (after template resolution)
- Add custom_id support to documents.add calls
- Update README with multi-container docs, search_mode, identity template,
  support links (Discord, email)
- Update memory-providers.md with new features and multi-container example
- Update memory-provider-plugin.md with minimal vs full schema guidance
- Add 12 new tests covering identity template, search_mode, multi-container,
  config schema, and env var override
2026-04-07 14:03:46 -07:00
Teknium
ab8f9c089e
feat: thinking-only prefill continuation for structured reasoning responses (#5931)
When the model produces structured reasoning (via API fields like .reasoning,
.reasoning_content, .reasoning_details) but no visible text content, append
the assistant message as prefill and continue the loop. The model sees its own
reasoning context on the next turn and produces the text portion.

Inspired by clawdbot's 'incomplete-text' recovery pattern. Up to 2 prefill
attempts before falling through to the existing '(empty)' terminal.

Key design decisions:
- Only triggers for structured reasoning (API fields), NOT inline <think> tags
- Prefill messages are popped on success to maintain strict role alternation
- _thinking_prefill marker stripped from all API message building paths
- Works across all providers: OpenAI (continuation), Anthropic (native prefill)

Verified with E2E tests: simulated thinking-only → real OpenRouter continuation
produces correct content. Also confirmed Qwen models consistently produce
structured-reasoning-only responses under token pressure.
2026-04-07 13:19:06 -07:00
kshitijk4poor
f4528c885b feat(clipboard): add native Windows image paste support
Add win32 platform branch to clipboard.py so Ctrl+V image paste
works on native Windows (PowerShell / Windows Terminal), not just
WSL2.

Uses the same .NET System.Windows.Forms.Clipboard approach as the
WSL path but calls PowerShell directly instead of powershell.exe
(the WSL cross-call path).  Tries 'powershell' first (Windows
PowerShell 5.1, always available), then 'pwsh' (PowerShell 7+).

PowerShell executable is discovered once and cached for the process
lifetime.

Includes 14 new tests covering:
- Platform dispatch (save_clipboard_image + has_clipboard_image)
- Image detection via PowerShell .NET check
- Base64 PNG extraction and decode
- Edge cases: no PowerShell, empty output, invalid base64, timeout
2026-04-07 12:49:39 -07:00
Teknium
c040b0e4ae test: add unit tests for media helper — video, document, multi-file, failure isolation
Adapted from PR #5679 (0xbyt4) to cover edge cases not in the integration tests:
video routing, unknown extension fallback to send_document, multi-file delivery,
and single-failure isolation.
2026-04-07 12:49:25 -07:00
kshitijk4poor
0f3895ba29 fix(cron): deliver MEDIA files as native platform attachments
The cron delivery path sent raw 'MEDIA:/path/to/file' text instead
of uploading the file as a native attachment.  The standalone path
(via _send_to_platform) already extracted MEDIA tags and forwarded
them as media_files, but the live adapter path passed the unprocessed
delivery_content directly to adapter.send().

Two bugs fixed:
1. Live adapter path now sends cleaned text (MEDIA tags stripped)
   instead of raw content — prevents 'MEDIA:/path' from appearing
   as literal text in Discord/Telegram/etc.
2. Live adapter path now sends each extracted media file via the
   adapter's native method (send_voice for audio, send_image_file
   for images, send_video for video, send_document as fallback) —
   files are uploaded as proper platform attachments.

The file-type routing mirrors BasePlatformAdapter._process_message_background
to ensure consistent behavior between normal gateway responses and
cron-delivered responses.

Adds 2 tests:
- test_live_adapter_sends_media_as_attachments: verifies Discord
  adapter receives send_voice call for .mp3 file
- test_live_adapter_sends_cleaned_text_not_raw: verifies MEDIA tag
  stripped from text sent via live adapter
2026-04-07 12:49:25 -07:00
Teknium
69c753c19b
fix: thread gateway user_id to memory plugins for per-user scoping (#5895)
Memory plugins (Mem0, Honcho) used static identifiers ('hermes-user',
config peerName) meaning all gateway users shared the same memory bucket.

Changes:
- AIAgent.__init__: add user_id parameter, store as self._user_id
- run_agent.py: include user_id in _init_kwargs passed to memory providers
- gateway/run.py: pass source.user_id to AIAgent in primary + background paths
- Mem0 plugin: prefer kwargs user_id over config default
- Honcho plugin: override cfg.peer_name with gateway user_id when present

CLI sessions (user_id=None) preserve existing defaults. Only gateway
sessions with a real platform user_id get per-user memory scoping.

Reported by plev333.
2026-04-07 11:14:12 -07:00
Teknium
e49c8bbbbb
feat(slack): thread engagement — auto-respond in bot-started and mentioned threads (#5897)
When the bot sends a message in a thread, track its ts in _bot_message_ts.
When the bot is @mentioned in a thread, register it in _mentioned_threads.
Both sets enable auto-responding to future messages in those threads
without requiring repeated @mentions — making the bot behave like a
team member that stays engaged once a conversation starts.

Channel message gating now checks 4 signals (in order):
  1. @mention in this message
  2. Reply in a thread the bot started/participated in (_bot_message_ts)
  3. Message in a thread where the bot was previously @mentioned (_mentioned_threads)
  4. Existing session for this thread (_has_active_session_for_thread — survives restarts)

Thread context fetching now triggers on ANY first-entry path (not just
@mention), so the agent gets context whether it's entering via a mention,
a bot-thread reply, or a mentioned-thread auto-trigger.

Both tracking sets are bounded (5000 cap with prune-oldest-half) to prevent
unbounded memory growth in long-running deployments.

Salvaged from PR #5754 by @hhhonzik. Preserves our existing approval buttons,
thread context fetching, and session key fix. Does NOT include the
edit_message format_message() removal (that was a regression in the original PR).

Tests: 4 new tests for bot-ts tracking and mentioned-thread bounds.
2026-04-07 11:12:08 -07:00
Teknium
1a2a03ca69
feat(gateway): approval buttons for Slack & Telegram + Slack thread context (#5890)
Slack:
- Add Block Kit interactive buttons for command approval (Allow Once,
  Allow Session, Always Allow, Deny) via send_exec_approval()
- Register @app.action handlers for each approval button
- Add _fetch_thread_context() — fetches thread history via
  conversations.replies when bot is first @mentioned mid-thread
- Fix _has_active_session_for_thread() to use build_session_key()
  instead of manual key construction (fixes session key mismatch bug
  where thread_sessions_per_user flag was ignored, ref PR #5833)

Telegram:
- Add InlineKeyboard approval buttons via send_exec_approval()
- Add ea:* callback handling in _handle_callback_query()
- Uses monotonic counter + _approval_state dict to map button clicks
  back to session keys (avoids 64-byte callback_data limit)

Both platforms now auto-detected by the gateway runner's
_approval_notify_sync() — any adapter with send_exec_approval() on
its class gets button-based approval instead of text fallback.

Inspired by community PRs #3898 (LevSky22), #2953 (ygd58), #5833
(heathley). Implemented fresh on current main.

Tests: 24 new tests covering button rendering, action handling,
thread context fetching, session key fix, double-click prevention.
2026-04-07 11:03:14 -07:00
Teknium
caded0a5e7
fix: repair 57 failing CI tests across 14 files (#5823)
* fix: repair 57 failing CI tests across 14 files

Categories of fixes:

**Test isolation under xdist (-n auto):**
- test_hermes_logging: Strip ALL RotatingFileHandlers before each test
  to prevent handlers leaked from other xdist workers from polluting counts
- test_code_execution: Force TERMINAL_ENV=local in setUp — prevents Modal
  AuthError when another test leaks TERMINAL_ENV=modal
- test_timezone: Same TERMINAL_ENV fix for execute_code timezone tests
- test_codex_execution_paths: Mock _resolve_turn_agent_config to ensure
  model resolution works regardless of xdist worker state

**Matrix adapter tests (nio not installed in CI):**
- Add _make_fake_nio() helper with real response classes for isinstance()
  checks in production code
- Replace MagicMock(spec=nio.XxxResponse) with fake_nio instances
- Wrap production method calls with patch.dict('sys.modules', {'nio': ...})
  so import nio succeeds in method bodies
- Use try/except instead of pytest.importorskip for nio.crypto imports
  (importorskip can be fooled by MagicMock in sys.modules)
- test_matrix_voice: Skip entire file if nio is a mock, not just missing

**Stale test expectations:**
- test_cli_provider_resolution: _prompt_provider_choice now takes **kwargs
  (default param added); mock getpass.getpass alongside input
- test_anthropic_oauth_flow: Mock getpass.getpass (code switched from input)
- test_gemini_provider: Mock models.dev + OpenRouter API lookups to test
  hardcoded defaults without external API variance
- test_code_execution: Add notify_on_complete to blocked terminal params
- test_setup_openclaw_migration: Mock prompt_choice to select 'Full setup'
  (new quick-setup path leads to _require_tty → sys.exit in CI)
- test_skill_manager_tool: Patch get_all_skills_dirs alongside SKILLS_DIR
  so _find_skill searches tmp_path, not real ~/.hermes/skills/

**Missing attributes in object.__new__ test runners:**
- test_platform_reconnect: Add session_store to _make_runner()
- test_session_race_guard: Add hooks, _running_agents_ts, session_store,
  delivery_router to _make_runner()

**Production bug fix (gateway/run.py):**
- Fix sentinel eviction race: _AGENT_PENDING_SENTINEL was immediately
  evicted by the stale-detection logic because sentinels have no
  get_activity_summary() method, causing _stale_idle=inf >= timeout.
  Guard _should_evict with 'is not _AGENT_PENDING_SENTINEL'.

* fix: address remaining CI failures

- test_setup_openclaw_migration: Also mock _offer_launch_chat (called at
  end of both quick and full setup paths)
- test_code_execution: Move TERMINAL_ENV=local to module level to protect
  ALL test classes (TestEnvVarFiltering, TestExecuteCodeEdgeCases,
  TestInterruptHandling, TestHeadTailTruncation) from xdist env leaks
- test_matrix: Use try/except for nio.crypto imports (importorskip can be
  fooled by MagicMock in sys.modules under xdist)
2026-04-07 09:58:45 -07:00
Teknium
47ddc2bde5
fix(nous): add 3-minute TTL cache to free-tier detection
check_nous_free_tier() now caches its result for 180 seconds to avoid
redundant Portal API calls during a session (auxiliary client init,
model selection, login flow all call it independently).

The TTL is short enough that an account upgrade from free to paid is
reflected within 3 minutes. clear_nous_free_tier_cache() is exposed
for explicit invalidation on login/logout.

Adds 4 tests for cache hit, TTL expiry, explicit clear, and TTL bound.
2026-04-07 09:30:26 -07:00
emozilla
29065cb9b5
feat(nous): free-tier model gating, pricing display, and vision fallback
- Show pricing during initial Nous Portal login (was missing from
  _login_nous, only shown in the already-logged-in hermes model path)

- Filter free models for paid subscribers: non-allowlisted free models
  are hidden; allowlisted models (xiaomi/mimo-v2-pro, xiaomi/mimo-v2-omni)
  only appear when actually priced as free

- Detect free-tier accounts via portal api/oauth/account endpoint
  (monthly_charge == 0); free-tier users see only free models as
  selectable, with paid models shown dimmed and unselectable

- Use xiaomi/mimo-v2-omni as the auxiliary vision model for free-tier
  Nous users so vision_analyze and browser_vision work without paid
  model access (replaces the default google/gemini-3-flash-preview)

- Unavailable models rendered via print() before TerminalMenu to avoid
  simple_term_menu line-width padding artifacts; upgrade URL resolved
  from auth state portal_base_url (supports staging/custom portals)

- Add 21 tests covering filter_nous_free_models, is_nous_free_tier,
  and partition_nous_models_by_tier
2026-04-07 09:21:48 -07:00
Ben Barclay
b2f477a30b
feat: switch managed browser provider from Browserbase to Browser Use (#5750)
* feat: switch managed browser provider from Browserbase to Browser Use

The Nous subscription tool gateway now routes browser automation through
Browser Use instead of Browserbase. This commit:

- Adds managed Nous gateway support to BrowserUseProvider (idempotency
  keys, X-BB-API-Key auth header, external_call_id persistence)
- Removes managed gateway support from BrowserbaseProvider (now
  direct-only via BROWSERBASE_API_KEY/BROWSERBASE_PROJECT_ID)
- Updates browser_tool.py fallback: prefers Browser Use over Browserbase
- Updates nous_subscription.py: gateway vendor 'browser-use', auto-config
  sets cloud_provider='browser-use' for new subscribers
- Updates tools_config.py: Nous Subscription entry now uses Browser Use
- Updates setup.py, cli.py, status.py, prompt_builder.py display strings
- Updates all affected tests to match new behavior

Browserbase remains fully functional for users with direct API credentials.
The change only affects the managed/subscription path.

* chore: remove redundant Browser Use hint from system prompt

* fix: upgrade Browser Use provider to v3 API

- Base URL: api/v2 -> api/v3 (v2 is legacy)
- Unified all endpoints to use native Browser Use paths:
  - POST /browsers (create session, returns cdpUrl)
  - PATCH /browsers/{id} with {action: stop} (close session)
- Removed managed-mode branching that used Browserbase-style
  /v1/sessions paths — v3 gateway now supports /browsers directly
- Removed unused managed_mode variable in close_session

* fix(browser-use): use X-Browser-Use-API-Key header for managed mode

The managed gateway expects X-Browser-Use-API-Key, not X-BB-API-Key
(which is a Browserbase-specific header). Using the wrong header caused
a 401 AUTH_ERROR on every managed-mode browser session create.

Simplified _headers() to always use X-Browser-Use-API-Key regardless
of direct vs managed mode.

* fix(nous_subscription): browserbase explicit provider is direct-only

Since managed Nous gateway now routes through Browser Use, the
browserbase explicit provider path should not check managed_browser_available
(which resolves against the browser-use gateway). Simplified to direct-only
with managed=False.

* fix(browser-use): port missing improvements from PR #5605

- CDP URL normalization: resolve HTTP discovery URLs to websocket after
  cloud provider create_session() (prevents agent-browser failures)
- Managed session payload: send timeout=5 and proxyCountryCode=us for
  gateway-backed sessions (prevents billing overruns)
- Update prompt builder, browser_close schema, and module docstring to
  replace remaining Browserbase references with Browser Use
- Dynamic /browser status detection via _get_cloud_provider() instead
  of hardcoded env var checks (future-proof for new providers)
- Rename post_setup key from 'browserbase' to 'agent_browser'
- Update setup hint to mention Browser Use alongside Browserbase
- Add tests: CDP normalization, browserbase direct-only guard,
  managed browser-use gateway, direct browserbase fallback

---------

Co-authored-by: rob-maron <132852777+rob-maron@users.noreply.github.com>
2026-04-07 08:40:22 -04:00
Teknium
8b861b77c1
refactor: remove browser_close tool — auto-cleanup handles it (#5792)
* refactor: remove browser_close tool — auto-cleanup handles it

The browser_close tool was called in only 9% of browser sessions (13/144
navigations across 66 sessions), always redundantly — cleanup_browser()
already runs via _cleanup_task_resources() at conversation end, and the
background inactivity reaper catches anything else.

Removing it saves one tool schema slot in every browser-enabled API call.

Also fixes a latent bug: cleanup_browser() now handles Camofox sessions
too (previously only Browserbase). Camofox sessions were never auto-cleaned
per-task because they live in a separate dict from _active_sessions.

Files changed (13):
- tools/browser_tool.py: remove function, schema, registry entry; add
  camofox cleanup to cleanup_browser()
- toolsets.py, model_tools.py, prompt_builder.py, display.py,
  acp_adapter/tools.py: remove browser_close from all tool lists
- tests/: remove browser_close test, update toolset assertion
- docs/skills: remove all browser_close references

* fix: repeat browser_scroll 5x per call for meaningful page movement

Most backends scroll ~100px per call — barely visible on a typical
viewport. Repeating 5x gives ~500px (~half a viewport), making each
scroll tool call actually useful.

Backend-agnostic approach: works across all 7+ browser backends without
needing to configure each one's scroll amount individually. Breaks
early on error for the agent-browser path.

* feat: auto-return compact snapshot from browser_navigate

Every browser session starts with navigate → snapshot. Now navigate
returns the compact accessibility tree snapshot inline, saving one
tool call per browser task.

The snapshot captures the full page DOM (not viewport-limited), so
scroll position doesn't affect it. browser_snapshot remains available
for refreshing after interactions or getting full=true content.

Both Browserbase and Camofox paths auto-snapshot. If the snapshot
fails for any reason, navigation still succeeds — the snapshot is
a bonus, not a requirement.

Schema descriptions updated to guide models: navigate mentions it
returns a snapshot, snapshot mentions it's for refresh/full content.

* refactor: slim cronjob tool schema — consolidate model/provider, drop unused params

Session data (151 calls across 67 sessions) showed several schema
properties were never used by models. Consolidated and cleaned up:

Removed from schema (still work via backend/CLI):
- skill (singular): use skills array instead
- reason: pause-only, unnecessary
- include_disabled: now defaults to true
- base_url: extreme edge case, zero usage
- provider (standalone): merged into model object

Consolidated:
- model + provider → single 'model' object with {model, provider} fields.
  If provider is omitted, the current main provider is pinned at creation
  time so the job stays stable even if the user changes their default.

Kept:
- script: useful data collection feature
- skills array: standard interface for skill loading

Schema shrinks from 14 to 10 properties. All backend functionality
preserved — the Python function signature and handler lambda still
accept every parameter.

* fix: remove mixture_of_agents from core toolsets — opt-in only via hermes tools

MoA was in _HERMES_CORE_TOOLS and composite toolsets (hermes-cli,
hermes-messaging, safe), which meant it appeared in every session
for anyone with OPENROUTER_API_KEY set. The _DEFAULT_OFF_TOOLSETS
gate only works after running 'hermes tools' explicitly.

Now MoA only appears when a user explicitly enables it via
'hermes tools'. The moa toolset definition and check_fn remain
unchanged — it just needs to be opted into.
2026-04-07 03:28:44 -07:00
Teknium
e120d2afac
feat: notify_on_complete for background processes (#5779)
* feat: notify_on_complete for background processes

When terminal(background=true, notify_on_complete=true), the system
auto-triggers a new agent turn when the process exits — no polling needed.

Changes:
- ProcessSession: add notify_on_complete field
- ProcessRegistry: add completion_queue, populate on _move_to_finished()
- Terminal tool: add notify_on_complete parameter to schema + handler
- CLI: drain completion_queue after agent turn AND during idle loop
- Gateway: enhanced _run_process_watcher injects synthetic MessageEvent
  on completion, triggering a full agent turn
- Checkpoint persistence includes notify_on_complete for crash recovery
- code_execution_tool: block notify_on_complete in sandbox scripts
- 15 new tests covering queue mechanics, checkpoint round-trip, schema

* docs: update terminal tool descriptions for notify_on_complete

- background: remove 'ONLY for servers' language, describe both patterns
  (long-lived processes AND long-running tasks with notify_on_complete)
- notify_on_complete: more prescriptive about when to use it
- TERMINAL_TOOL_DESCRIPTION: remove 'Do NOT use background for builds'
  guidance that contradicted the new feature
2026-04-07 02:40:16 -07:00
Teknium
eb7c408445
fix(gateway): /stop and /new bypass Level 1 active-session guard (#5765)
* fix(gateway): /stop and /new bypass Level 1 active-session guard

The base adapter's Level 1 guard intercepted ALL messages while an
agent was running, including /stop and /new. These commands were queued
as pending messages instead of being dispatched to the gateway runner's
Level 2 handler. When the agent eventually stopped (via the interrupt
mechanism), the command text leaked into the conversation as a user
message — the model would receive '/stop' as input and respond to it.

Fix: Add /stop, /new, and /reset to the bypass set in base.py alongside
/approve, /deny, and /status. Consolidate the three separate bypass
blocks into one. Commands in the bypass set are dispatched inline to the
gateway runner, where Level 2 handles them correctly (hard-kill for
/stop, session reset for /new).

Also add a safety net in _run_agent's pending-message processing: if the
pending text resolves to a known slash command, discard it instead of
passing it to the agent. This catches edge cases where command text
leaks through the interrupt_message fallback.

Refs: #5244

* test: regression tests for command bypass of active-session guard

17 tests covering:
- /stop, /new, /reset bypass the Level 1 guard when agent is running
- /approve, /deny, /status bypass (existing behavior, now tested)
- Regular text and unknown commands still queued (not bypassed)
- File paths like '/path/to/file' not treated as commands
- Telegram @botname suffix handled correctly
- Safety net command resolution (resolve_command detects known commands)
2026-04-07 00:53:45 -07:00
Yang Zhi
9e844160f9 fix(credential_pool): auto-detect Z.AI endpoint via probe and cache
The credential pool seeder and runtime credential resolver hardcoded
api.z.ai/api/paas/v4 for all Z.AI keys.  Keys on the Coding Plan (or CN
endpoint) would hit the wrong endpoint, causing 401/429 errors on the
first request even though a working endpoint exists.

Add _resolve_zai_base_url() that:
- Respects GLM_BASE_URL env var (no probe when explicitly set)
- Probes all candidate endpoints (global, cn, coding-global, coding-cn)
  via detect_zai_endpoint() to find one that returns HTTP 200
- Caches the detected endpoint in provider state (auth.json) keyed on
  a SHA-256 hash of the API key so subsequent starts skip the probe
- Falls back to the default URL if all probes fail

Wire into both _seed_from_env() in the credential pool and
resolve_api_key_provider_credentials() in the runtime resolver,
matching the pattern from the kimi-coding fix (PR #5566).

Fixes the same class of bug as #5561 but for the zai provider.
2026-04-07 00:00:08 -07:00
Teknium
8e64f795a1
fix: stale OAuth credentials block OpenRouter users on auto-detect (#5746)
When resolve_runtime_provider is called with requested='auto' and
auth.json has a stale active_provider (nous or openai-codex) whose
OAuth refresh token has been revoked, the AuthError now falls through
to the next provider in the chain (e.g. OpenRouter via env vars)
instead of propagating to the user as a blocking error.

When the user explicitly requested the OAuth provider, the error
still propagates so they know to re-authenticate.

Root cause: resolve_provider('auto') checks auth.json for an active
OAuth provider before checking env vars. get_nous_auth_status()
reports logged_in=True if any access_token exists (even expired),
so the Nous path is taken. resolve_nous_runtime_credentials() then
tries to refresh the token, fails with 'Refresh session has been
revoked', and the AuthError bubbles up to the CLI bold-red display.

Adds 3 tests: Nous fallthrough, Codex fallthrough, explicit-request
still raises.
2026-04-06 23:01:43 -07:00
Mateus Scheuer Macedo
f2c11ff30c fix(delegate): share credential pools with subagents + per-task leasing
Cherry-picked from PR #5580 by MestreY0d4-Uninter.

- Share parent's credential pool with child agents for key rotation
- Leasing layer spreads parallel children across keys (least-loaded)
- Thread-safe acquire_lease/release_lease in CredentialPool
- Reverted sneaked-in tool-name restoration change (kept original
  getattr + isinstance guard pattern)
2026-04-06 23:01:11 -07:00
Teknium
8dee82ea1e
fix: stream consumer creates new message after tool boundaries (#5739)
When streaming was enabled on the gateway, the stream consumer created a
single message at the start and kept editing it as tokens arrived. Tool
progress messages were sent as separate messages below it. Since edits
don't change message position on Telegram/Matrix/Discord, the final
response ended up stuck above all tool progress messages — users had to
scroll up past potentially dozens of tool call lines to read the answer.

The agent already sends stream_delta_callback(None) at tool boundaries
(before _execute_tool_calls). The stream consumer was ignoring this
signal. Now it treats None as a segment break: finalizes the current
message (removes cursor), resets _message_id, and the next text chunk
creates a fresh message below the tool progress messages.

Timeline before:
  [msg 1: 'Let me search...' → edits → 'Here is the answer'] ← top
  [msg 2: tool progress lines]                                ← bottom

Timeline after:
  [msg 1: 'Let me search...']          ← top
  [msg 2: tool progress lines]
  [msg 3: 'Here is the answer']        ← bottom (visible)

Reported by SkyLinx on Discord.
2026-04-06 23:00:14 -07:00
Hermes Agent
4fc7f3eaa5 fix(memory): clean up supermemory provider threads 2026-04-06 22:15:58 -07:00
Hermes Agent
dc333388ec docs(memory): add Supermemory PR draft and cleanup 2026-04-06 22:15:58 -07:00
Hermes Agent
76f19775c3 feat(memory): add Supermemory memory provider 2026-04-06 22:15:58 -07:00
eizus
4ec615b0c2 feat(gateway): Enable Slack thread replies without explicit @mentions
When a user replies in a Slack thread where the bot has an active
conversation session, the bot now processes the message even without
an explicit @mention. This improves UX for ongoing threaded
discussions.

Changes:
- Added set_session_store() to BasePlatformAdapter for adapters to
  check active sessions
- Modified SlackAdapter to detect thread replies and check if a
  session exists for that thread before requiring @mentions
- Updated GatewayRunner to inject the session store into adapters
- Added comprehensive tests for the new behavior

Fixes: Thread replies without @jarvis are now processed if there is
an active session, matching user expectations for conversation flow
2026-04-06 21:27:16 -07:00
Teknium
adb418fb53 fix: cross-platform browser test path separators
Use os.path.join for Windows install path so test passes on Linux
(os.path.join uses / on Linux, \ on Windows).
2026-04-06 16:54:16 -07:00
jtuki
57abc99315 feat(gateway): add per-group access control for Feishu
Add fine-grained authorization policies per Feishu group chat via
platforms.feishu.extra configuration.

- Add global bot-level admins that bypass all group restrictions
- Add per-group policies: open, allowlist, blacklist, admin_only, disabled
- Add default_group_policy fallback for chats without explicit rules
- Thread chat_id through group message gate for per-chat rule selection
- Match both open_id and user_id for backward compatibility
- Preserve existing FEISHU_ALLOWED_USERS / FEISHU_GROUP_POLICY behavior
- Add focused regression tests for all policy modes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:54:16 -07:00
jtuki
18727ca9aa refactor(gateway): simplify Feishu websocket config helpers
Consolidate coercion functions, extract loop readiness check, and deduplicate test mock setup to improve maintainability without changing behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:54:16 -07:00
jtuki
157d6184e3 fix(gateway): make Feishu websocket overrides effective at runtime
Reapply local reconnect and ping settings after the Feishu SDK refreshes its client config so user-provided websocket tuning actually takes effect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:54:16 -07:00
jtuki
ea31d9077c feat(gateway): add Feishu websocket ping timing overrides
Allow Feishu websocket keepalive timing to be configured via platform
extra config so disconnects can be detected faster in unstable networks.

New optional extra settings:
- ws_ping_interval
- ws_ping_timeout

These values are applied only when explicitly configured. Invalid values
fall back to the websocket library defaults by leaving the options unset.

This complements the reconnect timing settings added previously and helps
reduce total recovery time after network interruptions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:54:16 -07:00
jtuki
7d0bf15121 feat(gateway): add configurable Feishu websocket reconnect timing
Allow users to configure websocket reconnect behavior via platform extra
config to reduce reconnect latency in production environments.

The official Feishu SDK defaults to:
- First reconnect: random jitter 0-30 seconds
- Subsequent retries: 120 second intervals

This can cause 20-30 second delays before reconnection after network
interruptions. This commit makes these values configurable while keeping
the SDK defaults for backward compatibility.

Configuration via ~/.hermes/config.yaml:
```yaml
platforms:
  feishu:
    extra:
      ws_reconnect_nonce: 0        # Disable first-reconnect jitter (default: 30)
      ws_reconnect_interval: 3     # Retry every 3 seconds (default: 120)
```

Invalid values (negative numbers, non-integers) fall back to SDK defaults.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:54:16 -07:00
jtuki
7cf4bd06bf fix(gateway): fix Feishu reconnect message drops and shutdown hang
This commit fixes two critical bugs in the Feishu adapter that affect
message reliability and process lifecycle.

**Bug Fix 1: Intermittent Message Drops**

Root cause: Event handler was created once in __init__ and reused across
reconnects, causing callbacks to capture stale loop references. When the
adapter disconnected and reconnected, old callbacks continued firing with
invalid loop references, resulting in dropped messages with warnings:
"[Feishu] Dropping inbound message before adapter loop is ready"

Fix:
- Rebuild event handler on each connect (websocket/webhook)
- Clear handler on disconnect
- Ensure callbacks always capture current valid loop
- Add defensive loop.is_closed() checks with getattr for test compatibility
- Unify webhook dispatch path to use same loop checks as websocket mode

**Bug Fix 2: Process Hangs on Ctrl+C / SIGTERM**

Root cause: Feishu SDK's websocket client runs in a background thread with
an infinite _select() loop that never exits naturally. The thread was never
properly joined on disconnect, causing processes to hang indefinitely after
Ctrl+C or gateway stop commands.

Fix:
- Store reference to thread-local event loop (_ws_thread_loop)
- On disconnect, cancel all tasks in thread loop and stop it gracefully
  via call_soon_threadsafe()
- Await thread future with 10s timeout
- Clean up pending tasks in thread's finally block before closing loop
- Add detailed debug logging for disconnect flow

**Additional Improvements:**
- Add regression tests for disconnect cleanup and webhook dispatch
- Ensure all event callbacks check loop readiness before dispatching

Tested on Linux with websocket mode. All Feishu tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:54:16 -07:00
Ruzzgar
abd24d381b Implement comprehensive browser path discovery for Windows 2026-04-06 16:54:16 -07:00
Tianxiao
8a29b49036 fix(cli): handle CJK wide chars in TUI input height 2026-04-06 16:54:16 -07:00
kshitijk4poor
05f9267938 fix(matrix): hard-fail E2EE when python-olm missing + stable MATRIX_DEVICE_ID
Two issues caused Matrix E2EE to silently not work in encrypted rooms:

1. When matrix-nio is installed without the [e2e] extra (no python-olm /
   libolm), nio.crypto.ENCRYPTION_ENABLED is False and client.olm is
   never initialized. The adapter logged warnings but returned True from
   connect(), so the bot appeared online but could never decrypt messages.
   Now: check_matrix_requirements() and connect() both hard-fail with a
   clear error message when MATRIX_ENCRYPTION=true but E2EE deps are
   missing.

2. Without a stable device_id, the bot gets a new device identity on each
   restart. Other clients see it as "unknown device" and refuse to share
   Megolm session keys. Now: MATRIX_DEVICE_ID env var lets users pin a
   stable device identity that persists across restarts and is passed to
   nio.AsyncClient constructor + restore_login().

Changes:
- gateway/platforms/matrix.py: add _check_e2ee_deps(), hard-fail in
  connect() and check_matrix_requirements(), MATRIX_DEVICE_ID support
  in constructor + restore_login
- gateway/config.py: plumb MATRIX_DEVICE_ID into platform extras
- hermes_cli/config.py: add MATRIX_DEVICE_ID to OPTIONAL_ENV_VARS

Closes #3521
2026-04-06 16:54:16 -07:00
WAXLYY
1c0183ec71 fix(gateway): sanitize media URLs in base platform logs 2026-04-06 16:50:05 -07:00
KangYu
b26e85bf9d Fix compaction summary retries for temperature-restricted models 2026-04-06 16:49:57 -07:00
WAXLYY
c1818b7e9e fix(tools): redact query secrets in send_message errors 2026-04-06 16:49:52 -07:00
ryanautomated
0f9aa57069 fix: silent memory flush failure on /new and /resume commands
The _async_flush_memories() helper accepts (session_id) but both the
/new and /resume handlers passed two arguments (session_id, session_key).
The TypeError was silently swallowed at DEBUG level, so memory extraction
never ran when users typed /new or /resume.

One call site (the session expiry watcher) was already fixed in 9c96f669,
but /new and /resume were missed.

- gateway/run.py:3247 — remove stray session_key from /new handler
- gateway/run.py:4989 — remove stray session_key from /resume handler
- tests/gateway/test_resume_command.py:222 — update test assertion
2026-04-06 16:49:42 -07:00
Myeongwon Choi
ea16949422 fix(cron): suppress delivery when [SILENT] appears anywhere in response
Previously the scheduler checked startswith('[SILENT]'), so agents that
appended [SILENT] after an explanation (e.g. 'N items filtered.\n\n[SILENT]')
would still trigger delivery.

Change the check to 'in' so the marker is caught regardless of position.
Add test_silent_trailing_suppresses_delivery to cover this case.
2026-04-06 16:49:40 -07:00
Nick
4f03b9a419 feat(webhook): add {__raw__} template token and thread_id passthrough for forum topics
- {__raw__} in webhook prompt templates dumps the full JSON payload (truncated at 4000 chars)
- _deliver_cross_platform now passes thread_id/message_thread_id from deliver_extra as metadata, enabling Telegram forum topic delivery
- Tests for both features
2026-04-06 16:42:52 -07:00
kshitijk4poor
9201370c7e feat(plugins): prompt for required env vars during hermes plugins install
Read requires_env from plugin.yaml after install and interactively
prompt for any missing environment variables, saving them to
~/.hermes/.env.

Supports two manifest formats:

  Simple (backwards-compatible):
    requires_env:
      - MY_API_KEY

  Rich (with metadata):
    requires_env:
      - name: MY_API_KEY
        description: "API key for Acme"
        url: "https://acme.com/keys"
        secret: true

Already-set variables are skipped. Empty input skips gracefully.
Secret values use getpass (hidden input). Ctrl+C aborts remaining
prompts without error.
2026-04-06 16:37:53 -07:00
Siddharth Balyan
7b129636f0
feat(tools): add Firecrawl cloud browser provider (#5628)
* feat(tools): add Firecrawl cloud browser provider

Adds Firecrawl (https://firecrawl.dev) as a cloud browser provider
alongside Browserbase and Browser Use. All browser tools route through
Firecrawl's cloud browser via CDP when selected.

- tools/browser_providers/firecrawl.py — FirecrawlProvider
- tools/browser_tool.py — register in _PROVIDER_REGISTRY
- hermes_cli/tools_config.py — add to onboarding provider picker
- hermes_cli/setup.py — add to setup summary
- hermes_cli/config.py — add FIRECRAWL_BROWSER_TTL config
- website/docs/ — browser docs and env var reference

Based on #4490 by @developersdigest.

Co-Authored-By: Developers Digest <124798203+developersdigest@users.noreply.github.com>

* refactor: simplify FirecrawlProvider.emergency_cleanup

Use self._headers() and self._api_url() instead of duplicating
env-var reads and header construction.

* fix: recognize Firecrawl in subscription browser detection

_resolve_browser_feature_state() now handles "firecrawl" as a direct
browser provider (same pattern as "browser-use"), so hermes setup
summary correctly shows "Browser Automation (Firecrawl)" instead of
misreporting as "Local browser".

Also fixes test_config_version_unchanged assertion (11 → 12).

---------

Co-authored-by: Developers Digest <124798203+developersdigest@users.noreply.github.com>
2026-04-07 02:35:26 +05:30
Mikita Lisavets
9afb9a6cb2 fix: clear session-scoped model overrides during session reset 2026-04-06 13:20:01 -07:00
Ayman Kamal
96f85b03cd fix: handle launchctl kickstart exit code 113 in launchd_start()
launchctl kickstart returns exit code 113 ("Could not find service") when
the plist exists but the job hasn't been bootstrapped into the runtime domain.
The existing recovery path only caught exit code 3 ("unloaded"), causing an
unhandled CalledProcessError.

Exit code 113 means the same thing practically -- the service definition needs
bootstrapping before it can be kicked. Add it to the same recovery path that
already handles exit 3, matching the existing pattern in launchd_stop().

Follow-up: add a unit test covering the 113 recovery path.
2026-04-06 13:20:01 -07:00
Dusk1e
1a2f109d8e Ensure atomic writes for gateway channel directory cache to prevent truncation 2026-04-06 13:20:01 -07:00
Awsh1
878b1d3d33 fix(cron): harden scheduler against path traversal and env leaks
Cherry-picked from PR #5503 by Awsh1.

- Validate ALL script paths (absolute, relative, tilde) against scripts_dir boundary
- Add API-boundary validation in cronjob_tools.py
- Move os.environ injections inside try block so finally cleanup always runs
- Comprehensive regression tests for path containment bypass
2026-04-06 12:42:16 -07:00
Teknium
da02a4e283
fix: auxiliary client payment fallback — retry with next provider on 402 (#5599)
When a user runs out of OpenRouter credits and switches to Codex (or any
other provider), auxiliary tasks (compression, vision, web_extract) would
still try OpenRouter first and fail with 402.  Two fixes:

1. Payment fallback in call_llm(): When a resolved provider returns HTTP 402
   or a credit-related error, automatically retry with the next available
   provider in the auto-detection chain.  Skips the depleted provider and
   tries Nous → Custom → Codex → API-key providers.

2. Remove hardcoded OpenRouter fallback: The old code fell back specifically
   to OpenRouter when auto/custom resolution returned no client.  Now falls
   back to the full auto-detection chain, which handles any available
   provider — not just OpenRouter.

Also extracts _get_provider_chain() as a shared function (replaces inline
tuple in _resolve_auto and the new fallback), built at call time so test
patches on _try_* functions remain visible.

Adds 16 tests covering _is_payment_error(), _get_provider_chain(),
_try_payment_fallback(), and call_llm() integration with 402 retry.
2026-04-06 12:41:40 -07:00
Teknium
8ffd44a6f9
feat(discord): register skills as native slash commands via shared gateway logic (#5603)
Centralize the skill → slash command registration that Telegram already had
in commands.py so Discord uses the exact same priority system, filtering,
and cap enforcement:

  1. Core/built-in commands (never trimmed)
  2. Plugin commands (never trimmed)
  3. Skill commands (fill remaining slots, alphabetical, only tier trimmed)

Changes:

hermes_cli/commands.py:
  - Rename _TG_NAME_LIMIT → _CMD_NAME_LIMIT (32 chars shared by both platforms)
  - Rename _clamp_telegram_names → _clamp_command_names (generic)
  - Extract _collect_gateway_skill_entries() — shared plugin + skill
    collection with platform filtering, name sanitization, description
    truncation, and cap enforcement
  - Refactor telegram_menu_commands() to use the shared helper
  - Add discord_skill_commands() that returns (name, desc, cmd_key) triples
  - Preserve _sanitize_telegram_name() for Telegram-specific name cleaning

gateway/platforms/discord.py:
  - Call discord_skill_commands() from _register_slash_commands()
  - Create app_commands.Command per skill entry with cmd_key callback
  - Respect 100-command global Discord limit
  - Log warning when skills are skipped due to cap

Backward-compat aliases preserved for _TG_NAME_LIMIT and
_clamp_telegram_names.

Tests: 9 new tests (7 Discord + 2 backward-compat), 98 total pass.

Inspired by PR #5498 (sprmn24). Closes #5480.
2026-04-06 12:09:36 -07:00
Teknium
3d08a2fa1b
fix: extract MEDIA: tags from cron delivery before sending (#5598)
The cron scheduler delivery path passed raw text including MEDIA: tags
to _send_to_platform(), so media attachments were delivered as literal
text instead of actual files. The send function already supports
media_files= but the cron path never used it.

Now calls BasePlatformAdapter.extract_media() to split media paths
from text before sending, matching the gateway's normal message flow.

Salvaged from PR #4877 by robert-hoffmann.
2026-04-06 11:42:44 -07:00
kshitijk4poor
5e88eb2ba0 fix(signal): implement send_image_file, send_voice, and send_video for MEDIA: tag delivery
The Signal adapter inherited base class defaults for send_image_file(),
send_voice(), and send_video() which only sent the file path as text
(e.g. '🖼️ Image: /tmp/chart.png') instead of actually delivering the file
as a Signal attachment.

When agent responses contain MEDIA:/path/to/file tags, the gateway
media pipeline extracts them and routes through these methods by file
type. Without proper overrides, image/audio/video files were never
actually delivered to Signal users.

Extract a shared _send_attachment() helper that handles all file
validation, size checking, group/DM routing, and RPC dispatch. The four
public methods (send_document, send_image_file, send_voice, send_video)
now delegate to this helper, following the same pattern used by WhatsApp
(_send_media_to_bridge) and Discord (_send_file_attachment).

The helper also uses a single stat() call with try/except FileNotFoundError
instead of the previous exists() + stat() two-syscall pattern, eliminating
a TOCTOU race. As a bonus, send_document() now gains the 100MB size check
that was previously missing (inconsistency with send_image).

Add 25 tests covering all methods plus MEDIA: tag extraction integration,
method-override guards, and send_document's new size check.

Fixes #5105
2026-04-06 11:41:34 -07:00
kshitijk4poor
214e60c951 fix: sanitize Telegram command names to strip invalid characters
Telegram Bot API requires command names to contain only lowercase a-z,
digits 0-9, and underscores. Skill/plugin names containing characters
like +, /, @, or . caused set_my_commands to fail with
Bot_command_invalid.

Two-layer fix:
- scan_skill_commands(): strip non-alphanumeric/non-hyphen chars from
  cmd_key at source, collapse consecutive hyphens, trim edges, skip
  names that sanitize to empty string
- _sanitize_telegram_name(): centralized helper used by all 3 Telegram
  name generation sites (core commands, plugin commands, skill commands)
  with empty-name guard at each call site

Closes #5534
2026-04-06 11:27:28 -07:00
ClintonEmok
f77be22c65 Fix #5211: Preserve dots in OpenCode Go model names
OpenCode Go model names with dots (minimax-m2.7, glm-4.5, kimi-k2.5)
were being mangled to hyphens (minimax-m2-7), causing HTTP 401 errors.

Two code paths were affected:
1. model_normalize.py: opencode-go was incorrectly in DOT_TO_HYPHEN_PROVIDERS
2. run_agent.py: _anthropic_preserve_dots() did not check for opencode-go

Fix:
- Remove opencode-go from _DOT_TO_HYPHEN_PROVIDERS (dots are correct for Go)
- Add opencode-go to _anthropic_preserve_dots() provider check
- Add opencode.ai/zen/go to base_url fallback check
- Add regression tests in tests/test_model_normalize.py

Co-authored-by: jacob3712 <jacob3712@users.noreply.github.com>
2026-04-06 11:25:06 -07:00
Teknium
582dbbbbf7
feat: add grok to TOOL_USE_ENFORCEMENT_MODELS for direct xAI usage (#5595)
Grok models (x-ai/grok-4.20-beta, grok-code-fast-1) now receive tool-use
enforcement guidance, steering them to actually call tools instead of
describing intended actions. Matches both OpenRouter (x-ai/grok-*) and
direct xAI API usage.
2026-04-06 11:22:07 -07:00
Teknium
cc7136b1ac fix: update Gemini model catalog + wire models.dev as live model source
Follow-up for salvaged PR #5494:
- Update model catalog to Gemini 3.x + Gemma 4 (drop deprecated 2.0)
- Add list_agentic_models() to models_dev.py with noise filter
- Wire models.dev into _model_flow_api_key_provider as primary source
  (static curated list serves as offline fallback)
- Add gemini -> google mapping in PROVIDER_TO_MODELS_DEV
- Fix Gemma 4 context lengths to 256K (models.dev values)
- Update auxiliary model to gemini-3-flash-preview
- Expand tests: 3.x catalog, context lengths, models.dev integration
2026-04-06 10:28:03 -07:00
Teknium
6dfab35501 feat(providers): add Google AI Studio (Gemini) as a first-class provider
Cherry-picked from PR #5494 by kshitijk4poor.
Adds native Gemini support via Google's OpenAI-compatible endpoint.
Zero new dependencies.
2026-04-06 10:28:03 -07:00
Teknium
5747590770 fix: follow-up improvements for salvaged PR #5456
- SQLite write queue: thread-local connection pooling instead of
  creating+closing a new connection per operation
- Prefetch threads: join previous batch before spawning new ones to
  prevent thread accumulation on rapid queue_prefetch() calls
- Shutdown: join prefetch threads before stopping write queue
- Add 73 tests covering _Client HTTP payloads, _WriteQueue crash
  recovery & connection reuse, _build_overlay deduplication,
  RetainDBMemoryProvider lifecycle/tools/prefetch/hooks, thread
  accumulation guard, and reasoning_level heuristic
2026-04-06 02:00:55 -07:00
kshitijk4poor
d3d5b895f6 refactor: simplify _get_service_pids — dedupe systemd scopes, fix self-import, harden launchd parsing
- Loop over user/system scope args instead of duplicating the systemd block
- Call get_launchd_label() directly instead of self-importing from hermes_cli.gateway
- Validate launchd output by checking parts[2] matches expected label (skip header)
- Add race-condition assumption docstring
2026-04-06 00:09:06 -07:00
kshitijk4poor
a2a9ad7431 fix: hermes update kills freshly-restarted gateway service
After restarting a service-managed gateway (systemd/launchd), the
stale-process sweep calls find_gateway_pids() which returns ALL gateway
PIDs via ps aux — including the one just spawned by the service manager.
The sweep kills it, leaving the user with a stopped gateway and a
confusing 'Restart manually' message.

Fix: add _get_service_pids() to query systemd MainPID and launchd PID
for active gateway services, then exclude those PIDs from the sweep.
Also add exclude_pids parameter to find_gateway_pids() and
kill_gateway_processes() so callers can skip known service-managed PIDs.

Adds 9 targeted tests covering:
- _get_service_pids() for systemd, launchd, empty, and zero-PID cases
- find_gateway_pids() exclude_pids filtering
- cmd_update integration: service PID not killed after restart
- cmd_update integration: manual PID killed while service PID preserved
2026-04-06 00:09:06 -07:00
Teknium
9c96f669a1
feat: centralized logging, instrumentation, hermes logs CLI, gateway noise fix (#5430)
Adds comprehensive logging infrastructure to Hermes Agent across 4 phases:

**Phase 1 — Centralized logging**
- New hermes_logging.py with idempotent setup_logging() used by CLI, gateway, and cron
- agent.log (INFO+) and errors.log (WARNING+) with RotatingFileHandler + RedactingFormatter
- config.yaml logging: section (level, max_size_mb, backup_count)
- All entry points wired (cli.py, main.py, gateway/run.py, run_agent.py)
- Fixed debug_helpers.py writing to ./logs/ instead of ~/.hermes/logs/

**Phase 2 — Event instrumentation**
- API calls: model, provider, tokens, latency, cache hit %
- Tool execution: name, duration, result size (both sequential + concurrent)
- Session lifecycle: turn start (session/model/provider/platform), compression (before/after)
- Credential pool: rotation events, exhaustion tracking

**Phase 3 — hermes logs CLI command**
- hermes logs / hermes logs -f / hermes logs errors / hermes logs gateway
- --level, --session, --since filters
- hermes logs list (file sizes + ages)

**Phase 4 — Gateway bug fix + noise reduction**
- fix: _async_flush_memories() called with wrong arg count — sessions never flushed
- Batched session expiry logs: 6 lines/cycle → 2 summary lines
- Added inbound message + response time logging

75 new tests, zero regressions on the full suite.
2026-04-06 00:08:20 -07:00
Teknium
89db3aeb2c
fix(cron): add delivery guidance to cron prompt — stop send_message thrashing (#5444)
Cron agents were burning iterations trying to use send_message (which is
disabled via messaging toolset) because their prompts said things like
'send the report to Telegram'. The scheduler handles delivery
automatically via the deliver setting, but nothing told the agent that.

Add a delivery guidance hint to _build_job_prompt alongside the existing
[SILENT] hint: tells agents their final response is auto-delivered and
they should NOT use send_message.

Before: only [SILENT] suppression hint
After: delivery guidance ('do NOT use send_message') + [SILENT] hint
2026-04-05 23:58:45 -07:00
Teknium
d6ef7fdf92
fix(cron): replace wall-clock timeout with inactivity-based timeout (#5440)
Port the gateway's inactivity-based timeout pattern (PR #5389) to the
cron scheduler. The agent can now run for hours if it's actively calling
tools or receiving stream tokens — only genuine inactivity (no activity
for HERMES_CRON_TIMEOUT seconds, default 600s) triggers a timeout.

This fixes the Sunday PR scouts (openclaw, nanoclaw, ironclaw) which
all hit the hard 600s wall-clock limit while actively working.

Changes:
- Replace flat future.result(timeout=N) with a polling loop that checks
  agent.get_activity_summary() every 5s (same pattern as gateway)
- Timeout error now includes diagnostic info: last activity description,
  idle duration, current tool, iteration count
- HERMES_CRON_TIMEOUT=0 means unlimited (no timeout)
- Move sys.path.insert before repo-level imports to fix
  ModuleNotFoundError for hermes_time on stale gateway processes
- Add time import needed by the polling loop
- Add 9 tests covering active/idle/unlimited/env-var/diagnostic scenarios
2026-04-05 23:49:42 -07:00
kshitijk4poor
38bcaa1e86 chore: remove langfuse doc, smoketest script, and installed-plugin test
Made-with: Cursor
2026-04-05 23:31:29 -07:00
kshitijk4poor
f530ef1835 feat(plugins): pre_api_request/post_api_request with narrow payloads
- Rename per-LLM-call hooks from pre_llm_request/post_llm_request for clarity vs pre_llm_call
- Emit summary kwargs only (counts, usage dict from normalize_usage); keep env_var_enabled for HERMES_DUMP_REQUESTS
- Add is_truthy_value/env_var_enabled to utils; wire hermes_cli.plugins._env_enabled through it
- Update Langfuse local setup doc; add scripts/langfuse_smoketest.py and optional ~/.hermes plugin tests

Made-with: Cursor
2026-04-05 23:31:29 -07:00
kshitijk4poor
9e820dda37 Add request-scoped plugin lifecycle hooks 2026-04-05 23:31:29 -07:00
Teknium
dce5f51c7c
feat: config structure validation — detect malformed YAML at startup (#5426)
Add validate_config_structure() that catches common config.yaml mistakes:
- custom_providers as dict instead of list (missing '-' in YAML)
- fallback_model accidentally nested inside another section
- custom_providers entries missing required fields (name, base_url)
- Missing model section when custom_providers is configured
- Root-level keys that look like misplaced custom_providers fields

Surface these diagnostics at three levels:
1. Startup: print_config_warnings() runs at CLI and gateway module load,
   so users see issues before hitting cryptic errors
2. Error time: 'Unknown provider' errors in auth.py and model_switch.py
   now include config diagnostics with fix suggestions
3. Doctor: 'hermes doctor' shows a Config Structure section with all
   issues and fix hints

Also adds a warning log in runtime_provider.py when custom_providers
is a dict (previously returned None silently).

Motivated by a Discord user who had malformed custom_providers YAML
and got only 'Unknown Provider' with no guidance on what was wrong.

17 new tests covering all validation paths.
2026-04-05 23:31:20 -07:00
Teknium
9ca954a274
fix: mem0 API v2 compat, prefetch context fencing, secret redaction (#5423)
Consolidated salvage from PRs #5301 (qaqcvc), #5339 (lance0),
#5058 and #5098 (maymuneth).

Mem0 API v2 compatibility (#5301):
- All reads use filters={user_id: ...} instead of bare user_id= kwarg
- All writes use filters with user_id + agent_id for attribution
- Response unwrapping for v2 dict format {results: [...]}
- Split _read_filters() vs _write_filters() — reads are user-scoped
  only for cross-session recall, writes include agent_id
- Preserved 'hermes-user' default (no breaking change for existing users)
- Omitted run_id scoping from #5301 — cross-session memory is Mem0's
  core value, session-scoping reads would defeat that purpose

Memory prefetch context fencing (#5339):
- Wraps prefetched memory in <memory-context> fenced blocks with system
  note marking content as recalled context, NOT user input
- Sanitizes provider output to strip fence-escape sequences, preventing
  injection where memory content breaks out of the fence
- API-call-time only — never persisted to session history

Secret redaction (#5058, #5098):
- Added prefix patterns for Groq (gsk_), Matrix (syt_), RetainDB
  (retaindb_), Hindsight (hsk-), Mem0 (mem0_), ByteRover (brv_)
2026-04-05 22:43:33 -07:00
Teknium
786970925e
fix(cli): add missing subprocess.run() timeouts in gateway CLI (#5424)
All 35 subprocess.run() calls in hermes_cli/gateway.py lacked timeout
parameters. If systemctl, launchctl, loginctl, wmic, or ps blocks,
hermes gateway start/stop/restart/status/install/uninstall hangs
indefinitely with no feedback.

Timeouts tiered by operation type:
- 10s: instant queries (is-active, status, list, ps, tail, journalctl)
- 30s: fast lifecycle (daemon-reload, enable, start, bootstrap, kickstart)
- 90s: graceful shutdown (stop, restart, bootout, kickstart -k) — exceeds
  our TimeoutStopSec=60 to avoid premature timeout during shutdown

Special handling: _is_service_running() and launchd_status() catch
TimeoutExpired and treat it as not-running/not-loaded, consistent with
how non-zero return codes are already handled.

Inspired by PR #3732 (dlkakbs) and issue #4057 (SHL0MS).
Reimplemented on current main which has significantly changed launchctl
handling (bootout/bootstrap/kickstart vs legacy load/unload/start/stop).
2026-04-05 22:41:42 -07:00
Teknium
38d8446011
feat: implement MCP OAuth 2.1 PKCE client support (#5420)
Implement tools/mcp_oauth.py — the OAuth adapter that mcp_tool.py's
existing auth: oauth hook has been waiting for.

Components:
- HermesTokenStorage: persists tokens + client registration to
  HERMES_HOME/mcp-tokens/<server>.json with 0o600 permissions
- Callback handler factory: per-flow isolated HTTP handlers (safe for
  concurrent OAuth flows across multiple MCP servers)
- OAuthClientProvider integration: wraps the MCP SDK's httpx.Auth
  subclass which handles discovery, DCR, PKCE, token exchange,
  refresh, and step-up auth (403 insufficient_scope) automatically
- Non-interactive detection: warns when gateway/cron environments
  try to OAuth without cached tokens
- Pre-registered client support: injects client_id/secret from config
  for servers that don't support Dynamic Client Registration (e.g. Slack)
- Path traversal protection on server names
- remove_oauth_tokens() for cleanup

Config format:
  mcp_servers:
    sentry:
      url: 'https://mcp.sentry.dev/mcp'
      auth: oauth
      oauth:                          # all optional
        client_id: '...'              # skip DCR
        client_secret: '...'          # confidential client
        scope: 'read write'           # server-provided by default

Also passes oauth config dict through from mcp_tool.py (was passing
only server_name and url before).

E2E verified: full OAuth flow (401 → discovery → DCR → authorize →
token exchange → authenticated request → tokens persisted) against
local test servers. 23 unit tests + 186 MCP suite tests pass.
2026-04-05 22:08:00 -07:00
emozilla
0365f6202c feat: show model pricing for OpenRouter and Nous Portal providers
Display live per-million-token pricing from /v1/models when listing
models for OpenRouter or Nous Portal. Prices are shown in a
column-aligned table with decimal points vertically aligned for
easy comparison.

Pricing appears in three places:
- /provider slash command (table with In/Out headers)
- hermes model picker (aligned columns in both TerminalMenu and
  numbered fallback)

Implementation:
- Add fetch_models_with_pricing() in models.py with per-base_url
  module-level cache (one network call per endpoint per session)
- Add _format_price_per_mtok() with fixed 2-decimal formatting
- Add format_model_pricing_table() for terminal table display
- Add get_pricing_for_provider() convenience wrapper
- Update _prompt_model_selection() to accept optional pricing dict
- Wire pricing through _model_flow_openrouter/nous in main.py
- Update test mocks for new pricing parameter
2026-04-05 22:02:21 -07:00
Teknium
0efe7dace7
feat: add GPT/Codex execution discipline guidance for tool persistence (#5414)
Adds OPENAI_MODEL_EXECUTION_GUIDANCE — XML-tagged behavioral guidance
injected for GPT and Codex models alongside the existing tool-use
enforcement. Targets four specific failure modes:

- <tool_persistence>: retry on empty/partial results instead of giving up
- <prerequisite_checks>: do discovery/lookup before jumping to final action
- <verification>: check correctness/grounding/formatting before finalizing
- <missing_context>: use lookup tools instead of hallucinating

Follows the same injection pattern as GOOGLE_MODEL_OPERATIONAL_GUIDANCE
for Gemini/Gemma models. Inspired by OpenClaw PR #38953 and OpenAI's
GPT-5.4 prompting guide patterns.
2026-04-05 21:51:07 -07:00
Teknium
89c812d1d2
feat: shared thread sessions by default — multi-user thread support (#5391)
Threads (Telegram forum topics, Discord threads, Slack threads) now default
to shared sessions where all participants see the same conversation. This is
the expected UX for threaded conversations where multiple users @mention the
bot and interact collaboratively.

Changes:
- build_session_key(): when thread_id is present, user_id is no longer
  appended to the session key (threads are shared by default)
- New config: thread_sessions_per_user (default: false) — opt-in to restore
  per-user isolation in threads if needed
- Sender attribution: messages in shared threads are prefixed with
  [sender name] so the agent can tell participants apart
- System prompt: shared threads show 'Multi-user thread' note instead of
  a per-turn User line (avoids busting prompt cache)
- Wired through all callers: gateway/run.py, base.py, telegram.py, feishu.py
- Regular group messages (no thread) remain per-user isolated (unchanged)
- DM threads are unaffected (they have their own keying logic)

Closes community request from demontut_ re: thread-based shared sessions.
2026-04-05 19:46:58 -07:00
Dusk1e
e9ddfee4fd fix(plugins): reject plugin names that resolve to the plugins root
Reject "." as a plugin name — it resolves to the plugins directory
itself, which in force-install flows causes shutil.rmtree to wipe the
entire plugins tree.

- reject "." early with a clear error message
- explicit check for target == plugins_resolved (raise instead of allow)
- switch boundary check from string-prefix to Path.relative_to()
- add regression tests for sanitizer + install flow

Co-authored-by: Dusk1e <yusufalweshdemir@gmail.com>
2026-04-05 18:40:45 -07:00
Teknium
4494fba140
feat: OSV malware check for MCP extension packages (#5305)
Before launching an MCP server via npx/uvx, queries the OSV (Open Source
Vulnerabilities) API to check if the package has known malware advisories
(MAL-* IDs). Regular CVEs are ignored — only confirmed malware is blocked.

- Free, public API (Google-maintained), ~300ms per query
- Runs once per MCP server launch, inside _run_stdio() before subprocess spawn
- Parallel with other MCP servers (asyncio.gather already in place)
- Fail-open: network errors, timeouts, unrecognized commands → allow
- Parses npm (scoped @scope/pkg@version) and PyPI (name[extras]==version)

Inspired by Block/goose extension malware check.
2026-04-05 12:46:07 -07:00
Teknium
8d5226753f fix: add missing ButtonStyle.grey to discord mock for test compatibility 2026-04-05 12:42:47 -07:00
Abhey
66d0fa1778 fix: avoid unnecessary Discord members intent on startup
Only request the privileged members intent when DISCORD_ALLOWED_USERS includes non-numeric entries that need username resolution. Also release the Discord token lock when startup fails so retries and restarts are not blocked by a stale lock.\n\nAdds regression tests for conditional intents and startup lock cleanup.
2026-04-05 12:42:47 -07:00
Teknium
583d9f9597 fix(honcho): migration guard for observation mode default change
Existing honcho.json configs without an explicit observationMode now
default to 'unified' (the old default) instead of being silently
switched to 'directional'. New installations get 'directional' as
the new default.

Detection: _explicitly_configured (host block exists or enabled=true)
signals an existing config. When true and no observationMode is set
anywhere in the config chain, falls back to 'unified'. When false
(fresh install), uses 'directional'.

Users who explicitly set observationMode or granular observation
booleans are unaffected — explicit config always wins.

5 new tests covering all migration paths.
2026-04-05 12:34:11 -07:00
Teknium
0f813c422c fix(plugins): only register CLI commands for the active memory provider
discover_plugin_cli_commands() now reads memory.provider from config.yaml
and only loads CLI registration for the active provider. If no memory
provider is set, no plugin CLI commands appear in the CLI.

Only one memory provider can be active at a time — at most one set of
plugin CLI commands is registered. Users who haven't configured honcho
(or any memory provider) won't see 'hermes honcho' in their help output.

Adds test for inactive provider returning empty results.
2026-04-05 12:34:11 -07:00
Teknium
b074b0b13a test: add plugin CLI registration tests
11 tests covering:
- PluginContext.register_cli_command() storage and overwrite
- get_plugin_cli_commands() return semantics
- Memory plugin discover_plugin_cli_commands() with register_cli convention
- Skipping plugins without register_cli or cli.py
- Honcho register_cli() subcommand tree structure
- Mode choices updated to recall modes (hybrid/context/tools)
- _ProviderCollector.register_cli_command no-op safety
2026-04-05 12:34:11 -07:00
erosika
c02c3dc723 fix(honcho): plugin drift overhaul -- observation config, chunking, setup wizard, docs, dead code cleanup
Salvaged from PR #5045 by erosika.

- Replace memoryMode/peer_memory_modes with granular per-peer observation config
- Add message chunking for Honcho API limits (25k chars default)
- Add dialectic input guard (10k chars default)
- Add dialecticDynamic toggle for reasoning level auto-bump
- Rewrite setup wizard with cloud/local deployment picker
- Switch peer card/profile/search from session.context() to direct peer APIs
- Add server-side observation sync via get_peer_configuration()
- Fix base_url/baseUrl config mismatch for self-hosted setups
- Fix local auth leak (cloud API keys no longer sent to local instances)
- Remove dead code: memoryMode, peer_memory_modes, linkedHosts, suppress flags, SOUL.md aiPeer sync
- Add post_setup hook to memory_setup.py for provider-specific setup wizards
- Comprehensive README rewrite with full config reference
- New optional skill: autonomous-ai-agents/honcho
- Expanded memory-providers.md with multi-profile docs
- 9 new tests (chunking, dialectic guard, peer lookups), 14 dead tests removed
- Fix 2 pre-existing TestResolveConfigPath filesystem isolation failures
2026-04-05 12:34:11 -07:00
Teknium
12724e6295
feat: progressive subdirectory hint discovery (#5291)
As the agent navigates into subdirectories via tool calls (read_file,
terminal, search_files, etc.), automatically discover and load project
context files (AGENTS.md, CLAUDE.md, .cursorrules) from those directories.

Previously, context files were only loaded from the CWD at session start.
If the agent moved into backend/, frontend/, or any subdirectory with its
own AGENTS.md, those instructions were never seen.

Now, SubdirectoryHintTracker watches tool call arguments for file paths
and shell commands, resolves directories, and loads hint files on first
access. Discovered hints are appended to the tool result so the model
gets relevant context at the moment it starts working in a new area —
without modifying the system prompt (preserving prompt caching).

Features:
- Extracts paths from tool args (path, workdir) and shell commands
- Loads AGENTS.md, CLAUDE.md, .cursorrules (first match per directory)
- Deduplicates — each directory loaded at most once per session
- Ignores paths outside the working directory
- Truncates large hint files at 8K chars
- Works on both sequential and concurrent tool execution paths

Inspired by Block/goose SubdirectoryHintTracker.
2026-04-05 12:33:47 -07:00
MichaelWDanko
c6793d6fc3 fix(gateway): wrap cron helpers with staticmethod to prevent self-binding
Plain functions imported as class attributes in APIServerAdapter get
auto-bound as methods via Python's descriptor protocol.  Every
self._cron_*() call injected self as the first positional argument,
causing TypeError on all 8 cron API endpoints at runtime.

Wrap each import with staticmethod() so self._cron_*() calls dispatch
correctly without modifying any call sites.

Co-authored-by: teknium <teknium@nousresearch.com>
2026-04-05 12:31:10 -07:00
Mibayy
cc2b56b26a feat(api): structured run events via /v1/runs SSE endpoint
Add POST /v1/runs to start async agent runs and GET /v1/runs/{run_id}/events
for SSE streaming of typed lifecycle events (tool.started, tool.completed,
message.delta, reasoning.available, run.completed, run.failed).

Changes the internal tool_progress_callback signature from positional
(tool_name, preview, args) to event-type-first
(event_type, tool_name, preview, args, **kwargs). Existing consumers
filter on event_type and remain backward-compatible.

Adds concurrency limit (_MAX_CONCURRENT_RUNS=10) and orphaned run sweep.

Fixes logic inversion in cli.py _on_tool_progress where the original PR
would have displayed internal tools instead of non-internal ones.

Co-authored-by: Mibayy <mibayy@users.noreply.github.com>
2026-04-05 12:05:13 -07:00
NexVeridian
c71b1d197f fix(acp): advertise slash commands via ACP protocol
Send AvailableCommandsUpdate on session create/load/resume/fork so ACP
clients (Zed, etc.) can discover /help, /model, /tools, /compact, etc.
Also rewrites /compact to use agent._compress_context() properly with
token estimation and session DB isolation.

Co-authored-by: NexVeridian <NexVeridian@users.noreply.github.com>
2026-04-05 12:05:13 -07:00
Git-on-my-level
fcdd5447e2 fix: keep ACP stdout protocol-clean
Route AIAgent print output to stderr via _print_fn for ACP stdio sessions.
Gate quiet-mode spinner startup on _should_start_quiet_spinner() so JSON-RPC
on stdout isn't corrupted. Child agents inherit the redirect.

Co-authored-by: Git-on-my-level <Git-on-my-level@users.noreply.github.com>
2026-04-05 12:05:13 -07:00
Teknium
6ee90a7cf6
fix: hermes auth remove now clears env-seeded credentials permanently (#5285)
Removing an env-seeded credential (e.g. from OPENROUTER_API_KEY) via
'hermes auth' previously had no lasting effect -- the entry was deleted
from auth.json but load_pool() re-created it on the next call because
the env var was still set.

Now auth_remove_command detects env-sourced entries (source starts with
'env:') and calls the new remove_env_value() to strip the var from both
.env and os.environ, preventing re-seeding.

Changes:
- hermes_cli/config.py: add remove_env_value() -- atomically removes a
  line from .env and pops from os.environ
- hermes_cli/auth_commands.py: auth_remove_command clears env var when
  removing an env-seeded pool entry
- 8 new tests covering remove_env_value and the full zombie-credential
  lifecycle (remove -> reload -> stays gone)
2026-04-05 12:00:53 -07:00
Teknium
0c95e91059 fix: follow-up fixes for salvaged PRs
- Fix GatewayApp → GatewayRunner import in api_server.py (PR #4976)
- Update launchd test assertions for new bootstrap/bootout/kickstart commands (PR #4892)
- Add nonlocal message declaration in run_sync() to fix UnboundLocalError (pre-existing scoping bug)
2026-04-05 11:59:28 -07:00
analista
e8053e8b93 fix(gateway): surface unknown /commands instead of leaking them to the LLM
Previously, typing a /command that isn't a built-in, plugin, or skill
would silently fall through to the LLM as plain text. The model often
interprets it as a loose instruction and invents unrelated tool calls —
e.g. a stray /claude_code slipped through and the model fabricated a
delegate_task invocation that got stuck in an OAuth loop.

Now we check GATEWAY_KNOWN_COMMANDS after the skill / plugin /
unavailable-skill lookups and return an actionable message pointing the
user at /commands. The user gets feedback, and the agent doesn't waste
a round-trip guessing what /foo-bar was supposed to mean.
2026-04-05 11:59:28 -07:00
analista
4a75aec433 fix(gateway): resolve Telegram's underscored /commands to skill/plugin keys
Telegram's Bot API disallows hyphens in command names, so
_build_telegram_menu registers /claude-code as /claude_code. When the
user taps it from autocomplete, the gateway dispatch did a direct
lookup against skill_cmds (keyed on the hyphenated form) and missed,
silently falling through to the LLM as plain text. The model would
then typically call delegate_task, spawning a Hermes subagent instead
of invoking the intended skill.

Normalize underscores to hyphens in skill and plugin command lookup,
matching the existing pattern in _check_unavailable_skill.
2026-04-05 11:59:28 -07:00
Damian P
afccbf253c fix: resolve listed messaging targets consistently 2026-04-05 11:59:28 -07:00
kshitijk4poor
1d2e34c7eb Prevent Telegram polling handoffs and flood-control send failures
Telegram polling can inherit a stale webhook registration when a deployment
switches transport modes, which leaves getUpdates idle even though the gateway
starts cleanly. Outbound send also treats Telegram retry_after responses as
terminal errors, so brief flood control can drop tool progress and replies.

Constraint: Keep the PR narrowly scoped to upstream/main Telegram adapter behavior
Rejected: Port OpenClaw's broader polling supervisor and offset persistence | too broad for an isolated fix PR
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Polling mode should clear webhook state before starting getUpdates, and send-path retry logic must distinguish flood control from timeouts
Tested: uv run --extra dev pytest tests/gateway/test_telegram_* -q
Not-tested: Live Telegram webhook-to-polling migration and real Bot API 429 behavior
2026-04-05 11:59:28 -07:00
Trevin Chow
74ff62f5ac fix(gateway): use kickstart -k for atomic launchd restart
Replace the two-step stop/start restart with a single
launchctl kickstart -k call. When the gateway triggers a
restart from inside its own process tree, the old stop
command kills the shell before the start half is reached.
kickstart -k lets launchd handle the kill+restart atomically.
2026-04-05 11:59:28 -07:00
Trevin Chow
aab74b582c fix(gateway): replace deprecated launchctl start/stop with kickstart/kill
launchctl load/unload/start/stop are deprecated on macOS since 10.10
and fail silently on modern versions. This replaces them with the
current equivalents:

- load -> bootstrap gui/<uid> <plist>
- unload -> bootout gui/<uid>/<label>
- start -> kickstart gui/<uid>/<label>
- stop -> kill SIGTERM gui/<uid>/<label>

Adds _launchd_domain() helper returning the gui/<uid> target domain.
Updates test assertions to match the new command signatures.

Fixes #4820
2026-04-05 11:59:28 -07:00
teyrebaz33
6df0f07ff3 fix: /status command bypasses active-session guard during agent run (#5046)
When an agent was actively processing a message, /status sent via Telegram
(or any gateway) was queued as a pending interrupt instead of being dispatched
immediately. The base platform adapter's handle_message() only had special-case
bypass logic for /approve and /deny, so /status fell through to the default
interrupt path and was never processed as a system command.

Apply the same bypass pattern used by /approve//deny: detect cmd == 'status'
inside the active-session guard, dispatch directly to the message handler, and
send the response without touching session lifecycle or interrupt state.

Adds a regression test that verifies /status is dispatched and responded to
immediately even when _active_sessions contains an entry for the session.
2026-04-05 11:59:28 -07:00
Teknium
a0a1b86c2e
fix: accept reasoning-only responses without retries — set content to "(empty)" (#5278)
* feat: coerce tool call arguments to match JSON Schema types

LLMs frequently return numbers as strings ("42" instead of 42) and
booleans as strings ("true" instead of true). This causes silent
failures with MCP tools and any tool with strictly-typed parameters.

Added coerce_tool_args() in model_tools.py that runs before every tool
dispatch. For each argument, it checks the tool registry schema and
attempts safe coercion:
  - "42" → 42 when schema says "type": "integer"
  - "3.14" → 3.14 when schema says "type": "number"
  - "true"/"false" → True/False when schema says "type": "boolean"
  - Union types tried in order
  - Original values preserved when coercion fails or is not applicable

Inspired by Block/goose tool argument coercion system.

* fix: accept reasoning-only responses without retries — set content to "(empty)"

Previously, when a model returned reasoning/thinking but no visible
content, we entered a 120-line retry/classify/compress/salvage cascade
that wasted 3+ API calls trying to "fix" the response. The model was
done thinking — retrying with the same input just burned money.

Now reasoning-only responses are accepted immediately:
- Reasoning stays in the `reasoning` field (semantically correct)
- Content set to "(empty)" — valid non-empty string every provider accepts
- No retries, no compression triggers, no salvage logic
- Session history contains "(empty)" not "" — prevents #2128 session
  poisoning where empty assistant content caused prefill rejections

Removes ~120 lines, adds ~15. Saves 2-3 API calls per reasoning-only
response. Fixes #2128.
2026-04-05 11:30:52 -07:00
nepenth
534511bebb feat(matrix): Tier 1 enhancement — reactions, read receipts, rich formatting, room management
Cherry-picked from PR #4338 by nepenth, resolved against current main.

Adds:
- Processing lifecycle reactions (eyes/checkmark/cross) via MATRIX_REACTIONS env
- Reaction send/receive with ReactionEvent + UnknownEvent fallback for older nio
- Fire-and-forget read receipts on text and media messages
- Message redaction, room history fetch, room creation, user invite
- Presence status control (online/offline/unavailable)
- Emote (/me) and notice message types with HTML rendering
- XSS-hardened markdown-to-HTML converter (strips raw HTML preprocessor,
  sanitizes link URLs against javascript:/data:/vbscript: schemes)
- Comprehensive regex fallback with full block/inline markdown support
- Markdown>=3.6 added to [matrix] extras in pyproject.toml
- 46 new tests covering all features and security hardening
2026-04-05 11:19:54 -07:00
chalkers
bec02f3731 fix(matrix): handle encrypted media events and cache decrypted attachments
Cherry-picked from PR #3140 by chalkers, resolved against current main.
Registers RoomEncryptedImage/Audio/Video/File callbacks, decrypts
attachments via nio.crypto, caches all media types (images, audio,
documents), prevents ciphertext URL fallback for encrypted media.
Unifies the separate voice-message download into the main cache block.
Preserves main's MATRIX_REQUIRE_MENTION, auto-thread, and mention
stripping features. Includes 355 lines of encrypted media tests.
2026-04-05 11:07:47 -07:00
binhnt92
b65e67545a fix(gateway): stop Matrix/Mattermost reconnect on permanent auth failures
Cherry-picked from PR #3695 by binhnt92.
Matrix _sync_loop() and Mattermost _ws_loop() were retrying all errors
forever, including permanent auth failures (expired tokens, revoked
access). Now detects M_UNKNOWN_TOKEN, M_FORBIDDEN, 401/403 and stops
instead of spinning. Includes 216 lines of tests.
2026-04-05 11:07:47 -07:00
thakoreh
914f7461dc fix: add missing shutil import for Matrix E2EE setup
Cherry-picked from PR #5136 by thakoreh.
setup_gateway() uses shutil.which('uv') at line 2126 but shutil was
never imported at module level, causing NameError during Matrix E2EE
auto-install. Adds top-level import and regression test.
2026-04-05 11:07:47 -07:00
LucidPaths
70f798043b fix: Ollama Cloud auth, /model switch persistence, and alias tab completion
- Add OLLAMA_API_KEY to credential resolution chain for ollama.com endpoints
- Update requested_provider/_explicit_api_key/_explicit_base_url after /model
  switch so _ensure_runtime_credentials() doesn't revert the switch
- Pass base_url/api_key from fallback config to resolve_provider_client()
- Add DirectAlias system: user-configurable model_aliases in config.yaml
  checked before catalog resolution, with reverse lookup by model ID
- Add /model tab completion showing aliases with provider metadata

Co-authored-by: LucidPaths <LucidPaths@users.noreply.github.com>
2026-04-05 11:06:06 -07:00
Teknium
35d280d0bd
feat: coerce tool call arguments to match JSON Schema types (#5265)
LLMs frequently return numbers as strings ("42" instead of 42) and
booleans as strings ("true" instead of true). This causes silent
failures with MCP tools and any tool with strictly-typed parameters.

Added coerce_tool_args() in model_tools.py that runs before every tool
dispatch. For each argument, it checks the tool registry schema and
attempts safe coercion:
  - "42" → 42 when schema says "type": "integer"
  - "3.14" → 3.14 when schema says "type": "number"
  - "true"/"false" → True/False when schema says "type": "boolean"
  - Union types tried in order
  - Original values preserved when coercion fails or is not applicable

Inspired by Block/goose tool argument coercion system.
2026-04-05 10:57:34 -07:00
Teknium
51ed7dc2f3
feat: save oversized tool results to file instead of destructive truncation (#5210)
Previously, tool results exceeding 100K characters were silently chopped
with only a '[Truncated]' notice — the rest of the content was lost
permanently. The model had no way to access the truncated portion.

Now, oversized results are written to HERMES_HOME/cache/tool_responses/
and the model receives:
  - A 1,500-char head preview for immediate context
  - The file path so it can use read_file/search_files on the full output

This preserves the context window protection (inline content stays small)
while making the full data recoverable. Falls back to the old destructive
truncation if the file write fails.

Inspired by Block/goose's large response handler pattern.
2026-04-05 10:29:57 -07:00
Teknium
0c54da8aaf
feat(gateway): live-stream /update output + interactive prompt buttons (#5180)
* feat(gateway): live-stream /update output + forward interactive prompts

Adds real-time output streaming and interactive prompt forwarding for
the gateway /update command, so users on Telegram/Discord/etc see the
full update progress and can respond to prompts (stash restore, config
migration) without needing terminal access.

Changes:

hermes_cli/main.py:
- Add --gateway flag to 'hermes update' argparse
- Add _gateway_prompt() file-based IPC function that writes
  .update_prompt.json and polls for .update_response
- Modify _restore_stashed_changes() to accept optional input_fn
  parameter for gateway mode prompt forwarding
- cmd_update() uses _gateway_prompt when --gateway is set, enabling
  interactive stash restore and config migration prompts

gateway/run.py:
- _handle_update_command: spawn with --gateway flag and
  PYTHONUNBUFFERED=1 for real-time output flushing
- Store session_key in .update_pending.json for cross-restart
  session matching
- Add _update_prompt_pending dict to track sessions awaiting
  update prompt responses
- Replace _watch_for_update_completion with _watch_update_progress:
  streams output chunks every ~4s, detects .update_prompt.json and
  forwards prompts to the user, handles completion/failure/timeout
- Add update prompt interception in _handle_message: when a prompt
  is pending, the user's next message is written to .update_response
  instead of being processed normally
- Preserve _send_update_notification as legacy fallback for
  post-restart cases where adapter isn't available yet

File-based IPC protocol:
- .update_prompt.json: written by update process with prompt text,
  default value, and unique ID
- .update_response: written by gateway with user's answer
- .update_output.txt: existing, now streamed in real-time
- .update_exit_code: existing completion marker

Tests: 16 new tests covering _gateway_prompt IPC, output streaming,
prompt detection/forwarding, message interception, and cleanup.

* feat: interactive buttons for update prompts (Telegram + Discord)

Telegram: Inline keyboard with ✓ Yes / ✗ No buttons. Clicking a button
answers the callback query, edits the message to show the choice, and
writes .update_response directly. CallbackQueryHandler registered on
the update_prompt: prefix.

Discord: UpdatePromptView (discord.ui.View) with green Yes / red No
buttons. Follows the ExecApprovalView pattern — auth check, embed color
update, disabled-after-click. Writes .update_response on click.

All platforms: /approve and /deny (and /yes, /no) now work as shorthand
for yes/no when an update prompt is pending. The text fallback message
instructs users to use these commands. Raw message interception still
works as a fallback for non-command responses.

Gateway watcher checks adapter for send_update_prompt method (class-level
check to avoid MagicMock false positives) and falls back to text prompt
with /approve instructions when unavailable.

* fix: block /update on non-messaging platforms (API, webhooks, ACP)

Add _UPDATE_ALLOWED_PLATFORMS frozenset that explicitly lists messaging
platforms where /update is permitted. API server, webhook, and ACP
platforms get a clear error directing them to run hermes update from
the terminal instead.

ACP and API server already don't reach _handle_message (separate
codepaths), and webhooks have distinct session keys that can't collide
with messaging sessions. This guard is belt-and-suspenders.
2026-04-05 00:28:58 -07:00
kshitijk4poor
4437354198 Preserve numeric credential labels in auth removal
Resolve exact label matches before treating digit-only input as a positional index so destructive auth removal does not mis-target credentials named with numeric labels.

Constraint: The CLI remove path must keep supporting existing index-based usage while adding safer label targeting
Rejected: Ban numeric labels | labels are free-form and existing users may already rely on them
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: When a destructive command accepts multiple identifier forms, prefer exact identity matches before fallback parsing heuristics
Tested: Focused pytest slice for auth commands, credential pool recovery, and routing (273 passed); py_compile on changed Python files
Not-tested: Full repository pytest suite
2026-04-05 00:20:53 -07:00
kshitijk4poor
65952ac00c Honor provider reset windows in pooled credential failover
Persist structured exhaustion metadata from provider errors, use explicit reset timestamps when available, and expose label-based credential targeting in the auth CLI. This keeps long-lived Codex cooldowns from being misreported as one-hour waits and avoids forcing operators to manage entries by list position alone.

Constraint: Existing credential pool JSON needs to remain backward compatible with stored entries that only record status code and timestamp
Constraint: Runtime recovery must keep the existing retry-then-rotate semantics for 429s while enriching pool state with provider metadata
Rejected: Add a separate credential scheduler subsystem | too large for the Hermes pool architecture and unnecessary for this fix
Rejected: Only change CLI formatting | would leave runtime rotation blind to resets_at and preserve the serial-failure behavior
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Preserve structured rate-limit metadata when new providers expose reset hints; do not collapse back to status-code-only exhaustion tracking
Tested: Focused pytest slice for auth commands, credential pool recovery, and routing (272 passed); py_compile on changed Python files; hermes -w auth list/remove smoke test with temporary HERMES_HOME
Not-tested: Full repository pytest suite, broader gateway/integration flows outside the touched auth and pool paths
2026-04-05 00:20:53 -07:00
Lume
8545343cba test: add strict API validation tests for Fireworks compatibility
Adds comprehensive tests verifying:
- Fireworks-compatible messages after sanitization
- Codex mode preserves fields for Responses API replay
- Fireworks provider triggers sanitization correctly
- Codex responses mode correctly skips sanitization

Prevents regression of 400 validation errors on strict APIs.
2026-04-05 00:13:25 -07:00
Lume
9be2b18064 test: add test for _should_sanitize_tool_calls()
Adds test verifying that:
- Codex mode returns False (no sanitization needed)
- Chat completions mode returns True (sanitization needed)
- Anthropic mode returns True (sanitization needed)

This ensures strict APIs like Fireworks receive properly sanitized tool_calls.
2026-04-05 00:13:25 -07:00
Teknium
5b003ca4a0
test(redact): add regression tests for lowercase variable redaction (#4367) (#5185)
Add 5 regression tests from PR #4476 (gnanam1990) to prevent re-introducing
the IGNORECASE bug that caused lowercase Python/TypeScript variable assignments
to be incorrectly redacted as secrets. The core fix landed in 6367e1c4.

Tests cover:
- Lowercase Python variable with 'token' in name
- Lowercase Python variable with 'api_key' in name
- TypeScript 'await' not treated as secret value
- TypeScript 'secret' variable assignment
- 'export' prefix preserved for uppercase env vars

Co-authored-by: gnanam1990 <gnanam1990@users.noreply.github.com>
2026-04-05 00:10:16 -07:00
Teknium
85cefc7a5a
fix(telegram): prevent duplicate message delivery on send timeout (#5153)
TimedOut is a subclass of NetworkError in python-telegram-bot. The
inner retry loop in send() and the outer _send_with_retry() in base.py
both treated it as a transient connection error and retried — but
send_message is not idempotent. When the request reaches Telegram but
the HTTP response times out, the message is already delivered. Retrying
sends duplicates. Worst case: up to 9 copies (inner 3x × outer 3x).

Inner loop (telegram.py):
- Import TimedOut separately, isinstance-check before generic
  NetworkError retry (same pattern as BadRequest carve-out from #3390)
- Re-raise immediately — no retry
- Mark as retryable=False in outer exception handler

Outer loop (base.py):
- Remove 'timeout', 'timed out', 'readtimeout', 'writetimeout' from
  _RETRYABLE_ERROR_PATTERNS (read/write timeouts are delivery-ambiguous)
- Add 'connecttimeout' (safe — connection never established)
- Keep 'network' (other platforms still need it)
- Add _is_timeout_error() + early return to prevent plain-text fallback
  on timeout errors (would also cause duplicate delivery)

Connection errors (ConnectionReset, ConnectError, etc.) are still
retried — these fail before the request reaches the server.

Credit: tmdgusya (PR #3899), barun1997 (PR #3904) for identifying the
bug and proposing fixes.

Closes #3899, closes #3904.
2026-04-04 19:05:34 -07:00
Teknium
c8220e69a1
fix: strip MEDIA: directives from streamed gateway messages (#5152)
When streaming is enabled, the GatewayStreamConsumer sends raw text
chunks directly to the platform without post-processing. This causes
MEDIA:/path/to/file tags and [[audio_as_voice]] directives to appear
as visible text in the user's chat instead of being stripped.

The non-streaming path already handles this correctly via
extract_media() in base.py, but the streaming path was missing
equivalent cleanup.

Add _clean_for_display() to GatewayStreamConsumer that strips MEDIA:
tags and internal markers before any text reaches the platform. The
actual media file delivery is unaffected — _deliver_media_from_response()
in gateway/run.py still extracts files from the agent's final_response
(separate from the stream consumer's display text).

Reported by Ao [FotM] on Discord.
2026-04-04 19:05:27 -07:00
Teknium
aa475aef31
feat: add exit code context for common CLI tools in terminal results (#5144)
When commands like grep, diff, test, or find return non-zero exit codes
that aren't actual errors (grep 1 = no matches, diff 1 = files differ),
the model wastes turns investigating non-problems. This adds an
exit_code_meaning field to the terminal JSON result that explains
informational exit codes, so the agent can move on instead of debugging.

Covers grep/rg/ag/ack (no matches), diff (files differ), find (partial
access), test/[ (condition false), curl (timeouts, DNS, HTTP errors),
and git (context-dependent). Correctly extracts the last command from
pipelines and chains, strips full paths and env var assignments.

The exit_code field itself is unchanged — this is purely additive context.
2026-04-04 16:57:24 -07:00
Teknium
5879b3ef82
fix: move pre_llm_call plugin context to user message, preserve prompt cache (#5146)
Plugin context from pre_llm_call hooks was injected into the system
prompt, breaking the prompt cache prefix every turn when content
changed (typical for memory plugins). Now all plugin context goes
into the current turn's user message — the system prompt stays
identical across turns, preserving cached tokens.

The system prompt is reserved for Hermes internals. Plugins
contribute context alongside the user's input.

Also adds comprehensive documentation for all 6 plugin hooks:
pre_tool_call, post_tool_call, pre_llm_call, post_llm_call,
on_session_start, on_session_end — each with full callback
signatures, parameter tables, firing conditions, and examples.

Supersedes #5138 which identified the same cache-busting bug
and proposed an uncached system suffix approach. This fix goes
further by removing system prompt injection entirely.

Co-identified-by: OutThisLife (PR #5138)
2026-04-04 16:55:44 -07:00
Teknium
96e96a79ad
fix: --yolo and other flags silently dropped when placed before 'chat' subcommand (#5145)
When --yolo, -w, -s, -r, -c, and --pass-session-id exist on both the parent
parser and the 'chat' subparser with explicit defaults (default=False or
default=None), argparse's subparser initialization overwrites the parent's
parsed value. So 'hermes --yolo chat' silently drops --yolo, making it appear
broken.

Fix: use default=argparse.SUPPRESS on all duplicated arguments in the chat
subparser. SUPPRESS means 'don't set this attribute if the user didn't
explicitly provide it', so the parent parser's value survives through.

Affected flags: --yolo, --worktree/-w, --skills/-s, --pass-session-id,
--resume/-r, --continue/-c.

Adds 15 regression tests covering flag-before-subcommand, flag-after-subcommand,
no-subcommand, and env var propagation scenarios.
2026-04-04 16:55:13 -07:00
Fran Fitzpatrick
2556cfdab1 fix(gateway): match Discord mention-stripping behavior in Matrix adapter
Move mention stripping outside the `if not is_dm` guard so mentions
are stripped in DMs too. Remove the bare-mention early return so a
message containing only a mention passes through as empty string,
matching Discord's behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 13:09:27 -07:00
Fran Fitzpatrick
d86be33161 feat(gateway): add MATRIX_REQUIRE_MENTION and MATRIX_AUTO_THREAD support
Bring Matrix feature parity with Discord by adding mention gating and
auto-threading. Both default to true, matching Discord behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 13:09:27 -07:00
Chris Bartholomew
28e1e210ee fix(hindsight): overhaul hindsight memory plugin and memory setup wizard
- Dedicated asyncio event loop for Hindsight async calls (fixes aiohttp session leaks)
- Client caching (reuse instead of creating per-call)
- Local mode daemon management with config change detection and auto-restart
- Memory mode support (hybrid/context/tools) and prefetch method (recall/reflect)
- Proper shutdown with event loop and client cleanup
- Disable HindsightEmbedded.__del__ to avoid GC loop errors
- Update API URLs (app -> ui.hindsight.vectorize.io, api_url -> base_url)
- Setup wizard: conditional fields (when clause), dynamic defaults (default_from)
- Switch dependency install from pip to uv (correct for uv-based venvs)
- Add hindsight-all to plugin.yaml and import mapping
- 12 new tests for dispatch routing and setup field filtering

Original PR #5044 by cdbartholomew.
2026-04-04 12:18:46 -07:00
Teknium
5d0f55cac4
feat(cron): add script field for pre-run data collection (#5082)
Add an optional 'script' parameter to cron jobs that references a Python
script. The script runs before each agent turn, and its stdout is injected
into the prompt as context. This enables stateful monitoring — the script
handles data collection and change detection, the LLM analyzes and reports.

- cron/jobs.py: add script field to create_job(), stored in job dict
- cron/scheduler.py: add _run_job_script() executor with timeout handling,
  inject script output/errors into _build_job_prompt()
- tools/cronjob_tools.py: add script to tool schema, create/update handlers,
  _format_job display
- hermes_cli/cron.py: add --script to create/edit, display in list/edit output
- hermes_cli/main.py: add --script argparse for cron create/edit subcommands
- tests/cron/test_cron_script.py: 20 tests covering job CRUD, script
  execution, path resolution, error handling, prompt injection, tool API

Script paths can be absolute or relative (resolved against ~/.hermes/scripts/).
Scripts run with a 120s timeout. Failures are injected as error context so
the LLM can report the problem. Empty string clears an attached script.
2026-04-04 10:43:39 -07:00
LucidPaths
6367e1c4c0 fix: remove stale test skips, fix regex backtracking, file search bug, and test flakiness
Bug fixes:
- agent/redact.py: catastrophic regex backtracking in _ENV_ASSIGN_RE — removed
  re.IGNORECASE and changed [A-Z_]* to [A-Z0-9_]* to restrict matching to actual
  env var name chars. Without this, the pattern backtracks exponentially on large
  strings (e.g. 100K tool output), causing test_file_read_guards to time out.
- tools/file_operations.py: over-escaped newline in find -printf format string
  produced literal backslash-n instead of a real newline, breaking file search
  result parsing (total_count always 1, paths concatenated).

Test fixes:
- Remove stale pytestmark.skip from 4 test modules that were blanket-skipped as
  'Hangs in non-interactive environments' but actually run fine:
  - test_413_compression.py (12 tests, 25s)
  - test_file_tools_live.py (71 tests, 24s)
  - test_code_execution.py (61 tests, 99s)
  - test_agent_loop_tool_calling.py (has proper OPENROUTER_API_KEY skip already)
- test_413_compression.py: fix threshold values in 2 preflight compression tests
  where context_length was too small for the compressed output to fit in one pass.
- test_mcp_probe.py: add missing _MCP_AVAILABLE mock so tests work without MCP SDK.
- test_mcp_tool_issue_948.py: inject MCP symbols (StdioServerParameters etc.) when
  SDK is not installed so patch() targets exist.
- test_approve_deny_commands.py: replace time.sleep(0.3) with deterministic polling
  of _gateway_queues — fixes race condition where resolve fires before threads
  register their approval entries, causing the test to hang indefinitely.

Net effect: +256 tests recovered from skip, 8 real failures fixed.
2026-04-04 10:18:57 -07:00
Teknium
43d3efd5c8
feat: add docker_env config for explicit container environment variables (#4738)
Add docker_env option to terminal config — a dict of key-value pairs that
get set inside Docker containers via -e flags at both container creation
(docker run) and per-command execution (docker exec) time.

This complements docker_forward_env (which reads values dynamically from
the host process environment). docker_env is useful when Hermes runs as a
systemd service without access to the user's shell environment — e.g.
setting SSH_AUTH_SOCK or GNUPGHOME to known stable paths for SSH/GPG
agent socket forwarding.

Precedence: docker_env provides baseline values; docker_forward_env
overrides for the same key.

Config example:
  terminal:
    docker_env:
      SSH_AUTH_SOCK: /run/user/1000/ssh-agent.sock
      GNUPGHOME: /root/.gnupg
    docker_volumes:
      - /run/user/1000/ssh-agent.sock:/run/user/1000/ssh-agent.sock
      - /run/user/1000/gnupg/S.gpg-agent:/root/.gnupg/S.gpg-agent
2026-04-03 23:30:12 -07:00
Teknium
b93fa234df
fix: clear ghost status-bar lines on terminal resize (#4960)
* feat: add /branch (/fork) command for session branching

Inspired by Claude Code's /branch command. Creates a copy of the current
session's conversation history in a new session, allowing the user to
explore a different approach without losing the original.

Works like 'git checkout -b' for conversations:
- /branch            — auto-generates a title from the parent session
- /branch my-idea    — uses a custom title
- /fork              — alias for /branch

Implementation:
- CLI: _handle_branch_command() in cli.py
- Gateway: _handle_branch_command() in gateway/run.py
- CommandDef with 'fork' alias in commands.py
- Uses existing parent_session_id field in session DB
- Uses get_next_title_in_lineage() for auto-numbered branches
- 14 tests covering session creation, history copy, parent links,
  title generation, edge cases, and agent sync

* fix: clear ghost status-bar lines on terminal resize

When the terminal shrinks (e.g. un-maximize), the emulator reflows
previously full-width rows (status bar, input rules) into multiple
narrower rows. prompt_toolkit's _on_resize only cursor_up()s by the
stored layout height, missing the extra rows from reflow — leaving
ghost duplicates of the status bar visible.

Fix: monkey-patch Application._on_resize to detect width shrinks,
calculate the extra rows created by reflow, and inflate the renderer's
cursor_pos.y so the erase moves up far enough to clear ghosts.
2026-04-03 22:43:45 -07:00
Teknium
ad4feeaf0d
feat: wire skills.external_dirs into all remaining discovery paths
The config key skills.external_dirs and core resolution (get_all_skills_dirs,
get_external_skills_dirs in agent/skill_utils.py) already existed but several
code paths still only scanned SKILLS_DIR. Now external dirs are respected
everywhere:

- skills_categories(): scan all dirs for category discovery
- _get_category_from_path(): resolve categories against any skills root
- skill_manager_tool._find_skill(): search all dirs for edit/patch/delete
- credential_files.get_skills_directory_mount(): mount all dirs into
  Docker/Singularity containers (external dirs at external_skills/<idx>)
- credential_files.iter_skills_files(): list files from all dirs for
  Modal/Daytona upload
- tools/environments/ssh.py: rsync all skill dirs to remote hosts
- gateway _check_unavailable_skill(): check disabled skills across all dirs

Usage in config.yaml:
  skills:
    external_dirs:
      - ~/repos/agent-skills/hermes
      - /shared/team-skills
2026-04-03 21:14:42 -07:00
Teknium
36aace34aa
fix(opencode-go): strip trailing /v1 from base URL for Anthropic models (#4918)
The Anthropic SDK appends /v1/messages to the base_url, so OpenCode's
base URL https://opencode.ai/zen/go/v1 produced a double /v1 path
(https://opencode.ai/zen/go/v1/v1/messages), causing 404s for MiniMax
models. Strip trailing /v1 when api_mode is anthropic_messages.

Also adds MiMo-V2-Pro, MiMo-V2-Omni, and MiniMax-M2.5 to the OpenCode
Go model lists per their updated docs.

Fixes #4890
2026-04-03 18:47:51 -07:00
Teknium
d4bf517b19 test+docs: add group_topics tests and documentation
- 7 new tests covering skill binding, fallthrough, coercion
- Docs section in telegram.md with config format, field reference,
  comparison table, and thread_id discovery tip
2026-04-03 18:20:50 -07:00
Tranquil-Flow
3bfb39a25f fix(gateway): isolate approval session key per turn 2026-04-03 17:50:01 -07:00
kshitijk4poor
37e2ef6c3f fix: protect profile-scoped google workspace oauth tokens 2026-04-03 17:49:18 -07:00
kagura-agent
6bf5946bbe fix: filter transcript-only roles from chat-completions payload (#4715)
Add a provider-agnostic role allowlist guard to _sanitize_api_messages()
that drops messages with roles not accepted by the chat-completions API
(e.g. session_meta). This prevents CLI resume/session restore from
leaking transcript-only metadata into the outgoing messages payload.

Two layers of defense:

1. API-boundary guard: _sanitize_api_messages() now filters messages by
   role allowlist (system/user/assistant/tool/function/developer) before
   the existing orphaned tool-call repair logic. This protects all
   current and future call paths.

2. CLI restore defense-in-depth: Both session restore paths in cli.py
   now strip session_meta entries before loading history into
   conversation_history, matching the existing gateway behavior.

Closes #4715
2026-04-03 14:57:33 -07:00
Teknium
84a875ca02
fix: scope gateway stop/restart to current profile, --all for global kill
gateway stop and restart previously called kill_gateway_processes() which
scans ps aux and kills ALL gateway processes across all profiles. Starting
a profile gateway would nuke the main one (and vice versa).

Now:
- hermes gateway stop → only kills the current profile's gateway (PID file)
- hermes -p work gateway stop → only kills the 'work' profile's gateway
- hermes gateway stop --all → kills every gateway process (old behavior)
- hermes gateway restart → profile-scoped for manual fallback path
- hermes update → discovers and restarts ALL profile gateways (systemctl
  list-units hermes-gateway*) since the code update is shared

Added stop_profile_gateway() which uses the HERMES_HOME-scoped PID file
instead of global process scanning.
2026-04-03 14:21:44 -07:00
Teknium
b1756084a3
feat: add .zip document support and auto-mount cache dirs into remote backends (#4846)
- Add .zip to SUPPORTED_DOCUMENT_TYPES so gateway platforms (Telegram,
  Slack, Discord) cache uploaded zip files instead of rejecting them.
- Add get_cache_directory_mounts() and iter_cache_files() to
  credential_files.py for host-side cache directory passthrough
  (documents, images, audio, screenshots).
- Docker: bind-mount cache dirs read-only alongside credentials/skills.
  Changes are live (bind mount semantics).
- Modal: mount cache files at sandbox creation + resync before each
  command via _sync_files() with mtime+size change detection.
- Handles backward-compat with legacy dir names (document_cache,
  image_cache, audio_cache, browser_screenshots) via get_hermes_dir().
- Container paths always use the new cache/<subdir> layout regardless
  of host layout.

This replaces the need for a dedicated extract_archive tool (PR #4819)
— the agent can now use standard terminal commands (unzip, tar) on
uploaded files inside remote containers.

Closes: related to PR #4819 by kshitijk4poor
2026-04-03 13:16:26 -07:00
Teknium
8a384628a5
fix(memory): profile-scoped memory isolation and clone support (#4845)
Three fixes for memory+profile isolation bugs:

1. memory_tool.py: Replace module-level MEMORY_DIR constant with
   get_memory_dir() function that calls get_hermes_home() dynamically.
   The old constant was cached at import time and could go stale if
   HERMES_HOME changed after import. Internal MemoryStore methods now
   call get_memory_dir() directly. MEMORY_DIR kept as backward-compat
   alias.

2. profiles.py: profile create --clone now copies MEMORY.md and USER.md
   from the source profile. These curated memory files are part of the
   agent's identity (same as SOUL.md) and should carry over on clone.

3. holographic plugin: initialize() now expands $HERMES_HOME and
   ${HERMES_HOME} in the db_path config value, so users can write
   'db_path: $HERMES_HOME/memory_store.db' and it resolves to the
   active profile directory, not the default home.

Tests updated to mock get_memory_dir() alongside the legacy MEMORY_DIR.
2026-04-03 13:10:11 -07:00
Teknium
aecbf7fa4a
fix(discord): register /approve and /deny slash commands, wire up button-based approval UI (#4800)
Two fixes for Discord exec approval:

1. Register /approve and /deny as native Discord slash commands so they
   appear in Discord's command picker (autocomplete). Previously they
   were only handled as text commands, so users saw 'no commands found'
   when typing /approve.

2. Wire up the existing ExecApprovalView button UI (was dead code):
   - ExecApprovalView now calls resolve_gateway_approval() to actually
     unblock the waiting agent thread when a button is clicked
   - Gateway's _approval_notify_sync() detects adapters with
     send_exec_approval() and routes through the button UI
   - Added 'Allow Session' button for parity with /approve session
   - send_exec_approval() now accepts session_key and metadata for
     thread support
   - Graceful fallback to text-based /approve prompt if button send fails

Also updates test mocks to include grey/secondary ButtonStyle and
purple Color (used by new button styles).
2026-04-03 10:24:07 -07:00
Teknium
5db630aae4
fix: respect per-platform disabled skills in Telegram menu and gateway dispatch (#4799)
Three interconnected bugs caused `hermes skills config` per-platform
settings to be silently ignored:

1. telegram_menu_commands() never filtered disabled skills — all skills
   consumed menu slots regardless of platform config, hitting Telegram's
   100 command cap. Now loads disabled skills for 'telegram' and excludes
   them from the menu.

2. Gateway skill dispatch executed disabled skills because
   get_skill_commands() (process-global cache) only filters by the global
   disabled list at scan time. Added per-platform check before execution,
   returning an actionable 'skill is disabled' message.

3. get_disabled_skill_names() only checked HERMES_PLATFORM env var, but
   the gateway sets HERMES_SESSION_PLATFORM instead. Added
   HERMES_SESSION_PLATFORM as fallback, plus an explicit platform=
   parameter for callers that know their platform (menu builder, gateway
   dispatch). Also added platform to prompt_builder's skills cache key
   so multi-platform gateways get correct per-platform skill prompts.

Reported by SteveSkedasticity (CLAW community).
2026-04-03 10:10:53 -07:00
Teknium
cc54818d26
fix(mcp): stability fix pack — reload timeout, shutdown cleanup, event loop handler, OAuth non-blocking (#4757)
Four fixes for MCP server stability issues reported by community member
(terminal lockup, zombie processes, escape sequence pollution, startup hang):

1. MCP reload timeout guard (cli.py): _check_config_mcp_changes now runs
   _reload_mcp in a separate daemon thread with a 30s hard timeout. Previously,
   a hung MCP server could block the process_loop thread indefinitely, freezing
   the entire TUI (user can type but nothing happens, only Ctrl+D/Ctrl+\ work).

2. MCP stdio subprocess PID tracking (mcp_tool.py): Tracks child PIDs spawned
   by stdio_client via before/after snapshots of /proc children. On shutdown,
   _stop_mcp_loop force-kills any tracked PIDs that survived the SDK's graceful
   SIGTERM→SIGKILL cleanup. Prevents zombie MCP server processes from
   accumulating across sessions.

3. MCP event loop exception handler (mcp_tool.py): Installs
   _mcp_loop_exception_handler on the MCP background event loop — same pattern
   as the existing _suppress_closed_loop_errors on prompt_toolkit's loop.
   Suppresses benign 'Event loop is closed' RuntimeError from httpx transport
   __del__ during MCP shutdown. Salvaged from PR #2538 (acsezen).

4. MCP OAuth non-blocking (mcp_oauth.py): Replaces blocking input() call in
   _wait_for_callback with OAuthNonInteractiveError raise. Adds _is_interactive()
   TTY detection. In non-interactive environments, build_oauth_auth() still
   returns a provider (cached tokens + refresh work), but the callback handler
   raises immediately instead of blocking the MCP event loop for 120s. Re-raises
   OAuth setup failures in _run_http so failed servers are reported cleanly
   without blocking others. Salvaged from PRs #4521 (voidborne-d) and #4465
   (heathley).

Closes #2537, closes #4462
Related: #4128, #3436
2026-04-03 02:29:20 -07:00
Teknium
8fd9fafc84
fix: handle Anthropic Sonnet long-context tier 429 by reducing to 200k (#4747)
Anthropic returns HTTP 429 'Extra usage is required for long context
requests' when a Claude Max subscription doesn't include the 1M context
tier. This is NOT a transient rate limit — retrying won't help.

Only applies to Sonnet models (Opus 1M is general access). Detects
this specific error before the generic rate-limit handler and:
1. Reduces context_length from 1M to 200k (the standard tier)
2. Triggers context compression to fit
3. Retries with the reduced context

The reduction is session-scoped (not persisted) so it auto-recovers
if the user later enables extra usage on their subscription.

Fixes: Sonnet 4.6 instant rate limits on Claude Max without extra usage
2026-04-03 02:05:02 -07:00
Teknium
470c3ea51a
fix: handle Anthropic long-context tier 429 by reducing to 200k
Anthropic returns HTTP 429 'Extra usage is required for long context
requests' when a Claude Max subscription doesn't include the 1M context
tier. This is NOT a transient rate limit — retrying won't help.

Detect this specific error before the generic rate-limit handler and:
1. Reduce context_length from 1M to 200k (the standard tier)
2. Trigger context compression to fit
3. Retry with the reduced context

The reduction is session-scoped (not persisted) so it auto-recovers
if the user later enables extra usage on their subscription.

Fixes: Sonnet 4.6 instant rate limits on Claude Max without extra usage
2026-04-03 01:56:43 -07:00