Commit graph

5033 commits

Author SHA1 Message Date
Swift42
b68bc0ad33 Update SKILL.md
Use -q instead of the deprecated/not working -k
2026-04-20 04:15:04 -07:00
Swift42
d41ca86f74 Update duckduckgo.sh 2026-04-20 04:15:04 -07:00
Teknium
04068c5891
feat(plugins): add transform_tool_result hook for generic tool-result rewriting (#12972)
Closes #8933 more fully, extending the per-tool transform_terminal_output
hook from #12929 to a generic seam that fires after every tool dispatch.
Plugins can rewrite any tool's result string (normalize formats, redact
fields, summarize verbose output) without wrapping individual tools.

Changes
- hermes_cli/plugins.py: add "transform_tool_result" to VALID_HOOKS
- model_tools.py: invoke the hook in handle_function_call after
  post_tool_call (which remains observational); first valid str return
  replaces the result; fail-open
- tests/test_transform_tool_result_hook.py: 9 new tests covering no-op,
  None return, non-string return, first-match wins, kwargs, hook
  exception fallback, post_tool_call observation invariant, ordering
  vs post_tool_call, and an end-to-end real-plugin integration
- tests/hermes_cli/test_plugins.py: assert new hook in VALID_HOOKS
- tests/test_model_tools.py: extend the hook-call-sequence assertion
  to include the new hook

Design
- transform_tool_result runs AFTER post_tool_call so observers always
  see the original (untransformed) result. This keeps post_tool_call's
  observational contract.
