sync: refresh fork main from upstream and preserve Opik lineage fix#4
Merged
Conversation
Selected rows in the model/session/skills pickers and approval/clarify prompts only changed from dim gray to cornsilk, which reads as low contrast on lighter themes and LCDs (reported during TUI v2 blitz). Switch the selected row to `inverse bold` with the brand accent color across modelPicker, sessionPicker, skillsHub, and prompts so the highlight is terminal-portable and unambiguous. Unselected rows stay dim. Also extends the sessionPicker middle meta column (which was always dim) to inherit the row's selection state.
…ter /model Reported during the TUI v2 blitz test: switching from openrouter to anthropic via `/model <name> --provider anthropic` appeared to succeed, but the next turn kept hitting openrouter — the provider the user was deliberately moving away from. Two gaps caused this: 1. `Agent.switch_model` reset `_fallback_activated` / `_fallback_index` but left `_fallback_chain` intact. The chain was seeded from `fallback_providers:` at agent init for the *original* primary, so when the new primary returned 401 (invalid/expired Anthropic key), `_try_activate_fallback()` picked the old provider back up without informing the user. Prune entries matching either the old primary (user is moving away) or the new primary (redundant) whenever the primary provider actually changes. 2. `_apply_model_switch` persisted `HERMES_MODEL` but never updated `HERMES_INFERENCE_PROVIDER`. Any ambient re-resolution of the runtime (credential pool refresh, compressor rebuild, aux clients) falls through to that env var in `resolve_requested_provider`, so it kept reporting the original provider even after an in-memory switch. Adds three regression tests: fallback-chain prune on primary change, no-op on same-provider model swap, and env-var sync on explicit switch.
Reported during TUI v2 blitz testing: typing `@folder:` in the composer pulled up .dockerignore, .env, .gitignore, and every other file in the cwd alongside the actual directories. The completion loop yielded every entry regardless of the explicit prefix and auto-rewrote each completion to @file: vs @folder: based on is_dir — defeating the user's choice. Also fixed a pre-existing adjacent bug: a bare `@file:` or `@folder:` (no path) used expanded=="." as both search_dir AND match_prefix, filtering the list to dotfiles only. When expanded is empty or ".", search in cwd with no prefix filter. - want_dir = prefix == "@folder:" drives an explicit is_dir filter - preserve the typed prefix in completion text instead of rewriting - three regression tests cover: folder-only, file-only, and the bare- prefix case where completions keep the `@folder:` prefix
A6 added a fixed-height grid (Array.from({length: VISIBLE})), but the
row <Text> itself had no wrap prop so Ink defaulted to wrap="wrap".
A sufficiently long model or provider name would wrap to a second
visual line and bounce the overall picker height right back — which
is exactly what reappeared during the TUI v2 blitz retest on /model.
Pin every picker row (and the empty-state / padding rows) to
wrap="truncate-end" so each slot is guaranteed one line. Applies
across modelPicker, sessionPicker, and skillsHub.
The completion popup (e.g. typing `/model`) grew from 8 rows at compIdx=0 up to 16 rows at compIdx≥8 — the slice end was `compIdx + 8` so every arrow-down added another rendered row until the window filled. Reported during TUI v2 retest: "as i scroll and more options appear, for some reason more options appear and it expands the height". Fixed viewport (`COMPLETION_WINDOW = 16`) centered on compIdx, clamped so it never slides past the array bounds. Renders exactly `min(WINDOW, completions.length)` rows every frame.
The pager overlay backing /history, /toolsets, /help and any paged slash output only advanced with Enter/Space and closed at the end. Could not scroll back, scroll line-by-line, or jump to endpoints. Adds Up/Down (↑↓, j/k), PgUp (b), g/G for top/bottom, keeps existing Enter/Space/PgDn forward-and-auto-close, and clamps offset so over-scrolling past the last page is a no-op.
interruptTurn only flushed the in-flight streaming chunk (bufRef) to the transcript before calling idle(), which wiped segmentMessages and pendingSegmentTools. Every tool call and commentary line the agent had already emitted in the current turn disappeared the moment the user cancelled, even though that output is exactly what they want to keep when they hit Ctrl+C (quote from the blitz feedback: "everything was fine up until the point where you wanted to push to main"). Append each flushed segment message to the transcript first, then render the in-flight partial with the `*[interrupted]*` marker and its pendingSegmentTools. Sys-level "interrupted" note still fires when there is nothing to preserve.
`/skills browse` is documented to scan 6 sources and take ~15s, but the gateway dispatched `skills.manage` on the main RPC thread. While it ran, every other inbound RPC — completions, new slash commands, even `approval.respond` — blocked until the HTTP fetches finished, making the whole TUI feel frozen. Reported during TUI v2 retest: "/skills browse blocks everything else". `_LONG_HANDLERS` already exists precisely for this pattern (slash.exec, shell.exec, session.resume, etc. run on `_pool`). Add `skills.manage` to that set so browse/search/install run off the dispatcher; the fast `list` / `inspect` actions pay a negligible thread-pool hop.
…fix curl - Description truncated to 60 chars in system prompt (extract_skill_description), so the 500-char HF workflow description never reached the agent; shortened to 'llama.cpp local GGUF inference + HF Hub model discovery.' (56 chars). - Restore llama-cpp-python section (basic, chat+stream, embeddings, Llama.from_pretrained) and frontmatter dependencies entry. - Fix broken 'Authorization: Bearer ***' curl line (missing closing quote; llama-server doesn't require auth by default).
…xt is absent (NousResearch#13676) The [Replying to: "..."] prefix is disambiguation, not deduplication. When a user explicitly replies to a prior message, the agent needs a pointer to which specific message they're referencing — even when the quoted text already exists somewhere in history. History can contain the same or similar text multiple times; without an explicit pointer the agent has to guess (or answer for both subjects), and the reply signal is silently dropped. Example: in a conversation comparing Japan and Italy, replying to the "Japan is great for culture..." message and asking "What's the best time to go?" — previously the found_in_history check suppressed the prefix because the quoted text was already in history, leaving the agent to guess which destination the user meant. Now the pointer is always present. Drops the found_in_history guard added in NousResearch#1594. Token overhead is minimal (snippet capped at 500 chars on the new user turn; cached prefix unaffected). Behavior becomes deterministic: reply sent ⇒ pointer present. Thanks to smartyi for flagging this.
Adds OpenAI's new GPT Image 2 model via FAL.ai, selectable through `hermes tools` → Image Generation. SOTA text rendering (including CJK) and world-aware photorealism. - FAL_MODELS entry with image_size_preset style - 4:3 presets on all aspect ratios — 16:9 (1024x576) falls below GPT-Image-2's 655,360 min-pixel floor and would be rejected - quality pinned to medium (same rule as gpt-image-1.5) for predictable Nous Portal billing - BYOK (openai_api_key) deliberately omitted from supports so all users stay on shared FAL billing - 6 new tests covering preset mapping, quality pinning, and supports-whitelist integrity - Docs table + aspect-ratio map updated Live-tested end-to-end: 39.9s cold request, clean 1024x768 PNG
delegation.default_toolsets was declared in cli.py's CLI_CONFIG default dict and documented in cli-config.yaml.example, but never read: none of tools/delegate_tool.py, _load_config(), or any call site ever looked it up. The live fallback is the DEFAULT_TOOLSETS module constant at tools/delegate_tool.py:101, which stays as-is. hermes_cli/config.py's DEFAULT_CONFIG["delegation"] already omits the key — this commit aligns cli.py with that. Adds a regression test in tests/hermes_cli/test_config_drift.py so a future refactor that re-adds the key without wiring it up to _load_config() fails loudly. Part of Initiative 2 / M0.5.
Matches the default-config removal in the preceding commit. default_toolsets was documented for users to set but was never actually read at runtime, so showing it in the example config and the delegation user guide was misleading. No deprecation note is added: the key was always a no-op, so users who copied it from the example continue to see no behavior change. Their config.yaml still parses; the key is just silently unused, same as before. Part of Initiative 2 / M0.5.
…config
The prior form of this test asserted on CLI_CONFIG["delegation"] after
importing cli, which only passed by accident of pytest-xdist worker
scheduling. cli._hermes_home is frozen at module import time (cli.py:76),
before the tests/conftest.py autouse HERMES_HOME-isolation fixture can
fire, so CLI_CONFIG ends up populated by deep-merging the contributor's
actual ~/.hermes/config.yaml over the defaults (cli.py:359-366). Any
contributor (like me) who still has the legacy key set in their own
config causes a false failure the moment another test file in the same
xdist worker imports cli at module level.
Asserting on the source of load_cli_config() instead sidesteps all of
that: the test now checks the defaults literal directly and is
independent of user config, HERMES_HOME, import order, and worker
scheduling.
Demonstrated failure mode before this fix:
pytest tests/hermes_cli/test_config_drift.py \
tests/hermes_cli/test_skills_hub.py -o addopts=""
-> FAILED (CLI_CONFIG["delegation"] contained "default_toolsets"
from the user's ~/.hermes/config.yaml)
Part of Initiative 2 / M0.5.
…13683) PDFs emitted by tools (report generators, document exporters, etc.) now deliver as native attachments when wrapped in MEDIA: — same as images, audio, and video. Bare .pdf paths are intentionally NOT added to extract_local_files(), so the agent can still reference PDFs in text without auto-sending them.
…-polish fix(tui): picker polish — stable height, inverse-bold selection, dropdown pinned
…unknown-subcommand fix(tui): delegate unknown /tools subcommand to slash.exec
…applies-path-completion fix(tui): apply path/@ completion on Enter
…-manage-async fix(tui): /skills browse no longer blocks the whole gateway
Models frequently emit bare codepoints like U+26A0 (⚠), U+2139 (ℹ), U+2764 (❤), U+2714 (✔), U+2600 (☀), U+263A (☺) which, per Unicode, have Emoji_Presentation=No and render as monochrome text-style glyphs in terminals unless followed by VS16 (U+FE0F). Agent output leaked through the TUI like `⚠ careful` instead of `⚠️ careful`. Added `ensureEmojiPresentation` (lib/emoji.ts): scans for the curated set of text-default codepoints and appends VS16 when the next char is not already VS16, ZWJ, or a keycap-enclosing mark. Idempotent and fast-pathed by a Unicode-range regex so ASCII-heavy text is untouched. Applied once at the top of `Md`'s line parse. Hermes-ink's stringWidth already accounts for VS16, so cursor/layout stays correct.
…vs16-injection fix(tui): inject VS16 so text-default emoji render as color glyphs
…scroll fix(tui): pager supports scrolling (up/down/page/top/bottom)
…lt flat) Adds role='leaf'|'orchestrator' to delegate_task. With max_spawn_depth>=2, an orchestrator child retains the 'delegation' toolset and can spawn its own workers; leaf children cannot delegate further (identical to today). Default posture is flat — max_spawn_depth=1 means a depth-0 parent's children land at the depth-1 floor and orchestrator role silently degrades to leaf. Users opt into nested delegation by raising max_spawn_depth to 2 or 3 in config.yaml. Also threads acp_command/acp_args through the main agent loop's delegate dispatch (previously silently dropped in the schema) via a new _dispatch_delegate_task helper, and adds a DelegateEvent enum with legacy-string back-compat for gateway/ACP/CLI progress consumers. Config (hermes_cli/config.py defaults): delegation.max_concurrent_children: 3 # floor-only, no upper cap delegation.max_spawn_depth: 1 # 1=flat (default), 2-3 unlock nested delegation.orchestrator_enabled: true # global kill switch Salvaged from @pefontana's PR NousResearch#11215. Overrides vs. the original PR: concurrency stays at 3 (PR bumped to 5 + cap 8 — we keep the floor only, no hard ceiling); max_spawn_depth defaults to 1 (PR defaulted to 2 which silently enabled one level of orchestration for every user). Co-authored-by: pefontana <fontana.pedro93@gmail.com>
…s goal/context (NousResearch#13698) The 'subagents know nothing' warning and the 'no conversation history' constraint both said the user provides the goal/context fields. In practice the LLM parent agent calls delegate_task; the user configures the feature but doesn't write delegation calls. Rewording to point at the parent agent matches how the tool actually works.
Two changes: 1. _PROVIDER_VISION_MODELS: add 'nous' -> 'xiaomi/mimo-v2-omni' entry so the vision auto-detect chain picks the correct multimodal model. 2. resolve_provider_client: detect when the requested model is a vision model (from _PROVIDER_VISION_MODELS or known vision model names) and pass vision=True to _try_nous(). Previously, _try_nous() was always called without vision=True in resolve_provider_client(), causing it to return the default text model (gemini-3-flash-preview or mimo-v2-pro) instead of the vision-capable mimo-v2-omni. The _try_nous() function already handled free-tier vision correctly, but the resolve_provider_client() path (used by the auto-detect vision chain) never signaled that a vision task was in progress. Verified: xiaomi/mimo-v2-omni returns HTTP 200 with image inputs on Nous inference API. google/gemini-3-flash-preview returns 404 with images.
…ead-local callback invisible to agent Two bugs that allow dangerous commands to execute without informed user consent. TUI (Ink): useInputHandlers consumes the isBlocked return path, but Ink's EventEmitter delivers keystrokes to ALL registered useInput listeners. The ApprovalPrompt component receives arrow keys, number keys, and Enter even though the overlay appears frozen. The user sees no visual feedback, but keystrokes are processed — allowing blind approval, session-wide auto-approve (choice "session"), or permanent allowlist writes (choice "always") without the user knowing. Discovered while replicating NousResearch#13618 (TUI approval overlay freezes terminal). Fix: in useInputHandlers, when overlay.approval/clarify/confirm is active, only intercept Ctrl+C. All other keys pass through. This makes the overlay visually responsive so the user can see what they are selecting. CLI (prompt_toolkit): _callback_tls in terminal_tool.py is threading.local(). set_approval_callback() is called in the main thread during run(), but the agent executes in a background thread. _get_approval_callback() returns None in the agent thread, falling back to stdin input() which prompt_toolkit blocks. The user sees the approval text but cannot respond — the terminal is unusable until the 60s timeout expires with a default "deny". Fix: set callbacks inside run_agent() (the thread target), matching the pattern already used by acp_adapter/server.py. Clear on thread exit to avoid stale references. Closes NousResearch#13618
Two unit tests that pin down the threading.local semantics the CLI freeze fix (NousResearch#13617 / NousResearch#13618) relies on: - main-thread registration must be invisible to child threads (documents the underlying bug — if this ever starts passing visible, ACP's GHSA-qg5c-hvr5-hjgr race has returned) - child-thread registration must be visible from that same thread AND cleared by the finally block (documents the fix pattern used by cli.py's run_agent closure and acp_adapter/server.py) Pairs with the fix in the preceding commit by @Societus.
When a user manually sets fallback_model as a YAML list instead of a
dict, save_config_value() crashes with:
AttributeError: 'list' object has no attribute 'get'
at the fb.get('provider') call on hermes_cli/config.py.
The fix adds isinstance(fb, dict) so list-format values are treated as
unconfigured — the fallback_model comment block is appended to guide
correct usage — instead of crashing.
Fixes NousResearch#4091
Co-authored-by: [AI-assisted — Claude Sonnet 4.6 via Milo/Hermes]
On Windows, Path.open() defaults to the system ANSI code page (cp1252). If the .env file contains UTF-8 characters, decoding fails with 'gbk codec can't decode byte 0x94'. Specify encoding='utf-8' explicitly to ensure consistent behavior across platforms.
Follow-up to the /resume and /branch cleanup in the previous commit: /new is a conversation-boundary operation too, so session-scoped dangerous-command approvals and /yolo state must not survive it. Adds a scoped unit test for _clear_session_boundary_security_state that also covers the /new path (which calls the same helper).
Consolidate 4 per-transport lazy singleton helpers (_get_anthropic_transport, _get_codex_transport, _get_chat_completions_transport, _get_bedrock_transport) into one generic _get_transport(api_mode) with a shared dict cache. Collapse the 65-line main normalize block (3 api_mode branches, each with its own SimpleNamespace shim) into 7 lines: one _get_transport() call + one _nr_to_assistant_message() shared shim. The shim extracts provider_data fields (codex_reasoning_items, reasoning_details, call_id, response_item_id) into the SimpleNamespace shape downstream code expects. Wire chat_completions and bedrock_converse normalize through their transports for the first time — these were previously falling into the raw response.choices[0].message else branch. Remove 8 dead codex adapter imports that have zero callers after PRs 1-6. Transport lifecycle improvements: - Eagerly warm transport cache at __init__ (surfaces import errors early) - Invalidate transport cache on api_mode change (switch_model, fallback activation, fallback restore, transport recovery) — prevents stale transport after mid-session provider switch run_agent.py: -32 net lines (11,988 -> 11,956). PR 7 of the provider transport refactor.
…odex_responses Follow-up for NousResearch#13862 — the post-init api_mode upgrade at __init__ (direct OpenAI / gpt-5-requires-responses path) runs AFTER the eager transport warm. Clear the cache so the stale chat_completions entry is evicted. Cosmetic: correctness was already fine since _get_transport() keys by current api_mode, but this avoids leaving unused cache state behind.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Port from openai/codex#18646. Adds two flags to 'hermes chat' that fully isolate a run from user-level configuration and rules: * --ignore-user-config: skip ~/.hermes/config.yaml and fall back to built-in defaults. Credentials in .env are still loaded so the agent can actually call a provider. * --ignore-rules: skip auto-injection of AGENTS.md, SOUL.md, .cursorrules, and persistent memory (maps to AIAgent(skip_context_files=True, skip_memory=True)). Primary use cases: - Reproducible CI runs that should not pick up developer-local config - Third-party integrations (e.g. Chronicle in Codex) that bring their own config and don't want user preferences leaking in - Bug-report reproduction without the reporter's personal overrides - Debugging: bisect 'was it my config?' vs 'real bug' in one command Both flags are registered on the parent parser AND the 'chat' subparser (with argparse.SUPPRESS on the subparser to avoid overwriting the parent value when the flag is placed before the subcommand, matching the existing --yolo/--worktree/--pass-session-id pattern). Env vars HERMES_IGNORE_USER_CONFIG=1 and HERMES_IGNORE_RULES=1 are set by cmd_chat BEFORE 'from cli import main' runs, which is critical because cli.py evaluates CLI_CONFIG = load_cli_config() at module import time. The cli.py / hermes_cli.config.load_cli_config() function checks the env var and skips ~/.hermes/config.yaml when set. Tests: 11 new tests in tests/hermes_cli/test_ignore_user_config_flags.py covering the env gate, constructor wiring, cmd_chat simulation, and argparse flag registration. All pass; existing hermes_cli + cli suites unaffected (3005 pass, 2 pre-existing unrelated failures).
Add epub, pdf, zip, rar, 7z, docx, xlsx, pptx, txt, csv, apk, ipa to the MEDIA: path regex in extract_media(). These file types were already routed to send_document() in the delivery loop (base.py:1705), but the extraction regex only matched media extensions (audio/video/image), causing document paths to fall through to the generic \S+ branch which could fail silently in some cases. This explicit list ensures reliable matching and delivery for all common document formats.
Adds an Exa-specific setup note next to the Parallel search-modes line documenting EXA_API_KEY, category filtering (company, research paper, news, people, personal site, pdf), and domain/date filters. Reapplied onto current main from @10ishq's PR NousResearch#6697 — the original branch was too far behind main to cherry-pick directly (touched 1,456 unrelated files from deleted/renamed paths). Co-authored-by: 10ishq <tanishq@exa.ai>
Filter out .DS_Store (Desktop Services Store)
Add discord.slash_commands config option (default: true) to allow
users to disable Discord slash command registration when running
alongside other bots that use the same command names.
When set to false in config.yaml:
discord:
slash_commands: false
The _register_slash_commands() call is skipped while text-based
parsing of /commands continues to work normally.
Fixes NousResearch#4881
NousResearch#14317) New built-in image_gen backend at plugins/image_gen/openai-codex/ that exposes the same gpt-image-2 low/medium/high tier catalog as the existing 'openai' plugin, but routes generation through the ChatGPT/ Codex Responses image_generation tool path. Available whenever the user has Codex OAuth signed in; no OPENAI_API_KEY required. The two plugins are independent — users select between them via 'hermes tools' → Image Generation, and image_gen.provider in config.yaml. The existing 'openai' (API-key) plugin is unchanged. Reuses _read_codex_access_token() and _codex_cloudflare_headers() from agent.auxiliary_client so token expiry / cred-pool / Cloudflare originator handling stays in one place. Inspired by NousResearch#14047 by @Hygaard, but re-implemented as a separate plugin instead of an in-place fork of the openai plugin. Closes NousResearch#11195
…26-04-22 # Conflicts: # tests/run_agent/test_run_agent.py
f5081fa to
b6303b8
Compare
🚨 CRITICAL Supply Chain Risk DetectedThis PR contains a pattern that has been used in real supply chain attacks. A maintainer must review the flagged code carefully before merging. 🚨 CRITICAL: Install-hook file added or modifiedThese files can execute code during package installation or interpreter startup. Files: Scanner only fires on high-signal indicators: .pth files, base64+exec/eval combos, subprocess with encoded commands, or install-hook files. Low-signal warnings were removed intentionally — if you're seeing this comment, the finding is worth inspecting. |
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
Why
PRs 1 and 2 are blocked because TFITZ57/main is 1329 commits behind upstream and lacks required base code.
Test