fix: resolve 7 identified issues [automated] - #1
Closed
Sldark23 wants to merge 486 commits into
Closed
Conversation
For 14 of 74 compressed skills, the original description contained trigger keywords, technique counts, attribution, or use-case phrases not covered by the existing body content. Prepends a 'When to use' / 'What's inside' block near the top so the agent still has the full context when the skill is loaded. Skills salvaged: - codex, ascii-video, creative-ideation, excalidraw, manim-video, p5js - gif-search, heartmula, youtube-content - lm-evaluation-harness, obliteratus, vllm, axolotl - powerpoint Remaining 60 skills were verified to already cover the dropped content in their existing body sections (When to Use, overview, intro prose) or had short descriptions fully captured by the new compressed form.
…try (NousResearch#16372) When 'hermes model' runs against a providers: (keyed-schema) entry that relies only on key_env, the picker resolves the env var for the live /models request and then wrote a synthesized 'api_key: ${KEY_ENV}' back to the providers.<key> entry. That's redundant — the runtime already resolves from key_env directly — and it clutters configs that intentionally keep credentials out of config.yaml. Only persist provider_entry['api_key'] when the user originally had an inline value (literal secret or ${VAR} template). Entries that declared only key_env stay clean on save. Fixes NousResearch#15803.
Telegram groups emit a single bot_command entity covering the whole /cmd@botname span with no accompanying mention entity, so the existing mention gate in _message_mentions_bot dropped slash commands sent via the bot-menu autocomplete whenever require_mention is enabled. Recognise bot_command entities whose @botName suffix matches the bot username (case-insensitive) as a direct mention, and keep rejecting commands addressed at other bots. Fixes NousResearch#15415.
NousResearch#16382) read_file's dedup path returned a lightweight stub on re-reads of an unchanged file, then returned early — so the consecutive-read loop guard (hard block at count>=4) at the bottom of read_file_tool never ran for stub-looped calls. Weaker tool-following models (local Qwen3.6 variants in the reported case) ignore the passive 'refer to earlier result' hint and hammer the same read_file call until iteration budget runs out. Track per-key stub returns in task_data['dedup_hits'] and, on the second stub for the same (path, offset, limit), return a hard BLOCKED error mirroring the wording the real-read path already uses. A real read, an intervening non-read tool call (notify_other_tool_call), or reset_file_dedup (on context compression) all clear the counter so the guard never stays engaged longer than the actual loop. Closes NousResearch#15759
Quick state snapshot now includes pairing JSONs (generic + legacy + Feishu comment pairing), and `hermes update` takes a pre-update snapshot labeled `pre-update` before pulling. Pairing data lives outside state.db in platform-specific JSONs under ~/.hermes/pairing/, ~/.hermes/platforms/pairing/, and ~/.hermes/feishu_comment_pairing.json. The update command already couldn't touch $HERMES_HOME, but NousResearch#15733 reports lost pairing after an update — this gives users something to restore from via `/snapshot list` / `/snapshot restore <id>` if anything clobbers the approved-user lists. - Extend _QUICK_STATE_FILES with pairing paths (files + dirs) - Snapshot walks directories recursively and records each file in the manifest individually so restore logic is unchanged - _cmd_update_impl calls create_quick_snapshot(label='pre-update') after 'Found N new commits' and before 'Pulling updates' - Snapshot failures are logged at debug and never block the update Refs NousResearch#15733.
…ousResearch#15459) On macOS (bash 3.2 and some Homebrew bash builds) `source`ing a file that contains `declare -x` statements prints each declaration to stdout. The persistent-shell wrapper in tools/environments/base.py was only redirecting stderr when sourcing the session snapshot, so ~60 lines of env vars leaked into every terminal tool response — blowing out context and triggering HTTP 400s on context-limited providers. Fix: redirect both stdout and stderr when sourcing the snapshot. Linux bash is silent here, so the redirect is harmless there; macOS no longer leaks. Closes NousResearch#15459 Co-authored-by: Sanjays2402 <51058514+Sanjays2402@users.noreply.github.com>
…rch#16431) Previously 'hermes debug share' uploads only got DELETEd when the user ran 'hermes debug share' again — opportunistic-sweep-on-invoke was the only cleanup path. A user who uploaded once and never ran debug again left pastes up until paste.rs's retention kicked in (which, empirically, never actually expires them). Hook _sweep_expired_pastes into the gateway cron ticker at the same hourly cadence as the image/document cache cleanups. The opportunistic sweep in 'hermes debug share' stays as a fallback for CLI-only users who never start the gateway.
NousResearch#16497) Defensive: when the generator encounters a fenced code block containing Unicode box-drawing characters, wrap it in `<!-- ascii-guard-ignore -->` markers so the docs-site-checks lint (which scans inside code fences) can't reject the page for a skill's own diagram. Plain bash/python code blocks stay uncluttered — only blocks with box chars get wrapped. Skill authors no longer have to remember to add the ignore markers in every SKILL.md with ASCII art. Fixes NousResearch#15305.
…sResearch#15748) (NousResearch#16500) On provider switches mid-session (e.g. MiniMax -> DeepSeek), the source assistant turn carries a 'reasoning' field written by the prior provider but no 'reasoning_content' key. _copy_reasoning_content_for_api would promote that foreign 'reasoning' to 'reasoning_content' on the outbound DeepSeek request, leaking a cross-provider chain of thought and in practice causing HTTP 400. DeepSeek's own _build_assistant_message always pins reasoning_content='' at creation time for tool-call turns, so the shape (reasoning set, reasoning_content absent, tool_calls present) is unreachable from same-provider DeepSeek history — it can only come from a prior provider. Pad with '' in that case instead of promoting. Healthy same-provider 'reasoning' promotion (no tool_calls, or on providers that do not require the empty-string pin) is unchanged.
…drift The CLI renders through prompt_toolkit in non-full-screen mode, so every repaint uses the renderer's tracked _cursor_pos.y to cursor_up() + erase before drawing the new frame. Any time that tracked position drifts from terminal reality, redraws stack on top of stale content instead of overwriting it. Four user-visible bugs share this root cause. Fixes: - NousResearch#5474 (SIGWINCH ghosts): the resize wrapper previously only handled column-shrink reflow. Generalize it to force a full screen-clear (erase_screen + cursor_goto(0,0)) and renderer.reset() on every resize — covers widen, row-shrink, and multiplexer SIGWINCH-less redraws. - NousResearch#8688 (cmux/tmux tab switch): no SIGWINCH fires on focus regain, so prompt_toolkit has no signal to recover. Add a _force_full_redraw() helper, bound to Ctrl+L (standard bash/zsh/vim convention) and exposed as /redraw. Users can manually clear drift without restarting Hermes. - NousResearch#14692 (DSR response leaks — ^[[53;1R): resize storms make prompt_toolkit's CSI 6n queries race past the input parser; the terminal's reply ends up as literal input text. Add a sibling of the bracketed-paste sanitizer that strips \x1b[<row>;<col>R and the caret-escape visible form from paste text, buffer text-filter, and the input-processing loop. The idle-redraw removal (NousResearch#12641) is in the preceding commit from @foxion37 — keeping them as separate commits preserves attribution.
…s questions (NousResearch#16535) Adds a short always-on pointer to the system prompt: when the user asks about configuring, setting up, troubleshooting, or using Hermes Agent itself, load the hermes-agent skill via skill_view(name='hermes-agent') and fall back to https://hermes-agent.nousresearch.com/docs via web_extract. Keeps sessions without skill_view loaded useful too — the docs URL + web_extract is enough to answer most questions. The guidance is appended right after DEFAULT_AGENT_IDENTITY (or SOUL.md) so it ships regardless of which toolset profile is active. Footprint is ~560 chars, behind the existing prompt cache.
…rch#16539) Every 'hermes update' now runs a full backup of ~/.hermes/ first, so users can always roll back to the exact state they had before the update if anything goes wrong (corrupted sessions.db, broken skills, config migrations that don't round-trip, etc.). Changes: - hermes_cli/backup.py: new create_pre_update_backup() helper. Writes to <HERMES_HOME>/backups/pre-update-<stamp>.zip using the same exclusion rules and SQLite safe-copy as 'hermes backup'. Auto-rotates (keep last N, pre-update-*.zip only — hand-dropped zips in backups/ are untouched). Adds 'backups' to _EXCLUDED_DIRS so subsequent backups don't nest prior ones. - hermes_cli/main.py: _run_pre_update_backup() wired into _cmd_update_impl before any git operation. Prints save path, restore command, and how to disable. Swallows failures so a broken backup never blocks the update itself. New --no-backup flag on 'hermes update' for one-off override. - hermes_cli/config.py: new 'updates' section in DEFAULT_CONFIG with pre_update_backup (default true) and backup_keep (default 5). Auto-surfaces in the dashboard config UI. - tests/hermes_cli/test_backup.py: +11 tests covering backup location, content parity with 'hermes backup', no-recursion, rotation, manual file preservation, config gate, --no-backup flag, flag-wins-over-config.
…ousResearch#16364) * feat(plugins): google_meet — bundled plugin for join+transcribe Meet calls v1 shipping transcribe-only. Spawns headless Chromium via Playwright, joins an explicit https://meet.google.com/ URL, enables live captions, and scrapes them into a transcript file the agent can read across turns. The agent then has the meeting content in context and can do followup work (send recap, file issues, schedule followups) with its regular tools. Surface: - Tools: meet_join, meet_status, meet_transcript, meet_leave, meet_say (meet_say is a v1 stub — returns not-implemented; v2 will wire realtime duplex audio via OpenAI Realtime / Gemini Live + BlackHole / PulseAudio null-sink.) - CLI: hermes meet setup | auth | join | status | transcript | stop - Lifecycle: on_session_end auto-leaves any still-running bot. Safety: - URL regex rejects anything that isn't https://meet.google.com/... - No calendar scanning, no auto-dial, no auto-consent announcement. - Single active meeting per install; a second meet_join leaves the first. - Platform-gated to Linux + macOS (Windows audio routing for v2 untested). - Opt-in: standalone plugin, user must add 'google_meet' to plugins.enabled in config.yaml. Zero core changes. Plugin uses existing register_tool / register_cli_command / register_hook surfaces. 21 new unit tests cover the URL safety gate, transcript dedup + status round-trip, process-manager refusals/start/stop paths, tool-handler JSON shape under each branch, session-end cleanup, and platform-gated register(). * feat(plugins/google_meet): v2 realtime audio + v3 remote node host v2 \u2014 agent speaks in-meeting audio_bridge.py: PulseAudio null-sink (Linux) + BlackHole probe (macOS). On Linux we load pactl module-null-sink + module-virtual-source, track module ids for teardown; Chrome gets PULSE_SOURCE=<virt src> env so its fake mic reads what we write to the sink. macOS just probes BlackHole 2ch and returns its device name \u2014 the plugin refuses to switch the user's default audio input (that would surprise them). realtime/openai_client.py: sync WebSocket client for the OpenAI Realtime API. RealtimeSession.speak(text) sends conversation.item.create + response.create, accumulates response.audio.delta PCM bytes, appends them to a file. RealtimeSpeaker runs a JSONL-queue loop consuming meet_say calls. 'websockets' is an optional dep imported lazily. meet_bot.py: when HERMES_MEET_MODE=realtime, provisions AudioBridge, starts RealtimeSession + speaker thread, spawns paplay to pump PCM into the null-sink, then cleans everything up on SIGTERM. If any realtime setup step fails, falls back cleanly to transcribe mode with an error flagged in status.json. process_manager.enqueue_say(): writes a JSONL line to say_queue.jsonl; refuses when no active meeting or active meeting is transcribe-only. tools.meet_say: real implementation; requires active mode='realtime'. meet_join: adds mode='transcribe'|'realtime' param. v3 \u2014 remote node host node/protocol.py: JSON envelope (type, id, token, payload) + validate. node/registry.py: $HERMES_HOME/workspace/meetings/nodes.json, with resolve() auto-selecting the sole registered node when name is None. node/server.py: NodeServer \u2014 websockets.serve, bearer-token auth, dispatches start_bot/stop/status/transcript/say/ping onto the local process_manager. Token auto-generated + persisted on first run. node/client.py: NodeClient \u2014 short-lived sync WS per RPC, raises RuntimeError on error envelopes, clean API matching the server. node/cli.py: 'hermes meet node {run,list,approve,remove,status,ping}' subtree; wired into the main meet CLI by cli.py so 'hermes meet node' Just Works. tools.py: every meet_* tool accepts node='<name>'|'auto'; when set, routes through NodeClient to the remote bot instead of running locally. Unknown node \u2192 clear 'no registered meet node matches ...' error. cli.py: 'hermes meet join --node my-mac --mode realtime' and 'hermes meet say "..." --node my-mac' route to the node; 'hermes meet node approve <name> <url> <token>' registers one. Tests 21 v1 tests updated (meet_say is no longer a stub; active-record now carries mode). 20 new audio_bridge + realtime tests. 42 new node tests (protocol/registry/server/client/cli). 17 new v1/v2/v3 integration tests at the plugin level covering enqueue_say edge cases, env var passthrough, mode validation, node routing (known/unknown/auto/ambiguous), and argparse wiring for `hermes meet say` + `hermes meet node` + --mode/--node flags. Total: 100 plugin tests + 58 plugin-system tests = 158 passing. E2E verified on Linux with fresh HERMES_HOME: plugin loads, 5 tools register, on_session_end hook wires, 'hermes meet' CLI tree wires including the node subtree, NodeRegistry round-trips, meet_join routes correctly to NodeClient under node='my-mac' with mode='realtime', enqueue_say accepts realtime/rejects transcribe, argparse parses every new flag cleanly. Zero changes to core. All new code lives under plugins/google_meet/. * feat(plugins/google_meet): auto-install, admission detect, mac PCM pump, barge-in, richer status Ready-for-live-test follow-up on PR NousResearch#16364. Five additions that matter for the first live run on a real Meet, in priority order: 1. hermes meet install [--realtime] [--yes] pip install playwright websockets + python -m playwright install chromium --realtime: installs platform audio deps (pulseaudio-utils on Linux via sudo apt, blackhole-2ch + ffmpeg on macOS via brew). Prompts before sudo/brew unless --yes. Refuses on Windows. Refuses to auto-flip the macOS default input — user still selects BlackHole in System Settings (deliberate; surprise audio rerouting is worse than a manual step). 2. Admission detection _detect_admission(page): Leave-button visible OR caption region attached OR participants list present → we're in-call. _detect_denied(page): 'You can\'t join this video call' / 'You were removed' / 'No one responded to your request' → bail out. HERMES_MEET_LOBBY_TIMEOUT (default 300s) caps how long we sit in the lobby before giving up. in_call stays False until admitted. Status surfaces leaveReason: duration_expired | lobby_timeout | denied | page_closed. 3. macOS PCM pump ffmpeg reads speaker.pcm (24kHz s16le mono) and writes to the BlackHole AVFoundation output via -f audiotoolbox -audio_device_index <N>. _mac_audio_device_index() probes ffmpeg -f avfoundation -list_devices true to resolve 'BlackHole 2ch' → numeric index. Falls back to index 0 on probe failure. Linux paplay pump unchanged. 4. Richer status dict _BotState now tracks realtime, realtimeReady, realtimeDevice, audioBytesOut, lastAudioOutAt, lastBargeInAt, joinAttemptedAt, leaveReason. RealtimeSession.audio_bytes_out / last_audio_out_at counters fold into the status file once a second so meet_status() can show the agent's voice activity in near-real-time. 5. Barge-in RealtimeSession.cancel_response() sends type='response.cancel' over the same WS (lock-guarded so it's safe to call from the caption thread while speak() is reading frames). Handles response.cancelled as a terminal frame type. _looks_like_human_speaker() gates triggers so the bot's own name, 'You', 'Unknown', and blanks don't self-cancel. Called from the caption drain loop: when a new caption arrives attributed to a real participant while rt.session exists, we fire cancel_response() and stamp lastBargeInAt. Tests: 20 new unit tests across _BotState telemetry, barge-in gating, admission/denied probe error handling, cancel_response with and without a connected WS, and `hermes meet install` CLI wiring (flag parsing + end-to-end subprocess.run verification + Linux-already-installed fast path). Total 171 passing across all google_meet test files + the plugin-system regression suite. E2E verified on Linux: plugin loads, all 5 tools register, `hermes meet install --realtime --yes` parses, fresh-bot status.json has every new telemetry key, cancel_response on a disconnected session returns False without raising, barge-in helper gates the bot's own name correctly. Still out of scope (for a future PR, not blocking live test): mic → Realtime duplex (the agent listening to meeting audio via WebRTC), node-host TLS/pairing UX, Windows audio, Meet create+Twilio. Docs updated: SKILL.md now lists the installer subcommand, lobby timeout, barge-in caveat, and the full status-dict reference table. README.md quick-start uses hermes meet install.
…pability (NousResearch#16506) * feat(image-input): native multimodal routing based on model vision capability Attach user-sent images as OpenAI-style content parts on the user turn when the active model supports native vision, so vision-capable models see real pixels instead of a lossy text description from vision_analyze. Routing decision (agent/image_routing.py::decide_image_input_mode): agent.image_input_mode = auto | native | text (default: auto) In auto mode: - If auxiliary.vision.provider/model is explicitly configured, keep the text pipeline (user paid for a dedicated vision backend). - Else if models.dev reports supports_vision=True for the active provider/model, attach natively. - Else fall back to text (current behaviour). Call sites updated: gateway/run.py (all messaging platforms), tui_gateway (dashboard/Ink), cli.py (interactive /attach + drag-drop). run_agent.py changes: - _prepare_anthropic_messages_for_api now passes image parts through unchanged when the model supports vision — the Anthropic adapter translates them to native image blocks. Previous behaviour (vision_analyze → text) only runs for non-vision Anthropic models. - New _prepare_messages_for_non_vision_model mirrors the same contract for chat.completions and codex_responses paths, so non-vision models on any provider get text-fallback instead of failing at the provider. - New _model_supports_vision() helper reads models.dev caps. vision_analyze description rewritten: positions it as a tool for images NOT already visible in the conversation (URLs, tool output, deeper inspection). Prevents the model from redundantly calling it on images already attached natively. Config default: agent.image_input_mode = auto. Tests: 35 new (test_image_routing.py + test_vision_aware_preprocessing.py), all existing tests that reference _prepare_anthropic_messages_for_api still pass (198 targeted + new tests green). * feat(image-input): size-cap + resize oversized images, charge image tokens in compressor Two follow-ups that make the native image routing safer for long / heavy sessions: 1) Oversize handling in build_native_content_parts: - 20 MB ceiling per image (matches vision_tools._MAX_BASE64_BYTES, the most restrictive provider — Gemini inline data). - Delegates to vision_tools._resize_image_for_vision (Pillow-based, already battle-tested) to downscale to 5 MB first-try. - If Pillow is missing or resize still overshoots, the image is dropped and reported back in skipped[]; caller falls back to text enrichment for that image. 2) Image-token accounting in context_compressor: - New _IMAGE_TOKEN_ESTIMATE = 1600 (matches Claude Code's constant; within the realistic range for Anthropic/GPT-4o/Gemini billing). - _content_length_for_budget() helper: sums text-part lengths and charges _IMAGE_CHAR_EQUIVALENT (1600 * 4 chars) per image/image_url/ input_image part. Base64 payload inside image_url is NOT counted as chars — dimensions don't matter, only image-presence. - Both tail-cut sites (_prune_old_tool_results L527 and _find_tail_cut_by_tokens L1126) now call the helper so multi-image conversations don't slip past compression budget. Tests: 9 new in test_image_routing.py (oversize triggers resize, resize-fails-returns-None, oversize-skipped-reported), 11 new in test_compressor_image_tokens.py (flat charge per image, multiple images, Responses-API / Anthropic-native / OpenAI-chat shapes, no-inflation on raw base64, bounds-check on the constant, integration test that an image-heavy tail actually gets trimmed). * fix(image-input): replace blanket 20MB ceiling with empirically-verified per-provider limits The previous commit imposed a hardcoded 20 MB base64 ceiling on all providers, triggering auto-resize on anything larger. This was wrong in both directions: * Too loose for Anthropic — actual limit is 5 MB (returns HTTP 400 'image exceeds 5 MB maximum' above that). * Too strict for OpenAI / Codex / OpenRouter — accept 49 MB+ without complaint (empirically verified April 2026 with progressive PNG sizes). New behaviour: * _PROVIDER_BASE64_CEILING table: only anthropic and bedrock have a ceiling (5 MB, since bedrock-on-Claude shares Anthropic's decoder). * Providers NOT in the table get no ceiling — images attach at native size and we trust the provider to return its own error if it disagrees. A provider-specific 400 message is clearer than us guessing wrong and silently degrading image quality. * build_native_content_parts() gains a keyword-only provider arg; gateway/CLI/TUI pass the active provider so Anthropic users get auto-resize protection while OpenAI users don't pay it. * Resize target dropped from 5 MB to 4 MB to slide safely under Anthropic's boundary with header overhead. Empirical measurements (direct API, no Hermes in the loop): image b64 anthropic openrouter/gpt5.5 codex-oauth/gpt5.5 0.19 MB ✓ ✓ ✓ 12.37 MB ✗ 400 5MB ✓ ✓ 23.85 MB ✗ 400 5MB ✓ ✓ 49.46 MB ✗ 413 ✓ ✓ Tests: rewrote TestOversizeHandling (5 tests): no-ceiling pass-through, Anthropic resize fires, Anthropic skip on resize-fail, build_native_parts routes ceiling by provider, unknown provider gets no ceiling. All 52 targeted tests pass. * refactor(image-input): attempt native, shrink-and-retry on provider reject Replace proactive per-provider size ceilings with a reactive shrink path on the provider's actual rejection. All providers now attempt native full-size attachment first; if the provider returns an image-too-large error, the agent silently shrinks and retries once. Why the previous design was wrong: hardcoding provider ceilings (anthropic=5MB, others=unlimited) meant OpenAI users on a 10MB image paid no tax, but Anthropic users lost quality on anything >5MB even though the empirical behaviour at provider-reject time is the same (shrink + retry). Baking the table into the routing layer also requires updating Hermes every time a provider's limit changes. Reactive design: - image_routing.py: _file_to_data_url encodes native size, no ceiling. build_native_content_parts drops its provider kwarg. - error_classifier.py: new FailoverReason.image_too_large + pattern match ("image exceeds", "image too large", etc.) checked BEFORE context_overflow so Anthropic's 5MB rejection lands in the right bucket. - run_agent.py: new _try_shrink_image_parts_in_messages walks api messages in-place, re-encodes oversized data: URL image parts through vision_tools._resize_image_for_vision to fit under 4MB, handles both chat.completions (dict image_url) and Responses (string image_url) shapes, ignores http URLs (provider-fetched). New image_shrink_retry_attempted flag in the retry loop fires the shrink exactly once per turn after credential-pool recovery but before auth retries. E2E verified live against Anthropic claude-sonnet-4-6: - 17.9MB PNG (23.9MB b64) attached at native size - Anthropic returns 400 "image exceeds 5 MB maximum" - Agent logs '📐 Image(s) exceeded provider size limit — shrank and retrying...' - Retry succeeds, correct response delivered in 6.8s total. Tests: 12 new (8 shrink-helper shapes + 4 classifier signals), replaces 5 proactive-ceiling tests with 3 simpler 'native attach works' tests. 181 targeted tests pass. test_enum_members_exist in test_error_classifier.py updated for the new enum value.
…earch#16566) The zip backup could add minutes to every 'hermes update' on large HERMES_HOME directories. Flip the default to off and add a --backup flag for one-off opt-in runs. - updates.pre_update_backup default: True -> False - hermes update: new --backup flag (opposite of existing --no-backup) - Silent no-op when disabled (no message spam on every update) - Existing --no-backup still works and wins over --backup - Users who explicitly set pre_update_backup: true keep the old behavior - Tests updated to cover default-off, --backup opt-in, and config-enabled paths
Session-local trajectory cache — keyed by session hash, regenerated per-session, won't port to another machine anyway. On a large install this was multiple GB of pure noise in every zip. Also adds a regression test for the pre-existing backups/ exclusion so the two machine-local dirs share coverage.
…Research#15165) ``_cleanup_agent_resources`` previously invoked ``agent.shutdown_memory_provider()`` with no arguments, so every memory provider's ``on_session_end`` hook received an empty list. Providers with an early-return guard on empty input (Holographic, Hindsight) never extracted facts from the conversation, and users hit "抱歉,找不到相關的對話記錄" on the first turn after any gateway restart, session reset, or idle expiry. Forward ``agent._session_messages`` — the transcript the agent itself maintains and refreshes every turn via ``_persist_session`` — so providers see the actual conversation. Falls back to the legacy no-arg call whenever the attribute is absent or not a list (test stubs built via ``object.__new__`` or ``MagicMock``) to preserve backward compatibility with existing suites. ``AIAgent.shutdown_memory_provider`` already accepts ``messages: list = None`` (run_agent.py:4126), so this is a pure caller-side fix. Paths that use ``skip_memory=True`` temporary agents (memory flush, hygiene auto-compress, ``/compress``) are no-ops inside ``shutdown_memory_provider`` because ``self._memory_manager`` is None — no behaviour change for them. Covers Part A of the bug report. Part B (adding ``on_session_end`` to the Hindsight plugin) is a separate concern that would benefit from this fix landing first. Regression test added at ``tests/gateway/test_shutdown_memory_provider_messages.py`` covering: populated messages forwarded, empty list still forwarded, attribute missing falls back, non-list (MagicMock) falls back, provider exceptions don't block ``close()``, None agent no-op, and agent without ``shutdown_memory_provider`` tolerated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…arch#15165 sibling) The gateway fix in the previous commit forwards _session_messages on gateway session teardown. The CLI exit cleanup path had the same bug: it read getattr(agent, 'conversation_history', None) or [] — but AIAgent has no conversation_history attribute, so providers always received []. Switch to _session_messages (same attribute the gateway now uses), guarded by isinstance(..., list) to preserve the no-arg fallback for MagicMock-based CLI test stubs. Adds tests/cli/test_cli_shutdown_memory_messages.py (4 cases mirroring the gateway suite).
…lsets The background skill/memory review agent was created without toolset restrictions, inheriting the full default tool set. This allowed it to use terminal, send_message, delegate_task, and other tools outside its intended scope, potentially performing unrelated side effects after skill creation. Restrict the review agent to only memory and skills toolsets by passing enabled_toolsets=['memory', 'skills'] during AIAgent construction. Fixes NousResearch#15204
…reads When tools execute concurrently via ThreadPoolExecutor, worker threads could not see the thread-local approval/sudo callbacks registered by the CLI. This caused dangerous-command prompts to fall back to plain input(), which deadlocks against prompt_toolkit's raw terminal mode. Capture parent-thread callbacks before launching workers, register them locally in each _run_tool thread, and clear them on exit. Mirrors the existing fix pattern from cli.py run_agent() for the main agent worker thread (GHSA-qg5c-hvr5-hjgr / NousResearch#13617).
…esearch#15216) PR NousResearch#13734 fixed the concurrent-tool-executor vector (ThreadPoolExecutor workers didn't inherit the CLI's TLS approval callback). Two vectors remained that could still land in the deadlocking input() fallback: 1. _spawn_background_review spawns a raw threading.Thread with no approval callback installed, so any dangerous-command guard the review agent trips falls back to input() -> deadlock against the parent's prompt_toolkit TUI (same class as delegate_task subagents, fixed in 023b1bf / NousResearch#15491). Install a _bg_review_auto_deny callback at thread start, clear on finally. 2. prompt_dangerous_approval's fallback unconditionally spawned a daemon thread calling input() when approval_callback was None. That fallback can never succeed under prompt_toolkit because the user's Enter goes to pt's raw-mode stdin capture. Detect an active pt Application via get_app_or_none() and fail closed (deny + log) instead, so future threads that forget to install a callback degrade gracefully instead of hanging 60s invisibly. Regression guards: - tests/run_agent/test_background_review.py verifies the review worker thread sees a callable auto-deny callback mid-run and that the slot is cleared in the finally block. - tests/tools/test_approval.py TestFailClosedUnderPromptToolkit verifies prompt_dangerous_approval returns 'deny' fast under a mocked pt Application, and that a real callback still wins over the guard.
Release-notes contributor attribution for the salvaged PR NousResearch#13734 fix.
…6576) The backup takes a consistent snapshot of each .db via sqlite3.backup(), so shipping the live .db-wal / .db-shm / .db-journal alongside pairs the fresh snapshot with stale sidecar state and produces a torn restore on first open. Sidecars are transient and SQLite regenerates them on next connection anyway. This also trims multi-MB of junk from every zip — state.db-wal alone was ~9 MB here, doubled by the fact the WAL is the live write-ahead log, not data.
…search#16263 (NousResearch#16575) When a paste takes longer than 500ms to process on the prompt_toolkit event-loop thread, emit a logger.warning with elapsed time, byte size, line count, and sys.platform. Gives us concrete repro data for the recurring 'CLI freezes after paste on macOS' class of reports (issue NousResearch#16263, plus sibling reports across Claude Code / Cursor / Lightroom against macOS Tahoe 26). Pure diagnostic — no behavior change. Two time.perf_counter() calls and one conditional per paste event. Log line only fires when the handler is actually slow, so normal pastes add no log noise.
…alvage NousResearch#13979) (NousResearch#16598) * fix: clean gateway auxiliary client caches on teardown * fix(gateway): recover from stale pid files and close cron agents Two issues were keeping the gateway from surviving long runs: 1. `_cleanup_invalid_pid_path` delegated to `remove_pid_file`, which refuses to unlink when the file's pid differs from our own. That safety check exists for the --replace atexit handoff, but it also applied to stale-record cleanup, so after a crashy exit the pid file was orphaned: `write_pid_file()`'s O_EXCL create then failed with `FileExistsError`, and systemd looped on "PID file race lost to another gateway instance". Unlink unconditionally from this helper since the caller has already verified the record is dead. 2. The cron scheduler never closed the ephemeral `AIAgent` it creates per tick, and never swept the process-global auxiliary-client cache. Over days of 10-minute ticks this leaked subprocesses and async httpx transports until the gateway hit EMFILE. Release the agent and call `cleanup_stale_async_clients()` in `run_job`'s outer `finally`, matching the gateway's own per-turn cleanup. * chore(release): map bloodcarter@gmail.com -> bloodcarter --------- Co-authored-by: bloodcarter <bloodcarter@gmail.com>
…ousResearch#17004) Auxiliary callers that configure reasoning via auxiliary.<task>.extra_body.reasoning were having that config silently dropped by the Codex Responses adapter — it only forwarded messages/model/tools through to responses.stream(), never translating chat.completions-shaped reasoning hints into the Responses API's top-level reasoning + include fields. Mirror the main-agent translation from agent/transports/codex.py: - extra_body.reasoning.effort → resp_kwargs.reasoning.{effort, summary:"auto"} - 'minimal' → 'low' clamp (Codex backend rejects 'minimal') - Always include ['reasoning.encrypted_content'] when reasoning is enabled - {'enabled': False} → omit reasoning and include entirely - Non-dict reasoning values are ignored defensively Reported by @op (Apr 26 feedback bundle). ## Changes - agent/auxiliary_client.py: _CodexCompletionsAdapter.create() now reads and translates extra_body.reasoning before calling responses.stream() - tests/agent/test_auxiliary_client.py: 9 new tests covering all effort levels, the minimal→low clamp, the disabled path, the no-op paths, and defensive handling of wrong-shape inputs Co-authored-by: teknium1 <teknium@users.noreply.github.com>
…nect Network errors through proxies (e.g. sing-box) can leave httpx connections in a half-closed state occupying pool slots. After enough reconnect cycles the 256-connection default fills up entirely, causing Pool timeout: All connections in the connection pool are occupied. Fix: cycle only the getUpdates request object (_request[0]) via shut-down + re-initialize before restarting polling. This drains stale connections without touching the general request (_request[1]) that concurrent send_message / edit_message calls rely on. The drain is applied to both _handle_polling_network_error and _handle_polling_conflict reconnect paths via a shared _drain_polling_connections() helper. Failures in the drain are swallowed so reconnect always proceeds. Based on NousResearch#16466 by @Mirac1eSky.
… edits (NousResearch#17008) The gateway caches one AIAgent per session to preserve prompt-cache hits, keyed by _agent_config_signature(). The signature previously only fingerprinted model/credentials/toolsets/ephemeral-prompt — NOT the compression or context_length config. As a result, users who edited model.context_length or compression.threshold in config.yaml on a long-lived gateway saw no effect until they triggered an unrelated cache eviction (/model switch, /reset, gateway restart). Add a new cache_keys parameter to _agent_config_signature and a _CACHE_BUSTING_CONFIG_KEYS registry listing config values the agent bakes in at construction time. Call sites read the current config and pass it through — next gateway message with an edited config rebuilds the agent. Keys registered: - model.context_length - compression.enabled - compression.threshold - compression.target_ratio - compression.protect_last_n Reported by @op (Apr 26 feedback bundle). ## Changes - gateway/run.py: new _CACHE_BUSTING_CONFIG_KEYS tuple, _extract_cache_busting_config classmethod, cache_keys kwarg on _agent_config_signature, call site passes the extracted dict - tests/gateway/test_agent_cache.py: 11 new tests (5 on _agent_config_signature behavior, 6 on _extract_cache_busting_config) Co-authored-by: teknium1 <teknium@users.noreply.github.com>
…ousResearch#17022) delegate_task runs inside the parent turn and is cancelled when the parent is interrupted (new user message, /stop, /new). The child status payload (status=interrupted, exit_reason=interrupted) is already honest, but the tool schema and user-facing docs did not set the expectation, so users reasonably assumed delegated subagents would keep running in the background after interrupting the parent. Updates: - tools/delegate_tool.py DELEGATE_TASK_SCHEMA description adds a WHEN NOT TO USE bullet pointing at cronjob / terminal(background=True, notify_on_complete=True) for durable long-running work. - website/docs/user-guide/features/delegation.md gains a Lifetime and Durability callout above Key Properties. - website/docs/guides/delegation-patterns.md expands the Use something else list and the Constraints section with the same guidance. Reported by LizLiz (@lizliz404) via Teknium. Co-authored-by: teknium1 <teknium@users.noreply.github.com>
…esearch#16746) In Docker builds the `/dev/tty` device node is present in the mount namespace, so `[ -e /dev/tty ]` returns true — but opening it fails with `ENXIO: No such device or address`. Under the old gate the "no terminal available" skip never triggered, the setup wizard ran, and the build aborted a few lines later when bash tried `< /dev/tty`: /tmp/install.sh: line 1347: /dev/tty: No such device or address Replace the existence check with `(: </dev/tty) 2>/dev/null`, which actually attempts to open /dev/tty in a subshell. The probe succeeds when piped from `curl | bash` on a real terminal (the wizard's intended use case) and fails cleanly in Docker build / CI contexts so the skip kicks in before the redirect can crash. Add a regression test that statically asserts run_setup_wizard does not gate on the bare existence check and that the open-based probe is in place. Fixes NousResearch#16746. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Research#16750 Address the three Copilot inline findings on the regression test: - Switch _extract_run_setup_wizard() from str.index() with hard-coded markers (which raises ValueError if `maybe_start_gateway()` is renamed or the marker leaks into a comment) to an anchored regex on the function-definition + closing-brace boundaries. - Match `[ -e /dev/tty ]` with surrounding whitespace, optional quoting, and the `test -e /dev/tty` form so the regression guard catches every spelling of the existence-only check, not just the exact substring. - Replace the literal `(: </dev/tty)` substring assertion with a higher-level invariant — the gate must be an `if`/`if !` whose test redirects stdin from /dev/tty — so equivalent open-based probes (`exec 3</dev/tty` + close, brace-grouped variants, etc.) keep the test green while the bare existence check stays caught. Verified guard: both tests still pass on the fix and both fail on `origin/main` with the documented messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…h#16746) The contributor's PR (NousResearch#16750) scoped the fix to run_setup_wizard() and explicitly punted the two sibling sites. Both have the identical [ -e /dev/tty ] pattern followed by a < /dev/tty redirect and crash in Docker the same way: - scripts/install.sh:732 install_system_packages() -- apt sudo prompt fallback. sudo ... < /dev/tty dies with the same ENXIO. - scripts/install.sh:1395 maybe_start_gateway() -- gateway-install gate, same function path as the wizard reproducer. Fix both with the same (: </dev/tty) 2>/dev/null probe, and parametrize the regression test over all three gated functions so any future regression is caught regardless of which site breaks.
…esearch#17010) Mechanical cleanup across 43 files — removes 46 unused imports (F401) and 14 unused local variables (F841) detected by `ruff check --select F401,F841`. Net: -49 lines. Also fixes a latent NameError in rl_cli.py where `get_hermes_home()` was called at module line 32 before its import at line 65 — the module never imported successfully on main. The ruff audit surfaced this because it correctly saw the symbol as imported-but-unused (the call happened before the import ran); the fix moves the import to the top of the file alongside other stdlib imports. One `# noqa: F401` kept in hermes_cli/status.py for `subprocess`: tests monkeypatch `hermes_cli.status.subprocess` as a regression guard that systemctl isn't called on Termux, so the name must exist at module scope even though the module body doesn't reference it. Docstring explains the reason. Also fixes an invalid `# noqa:` directive in gateway/platforms/discord.py:308 that lacked a rule code. Co-authored-by: teknium1 <teknium@users.noreply.github.com>
…esearch#17026) Append a compact 'model · 68% · ~/projects/hermes' footer to the FINAL message of each turn, disabled by default (display.runtime_footer.enabled). Answers the Telegram-side parity ask: runtime context that the CLI status bar already shows is now available in messaging replies when enabled. Wiring: - gateway/runtime_footer.py: resolve_footer_config + format_runtime_footer + build_footer_line. Pure-function renderer; per-platform overrides under display.platforms.<platform>.runtime_footer. - gateway/run.py: appends footer to response right after reasoning prepend so it lands only on the final message (never tool progress or streaming chunks). When streaming already delivered the body (already_sent), the footer is sent as a small trailing message instead. - agent_result now exposes context_length alongside last_prompt_tokens so the footer can compute the pct; both gateway return paths updated. - /footer [on|off|status] slash command, wired in CLI (cli.py) and gateway (gateway/run.py both running-agent bypass and main dispatch). Global toggle only; per-platform overrides via config.yaml. Graceful degradation: - Missing context_length (unknown model) → pct field silently dropped (no '?%' artifact). - Empty final_response → no footer appended. - Unknown field names in config → silently ignored. Tests: 25-case unit suite (tests/gateway/test_runtime_footer.py) plus E2E harness covering streaming vs non-streaming branches, per-platform override, and the exact argument contract gateway/run.py uses. Co-authored-by: teknium1 <teknium@users.noreply.github.com>
…clients (NousResearch#17027) Auxiliary tasks (title_generation, vision, compression, web_extract, session_search) now pick the correct wire protocol based on the endpoint, not just on which resolve_provider_client branch built the client. Fixes 404s on Kimi Coding Plan and any other named provider whose endpoint speaks Anthropic Messages. Root cause: the 'api_key' branch of resolve_provider_client (and the Step 2 fallback chain inside _resolve_auto) always built a plain OpenAI client regardless of what the endpoint actually spoke. For provider=kimi-coding + model=kimi-for-coding, that meant: POST https://api.kimi.com/coding/v1/chat/completions { "model": "kimi-for-coding", ... } → 404 resource_not_found_error The /coding route only accepts the Anthropic Messages shape (the main agent already uses api_mode=anthropic_messages for it). Earlier fixes (NousResearch#16819, #22ddac4b1) patched the anonymous-custom, named-custom, and external-process branches — but the named api_key branch (kimi-coding, minimax, zai, future /anthropic providers) was the fourth sibling and never got the same treatment. Fix: one module-level helper _maybe_wrap_anthropic() that rewraps a plain OpenAI client in AnthropicAuxiliaryClient when: - api_mode is explicitly 'anthropic_messages', OR - the URL ends in '/anthropic', OR - the host is api.kimi.com + path contains '/coding', OR - the host is api.anthropic.com. Wired into _wrap_if_needed (covers all resolve_provider_client branches that already go through it) and into the Step 2 api_key fallback chain inside _resolve_auto. Explicit api_mode still wins: passing api_mode='chat_completions' forces OpenAI wire, and already- wrapped specialized adapters (Codex, Gemini native, CopilotACP) pass through unchanged. E2E verified: - resolve_provider_client('kimi-coding', 'kimi-for-coding') → AnthropicAuxiliaryClient (was plain OpenAI, which 404'd) - _resolve_auto Step 1 for kimi-coding runtime → AnthropicAuxiliaryClient - resolve_provider_client('openrouter', ...) → plain OpenAI (no regression) - api_mode='chat_completions' override → plain OpenAI (explicit wins) Tests: - tests/agent/test_auxiliary_transport_autodetect.py (new): 21 tests covering URL detection, wrap decisions, and integration. - 204/205 existing auxiliary tests pass (1 pre-existing failure on main, unrelated to this change). Co-authored-by: teknium1 <teknium@users.noreply.github.com>
…ousResearch#17030) extraPackages adds packages to the systemd service PATH, but the terminal backend's login-shell snapshot rebuilds PATH from NixOS system profiles, so tools added via extraPackages are invisible to terminal commands, skills, and cron jobs — the entire use case. Changes: - Mark the option description as deprecated with explanation - Emit a NixOS warning when extraPackages is non-empty, including a ready-to-paste environment.systemPackages replacement - Update docs: quick-reference table, plugin example, and options reference all point to environment.systemPackages The option still functions (non-breaking) so existing configs keep working while users migrate.
…rch#17028) The telegram.reactions key was already wired up (gateway/config.py bridges it to TELEGRAM_REACTIONS at startup) but was undocumented and missing from DEFAULT_CONFIG, so users had no way to discover it. Add it with the existing off-by-default behavior preserved. No behavior change — runtime default stays False. Co-authored-by: teknium1 <teknium@users.noreply.github.com>
…ure (NousResearch#16713) (NousResearch#17029) Discord's per-app command-management bucket is ~5 writes / 20 s. A mass-prune-plus-upsert reconcile (77 orphans + 30 desired = 107 writes in the reported case) can't finish under the old flat 30 s budget, and the subsequent reconnect retries inside the rate-limit cooldown also time out — leaving slash commands broken for ~60 min until the bucket fully recovers. Bump the timeout to 600 s so realistic bursts drain, update the warning message to point at the saturated bucket instead of a hardcoded 30 s. The 600 s cap still guards against a true hang. Credit to @Tranquil-Flow for PR NousResearch#16739 and @davidbordenwi for reporting NousResearch#16713 with the bucket-math diagnosis. Closes NousResearch#16713. Co-authored-by: Teknium <teknium@nousresearch.com>
…rror (NousResearch#17039) Previously, check_browser_requirements() only checked for the agent-browser CLI, not the Chromium binary it drives. When the CLI was present but Chromium wasn't (common in Docker images predating the playwright install step), the browser tool was advertised to the agent, every call hung for the full command timeout (~30s each, ~220s for a chained navigate), and the agent eventually gave up with no useful error — users saw 'browser not working' with empty errors.log. Changes: - tools/browser_tool.py: add _chromium_installed() checking PLAYWRIGHT_BROWSERS_PATH + default Playwright cache paths for chromium-* / chromium_headless_shell-* dirs; wire into check_browser_requirements() for local mode (cloud providers unaffected). _run_browser_command fails fast with an actionable Docker vs. host message instead of hanging. _running_in_docker() checks /.dockerenv and /proc/1/cgroup. - hermes_cli/tools_config.py: post_setup for 'Local Browser' now runs 'agent-browser install --with-deps' after npm install to actually download Chromium. In Docker, points user at the updated image pull instead of trying to install into a read-only layer. Cloud-provider post_setup (browserbase) skips Chromium install entirely. - tests/tools/test_browser_chromium_check.py: new tests covering search roots, install detection, requirements branches (local/cloud/ camofox), and the fast-fail guard in docker/non-docker contexts. - tests/tools/test_browser_homebrew_paths.py: 5 existing subprocess-path tests now mock _chromium_installed=True since they exercise the post-guard subprocess path. Co-authored-by: teknium1 <teknium@users.noreply.github.com>
…search#17041) load_config() and read_raw_config() now cache their result keyed on the config file's (mtime_ns, size). On cache hit they return a deepcopy of the cached value, skipping yaml.safe_load + deep-merge + normalize + env-var expansion entirely. save_config() + migrate_config() write via atomic_yaml_write which produces a fresh inode, so stat() sees a new mtime_ns and the next load repopulates automatically — no explicit invalidation hook needed. Measured per-call cost: load_config() cold: 13.3 ms load_config() cached: 0.23 ms (57x faster) read_raw_config() cached: 0.13 ms A single gateway turn hits the config 5-15 times (session context, auxiliary client resolution, memory config, plugin hooks, approval lookups, per-tool settings). That's 65-200 ms/turn of pure YAML re-parsing on main. After this change: 1-3 ms/turn. Also migrates gateway/run.py's 6 direct yaml.safe_load(config.yaml) call sites through _load_gateway_config, which now shares the read_raw_config cache when _hermes_home agrees with the canonical config path. The direct-read fallback is retained for tests that monkeypatch gateway_run._hermes_home without touching HERMES_HOME. Safety: - load_config() returns a deepcopy on every call; the 67+ call sites that mutate the result (cfg["model"]["default"] = ..., etc.) can't corrupt the cache. - save_config() / atomic_yaml_write bump mtime, naturally invalidating the cache for the next reader. - Cache is keyed on str(config_path), so HERMES_HOME profile switches don't collide. Verified: - 112 config tests pass (test_config, test_config_env_expansion, test_config_env_refs, test_config_drift, test_config_validation, test_aux_config). - 87 gateway tests pass (test_verbose_command, test_session_info, test_compress_focus, test_runtime_footer, test_resume_command, test_reasoning_command, test_approve_deny_commands, test_run_progress_interrupt). - Live hermes chat smoke — 2 turns + /model switch + tool calls, zero errors in agent.log. Co-authored-by: teknium1 <teknium@users.noreply.github.com>
…ompatible endpoints (NousResearch#17057) Custom Kimi-compatible endpoints (provider=custom, api_mode=anthropic_messages) also require reasoning_content on assistant tool-call messages when thinking is enabled, but the previous check only matched official Kimi/Moonshot hostnames. Detect Kimi-compatible custom endpoints by model family (model name contains 'kimi' or 'k2') in addition to the hostname check, so self-hosted proxies and third-party Kimi-compatible inference providers are also handled correctly.
…ousResearch#17080) hermes profile create --clone was copying the source profile's .env and config.yaml verbatim, including exclusive platform credentials (TELEGRAM_BOT_TOKEN, WEIXIN_TOKEN, DISCORD_BOT_TOKEN, etc.). These are one-to-one identity bindings — a token belongs to exactly one bot instance and cannot be shared across profiles. When two profiles hold the same token and start simultaneously, the platform adapter fails during scoped lock acquisition, causing a gateway health check timeout and a confusing 'profile loading error' in hermes-web-ui. Fix: define _EXCLUSIVE_PLATFORM_KEYS and _EXCLUSIVE_PLATFORM_CONFIG_PATHS, and apply _strip_env_exclusive_credentials() and _strip_config_exclusive_credentials() to cloned .env and config.yaml files. Exclusive credential keys are commented out in .env, and the corresponding platform entries are force-disabled (enabled: false) in config.yaml. A warning is printed so the user knows to configure them for the new profile.
…sResearch#17054) Slack's app manifest rejects slash command names that start with an underscore. _sanitize_slack_name() converts plugin command names (e.g. _reload_mcp) to valid Slack names by lowercasing and stripping invalid chars, but the underscore prefix was preserved. Commands like /_reload_mcp were included in the manifest and caused Slack to reject the entire app with 'invalid name'. Fix: skip any sanitized name that starts with an underscore before adding it to the slash command list.
…h api_mode=anthropic_messages (NousResearch#17086) When a custom auxiliary endpoint was configured with api_mode=anthropic_messages, resolve_provider_client() unconditionally passed the base URL through _to_openai_base_url(), which rewrites /anthropic → /v1. This caused all auxiliary tasks (title_generation, compression, vision, web_extract, etc.) to land on POST /v1/chat/completions — a 404 on Anthropic-native proxies. Fix: add the same api_mode==anthropic_messages guard that already exists in _try_custom_endpoint() and named-provider branches. When api_mode is anthropic_messages, pass the URL unchanged to build_anthropic_client() and route through AnthropicAuxiliaryClient directly.
…sResearch#17049) Fix two locations where wmic subprocess output was decoded with text=True (UTF-8) on Windows. Non-UTF-8 characters in process paths cause UnicodeDecodeError, and Python ≤3.11 can return None for stdout after decode failures causing AttributeError. Fix: capture raw bytes, decode with utf-8 then fall back to latin-1, and guard against None stdout.
…asoning_content (NousResearch#17052) Changed last_reasoning extraction to only use the most recent assistant message (current turn) rather than walking the full message history. Previously, when the model returned null reasoning for a simple prompt, the loop would skip the current message and match an older assistant message's reasoning, displaying it as if it belonged to the current response.
…tforms after 20 attempts (NousResearch#17063) Instead of removing a platform from _failed_platforms after 20 failed reconnect attempts, keep it in the retry queue with a fixed 10-minute backoff. This prevents transient network/proxy outages that last hours from permanently disabling a platform (e.g. Telegram) until the gateway is manually restarted.
…ands (NousResearch#16843) Make redact_sensitive_text JSON field pattern use exact key match instead of substring match to avoid false positives like 'htpasswd_password' being treated as a credential field. Also restrict terminal_tool redaction to structured output (JSON, logs) only — arbitrary command output passes through unchanged so that functional commands like htpasswd that output passwords as results continue to work correctly.
Sldark23
pushed a commit
that referenced
this pull request
May 4, 2026
* ci(nix): auto-fix stale npm hashes on push to main When a PR merges to main with updated package-lock.json or package.json in ui-tui/ or web/, the new auto-fix-main job detects stale npmDepsHash values and pushes a fix commit directly to main. This eliminates the recurring manual hash-bump PRs (NousResearch#15420, NousResearch#15314, NousResearch#15272, NousResearch#15244) by reusing the existing fix-lockfiles --apply pipeline. The fix commit only touches nix/*.nix files, which are outside the push path filter (package-lock.json / package.json), so it cannot re-trigger itself. Closes NousResearch#15314 * fix(ci): use GitHub App token for auto-fix-main push GITHUB_TOKEN commits are invisible to workflow triggers (GitHub's infinite-loop prevention). The auto-fix-main job pushes directly to main, so the fix commit never triggered downstream nix.yml verification. Mint a short-lived token via the repo's GitHub App (daimon-nous, APP_ID + APP_PRIVATE_KEY secrets) so the push is treated as a real event and nix.yml fires to verify the corrected hashes. Tested via workflow_dispatch dry-run: app token minted successfully, checkout with app token succeeded, fix job correctly gated. Resolves review feedback from Bugbot (r3144569551). * ci(nix): rename lockfile check job for required status check Rename 'check' → 'nix-lockfile-check' so the status check name is unambiguous when added as a required check on main. * fix(ci): harden auto-fix-main against races, loops, and silent failures Address adversarial review findings: 1. Race condition (#1): Job-level concurrency with cancel-in-progress collapses back-to-back pushes; ref: main checkout always gets latest branch state; explicit push target (origin HEAD:main). 2. Loop prevention (#2): File-whitelist check before commit aborts if any file outside nix/{tui,web}.nix was modified, preventing accidental self-triggering. 3. Silent infra failures (NousResearch#8): nix-lockfile-check now fails explicitly when fix-lockfiles exits without reporting stale status (catches nix setup failures, network errors, script bugs that bypass continue-on-error). 4. Commit traceability (NousResearch#11): Auto-fix commits include source SHA and workflow run URL in the commit body. 5. Explicit push target (NousResearch#12): git push origin HEAD:main instead of bare git push. --------- Co-authored-by: alt-glitch <alt-glitch@users.noreply.github.com>
Sldark23
pushed a commit
that referenced
this pull request
May 4, 2026
…ch#16706) * fix(tui): drop stale stream events after ctrl-c interrupt Once interruptTurn() flips this.interrupted, only recordMessageDelta short-circuited. recordReasoningDelta/Available, recordToolStart/ Progress/Complete, and recordInlineDiffToolComplete kept populating turnState until the python loop reached its next _interrupt_requested check (~1s on busy turns), making it look like ctrl-c was ignored while late "thinking" + tool calls kept landing in the UI. Add the same interrupted guard to every stream-side recorder, and clear the flag at startMessage() so the next turn isn't suppressed if the previous turn never delivered message.complete. * fix(tui): guard recordTodos against post-interrupt mutation; fake-timers in test Copilot review on PR NousResearch#16706: 1. `recordToolStart` is interruption-guarded, but `tool.start` handler also calls `recordTodos(payload.todos)` first — so a late tool.start carrying todos could still mutate `turnState.todos` after Ctrl-C, leaving ghost rows in the panel. Adds the same `if (this.interrupted) return` early-exit to `recordTodos` so *all* tool.start side-effects are dropped post-interrupt. 2. The interrupt test was leaking a real `setTimeout` (interrupt cooldown) across test files, which could fire later and mutate uiStore from the wrong test context. Wraps the test in `vi.useFakeTimers()` + `vi.runAllTimers()` and restores real timers in finally. 3. Extends the same test with a todos payload on the post-interrupt tool.start so we have explicit regression coverage for #1. * fix(tui): guard pushTrail post-interrupt; harden interrupt-test cleanup Round 2 Copilot review on PR NousResearch#16706: 1. `tool.generating` events route through `pushTrail`, which was not interruption-guarded — late events could still write 'drafting …' into `turnTrail` after Ctrl-C, leaving a stale shimmer in the UI. Adds the same `if (this.interrupted) return` early-exit. 2. Test cleanup moved `vi.runAllTimers()` into `finally` (before `vi.useRealTimers()`) so a mid-test assertion failure can't leak the interrupt-cooldown setTimeout across other test files. 3. Replaced the misleading 'pre-interrupt todos … expected to be cleared by the interrupt cycle' comment with an accurate one reflecting current behaviour (interrupt does NOT clear todos). 4. Added an explicit assertion that a post-interrupt `tool.generating` event does not extend `turnTrail` — regression coverage for #1.
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.
Resumo
Este PR corrige 7 issues identificados no repositório NousResearch/hermes-agent.
Issues Corrigidos
1. NousResearch#17086 - custom endpoint com api_mode=anthropic_messages falhava com 404
Arquivo:
agent/auxiliary_client.pyProblema: Quando provider=custom com api_mode=anthropic_messages e base_url terminando em /anthropic, a funcao _resolve_provider_client() convertia a URL para o formato /v1/messages, causando 404 em provedores Anthropic-compatíveis de terceiros.
Correcao: Mantem o path /anthropic quando api_mode=anthropic_messages e o base_url ja termina em /anthropic.
2. NousResearch#17076 - kimi-coding vision quebrado (404 em analise de imagem)
Arquivo:
agent/auxiliary_client.pyProblema: kimi-coding nao estava listado em _PROVIDER_VISION_MODELS, entao auxiliary.vision.provider: auto nao conseguia detectar o modelo de visao disponivel para Kimi.
Correcao: Adicionado kimi-coding e kimi-coding-cn ao mapa de modelos de visao.
3. NousResearch#17080 - hermes profile create --clone copiava credenciais exclusivas de plataforma
Arquivo:
hermes_cli/profiles.pyProblema: O clone de perfil copiava TELEGRAM_BOT_TOKEN, DISCORD_BOT_TOKEN, WEIXIN_TOKEN verbatim. Quando dois perfis iniciam simultaneamente com o mesmo token, o adaptador de plataforma falha durante a aquisicao de lock.
Correcao: Define _EXCLUSIVE_PLATFORM_KEYS e _EXCLUSIVE_PLATFORM_CONFIG_PATHS. Credenciais exclusivas sao comentadas em .env e entradas de plataforma sao enabled: false em config.yaml apos clone.
4. NousResearch#17054 - Slack manifest rejeitava nomes com underscore
Arquivo:
hermes_cli/commands.pyProblema: _sanitize_slack_name() convertia nomes de comandos como _reload_mcp para Slack mas nao removia o prefixo underscore, causando rejeicao do manifest.
Correcao: Adicionada verificacao para pular nomes que começam com underscore antes de adicionar a lista de slash commands.
5. NousResearch#17057 - custom Kimi-compatible endpoint falhava apos tool call com thinking habilitado
Arquivo:
run_agent.pyProblema: _needs_kimi_tool_reasoning() só verificava hostnames oficiais (api.kimi.com, moonshot.ai, moonshot.cn). Endpoints Kimi-compatíveis customizados nao eram detectados.
Correcao: Ampliada a verificacao para detectar endpoints customizados pela familia do modelo (nome contem "kimi" ou "k2") alem do hostname.
6. NousResearch#17049 - UnicodeDecodeError no scan de processos Windows (wmic)
Arquivo:
hermes_cli/gateway.pyProblema: wmic emitia saida em encoding local do Windows (cp1252/utf-16), causando UnicodeDecodeError e AttributeError durante parsing.
Correcao: O parsing agora usa errors=ignore no decode, tratando bytes invalidos como despreziveis.
7. NousResearch#17052 - stale reasoning reutilizado quando turn atual nao tem reasoning_content
Arquivo:
run_agent.pyProblema: Mensagens de assistant com tool_calls e reasoning_content eram reutilizadas indevidamente em turns que nao tinham reasoning_content, causando confabulacoes em provedores como Qwen3.6:27b via Ollama.
Correcao: O loop de replay agora detecta quando a mensagem atual e um assistant com tool_calls mas sem reasoning_content, e limpa msg[reasoning_content] e msg[reasoning] para evitar propagacao de estado de reasoning de turns anteriores.
Arquivos Modificados
agent/auxiliary_client.py- Bug: auxiliary tasks fail with HTTP 404 when using anthropic_messages custom endpoint — URL rewrite strips /anthropic/ path NousResearch/hermes-agent#17086, [Bug]: Native vision support for kimi-coding provider broken — error 404 on image analysis NousResearch/hermes-agent#17076hermes_cli/profiles.py-hermes profile create --clonecopies exclusive platform credentials causing multi-profile gateway conflicts NousResearch/hermes-agent#17080hermes_cli/commands.py- [Setup]: Slack manifest is broken NousResearch/hermes-agent#17054run_agent.py- Custom Kimi-compatible endpoint with api_mode=anthropic_messages fails after tool call when thinking is enabled NousResearch/hermes-agent#17057, Bug: stale reasoning reused when current turn has no reasoning_content NousResearch/hermes-agent#17052hermes_cli/gateway.py- [Bug]: UnicodeDecodeError and AttributeError in Windows gateway process scanning NousResearch/hermes-agent#17049Notas