- transform_terminal_output (from #12929) still runs earlier, inside
  terminal_tool, so plugins can canonicalize BEFORE the 50k truncation
  drops middle content. Both hooks coexist; they target different layers.
2026-04-20 03:48:08 -07:00
Teknium
9f22977fc0 chore(release): add haileymarshall to AUTHOR_MAP 2026-04-20 03:10:19 -07:00
haileymarshall
6b408e131c fix(gateway): pass session_key (not session_id) to active-process check during prune
SessionStore.prune_old_entries was calling
self._has_active_processes_fn(entry.session_id) but the callback wired
up in gateway/run.py is process_registry.has_active_for_session, which
compares against session_key, not session_id. Every other caller in
session.py (_is_session_expired, _should_reset) already passes
session_key, so prune was the only outlier — and because session_id and
session_key live in different namespaces, the guard never fired.

Result in production: sessions with live background processes (queued
cron output, detached agents, long-running Bash) were pruned out of
_entries despite the docstring promising they'd be preserved. When the
process finished and tried to deliver output, the session_key to
session_id mapping was gone and the work was effectively orphaned.

Also update the existing test_prune_skips_entries_with_active_processes,
which was checking the wrong interface (its mock callback took session_id
so it agreed with the buggy implementation). The test now uses a
session_key-based mock, matching the production callback's real contract,
and a new regression guard test pins the behaviour.

Swallowed exceptions inside the prune loop now log at debug level instead
of silently disappearing.
2026-04-20 03:10:19 -07:00
Teknium
eba7c869bb
fix(steer): drain /steer between individual tool calls, not at batch end (#12959)
Previously, /steer text was only injected after an entire tool batch
completed (_execute_tool_calls_sequential/concurrent returned). If the
batch had a long-running tool (delegate_task, terminal build), the
steer waited for ALL tools to finish before landing — functionally
identical to /queue from the user's perspective.

Now _apply_pending_steer_to_tool_results() is called after EACH
individual tool result is appended to messages, in both the sequential
and concurrent paths. A steer arriving during Tool 1 lands in Tool 1's
result before Tool 2 starts executing.

Also handles leftover steers in the gateway: if a steer arrives during
the final API call (no tool batch to drain into), it's now delivered as
the next user turn instead of being silently dropped.

Fixes user report from Utku.
2026-04-20 03:08:04 -07:00
Teknium
22efc81cd7
fix(sessions): surface compression tips in session lists and resume lookups (#12960)
After a conversation gets compressed, run_agent's _compress_context ends
the parent session and creates a continuation child with the same logical
conversation. Every list affordance in the codebase (list_sessions_rich
with its default include_children=False, plus the CLI/TUI/gateway/ACP
surfaces on top of it) hid those children, and resume-by-ID on the old
root landed on a dead parent with no messages.

Fix: lineage-aware projection on the read path.

- hermes_state.py::get_compression_tip(session_id) — walk the chain
  forward using parent.end_reason='compression' AND
  child.started_at >= parent.ended_at. The timing guard separates
  compression continuations from delegate subagents (which were created
  while the parent was still live) without needing a schema migration.
- hermes_state.py::list_sessions_rich — new project_compression_tips
  flag (default True). For each compressed root in the result, replace
  surfaced fields (id, ended_at, end_reason, message_count,
  tool_call_count, title, last_active, preview, model, system_prompt)
  with the tip's values. Preserve the root's started_at so chronological
  ordering stays stable. Projected rows carry _lineage_root_id for
  downstream consumers. Pass False to get raw roots (admin/debug).
- hermes_cli/main.py::_resolve_session_by_name_or_id — project forward
  after ID/title resolution, so users who remember an old root ID (from
  notes, or from exit summaries produced before the sibling Bug 1 fix)
  land on the live tip.

All downstream callers of list_sessions_rich benefit automatically:
- cli.py _list_recent_sessions (/resume, show_history affordance)
- hermes_cli/main.py sessions list / sessions browse
- tui_gateway session.list picker
- gateway/run.py /resume titled session listing
- tools/session_search_tool.py
- acp_adapter/session.py

Tests: 7 new in TestCompressionChainProjection covering full-chain walks,
delegate-child exclusion, tip surfacing with lineage tracking, raw-root
mode, chronological ordering, and broken-chain graceful fallback.

Verified live: ran a real _compress_context on a live Gemini-backed
session, confirmed the DB split, then verified
- db.list_sessions_rich surfaces tip with _lineage_root_id set
- hermes sessions list shows the tip, not the ended parent
- _resolve_session_by_name_or_id(old_root_id) -> tip_id
- _resolve_last_session -> tip_id

Addresses #10373.
2026-04-20 03:07:51 -07:00
Teknium
0cff992f0a chore(release): add alexzhu0 to AUTHOR_MAP 2026-04-20 03:07:32 -07:00
Alexazhu
64a1368210 fix(tools): keep SSH ControlMaster socket path under macOS 104-byte limit
On macOS, Unix domain socket paths are capped at 104 bytes (sun_path).
SSH appends a 16-byte random suffix to the ControlPath when operating
in ControlMaster mode. With an IPv6 host embedded literally in the
filename and a deeply-nested macOS $TMPDIR like
/var/folders/XX/YYYYYYYYYYYY/T/, the full path reliably exceeds the
limit — every terminal/file-op tool call then fails immediately with
``unix_listener: path "…" too long for Unix domain socket``.

Swap the ``user@host:port.sock`` filename for a sha256-derived 16-char
hex digest. The digest is deterministic for a given (user, host, port)
triple, so ControlMaster reuse across reconnects is preserved, and the
full path fits comfortably under the limit even after SSH's random
suffix. Collision space is 2^64 — effectively unreachable for the
handful of concurrent connections any single Hermes process holds.

Regression tests cover: path length under realistic macOS $TMPDIR with
the IPv6 host from the issue report, determinism for reconnects, and
distinctness across different (user, host, port) triples.

Closes #11840
2026-04-20 03:07:32 -07:00
Teknium
649ef5c8f1 chore(release): add sjz-ks to AUTHOR_MAP 2026-04-20 03:04:06 -07:00
sjz-ks
2081b71c42 feat(tools): add terminal output transform hook 2026-04-20 03:04:06 -07:00
Teknium
9d7aac7ed2 test(gateway): lock in /yolo /verbose bypass and /fast /reasoning catch-all
Four parametrized cases that pin down the running-agent guard behavior:
/yolo and /verbose dispatch mid-run; /fast and /reasoning get the
"can't run mid-turn" catch-all. Prevents the allowlist from silently
drifting in either direction.
2026-04-20 03:03:07 -07:00
elkimek
afd08b76c5 fix(gateway): run /yolo and /verbose mid-agent instead of rejecting them
/yolo and /verbose are safe to dispatch while an agent is running:
/yolo can unblock a pending approval prompt, /verbose cycles the
tool-progress display for the ongoing stream. Both modify session
state without needing agent interaction. Previously they fell through
to the running-agent catch-all (PR #12334) and returned the generic
busy message.

/fast and /reasoning stay on the catch-all — their handlers explicitly
say 'takes effect on next message', so nothing is gained by dispatching
them mid-turn.

Salvaged from #10116 (elkimek), scoped down.
2026-04-20 03:03:07 -07:00
Teknium
be472138f3
fix(send_message): accept E.164 phone numbers for signal/sms/whatsapp (#12936)
Follow-up to #12704. The SignalAdapter can resolve +E164 numbers to
UUIDs via listContacts, but _parse_target_ref() in the send_message
tool rejected '+' as non-digit and fell through to channel-name
resolution — which fails for contacts without a prior session entry.

Adds an E.164 branch in _parse_target_ref for phone-based platforms
(signal, sms, whatsapp) that preserves the leading '+' so downstream
adapters keep the format they expect. Non-phone platforms are
unaffected.

Reported by @qdrop17 on Discord after pulling #12704.
2026-04-20 03:02:44 -07:00
Teknium
8f4db7bbd5 chore(release): map withapurpose37@gmail.com -> StefanIsMe
Author mapping for the salvaged PR #8191 contributor.
2026-04-20 02:59:57 -07:00
Stefan
654d61ab6f feat(status-bar): per-prompt elapsed stopwatch
Adds a per-prompt elapsed timer to the CLI status bar (live ⏱ while the
turn runs, frozen ⏲ after completion, resets on next prompt).  Fills the
gap left by the KawaiiSpinner — the spinner only shows elapsed time while
actively animating, so it disappears between tool calls and after the
turn finishes.  Status bar is always pinned, so users can glance down
and see how long the current/last prompt has been running.

- New instance vars: _prompt_start_time, _prompt_duration
- Timer starts before agent_thread.start() and freezes once the thread
  has exited (both interrupt and normal-completion paths)
- _format_prompt_elapsed() formats s/m/h/d with seconds visible at all
  scales, trailing zeros hidden on exact boundaries, negative clamp
- Displayed in the wide (>=76 col) status bar as position 7, after the
  session duration timer
- Uses width-1 glyphs (⏱/⏲, no variation selector) to stay aligned in
  monospace terminals
2026-04-20 02:59:57 -07:00
Lumen Radley
a2b5627e6d feat(cli): add editor workflow for drafts 2026-04-20 02:53:40 -07:00
Teknium
09ced16ecc fix(cli): apply markdown stripping to background-task and /btw response panels
Follow-up to #12262 — extend final_response_markdown behavior to the other
two final-response Panel render sites (background task completion and /btw
responses) so users see consistent plain-text output everywhere.
2026-04-20 02:53:40 -07:00
Lumen Radley
177e6eb3da feat(cli): strip markdown formatting from final replies 2026-04-20 02:53:40 -07:00
Lumen Radley
22655ed1e6 feat(cli): improve multiline previews 2026-04-20 02:53:40 -07:00
Teknium
2614586306 chore(release): add lumenradley to AUTHOR_MAP 2026-04-20 02:53:40 -07:00
Teknium
93f9db59b2 fix(doctor): update config validation for current auth.py API
Follow-up for #3171 cherry-pick — the contributor's validation block
called get_provider_credentials() which doesn't exist on current main.
Replaces it with get_auth_status() limited to API-key providers in
PROVIDER_REGISTRY so providers without a registry entry (openrouter,
anthropic, custom) don't trigger false 'not authenticated' failures.
Also runs the provider name through resolve_provider() so aliases like
'glm'/'moonshot' validate correctly.

Adds StefanIsMe to AUTHOR_MAP.
2026-04-20 02:41:25 -07:00
Stefan
954dd8a4e0 fix(doctor): catch OpenRouter 402/429 and validate model/provider config
Discovered via real user session where hermes doctor missed two failures:

1. OpenRouter HTTP 402 (credits exhausted) fell through to the generic
   'else' branch — printed yellow but never added to issues, so
   'hermes doctor --fix' couldn't surface it. User had to manually
   find and run 'hermes config set model.provider minimax'.

2. A provider value 'main' (from a stale gateway state or config
   corruption) caused 'Unknown provider main' at runtime. Doctor
   checked that config.yaml existed but never validated that
   model.provider or model.default contained sane values.

Changes:
- OpenRouter health-check now catches 402 (out of credits) and 429
  (rate limited) separately, prints a red X, and adds a fixable
  issue with the exact command to run.
- New config validation after the config.yaml existence check:
  * Validates model.provider against PROVIDER_REGISTRY. Unknown
    provider names fail red with the full valid list.
  * Warns when model.default uses a provider-prefixed name (e.g.
    'anthropic/claude-opus-4') but provider is not openrouter/custom.
  * Warns when model.provider is configured but no API key or
    base_url is set for it.

Both fixes are fully general — they catch classes of errors, not
hardcoded values specific to one user's setup.
2026-04-20 02:41:25 -07:00
Teknium
c470a325f7 chore(release): add Linux2010 and elmatadorgh to AUTHOR_MAP 2026-04-20 02:40:20 -07:00
elmatadorgh
1ec4a34dcd test(error_classifier): broaden non-string message type coverage
Adds regression tests for list-typed, int-typed, and None-typed message
fields on top of the dict-typed coverage from #11496. Guards against
other provider quirks beyond the original Pydantic validation case.

Credit to @elmatadorgh (#11264) for the broader type coverage idea.
2026-04-20 02:40:20 -07:00
Linux2010
b869bf206c fix(error_classifier): handle dict-typed message fields without crashing
When API providers return Pydantic-style validation errors where
body['message'] or body['error']['message'] is a dict (e.g.
{"detail": [...]}), the error classifier was crashing with
AttributeError: 'dict' object has no attribute 'lower'.

The 'or ""' fallback only handles None/falsy values. A non-empty
dict is truthy and passes through to .lower(), which fails.

Fix: Wrap all 5 call sites with str() before calling .lower().
This is a no-op for strings and safely converts dicts to their
repr for pattern matching (no false positives on classification
patterns like 'rate limit', 'context length', etc.).

Closes #11233
2026-04-20 02:40:20 -07:00
Teknium
acca428c81 chore: add haileymarshall to AUTHOR_MAP 2026-04-20 02:10:53 -07:00
haileymarshall
49282b6e04 fix(gemini): assign unique stream indices to parallel tool calls
The streaming translator in agent/gemini_cloudcode_adapter.py keyed OpenAI
tool-call indices by function name, so when the model emitted multiple
parallel functionCall parts with the same name in a single turn (e.g.
three read_file calls in one response), they all collapsed onto index 0.
Downstream aggregators that key chunks by index would overwrite or drop
all but the first call.

Replace the name-keyed dict with a per-stream counter that persists across
SSE events. Each functionCall part now gets a fresh, unique index,
matching the non-streaming path which already uses enumerate(parts).

Add TestTranslateStreamEvent covering parallel-same-name calls, index
persistence across events, and finish-reason promotion to tool_calls.
2026-04-20 02:10:53 -07:00
Roy-oss1
d990fa52ed docs(feishu): tighten processing reactions section
Change-Id: I9547777b9a09f9cfeb333af9b016e4659a934e24
2026-04-20 02:04:57 -07:00
Roy-oss1
520edd3499 feat(feishu): show processing state via reactions on user messages
Replaces the permanent "OK" receipt reaction with a 3-phase visual
lifecycle:

- Typing animation appears when the agent starts processing.
- Cleared when processing succeeds — the reply message is the signal.
- Replaced with CrossMark when processing fails.
- Cleared when processing is cancelled or interrupted.

When Feishu rejects the reaction-delete call, we keep the Typing in
place and skip adding CrossMark. Showing both at once would leave the
user seeing both "still working" and "done/failed" simultaneously,
which is worse than a stuck Typing.

A FEISHU_REACTIONS env var (default on) disables the whole lifecycle.
User-added reactions with the same emoji still route through to the
agent; only bot-origin reactions are filtered to break the feedback
loop.

Change-Id: I527081da31f0f9d59b451f45de59df4ddab522ba
2026-04-20 02:04:57 -07:00
Ruzzgar
60236862ee fix(agent): fall back when rg is blocked for @folder references 2026-04-20 01:56:41 -07:00
Teknium
8a6aa5882e
fix(cli): sync session_id after compression and preserve original end_reason (#12920)
After context compression (manual /compress or auto), run_agent's
_compress_context ends the current session and creates a new continuation
child session, mutating agent.session_id. The classic CLI held its own
self.session_id that never resynced, so /status showed the ended parent,
the exit-summary --resume hint pointed at a closed row, and any later
end_session() call (from /resume <other> or /branch) targeted the wrong
row AND overwrote the parent's 'compression' end_reason.

This only affected the classic prompt_toolkit CLI. The gateway path was
already fixed in PR #1160 (March 2026); --tui and ACP use different
session plumbing and were unaffected.

Changes:
- cli.py::_manual_compress — sync self.session_id from self.agent.session_id
  after _compress_context, clear _pending_title
- cli.py chat loop — same sync post-run_conversation for auto-compression
- cli.py hermes -q single-query mode — same sync so stderr session_id
  output points at the continuation
- hermes_state.py::end_session — guard UPDATE with 'ended_at IS NULL' so
  the first end_reason wins; reopen_session() remains the explicit
  escape hatch for re-ending a closed row

Tests:
- 3 new in tests/cli/test_manual_compress.py (split sync, no-op guard,
  pending_title behavior)
- 2 new in tests/test_hermes_state.py (preserve compression end_reason
  on double-end; reopen-then-re-end still works)

Closes #12483. Credits @steve5636 for the same-day bug report and
@dieutx for PR #3529 which proposed the CLI sync approach.
2026-04-20 01:48:20 -07:00
Ruzzgar
f23123e7b4 fix(gateway): prevent scoped lock and resource leaks on connection failure 2026-04-20 01:44:36 -07:00
Teknium
a5063ff105
docs(providers): drop stale 'TODO: Phase 4' from get_provider docstring (#12902)
User-defined providers from config.yaml are already resolved via
resolve_provider_full() (which layers resolve_user_provider and
resolve_custom_provider on top of get_provider). Refresh the docstring
to reflect current reality and point future readers at the right entry
point. No behaviour change.

Closes #12309.
2026-04-20 01:41:27 -07:00
teyrebaz33
2d59afd3da fix(docker): pass docker_mount_cwd_to_workspace and docker_forward_env to container_config in file_tools
file_tools._get_file_ops() built a container_config dict for Docker/
Singularity/Modal/Daytona backends but omitted docker_mount_cwd_to_workspace
and docker_forward_env. Both are read by _create_environment() from
container_config, so file tools (read_file, write_file, patch, search)
silently ignored those config values when running in Docker.

Add the two missing keys to match the container_config already built by
terminal_tool.terminal_tool().

Fixes #2672.
2026-04-20 00:58:16 -07:00
Junass1
4c50b4689e fix(gateway): make Telegram DM topic config writes atomic 2026-04-20 00:57:53 -07:00
Teknium
4f24db4258
fix(compression): enforce 64k floor on aux model + auto-correct threshold (#12898)
Context compression silently failed when the auxiliary compression model's
context window was smaller than the main model's compression threshold
(e.g. GLM-4.5-air at 131k paired with a 150k threshold).  The feasibility
check warned but the session kept running and compression attempts errored
out mid-conversation.

Two changes in _check_compression_model_feasibility():

1. Hard floor: if detected aux context < MINIMUM_CONTEXT_LENGTH (64k),
   raise ValueError so the session refuses to start.  Mirrors the existing
   main-model rejection at AIAgent.__init__ line 1600.  A compression model
   below 64k cannot summarise a full threshold-sized window.

2. Auto-correct: when aux context is >= 64k but below the computed
   threshold, lower the live compressor's threshold_tokens to aux_context
   (and update threshold_percent to match so later update_model() calls
   stay in sync).  Warning reworded to say what was done and how to
   persist the fix in config.yaml.

Only ValueError re-raises; other exceptions in the check remain swallowed
as non-fatal.
2026-04-20 00:56:04 -07:00
helix4u
03e3c22e86 fix(config): add stale timeout settings 2026-04-20 00:52:50 -07:00
Teknium
440764e013 chore(release): add salt-555 to AUTHOR_MAP 2026-04-20 00:47:40 -07:00
salt-555
12c8cefbce fix(backup): handle files with pre-1980 timestamps
ZipFile.write() raises ValueError for files with mtime before 1980-01-01
(the ZIP format uses MS-DOS timestamps which can't represent earlier dates).
This crashes the entire backup. Add ValueError to the existing except clause
so these files are skipped and reported in the warnings summary, matching the
existing behavior for PermissionError and OSError.
2026-04-20 00:47:40 -07:00
helix4u
afba54364e docs(config): document session_search auxiliary controls 2026-04-20 00:47:39 -07:00
helix4u
6ab78401c9 fix(aux): add session_search extra_body and concurrency controls
Adds auxiliary.<task>.extra_body config passthrough so reasoning-heavy
OpenAI-compatible providers can receive provider-specific request fields
(e.g. enable_thinking: false on GLM) on auxiliary calls, and bounds
session_search summary fan-out with auxiliary.session_search.max_concurrency
(default 3, clamped 1-5) to avoid 429 bursts on small providers.

- agent/auxiliary_client.py: extract _get_auxiliary_task_config helper,
  add _get_task_extra_body, merge config+explicit extra_body with explicit winning
- hermes_cli/config.py: extra_body defaults on all aux tasks +
  session_search.max_concurrency; _config_version 19 -> 20
- tools/session_search_tool.py: semaphore around _summarize_all gather
- tests: coverage in test_auxiliary_client, test_session_search, test_aux_config
- docs: user-guide/configuration.md + fallback-providers.md

Co-authored-by: Teknium <teknium@nousresearch.com>
2026-04-20 00:47:39 -07:00
cresslank
904f20d622 fix(tui): stop empty idle dequeue from triggering ready-state OOM 2026-04-20 00:42:10 -07:00
Teknium
edf1aecacd chore(release): add cresslank to AUTHOR_MAP 2026-04-20 00:42:10 -07:00
helix4u
e96758291b fix(signal): normalize direct recipients to UUIDs 2026-04-20 00:35:55 -07:00
kshitijk4poor
fd5df5fe8e fix(camofox): honor auxiliary vision temperature\n\n- forward auxiliary.vision.temperature in camofox screenshot analysis\n- add regression tests for configured and default behavior 2026-04-20 00:32:09 -07:00
kshitijk4poor
9d88bdaf11 fix(browser): honor auxiliary.vision.temperature for screenshot analysis\n\n- mirror the vision tool's config bridge in browser_vision
- add regression tests for configured and default temperature forwarding
2026-04-20 00:32:09 -07:00
kshitijk4poor
098d554aac test: cover vision config temperature wiring\n\n- add regression tests for auxiliary.vision.temperature and timeout\n- add bugkill3r to AUTHOR_MAP for the salvaged commit 2026-04-20 00:32:09 -07:00
Saurabh
088bf9057f fix: vision tool respects auxiliary.vision.temperature from config (#4661)
The vision tool hardcoded temperature=0.1, ignoring the user's
config.yaml setting. This broke providers like Kimi/Moonshot that
require temperature=1 for vision models. Now reads temperature
from auxiliary.vision.temperature, falling back to 0.1.
2026-04-20 00:32:09 -07:00
kshitijk4poor
e485bc60cd test(kimi): cover api.moonshot.cn direct-call regressions\n\n- add run_agent coverage for the Moonshot China endpoint\n- add sync/async trajectory compressor coverage for api.moonshot.cn 2026-04-20 00:32:06 -07:00