fix(gateway): deliver MEDIA tags for extension-less files (Caddyfile, Makefile)#1
Closed
HexLab98 wants to merge 735 commits into
Closed
fix(gateway): deliver MEDIA tags for extension-less files (Caddyfile, Makefile)#1HexLab98 wants to merge 735 commits into
HexLab98 wants to merge 735 commits into
Conversation
…all leakage (NousResearch#54707) test_gateway_pid_scan_hides_wmic_and_powershell_windows flaked once in CI (slice 7/8) with 'KeyError: creationflags' while passing 15/15 under exact CI-parity locally. The positional 'kwargs["creationflags"]' indexing raises a bare KeyError the moment any stray subprocess.run call is captured, masking the real contract. Filter captured calls to the two intended Windows console spawns (wmic + PowerShell fallback) and assert each is windowless via .get('creationflags'); a leaked/extra call now surfaces as a readable len-mismatch with the full captured list, not a cryptic KeyError.
Group DMs (multi-person DMs, channel_type=mpim) were never delivered to
the Slack bot. The adapter already classifies mpim as a DM and replies
ambiently (adapter.py:2526, is_dm = channel_type in {im, mpim}), but the
generated app manifest only subscribed to message.im / im:history — the
1:1 DM pair. Without the message.mpim event subscription Slack drops
group-DM messages before the adapter ever sees them, so 1:1 DMs worked
while group-DM ambient mode was dead.
Add message.mpim to bot_events and mpim:history (the scope that event
requires per Slack docs) + mpim:read (mirrors im:read for the
conversations.info classification call) to bot_scopes. Update the
SLACK_BOT_TOKEN / SLACK_APP_TOKEN setup-help strings and the Slack docs
(EN + zh-Hans: scope table, event table, troubleshooting) so existing
installs are told to add the new scopes and reinstall.
Reported by an enterprise customer. Note: this is a manifest/scope
change, so it only takes effect after the app is reinstalled and the
new scopes are accepted.
Tests: assert message.mpim + mpim:history + mpim:read are in the
manifest (with and without assistant mode); both fail on current main
and pass with this change.
…im required Follow-up to the group-DM manifest fix. The manifest change only helps NEW installs; existing apps keep their old (mpim-less) scopes until the admin reinstalls. Since a missing message.mpim event delivers nothing (no runtime API error to catch), detect stale installs at connect time from the auth.test x-oauth-scopes header and log an actionable reinstall nudge when im:history is granted but mpim:history is not. Also promote message.mpim from Recommended to Required in the docs event tables so the default setup path can't drop it.
…tion
The Camoufox REST API server expects `listItemId` in the `POST /tabs`
body, but `_ensure_tab` was sending `sessionKey`. This caused a 400
Bad Request on every `browser_navigate` call.
The parameter name mismatch is visible in the same file: line 283
already reads `tab.get("listItemId")` when adopting existing tabs,
confirming the server-side field name.
Fixes NousResearch#37960
…MOFOX_API_KEY is set The five HTTP call sites in browser_camofox.py (_ensure_tab, _post, _get, _get_raw, _delete) did not include Authorization headers, causing 403 Forbidden when the Camofox server has API key auth enabled. Added _auth_headers() helper and wired it into all five call sites. The health check endpoint (/health) is left without auth since it is a connectivity probe, not a browser operation. Regression test covers: header present when key set, absent when unset, blank key produces empty headers. Fixes NousResearch#20476
The Camofox browser backend hardcoded a 30s HTTP timeout via _DEFAULT_TIMEOUT, ignoring the user's browser.command_timeout config. The main browser_tool path already reads this config via _get_command_timeout(). This commit adds an equivalent _get_command_timeout() to browser_camofox.py that reads browser.command_timeout from config with caching, and switches all HTTP helper methods (_post, _get, _get_raw, _delete) to use it as the default timeout. Fixes NousResearch#40843
When a Camofox browser tab is garbage collected (idle timeout, browser
recycle), the held tab_id becomes stale. The next browser_navigate call
hits /tabs/{stale_id}/navigate -> HTTP 404 -> unhandled HTTPError.
Catch the 404 in camofox_navigate, clear the stale tab_id, and create a
fresh tab via _ensure_tab. The agent recovers transparently without
requiring a session restart.
Other tab operations (snapshot, click, type, etc.) use the same pattern
but only fail if the tab dies between successful calls — much rarer.
The navigate fix covers 95%+ of cases since navigate is always the entry
point.
The auth-header fix reads CAMOFOX_API_KEY but it was never registered, so it didn't surface in `hermes setup` / `hermes tools`. Add it as an advanced password-category tool env var alongside CAMOFOX_URL.
The auth-header fix adds headers=_auth_headers() to all Camofox HTTP calls. Two _capture_post mocks in the persistence test lacked a headers parameter, so navigate raised TypeError and the success assertions failed. Add headers=None to both mock signatures.
A single agent turn can fan out N vision_analyze calls at once — the classic trigger is "analyze every frame of this video", where ffmpeg explodes a clip into dozens of frames and the model calls vision_analyze on each. Every call does a CPU-heavy base64-encode/resize burst AND holds a long-lived LLM stream open. The tool executor runs concurrent tool calls on a per-session ThreadPoolExecutor (_MAX_TOOL_WORKERS=8), and multiple agent sessions share one process (the dashboard runs the agent in-process), so there was no global ceiling. In prod (June 2026) a video-frame fan-out pinned a worker thread at ~100% CPU and starved the shared asyncio event loop that also serves the dashboard's /api/status liveness probe, flapping the instance to UNHEALTHY even though nothing had crashed. Add a process-global threading.BoundedSemaphore that bounds how many vision analyses run concurrently across the whole process, held across the entire analysis (image load + encode + LLM call) in the single _handle_vision_analyze chokepoint (covers both the native fast path and the legacy aux-LLM path). It is a threading semaphore, NOT asyncio: each vision call is dispatched through model_tools._run_async on a per-thread event loop, so an asyncio primitive bound to one loop cannot coordinate across them. The acquire is offloaded via run_in_executor so waiting for a slot never blocks the calling loop. Default: min(host CPUs, 4), floored at 1 — respect the host's concurrency, or lower. Override via auxiliary.vision.max_concurrency (config.yaml) or HERMES_VISION_MAX_CONCURRENCY (env). Values < 1 are ignored so the cap can never be disabled into an unbounded fan-out. Tests: bounded-fan-out regression guard + a control proving it would fail without the cap; resolver tests for host-cpu default, ceiling clamp, low-cpu host, env override, and sub-1 rejection. Pre-existing handler tests updated for the now-async _handle_vision_analyze. Verified via the real registry.dispatch -> _run_async per-thread-loop path (16 concurrent calls, peak bounded to cap).
The original cap held a process-global slot across the WHOLE vision analysis (image load + encode + LLM call) with a default of min(CPUs, 4). That serialized legitimate multi-image workflows — "compare these 6 screenshots", "read this 10-page scan", "analyze every frame" — behind a 4-wide gate, and on the native fast path it even throttled calls that make no LLM request at all. Excess calls queued (blocking acquire, nothing dropped), but the latency hit on real fan-out was the wrong tradeoff. The incident was CPU exhaustion, not call count: concurrent base64/resize bursts saturated every core and left none to service the shared event loop serving /api/status. So cap ONLY that: - A dedicated, bounded ThreadPoolExecutor (_vision_cpu_executor) runs the encode/resize/dimension-check off the caller's loop, sized to the host's usable core count with NO fixed ceiling — the cap tracks the actual exhausted resource (cores), not a magic number. Excess encodes queue on the executor; cores stay free for the loop. - The LLM call is deliberately OUTSIDE the executor, so multi-image workflows keep full request concurrency. - Override via auxiliary.vision.max_concurrency / HERMES_VISION_MAX_CONCURRENCY (honored verbatim, including above core count); sub-1 ignored. - _vision_concurrency_slot() is now a no-op shim for back-compat. Tests assert: resolver defaults to host cores with no ceiling; env/config override (incl. above cores); sub-1 rejection; the executor is dedicated and core-sized; encode runs on a vision-encode thread; and crucially that encode bursts are bounded to the cap while the analyses themselves stay fully concurrent (calls_peak > cap).
NousResearch#54740) The first ship of verify-on-stop (config v30) defaulted DEFAULT_CONFIG agent.verify_on_stop to a literal True, and migrate_config persists defaults with strip_defaults=False — so every install that updated through v30 had verify_on_stop: true written into config.yaml as a literal. The v30->v31 migration only flipped missing/'auto' values to false and deliberately preserved an explicit bool, so it skipped that entire population and left verify-on-stop ON for everyone who had updated. A literal true was never a user choice: the feature had no off-switch worth setting it against until v31 introduced one, so a true persisted before v32 is always the old machine default. v32 migration flips a literal true -> false once, for both v30 (skipped v31) and v31 (preserved-by-bug) installs. A true the user sets AFTER v32 is a deliberate opt-in and is never touched.
…ng None (NousResearch#14331) # Conflicts: # tools/browser_tool.py
Wire the salvaged _safe_command_timeout() guard into the surviving open-timeout call site. _get_open_command_timeout() feeds the browser_navigate 'open' path; this closes the last call site that could observe a None timeout from a torn cache (NousResearch#14331), since the original PR's max(_get_command_timeout(), 60) site no longer exists on main (now routed through _get_open_command_timeout).
resolve_custom_provider() previously returned api_key_env_vars=() for every custom provider entry, silently dropping the configured key_env field. This caused 401 errors for any custom provider that required an API key via environment variable (e.g. Xiaomi MiMo Token Plan, self-hosted OpenAI-compatible servers). The key_env field is already documented in _VALID_CUSTOM_PROVIDER_FIELDS and normalized by normalize_custom_provider_entry(), so this was just an oversight in the ProviderDef construction. Also adds a regression test that verifies key_env is properly propagated into the resolved ProviderDef.
find_alias_for_profile re-scanned the whole wrapper dir (~/.local/bin) and
read_text every file for EACH profile — including large unrelated binaries
(ffmpeg etc.) read 15x over. With 16 profiles this took ~6.4s, long enough
that the desktop's per-request backend calls timed out (15s) and the sidebar
rendered '全部智能体 0 / 会话 0'.
- Add build_alias_map(): single-pass {profile -> alias} reverse map, reads
only an 8KB head slice per wrapper, skips binaries via UnicodeDecodeError.
- find_alias_for_profile now delegates to it (behavior preserved).
- Cache _count_skills by skills-dir mtime signature (+30s TTL).
list_profiles: 6.37s -> 0.84s cold / 0.44s warm. 138 profile tests pass.
(cherry picked from commit 89e5937)
…sResearch#54523) (cherry picked from commit 09f10e2)
Fixes NousResearch#14238. During a compression/session split at the response boundary, the interim callback delivered unrelated commentary, setting response_previewed=True. The suppression logic treated that as proof the final reply had been delivered and skipped the normal send — the response was persisted to the child session but never sent to chat. Only suppress the normal final send when the stream consumer confirms final delivery (final_response_sent / final_content_delivered) or the exact final response text was delivered as a preview.
…module reload races (NousResearch#54775) The five _resolved_api_call_stale_timeout_base integration tests reloaded hermes_cli.config + hermes_cli.timeouts via importlib.reload to clear cached config. Under xdist that mutates module-global state shared across the worker process, so a sibling test could leave the config cache in a state that made get_provider_stale_timeout return a leaked value — intermittently failing test_reasoning_floor_applies_to_opus_4_thinking (shard 6 flake, NousResearch#52217 area). Patch run_agent.get_provider_stale_timeout per-test instead: floor-path tests get None (resolver falls through to the reasoning floor / env var / default), the explicit-config test gets 60.0 (priority-1 short-circuit). Same assertions, no shared-module mutation, deterministic under parallel execution.
… load When the dashboard gateway has no local session cookie, it rendered a click-through /login interstitial — even though the Nous portal's /oauth/authorize auto-approves any current member of the dashboard's org and is a silent 302 when the user already holds a portal session. For the common case (clicking a hosted-agent dashboard link while signed in to the portal) that interstitial click is pure friction. This makes the gate auto-initiate the OAuth redirect on an unauthenticated HTML document load instead of rendering the interstitial, when exactly one interactive provider is registered. A one-shot loop-guard cookie (hermes_sso_attempt, 60s TTL) ensures that a genuinely absent portal session (the portal bounces back still-unauthenticated) falls back to the /login page after exactly one bounce rather than ping-ponging forever. The marker is cleared on a successful callback and whenever the gate falls back to /login. Security: this removes a human CLICK, not a security check. The redirect lands on the existing /auth/login route and runs the unchanged PKCE auth-code flow; token verification, audience checks, redirect-URI match, and org-membership checks are all untouched. /api/* fetches still get the 401 JSON envelope (never a 302 a fetch() would follow opaquely), and with two or more providers the /login chooser still renders. Phase 1 of the cloud-auto-discovery work.
list_session_providers() already filters on supports_session=True, so the new helper re-filtered an already-filtered list. Call it directly at the single auto-SSO call site.
…NSERT OR IGNORE The gateway's get_or_create_session() creates a bare session row (source + user_id) before the agent exists. The agent's later create_session() carries the real model/model_config/system_prompt, but _insert_session_row used INSERT OR IGNORE — silently dropping that enrichment. Gateway sessions were left with NULL model and NULL billing metadata. Switch to INSERT ... ON CONFLICT(id) DO UPDATE with COALESCE so NULL columns get backfilled while values an earlier writer already set are never overwritten (a later bare write with source='unknown' can't clobber a real source/model). Credit: original report and fix direction by @LucidPaths (NousResearch#5048).
…dge path guard Salvages the two still-valid hardenings from NousResearch#5381 onto the relocated plugin adapters (the discord/feishu/whatsapp adapters moved to plugins/platforms/ since the PR was opened, and 4 of its 6 hunks are already on main or superseded). - feishu: rate limiter now denies untracked keys when the tracking table is at capacity after pruning stale entries (was: allow through without tracking). At-capacity-with-all-fresh-entries only happens under abuse, so allowing untracked requests let an attacker who flooded the table bypass the limiter entirely. Already-tracked keys and post-prune room are unaffected. - whatsapp: absolute file paths handed back by the Baileys bridge are now validated to resolve inside a known media cache dir before being attached. A compromised/buggy bridge could otherwise return an arbitrary path (e.g. /etc/passwd) that would be sent verbatim to the model. Guard resolves symlinks and accepts both the canonical cache/<kind> and legacy <kind>_cache layouts.
reset_had_activity gated on entry.total_tokens, which is never written (token counts migrated to agent-direct persistence) so it was always 0. That suppressed session-reset notifications for sessions that genuinely had activity. Switch to last_prompt_tokens, which is updated on every turn.
The reset-had-activity tests set total_tokens (dead state) to simulate activity; production records activity via last_prompt_tokens. Update the fixtures to match the field the fix and runtime actually use.
…ersal Session IDs can originate from untrusted input (e.g. the X-Hermes-Session-Id API header) and are interpolated raw into on-disk artifact filenames under ~/.hermes/sessions/. A traversal-shaped ID (../../../../etc/pwned) would let a caller write the session snapshot or request dump outside the sessions directory. _safe_session_filename_component() collapses every non [A-Za-z0-9_-] character to _, caps the length, and appends a short content hash when sanitization changed the string, always yielding a single traversal-free path segment. Closes NousResearch#5958.
… plugin module egilewski found the prior sink gate was transient: it only applied while PluginManager executed register(ctx). A plugin could defer a direct registry.register(..., override=True) to a post-load callback/thread, after the scope was cleared, and still replace a built-in. Make authorization durable by binding it to where the handler is DEFINED (handler.__globals__['__name__']) rather than to call timing. At load, each plugin's module namespace is mapped to its allow_tool_override opt-in in a table that is never cleared. The sink resolves the handler's owning plugin module and rejects an override from any plugin namespace without opt-in, regardless of when or on which thread the call happens. Plugin namespaces with no recorded policy are treated as not-opted-in (fail-closed). Built-in and MCP handlers live outside the plugin namespace and are unaffected. Adds a regression test for the delayed/post-load direct-registry override.
Builds on memosr's sink-level opt-in gate (NousResearch#29249). Enabling a non-bundled plugin now surfaces the privileged allow_tool_override decision at `hermes plugins enable` time instead of leaving the operator to discover the config key after a runtime rejection. - `hermes plugins enable <name>` prompts for non-bundled plugins: 'Allow this plugin to replace built-in tools?' Default is deny (blank Enter / non-interactive stdin / EOF all fail closed). - --allow-tool-override / --no-allow-tool-override flags for non-interactive and scripted use (and a future desktop checkbox). - Bundled plugins are trusted: never prompted, no entry written. - Writes plugins.entries.<key>.allow_tool_override, the same key the sink gate reads (manifest.key == discovery key), so consent and enforcement compose end to end.
…sResearch#55598) exact_moa_preset_name matched any bare model name equal to a preset key, regardless of the preset's enabled flag. On the no-explicit-provider switch path (PATH B in model_switch.py), a plain /model switch whose name collided with a preset key (e.g. "default") silently pivoted the session onto the MoA virtual provider — even when the user had set enabled: false to opt out (issue NousResearch#55187). The LLM driving a routine model switch could land on a broken moa provider with empty default_preset / unconfigured aggregator credentials. Gate the implicit bare-name match on the per-preset enabled flag. Explicit selection via --provider moa / the model picker uses PATH A and does not go through exact_moa_preset_name, so a disabled preset stays reachable when the user explicitly asks for it.
…_id signature churn Two independent bugs evicted the cached gateway AIAgent on every turn, preventing the prompt cache from ever warming: 1. Model normalization mismatch: the post-run fallback-eviction check compared _agent.model (stripped in AIAgent.__init__) against the raw _resolve_gateway_model() config string. For vendor-prefixed config on native providers (e.g. 'deepseek/deepseek-v4-pro' vs 'deepseek-v4-pro') this was always unequal, so the agent was evicted after every successful run. Normalize _cfg_model the same way (skip aggregators). 2. Discord triggering message_id leaked into the cached system prompt via build_session_context_prompt()'s Discord IDs block. message_id changes every turn, so the agent-cache signature (computed from the ephemeral prompt) changed every Discord turn -> rebuild every message. The id is now injected per-turn into the user message (where per-turn content belongs and does not touch the cache signature); the cached IDs block carries a static pointer to it, preserving reply/react/pin via the discord tools. Adapted from NousResearch#28846. Bug #1 fix is the contributor's; bug NousResearch#2 reworked to be non-destructive (keeps the triggering-id capability instead of deleting it). Redundant auto-reset eviction (already on main via NousResearch#9893/NousResearch#48031) and the wrong-premise reset_context_note plumbing from the original PR were dropped. Co-authored-by: Hermes Agent <hermes@nousresearch.com>
…rk test (NousResearch#55597) The concurrent-compression regression asserted the parent ends with exactly one child. Under heavy CI write contention the lock winner's child create_session can exhaust its SQLite retry budget, and _compress_context deliberately rolls the live id back to the still-indexed parent rather than orphaning a child (the create-failure rollback in agent/conversation_compression.py). That safe rollback leaves zero children and is correct — so the exact == 1 assertion flaked under load. Assert the actual invariant instead: children <= 1 (a 2+ fork is the bug Damien's incident is about), rotated <= 1, and rotated == n_children. A mutation check (force the lock to always acquire) confirms the relaxed assertion still fails hard on a real 2-child fork.
…ousResearch#29148, NousResearch#49147) (NousResearch#55603) * fix(agent): merge consecutive assistant messages in repair_message_sequence Strict OpenAI-compatible providers (DeepSeek v4, Moonshot/Kimi) reject a replayed history where an assistant message carrying tool_calls is immediately followed by another assistant message instead of its tool results — HTTP 400 'An assistant message with tool_calls must be followed by tool messages...'. repair_message_sequence (the defensive belt run before every API call) fixed orphan-tool and consecutive-user shapes but never merged consecutive assistant messages. Adds a Pass 0 that collapses adjacent assistant turns into one — union of tool_calls, concatenated content, carried reasoning_content — covering both reported shapes: - parallel tool calls split across two assistant turns (NousResearch#29148) - content-only assistant followed by tool_calls-only assistant (NousResearch#49147) A tool result or user turn between two assistants blocks the merge (distinct, valid rounds). Runs before Pass 1 so the merged union of tool_call ids is known to the orphan-tool filter. Closes NousResearch#29148, NousResearch#49147. Co-authored-by: Bartok9 <danielrpike9@gmail.com> Co-authored-by: woaini30050 <woaini30050@users.noreply.github.com> Co-authored-by: weidzhou <weidzhou@users.noreply.github.com> * fix(agent): exempt codex Responses interim turns from assistant merge The Pass 0 consecutive-assistant merge collapsed codex_responses interim turns, which legitimately stay separate — each carries its own encrypted continuation state (codex_reasoning_items / codex_message_items) that must replay verbatim. Skip the merge when either side is a codex interim (has codex_reasoning_items / codex_message_items / finish_reason=='incomplete'). Fixes the slice-2 regression in test_run_agent_codex_responses.py (test_duplicate_detection_distinguishes_different_codex_{reasoning,message_items}). --------- Co-authored-by: Bartok9 <danielrpike9@gmail.com> Co-authored-by: woaini30050 <woaini30050@users.noreply.github.com> Co-authored-by: weidzhou <weidzhou@users.noreply.github.com>
The STT-failure enrichment templates injected setup instructions — "no STT provider is configured", "a direct message has already been sent", and a "hermes-agent-setup" skill mention — into the LLM-visible prompt. That text persists in conversation history, so after one STT failure the model kept volunteering Whisper/Vosk setup advice on every later voice turn, even after transcription started working (observed in prod on gpt-5-nano). The gateway also fired a hardcoded English notice via _stt_adapter.send(), producing a second, wrong-language reply that TTS then spoke aloud. - Neutralize all enrichment templates: success passes the transcript through as a plain quoted line; every failure branch emits a single [voice message could not be transcribed] marker. - Move the operator-facing failure cause to logger.info so it stays diagnosable in container logs without leaking into the prompt. - Remove the hardcoded English _stt_adapter.send() notice; the LLM now produces one coherent reply in the user's language. - Update the gateway STT tests to assert the neutral contract. Co-authored-by: Hermes Agent <noreply@nousresearch.com>
… deadline test_background_task_registers_thread_local_approval_callbacks polled a 2s wall-clock deadline waiting for the background daemon thread to pop its entry from _background_tasks. Under loaded CI the thread's finally-block cleanup could lag the deadline, flaking the final 'assert not cli._background_tasks'. Join the actual worker thread (timeout=10) so the wait ends exactly when the thread finishes.
_resolve_task_provider_model() flattened any explicit base_url to provider=custom. Correct for bare/custom endpoints, but wrong for provider-backed routes (anthropic, qwen-oauth, minimax-oauth, openai-codex, etc.) whose provider branch adds auth refresh, transport, or request shaping. MoA reference slots resolved through those providers lost their identity before the aux call, so e.g. a Codex reference hit chatgpt.com/backend-api/codex without its Cloudflare headers and got HTML back (surfacing as a spurious rate-limit). Keep first-class providers intact when paired with a resolved base_url via _preserve_provider_with_base_url(); bare/custom/auto/unknown and the direct openai alias still route through custom. Co-authored-by: Hermes Agent <127238744+teknium1@users.noreply.github.com>
Sibling of NousResearch#15795's context_compressor fix. agent/moa_loop.py used the same response.choices[0].message.content access; while wrapped in try/except (so no crash), a dict/str-shaped message silently returned empty. Coerce defensively so the content is actually extracted.
…gent Interrupting the agent while an approval/clarify/sudo/secret prompt is up left the overlay state dict set with no thread servicing it. The prompt's worker thread is torn down on interrupt, but read_only (gated on _command_running) plus the keypress filter kept the CLI input locked until the prompt's own timeout expired — the terminal appeared frozen. Drain and clear all four input-blocking overlays on interrupt via a single helper (_clear_active_overlays_for_interrupt): approval -> deny, clarify/sudo/secret -> cancel, each guarded so a dead queue can't block the others; sudo restores the pre-modal draft. Wired into all three interrupt paths — new-message interrupt, Ctrl+C, and Ctrl+Q. Blocking overlays now clear AND fall through so one keypress both clears a stale overlay and interrupts a still-running agent; the /model picker and slash-confirm foreground prompts keep their cancel-and-return behavior. Closes NousResearch#13618.
Doctor and banner used the first check_fn registered for a toolset, so desktop-only read_terminal gated the whole terminal toolset even though terminal and process still expose at runtime. Fixes NousResearch#54820
Regression for NousResearch#54820: a desktop-only helper with a failing check_fn must not mark the whole terminal toolset unavailable when terminal/process still pass their per-tool gates.
…ailability Follow-up to the per-tool availability derivation: `_snapshot_toolset_checks` and `_evaluate_toolset_check` had no remaining callers once the four availability surfaces switched to `_toolset_has_exposable_tools`. Remove both, drop the no-op `quiet` param from the new helper, and document why `_toolset_checks` is still written (banner.py reads it via TOOLSET_REQUIREMENTS to classify unavailable toolsets as lazy-init vs disabled).
hermes tools persists the selected model to image_gen.model, but the OpenRouter-compatible provider only read scoped image_gen.<provider>.model and ignored the dispatch model kwarg — so Nous users always hit the default quality-first chain and fell back to Gemini.
Assert image_gen.model, explicit model kwargs, and Nous provider wiring so the config path mismatch cannot regress.
…w precedence The cherry-picked fix added explicit-kwarg and top-level image_gen.model resolution but left _resolve_model / _resolve_model_chain docstrings stating the old 'env override -> config -> DEFAULT_MODEL' order. Document the full precedence (explicit kwarg -> env -> scoped -> top-level -> default chain) to match the sibling krea/openai providers.
…t memory retry loops (NousResearch#42405) - replace() and remove() now return entry previews and current_entries when no entry matches old_text, matching the multi-match and add-limit error behavior - add() limit error also now returns previews for consistency - Agent can self-correct after a failed replace/remove instead of looping blindly until turn budget is exhausted with no user response
…ion failures (NousResearch#42405) Builds on the zero-match feedback fix (previous commit) to close the silent-hang symptom: when memory is at capacity, a failed `add`/`replace`/`remove` consolidation could loop the whole turn to iteration-budget exhaustion and deliver no user-facing reply. NousResearch#41755 turned the at-capacity overflow error into a *commanded* in-turn retry ("...then retry this add — all in this turn"); combined with the fragile substring-only `replace`/`remove` matching (LLMs can't reliably re-quote a long entry verbatim), the model loops add↔replace on inexact guesses until the turn dies. The existing tool_guardrails halt would catch this, but hard_stop_enabled is opt-in (off by default), so a default install still hangs. This fixes it at the memory layer without changing global guardrail behavior: - MemoryStore tracks per-turn consolidation failures; after a cap (3) it drops the "retry in this turn" instruction and returns a terminal "leave memory unchanged, continue your reply" result, so a failed memory side effect can never block the turn's reply. - The counter resets on any successful write (progress) and at each turn boundary (turn_context.reset_consolidation_failures, guarded via getattr so plugin memory stores without the method are a no-op). Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
🔎 Lint report:
|
HexLab98
force-pushed
the
fix/media-extensionless-files
branch
from
June 30, 2026 14:38
43e35d0 to
6601cbf
Compare
…alidates Files like Caddyfile or Makefile have no extension, so MEDIA_TAG_CLEANUP_RE never matched them and Telegram showed the raw MEDIA: line as text. Extract and strip validated extension-less tags via a second pass.
Add regression tests for Caddyfile-style paths in MEDIA: tags and for strip_media_directives_for_display on the streaming path.
HexLab98
force-pushed
the
fix/media-extensionless-files
branch
from
June 30, 2026 14:39
6601cbf to
773a48f
Compare
Owner
Author
|
Superseded by upstream PR to NousResearch/hermes-agent |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
MEDIA:/path/Caddyfile(no file extension), the gateway now extracts and delivers the file instead of leaving the rawMEDIA:line in the Telegram message.Root cause
MEDIA_TAG_CLEANUP_REonly matches paths ending in a known deliverable extension. Extension-less artifacts (Caddyfile, Dockerfile, Makefile, etc.) were never extracted, never uploaded, and the literalMEDIA:...text was shown to the user.Test plan
scripts/run_tests.sh tests/gateway/test_platform_base.py(newTestExtensionlessMediaDeliverycases pass)Caddyfileunder an allowed output dir, ask Hermes to send it via Telegram withMEDIA:/path/to/CaddyfileMade with Cursor