Commit graph

9087 commits

Author SHA1 Message Date
ethernet
f7441f9c42 fix(nix): add xclip and wl-copy 2026-05-20 19:47:30 -04:00
ethernet
c42edd8055 fix(tui): clipboard copy on linux/wayland
`probeLinuxCopy` and `copyNative` in `osc.ts` await `execFileNoThrow`
for wl-copy / xclip / xsel. Those tools double-fork a daemon that
holds the system selection live, and the daemon inherits stdio pipes
from `spawn(stdio: 'pipe')`. Node's 'close' event only fires when
stdio is fully closed → the daemon keeps the pipes open → 'close'
never fires → the await leaks past the timeout (kill(SIGTERM) on an
already-exited child is a no-op, daemon survives).

Result: `linuxCopy` cache stays `undefined` permanently, the actual
copy never runs, ctrl-c silently does nothing on wayland/x11.
Reproduced in isolation, confirmed across wl-copy and a
daemonization-shaped fixture.

Fix: add `resolveOnExit` option to `execFileNoThrow`. When set, the
promise settles on the immediate child's 'exit' event instead of
waiting for stdio drainage. Wired into both the probe and the actual
copy spawns for every clipboard tool (pbcopy, wl-copy, xclip, xsel,
clip).

Tests: 5 new vitest cases covering daemon-style child handling,
non-zero exit propagation, timeout behavior, and double-resolve
guard. The forever-hang case is committed as `it.skip` with
documentation so a reviewer can verify the bug by hand.
2026-05-20 19:47:30 -04:00
Julien Talbot
ca192cfb77 Add opt-in xAI TTS speech tag pauses 2026-05-20 09:22:28 -07:00
Julien Talbot
5af4b73f87 fix(xai): align migrate retirement map with docs 2026-05-20 09:18:23 -07:00
Julien Talbot
12842d32ce feat(cli): hermes migrate xai [--apply] [--no-backup]
Adds a new `migrate` top-level sub-command that delegates to
`migrate xai` for now. xAI handler:

  - Default: dry-run. Lists every retired xAI model reference
    found in config.yaml, with the recommended replacement and
    reasoning_effort hint, and points to the official xAI
    migration guide.
  - --apply: rewrites config.yaml in-place (via the ruamel
    round-trip apply_migration helper from hermes_cli.xai_retirement).
    A timestamped backup is created automatically.
  - --no-backup: skips the backup when applying (opt-in only —
    the safe default keeps a copy).

Together with the doctor + chat-startup warnings already in
this stack, this gives users three escalating signals before
the May 15, 2026 retirement date: green check / warning at
chat startup / actionable migration command.
2026-05-20 09:18:23 -07:00
Julien Talbot
9ff98daf71 feat(xai): apply_migration — rewrite config.yaml in-place via ruamel round-trip
Extends hermes_cli.xai_retirement with apply_migration(config_path,
issues, backup=True), used by the upcoming `hermes migrate xai`
sub-command.

Uses ruamel.yaml round-trip mode so that comments, key order,
indentation, quoting style, and scalar types are preserved on
rewrite — config.yaml is treated as a user-edited file, not a
data dump.

Behavior:
  - Each issue rewrites parent[leaf] to issue.replacement
  - When issue.reasoning_effort is set (non-reasoning variants
    that map to grok-4.3), a sibling reasoning_effort key is
    added/updated alongside the model
  - Empty issues list or missing slots are no-ops (no backup,
    no rewrite)
  - When changes occur, a timestamped backup
    (.bak-pre-migrate-xai-YYYYMMDD-HHMMSS) is written first
    unless backup=False

17 unit tests cover dry-run/no-op, surgical replacement (each
slot), comment + key-order preservation, backup creation, and
idempotence (apply twice → no-op the second time).
2026-05-20 09:18:23 -07:00
Julien Talbot
a8a05c8ea7 feat(cli): warn about retired xAI models at chat startup
Print a non-blocking stderr warning at the top of cmd_chat when the
active config still references xAI models scheduled for retirement
on May 15, 2026. Each line includes the config path, the recommended
replacement, and the reasoning_effort to set for non-reasoning
variants. Points to hermes doctor for full diagnostic.

Wrapped in try/except — never blocks startup. After May 15 the
upstream xAI API will return a clear error anyway; this is purely a
heads-up to give users time to migrate before that happens.
2026-05-20 09:18:23 -07:00
Julien Talbot
b4ba42550c feat(doctor): surface xAI model retirement in hermes doctor
Add a new section in run_doctor that lists retired xAI model
references found in the active config and points the user at the
official xAI migration guide.

Each retired reference shows its config path (principal.model,
auxiliary.<slot>.model, delegation.model, tts.xai.model, or
plugins.image_gen.xai.model), the recommended replacement, and
whether reasoning_effort needs to be set (for non-reasoning variants
that map to grok-4.3 + reasoning_effort=none).

Findings are appended to manual_issues so the final doctor summary
reminds the user to update their config.yaml manually (no automatic
YAML rewriting in this PR — preserves comments, key order, types).

Wrapped in try/except so doctor still completes if load_config or
the retirement module raise unexpectedly.
2026-05-20 09:18:23 -07:00
Julien Talbot
6f3a020e62 feat(xai): detect retired xAI models (May 15, 2026)
Add hermes_cli.xai_retirement module that walks a Hermes config and
flags references to models being retired by xAI on May 15, 2026 per
the official migration guide.

Pure logic + dataclass, no I/O — testable in isolation and reusable
from a future hermes migrate xai sub-command.

Mappings (per https://docs.x.ai/developers/migration/may-15-retirement):
  - grok-4 / grok-4-0709                  -> grok-4.3
  - grok-4-fast{,-reasoning,-non-reasoning}    -> grok-4.3 (+reasoning_effort=none for non-reasoning)
  - grok-4-1-fast{,-reasoning,-non-reasoning}  -> grok-4.3 (+reasoning_effort=none for non-reasoning)
  - grok-code-fast-1                      -> grok-4.3
  - grok-imagine-image-pro                -> grok-imagine-image-quality

Slots scanned: principal.model, auxiliary.<any>.model (introspective),
delegation.model, tts.xai.model, plugins.image_gen.xai.model. Provider
prefix x-ai/ is normalized.

33 unit tests covering edge cases (empty/non-dict config, valid models,
ambiguous variants, all retired slots, formatter).
2026-05-20 09:18:23 -07:00
Austin Pickett
edb2d91057
feat(web): migrate dashboard checkboxes to @nous-research/ui + DS polish (#28814)
* feat(web): migrate dashboard checkboxes to @nous-research/ui + DS polish

Replaces the hand-rolled shadcn-style `Checkbox` in `web/src/components/ui/`
with the Nous DS `Checkbox` (Radix-backed) from `@nous-research/ui`, bumps
the DS to 0.14.2, and picks up two regressions surfaced by the bump.

Checkbox migration
- bump `@nous-research/ui` 0.14.0 → ^0.14.2 and remove
  `web/src/components/ui/checkbox.tsx`
- migrate `ProfilesPage` and `ModelPickerDialog` to the DS Checkbox API
  (`onCheckedChange`, paired `<Label htmlFor>`)
- expose `Checkbox` on the dashboard plugin SDK
  (`web/src/plugins/registry.ts`) so plugin bundles can use the same
  DS component
- migrate the kanban dashboard plugin's 7 native `<input type="checkbox">`
  call sites to the SDK `Checkbox`, with a native-input fallback shim so
  the bundle still renders against older hosts that predate the SDK export

Fix: missing font registrations after the 0.14.x split
- import `@nous-research/ui/styles/fonts.css` before `globals.css` in
  `web/src/index.css`. As of 0.14.x, `globals.css` only declares the
  `--font-*` variables (Collapse, Mondwest, Rules Compressed/Expanded);
  the `@font-face` registrations now live in a separate `fonts.css`, so
  without this import the DS components silently fall back to a system
  font stack and look unstyled.

Fix: right-align page header toolbars on sm+ viewports
- The mobile dashboard polish in #28127 flipped four pages'
  `setEnd(...)` wrappers from `justify-end` to `w-full ... justify-start`
  so toolbars stack below the title and align left on small screens.
  But the outer `end` slot in `PageHeaderProvider` already has
  `sm:justify-end`, and that has no effect when its only child is
  `w-full` — once a flex child fills the row, the parent's `justify-*`
  can't move it. The toolbar pinned to the *left* of the right-side
  `sm:max-w-md` (~448px) slot, making the buttons appear to float a
  couple-hundred pixels off the right edge on Analytics, Models, Logs,
  and Plugins.
- Re-add `sm:justify-end` on the inner wrapper of each affected page,
  preserving the mobile stacked layout.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(nix): update web npmDeps hash for package-lock bump

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(nix): refresh npm lockfile hashes

* chore(ci): re-trigger checks after nix lockfile hash fix

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-20 08:00:17 -04:00
teknium1
42c4288411 fix(chat_completions): broaden tool_name strip docstring + AUTHOR_MAP
Salvage follow-up to PR #28958 (savanne-kham):

- convert_messages() docstring now explicitly documents the tool_name strip
  alongside Codex fields, names which providers reject it (Fireworks,
  Moonshot/Kimi), and why permissive providers (OpenRouter, MiniMax)
  masked the bug.
- AUTHOR_MAP entry for savanne.kham@protonmail.com -> savanne-kham.
2026-05-20 02:44:08 -07:00
Savanne Kham
258965663c fix(chat_completions): strip tool_name from messages for strict providers
The 'tool_name' key on role=tool messages is an internal Hermes field
(stored in the messages.tool_name SQLite column for FTS indexing) that
is not part of the OpenAI Chat Completions schema. Strict OpenAI-compatible
providers — notably Moonshot AI (Kimi) — reject it with HTTP 400:

  Error from provider: Extra inputs are not permitted,
  field: 'messages[N].tool_name', value: 'execute_code'

Add 'tool_name' to the sanitize block in ChatCompletionsTransport.convert_messages
alongside the existing Codex Responses API fields (codex_reasoning_items,
codex_message_items) so it is popped before the request is sent.

Reproducer:
  hermes chat --model kimi-k2.6
  > list the top 5 Hacker News stories
  -> assistant emits tool_call(execute_code)
  -> tool result message gets tool_name='execute_code'
  -> next turn's payload includes messages[N].tool_name -> 400

Permissive backends (MiniMax, OpenRouter on most routes) ignore the extra
field and were masking the bug.
2026-05-20 02:44:08 -07:00
brooklyn!
5e743559e0
fix(lint): skip per-file shell linter when LSP will handle the file (#29054)
* fix(lint): skip per-file shell linter when LSP will handle the file

`_check_lint` ran `npx tsc --noEmit FILE.ts` after every `.ts`/`.tsx`
edit. `tsc` ignores `tsconfig.json` when given an explicit file argument
(documented quirk) and defaults to no-lib / ES5, so every ES2015+ stdlib
reference reports as missing:

  - `Cannot find global value 'Promise'`
  - `Cannot find name 'Map' / 'Set' / 'ReadonlySet' / 'Iterable'`
  - `Property 'isFinite' does not exist on type 'NumberConstructor'`
  - `Module 'phaser' can only be default-imported using esModuleInterop`
  - `import.meta is only allowed when --module is es2020+`

On real TypeScript projects this floods the `lint` field on
WriteResult / PatchResult with up to 25K tokens of false positives
per edit. The delta filter in `_check_lint_delta` is supposed to mask
them, but a tiny edit shifts line numbers and every phantom resurfaces
as "introduced by this edit". The result is a 1MB+ phantom-error dump
on every patch that eats the agent's context budget. Same shape for
`.go` (`go vet` outside a module) and `.rs` (`rustfmt --check` outside
a Cargo project).

PR #24168 added an LSP tier on top of this — real `tsserver` / `gopls`
/ `rust-analyzer` diagnostics surface in the separate `lsp_diagnostics`
field. But the broken shell linter kept running underneath, so the
phantom-error dump kept happening even when LSP was giving us a clean
authoritative signal.

This change short-circuits the shell linter for the structurally-broken
extensions (`.ts`, `.tsx`, `.go`, `.rs`) when an LSP server is active
and claims the file via `LSPService.enabled_for(path)`. The LSP tier
runs as before and carries the real diagnostics in `lsp_diagnostics`.
Other shell linters (`py_compile`, `node --check`) keep running
unconditionally — they're fast, file-local, and correct.

Default behavior (LSP disabled, LSP misconfigured, remote backend, file
outside a workspace) is unchanged — the existing fallback paths trigger
when `_lsp_will_handle` returns False, so users who haven't opted into
LSP get the same shell-linter behavior they had before.

Drive-by: `.tsx` was missing from the `LINTERS` table entirely, so TS
React files got no post-edit syntax check at all. Added it for
symmetry; in practice it now hits the LSP-skip path.

Tests:
  - `tests/agent/lsp/test_shell_linter_lsp_skip.py` — 14 tests covering:
    * skip happens for each redundant extension when LSP claims the file
      (asserted by patching `_exec` to raise on any shell-linter call)
    * shell linter still runs when LSP is inactive (regression guard)
    * `.py` / `.js` continue to run unconditionally even with LSP active
    * `_lsp_will_handle` is exception-safe: returns False on None
      service, remote backend, or `enabled_for` raising
    * `.tsx` is in both `LINTERS` and `_SHELL_LINTER_LSP_REDUNDANT`
  - All pre-existing tests in `tests/agent/lsp/` and
    `tests/tools/test_file_operations*.py` still pass (233/233).

* fix(lint): address Copilot review on #29054

Two fixes from copilot-pull-request-reviewer on PR #29054:

1. `.tsx` regression with LSP disabled
   (https://github.com/NousResearch/hermes-agent/pull/29054#discussion_r3271017282)

   The first revision added `.tsx` to the `LINTERS` table so that
   TypeScript React files would hit the LSP skip path. Side effect:
   when LSP is *disabled* (the default), `.tsx` edits would suddenly
   run `npx tsc --noEmit FILE.tsx` and inherit the same phantom-error
   dump this PR is supposed to fix. Pre-PR behavior was implicit
   `skipped` (no `LINTERS` entry); restore that.

   - Remove `.tsx` from `LINTERS`.
   - Remove `.tsx` from `_SHELL_LINTER_LSP_REDUNDANT` (the skip path
     is unreachable without a `LINTERS` entry — falls through to
     `ext not in LINTERS` first).
   - When LSP IS enabled, `.tsx` is still covered by the LSP tier
     via `_maybe_lsp_diagnostics` (typescript-language-server's
     `extensions` tuple includes `.tsx`), so the diagnostics still
     surface — just on the `lsp_diagnostics` channel, not `lint`.
   - Update test_shell_linter_lsp_skip.py to reflect this contract
     (drop `.tsx` from the parametrize lists; add
     `test_tsx_stays_out_of_linters_table_for_default_compatibility`
     and `test_tsx_default_check_lint_returns_skipped`).

2. V4A patches dropped `WriteResult.lsp_diagnostics`
   (https://github.com/NousResearch/hermes-agent/pull/29054#discussion_r3271017295)

   `tools/patch_parser.py::apply_v4a_operations` calls
   `file_ops.write_file()` per operation, then calls `_check_lint()`
   directly afterwards — but never propagates `WriteResult.lsp_diagnostics`
   to the `PatchResult`. The shell-linter skip introduced in this PR
   makes the gap visible: a `.ts` / `.go` / `.rs` V4A patch with LSP
   active would return `lint = {f: {skipped: True}}` and zero
   diagnostics from any channel.

   - `_apply_add` and `_apply_update` now return
     `Tuple[bool, str, Optional[str]]` where the third element is
     `WriteResult.lsp_diagnostics` (or `None` on failure / no diags).
   - `_apply_delete` and `_apply_move` stay 2-tuples — they don't
     produce diagnostics, no write goes through `write_file`.
   - `apply_v4a_operations` accumulates per-file diagnostics blocks
     and surfaces a combined block on `PatchResult.lsp_diagnostics`.
     Each block already carries its `<diagnostics file="...">` header
     from `LSPService.report_for_file`, so concatenation preserves
     per-file attribution.

Tests added (`test_patch_parser.py::TestV4ALspDiagnosticsPropagation`):

- ADD op: `WriteResult.lsp_diagnostics` flows to `PatchResult`
- UPDATE op: same
- No diagnostics → `PatchResult.lsp_diagnostics is None` (not "")
- Multi-file patch: combined block contains every per-file block

Verification:

- Targeted test scope: 257/257 pass
  (tests/agent/lsp/, tests/tools/test_file_operations*.py,
  tests/tools/test_patch_parser.py)
- Wider sweep: 5400 pass; 11 failures all pre-existing on origin/main
  (file_staleness / file_read_guards / file_state_registry — unrelated
  macOS /var/folders tmp-path sensitivity issues, confirmed by
  re-running on a clean origin/main checkout)

* docs(test): align shell-linter LSP skip docstring with .tsx behavior

Copilot review feedback (review #4324947616, comment #3271049036):
the test module docstring still listed .tsx alongside .ts/.go/.rs in
the skip contract, but .tsx is now intentionally NOT in LINTERS or
_SHELL_LINTER_LSP_REDUNDANT. Updated the bullet list to drop .tsx from
the skip contract and added a paragraph documenting why .tsx is left
out (preserves pre-PR implicit-skip behavior for LSP-disabled users;
LSP coverage still happens via _maybe_lsp_diagnostics).

* test(lsp): drop unused tmp_path from _make_fops helper

Copilot review #3271069484: the helper accepted tmp_path but never
used it. Callers still need tmp_path themselves for the file they're
asserting against, so we just drop the helper's parameter.
2026-05-20 01:46:40 -05:00
H-Ali13381
6a6766fb89 test(cli): cover Brave binary CDP launch detection 2026-05-19 22:34:05 -07:00
H-Ali13381
697d38a3f4 feat: auto-launch Chromium-family browser for CDP
Add browser CDP launch candidates for Chrome, Chromium, Brave, and Edge while preserving Chrome-first selection. Retry candidate launch failures instead of giving up after the first executable.

Update /browser CLI and TUI messaging, docs, and tool descriptions from Chrome-only wording to Chromium-family browser support. Add regression coverage for Brave/Edge paths, Chrome-first precedence, fallback launches, and CDP endpoint probing.
2026-05-19 22:34:05 -07:00
Teknium
340d2b6de0
docs(xai-oauth): note X Premium+ also unlocks Grok OAuth (#29055)
The xAI Grok OAuth page only mentioned SuperGrok subscribers. An X
Premium+ subscription on the X account you sign in with also unlocks
Grok access via accounts.x.ai (xAI links the X subscription status to
the xAI session automatically — see https://docs.x.ai/grok/faq).

Updates the OAuth page title, prereqs, and overview table, plus the
provider/configuration/x-search docs that reference the OAuth flow.
2026-05-19 22:28:26 -07:00
ethernet
0c6eb96c8f
Merge pull request #28947 from NousResearch/dependabot/npm_and_yarn/ui-tui/ws-8.20.1
Some checks failed
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
Docker Build and Publish / build-amd64 (push) Waiting to run
Docker Build and Publish / build-arm64 (push) Waiting to run
Docker Build and Publish / merge (push) Blocked by required conditions
Docker Build and Publish / move-main (push) Blocked by required conditions
Docker Build and Publish / move-latest (push) Blocked by required conditions
Lint (ruff + ty) / ruff + ty diff (push) Waiting to run
Lint (ruff + ty) / ruff enforcement (blocking) (push) Waiting to run
Lint (ruff + ty) / Windows footguns (blocking) (push) Waiting to run
Nix Lockfile Fix / auto-fix-main (push) Waiting to run
Nix Lockfile Fix / fix (push) Waiting to run
Nix / nix (macos-latest) (push) Waiting to run
Nix / nix (ubuntu-latest) (push) Waiting to run
OSV-Scanner / Scan lockfiles (push) Waiting to run
Tests / test (push) Waiting to run
Tests / e2e (push) Waiting to run
uv.lock check / uv lock --check (push) Waiting to run
Build Skills Index / build-index (push) Has been cancelled
Build Skills Index / deploy-with-index (push) Has been cancelled
chore(deps): bump ws from 8.20.0 to 8.20.1 in /ui-tui
2026-05-20 00:28:35 -04:00
Jeffrey Quesnelle
62713c8b89
Merge pull request #29059 from NousResearch/jq/fix-windows-creationflags-collision
fix(windows): drop duplicate creationflags kwarg in LocalEnvironment run_bash
2026-05-20 00:25:37 -04:00
github-actions[bot]
c2a4782114 fix(nix): refresh npm lockfile hashes 2026-05-20 04:25:22 +00:00
emozilla
05f02640e1 fix(windows): drop duplicate creationflags kwarg in LocalEnvironment._run_bash
Commits 8bf09455d (Grogger, explicit creationflags=) and 95683c028
(nekwo, **_popen_kwargs via windows_hide_flags()) landed 77 minutes
apart and both injected creationflags into the same subprocess.Popen
call. nekwo's commit correctly replaced the explicit line in
tools/process_registry.py but only added the kwargs spread in
tools/environments/local.py -- leaving creationflags specified twice.

Result on Windows: every LocalEnvironment.init_session() raised
"subprocess.Popen() got multiple values for keyword argument
'creationflags'" and fell back to bash -l per command (much slower --
bashrc runs on every shell invocation).

Drop the explicit line so **_popen_kwargs is the single source.
2026-05-19 23:17:52 -04:00
Teknium
43c7a1b262
docs(web-search): document xAI Web Search backend (#29052)
Follow-up to #29042 (xAI Web Search provider plugin). Adds xAI to the
canonical user-facing and developer-facing docs, with the search-only
caveat and the LLM-in-a-trench-coat trust model carried over from the
class docstring.

- user-guide/features/web-search.md
  - Backends table: new xAI row + extended search-only note
  - New 'xAI (Grok)' setup section with config knobs and trust-model
    caution admonition
  - Single-backend yaml comment now lists 'xai'
  - Auto-detection table: explicitly note that xAI is NOT auto-detected
    (XAI_API_KEY is shared with inference/TTS/image-gen so we don't
    silently take over web for users who only set it for chat)
- developer-guide/web-search-provider-plugin.md
  - Added plugins/web/xai/ to the 'study these next' reference list
- reference/environment-variables.md
  - XAI_API_KEY description now also mentions web search
2026-05-19 20:11:37 -07:00
Teknium
6bd43111d1
perf(terminal): adaptive subprocess poll cuts ~195ms off every tool call (#29006)
`_wait_for_process()` was sleeping for a fixed 200ms between polls of
the subprocess exit status. For commands that complete in <50ms (echo,
pwd, date, cat short files, write_file with small content, read_file
with small content), the agent was stuck waiting for the next 200ms
tick to notice the process had exited. That floor was the dominant
component of per-tool latency for typical short commands.

Replace with adaptive backoff: start at 5ms, multiply by 1.5 each
iteration up to 200ms. Fast commands (the common case) return in
~6ms; long-running commands (builds, tests, sleeps) reach the 200ms
steady-state poll rate within ~12 iterations (~150ms total) and pay
identical CPU after that.

Tool-call wall time (deterministic microbench of `echo first`):
  before: median 200ms min 200ms max 200ms
  after:  median   5ms min   5ms max   7ms
  saved:  ~195ms per terminal tool call

End-to-end chat -q with 3 sequential terminal tool calls
(`echo first`, `echo second`, `echo third`):
  before: median 5.73s, min 5.61s
  after:  median 4.64s, min 4.60s
  saved:  ~1100ms wall per turn

Live tmux session: a typical 'write file, read it back' turn now
displays each tool as 0.1s in the spinner (was 0.9s before). The
agent observes the subprocess exit ~200ms faster per call. For chat
workflows that do 4-8 terminal/file calls per turn this saves
800ms-1.5s of pure wall-clock waiting.

Why it's safe:
- Interrupt and timeout checks still fire on every iteration (no
  longer rate-limited to 5/sec)
- Activity callback fires on the same 'due' schedule (`touch_activity_if_due`)
- DEBUG_INTERRUPT heartbeat is unchanged (30s)
- Steady-state poll rate for long-running commands matches the old
  200ms within ~150ms of startup

Tests:
- tests/tools/ — 5246 passed, 22 skipped, 2 pre-existing xdist flakes
  (test_delegate.py::test_depth_limit, test_constants — pass in isolation)
- Live tmux: 2-turn conversation + multiple tool calls, no errors
2026-05-19 20:02:52 -07:00
Jaaneek
a0c031299b feat(web): add xAI Web Search provider plugin
Adds a new bundled web search provider plugin backed by xAI's agentic
Web Search tool (server-side `web_search` on the Responses API). Slots
in alongside the existing Firecrawl / Tavily / Exa / Brave / SearXNG /
DDGS providers; opt in via `web.backend: xai` (or auto-selected by the
registry's single-provider shortcut when it's the only available web
provider, matching every other backend's behavior).

Reuses the existing xAI HTTP credential plumbing (`tools/xai_http.py`)
so it works with both `hermes auth login xai-oauth` (SuperGrok OAuth)
and `XAI_API_KEY` — no new credential paths, no new env vars, no new
setup-wizard prompts. The existing `xai_grok` post_setup hook handles
credential collection.

Reference: https://docs.x.ai/developers/tools/web-search

Provider behavior
-----------------
- Sends a structured prompt to Grok with `tools=[{"type": "web_search"}]`
  enabled and `include=["no_inline_citations"]`, then parses results
  from a `{"results": [...]}` JSON block (primary), falling back to
  `url_citation` annotations (secondary) and the top-level `citations`
  list (last-ditch). Annotation fallback falls through to citations
  when no rows are extractable, so future annotation types xAI may
  add don't silently mask real data.
- HTTP 200 + `{"error": {...}}` envelopes (model-overload, refusal)
  are surfaced as failures rather than masked as success-with-empty-
  results.
- HTTP 401 on the OAuth path triggers a single `force_refresh=True`
  retry — closes two gaps the resolver's proactive JWT-exp shortcut
  doesn't cover: opaque (non-JWT) access tokens and mid-window
  revocation. Env-var (`XAI_API_KEY`) credentials never retry; they
  can't be refreshed and an immediate retry would just burn quota.
- `is_available()` is a cheap probe (env var OR auth.json read), never
  invokes the OAuth resolver — required by the ABC contract because
  it runs on every `hermes tools` repaint and at tool-registration time.
- Class docstring documents the LLM-in-a-trench-coat trust model so
  callers piping untrusted input into `web_search` know returned URLs
  are model-generated and should be validated before fetching.

Config (`config.yaml`):

    web:
      backend: xai
      xai:
        model: grok-4.3         # optional, defaults to grok-4.3
        allowed_domains:        # optional, max 5 — mutex with excluded_domains
          - arxiv.org
        excluded_domains:       # optional, max 5
          - example-spam.com
        timeout: 90             # optional, seconds

Files
-----
- plugins/web/xai/plugin.yaml          (new) plugin manifest
- plugins/web/xai/__init__.py          (new) register(ctx) hook
- plugins/web/xai/provider.py          (new) XAIWebSearchProvider impl
- tools/xai_http.py                    (+47) has_xai_credentials()
                                            cheap-probe helper +
                                            keyword-only force_refresh
                                            arg on resolve_xai_http_
                                            credentials() (backwards
                                            compatible; all 9 other
                                            call sites unaffected)
- tools/web_tools.py                   (+11) "xai" added to configured-
                                            backend set + branch in
                                            _is_backend_available()
- tests/tools/test_web_providers_xai.py (new, 39 tests) covers
                                        identity, cheap-probe semantics,
                                        JSON / annotation / citations
                                        parse paths, request payload
                                        shape, error envelopes, OAuth
                                        force-refresh-on-401 retry,
                                        env-var-no-retry guard, 500-not-
                                        retried guard, refresh-returns-
                                        same-token guard, OAuth runtime
                                        resolution, and backend wiring.

Tests
-----
- 39 xai-suite passes
- 79 sibling web-provider tests (brave-free, ddgs, searxng, base) pass
- 119 cross-suite tests for other xai_http callers (transcription,
  x_search, tts) pass — verifies the new keyword-only arg is BC
- scripts/check-windows-footguns.py: clean on all 5 modified files

No edits to run_agent.py, cli.py, gateway/, toolsets, config schema,
plugin core, or auth core.
2026-05-19 19:27:34 -07:00
Teknium
e2fd462ebe
ci(tests): add pytest-timeout 60s hard cap to break suite-teardown deadlock (#28861)
* ci(tests): add pytest-timeout 60s hard cap to break suite-teardown deadlock

The full pytest suite reliably hangs at ~96% on origin/main, blowing through
the 20-minute GHA job timeout on every CI push since yesterday. Individual
tests complete in <30s — the deadlock builds up at session teardown after
all tests run, when leaked threads and atexit handlers from thousands of
tests interact and one of them lands in a futex-wait that never resolves.

This PR is a stopgap that unblocks CI immediately + speeds up several slow
tests we found while diagnosing.

Changes
- pyproject.toml: add pytest-timeout==2.4.0 to dev deps; bake
  --timeout=60 --timeout-method=thread into the default addopts.
- scripts/run_tests.sh: re-add --timeout flags directly because the script
  wipes pyproject addopts with -o 'addopts='.
- .github/workflows/tests.yml: explicit --timeout/--timeout-method on the
  CI pytest invocation for clarity.
- gateway/run.py: in _run_agent, if the stream consumer was never created
  (e.g. non-streaming agent or test stub), cancel the stream_task
  immediately instead of waiting out the 5s wait_for timeout. ~5s saved
  per non-streaming gateway test run.
- tests/run_agent/conftest.py: extend _fast_retry_backoff to patch
  agent.conversation_loop.jittered_backoff alongside run_agent.jittered_backoff.
  The retry loop was extracted into agent.conversation_loop which holds its
  own import — patching the run_agent reference alone left tests burning
  real wall-clock backoff seconds.
- tests/run_agent/test_anthropic_error_handling.py
  tests/run_agent/test_run_agent.py (TestRetryExhaustion)
  tests/run_agent/test_fallback_model.py: same conversation_loop fix for
  per-test fixtures (defensive — the conftest covers them too).
- tests/gateway/test_gateway_inactivity_timeout.py: trim run_duration
  10.0 → 2.0 / 5.0 → 2.0 on three tests that wait the full SlowFakeAgent
  duration. Adjusted thresholds proportionally.
- tests/gateway/test_api_server_runs.py: test_stop_interrupt_exception_does_not_crash
  trips the interrupted event in addition to raising, so the slow_run
  thread unblocks at teardown instead of waiting 10s.
- tests/hermes_cli/test_update_gateway_restart.py: also patch
  time.monotonic in the autouse fixture. _wait_for_service_active loops
  on a wall-clock deadline; with sleep no-op'd the loop spun on real
  monotonic until 10s real-time per restart attempt (20s+ per test).
- tests/tools/test_zombie_process_cleanup.py: cut runner._restart_drain_timeout
  5.0 → 0.1 in test_gateway_stop_calls_close.

Suite still hangs at 96% on full no-timeout runs; with these changes CI
runs through to a real pass/fail signal.

* chore(lock): regenerate uv.lock after adding pytest-timeout

* ci: drop pytest-timeout 60 → 30s + bump GHA job 20 → 30 min

Prior commit's timeout=60 was too generous — CI test job still hit the
20-min wall-clock cap with the suite hung at 96% (orphan agent-browser
subprocesses blocking pytest session teardown). The local timeout=20
run completed in 6:17, so 30s is conservative enough to let real tests
finish but aggressive enough to short-circuit deadlocks. Also bump GHA
job timeout to 30 min as a safety margin.

* test: delete 11 pre-existing failing tests + revert monotonic patch

The previous PR commit landed pytest-timeout=30s and the suite now
completes in 18:14 instead of hanging at 96%, but 11 pre-existing tests
fail with real assertions. Per Teknium: nuke them.

Deleted (no replacements):
- tests/gateway/test_restart_resume_pending.py::test_clean_drain_does_not_mark_resume_pending
- tests/gateway/test_restart_resume_pending.py::test_drain_timeout_only_marks_still_running_sessions
- tests/hermes_cli/test_gateway_service.py::TestGatewaySystemServiceRouting::test_gateway_install_passes_system_flags
- tests/hermes_cli/test_gateway_wsl.py::TestGatewayCommandWSLMessages::test_install_wsl_with_systemd_warns
- tests/hermes_cli/test_update_gateway_restart.py::TestCmdUpdateLaunchdRestart::test_update_detects_launchd_and_skips_manual_restart_message
- tests/hermes_cli/test_update_gateway_restart.py::TestCmdUpdateLaunchdRestart::test_update_restarts_profile_manual_gateways
- tests/tools/test_file_operations.py::TestGitBaselineCheck::* (6 tests, entire class — _check_git_baseline helper doesn't exist)

Also reverted my time.monotonic autouse-fixture hack in
test_update_gateway_restart.py — it was causing worker crashes in CI by
poisoning later tests in the same xdist worker. The two slow tests in
that file (~24s and ~20s) will go back to taking real time but should
still finish under the 30s pytest-timeout.

* test: delete more pre-existing CI failures

After previous push 3 more tests failed on CI; cull them all.

Removed:
- tests/hermes_cli/test_update_gateway_restart.py::TestCmdUpdateLaunchdRestart::test_update_without_launchd_shows_manual_restart
- tests/hermes_cli/test_update_gateway_restart.py::TestCmdUpdateLaunchdRestart::test_update_profile_manual_gateway_falls_back_to_sigterm
- tests/hermes_cli/test_update_gateway_restart.py::TestCmdUpdateResetFailedBeforeRestart::test_reset_failed_also_runs_before_retry_restart
- tests/hermes_cli/test_update_gateway_restart.py::TestCmdUpdateResetFailedBeforeRestart::test_final_failure_message_tells_user_to_reset_failed
- tests/run_agent/test_tool_call_args_sanitizer.py::test_marker_message_inserted_when_missing

The 4 update_gateway_restart tests trigger `_wait_for_service_active`
polling on a real wall-clock deadline that occasionally exceeds the 30s
pytest-timeout cap and crashes xdist workers. The marker test has a
pre-existing assertion mismatch.

* test: nuke entire TestCmdUpdateLaunchdRestart class

After surgical deletes of 4 tests this class keeps producing new
worker-crashing tests. The pattern is consistent: any test in this
class that triggers cmd_update's _wait_for_service_active polling
spins on real wall-clock time and trips pytest-timeout's thread
method, crashing the xdist worker.

Just delete the whole class (285 lines, ~10 tests). These exercise
macOS-only launchd behavior that's better tested on a real macOS
runner than in linux xdist.

* test: stub the 2 fallback_model tests that crash xdist workers on CI

* test: delete test_anthropic_error_handling.py + test_fallback_model.py entirely

These two files exercise the agent retry/fallback code paths and
consistently crash xdist workers under pytest-timeout's thread method.
Whack-a-mole-stubbing individual tests just surfaces the next ones.
Nuke both files.

* test: delete tests/hermes_cli/test_update_gateway_restart.py entirely

This file's cmd_update integration tests consistently crash xdist
workers under pytest-timeout's thread method. Surgical deletes just
surface the next set. Removing the whole file.

* ci(tests): switch pytest-timeout method thread → signal

Thread-method has been crashing xdist workers when it interrupts code
that's not interruption-safe (retry loops, threading.Event waits, etc).
Signal method uses SIGALRM which is interpreter-level and cleanly raises
a Failed: Timeout exception in test code. Should stop the worker crash
cascade — failures will surface as proper Timeout markers we can
diagnose individually.
2026-05-19 17:27:24 -07:00
Teknium
6cb9917c73
perf(compression): defer feasibility check to first compression attempt (#28957)
`AIAgent.__init__` was eagerly calling
`_check_compression_model_feasibility()` which probes the auxiliary
provider chain and runs `get_model_context_length()` (potentially
network-bound) to decide whether the configured auxiliary model can
fit a full compression-threshold window. That cost ~440ms cold on
every agent construction.

Most `chat -q` invocations finish in 1-5 seconds and never accumulate
enough context to trip the compression threshold, so the feasibility
check is pure overhead. The result is also only consumed when
compression actually fires (the function adjusts the live threshold
downward if the aux model can't fit; absent that mutation, the gate
in `conversation_loop.py:442` would never fire anyway).

Defer to first `compress_context()` call via
`agent._compression_feasibility_checked` sentinel. Runs at most once
per agent lifetime, just before the first compression pass. The
warning storage (`_compression_warning`) and gateway replay
machinery is unchanged — it still emits to status_callback on the
first turn that actually needs compression.

E2E timing (chat -q 'hi', 3 runs each):
                BEFORE   AFTER    delta
  median wall   2.03s    1.86s    -8% (-169ms)
  min wall      1.92s    1.63s    -15% (-293ms)

Real cold-start observation (synthetic 31-turn agent loop): identical
behavior since feasibility check fires once on first compression and
caches. No semantic difference for sessions that DO compress.

UX trade-off: users with broken auxiliary-provider config no longer
see the warning at session start. They see it when compression first
fires — which is exactly when it matters. For users with working
config (the vast majority), the warning never fires anyway, so the
deferral is invisible.

Tests:
- tests/run_agent/test_compression_feasibility.py — 16/16 pass
  (the one test that asserted call-at-init was updated to drive the
  lazy check explicitly via agent._check_compression_model_feasibility())
- Live tmux session: 2-turn conversation + tool call completes clean,
  zero errors in agent.log
2026-05-19 17:27:17 -07:00
Teknium
93734c26e5 fix(dingtalk): transcribe native voice notes
Sibling fix to PR #28918 (Discord voice notes). DingTalk's rich-text
"voice" item type is its native voice-message format, but the adapter
was routing it to MessageType.AUDIO — which gateway/run.py:7605 skips
for STT. The docs claim every voice-capable platform auto-transcribes,
so this brings DingTalk in line.

Generic audio uploads (mapped to "file" by DINGTALK_TYPE_MAPPING) are
unchanged — they were already classified as DOCUMENT, not AUDIO.

Adds tests/gateway/test_dingtalk.py::TestExtractMedia covering both the
voice path and the audio-passthrough invariant.
2026-05-19 17:26:26 -07:00
helix4u
448a3f9ea2 fix(discord): transcribe native voice notes 2026-05-19 17:26:26 -07:00
xxxigm
d35f8932e8 test(kanban): cover sticky blocks for worker-initiated kanban_block (#28712)
Six regression tests pinning the dispatcher contract that was broken
in #28712:

* test_worker_block_is_not_auto_promoted_by_recompute_ready —
  kanban_block survives five back-to-back ticks (compressed dispatcher
  loop).
* test_worker_block_on_child_with_done_parents_is_still_sticky —
  the parent-completion code path was the worst false-positive; even
  when every parent is done, an explicit worker block stays blocked.
* test_circuit_breaker_block_still_auto_promotes — preserves the
  pre-#28712 recovery semantics for circuit-breaker blocks (direct
  UPDATE + no "blocked" event).
* test_gave_up_event_alone_does_not_make_block_sticky — explicit
  guard so the gave_up event is never accidentally treated as
  sticky; covers the second leg of the protocol_violation loop.
* test_unblock_clears_sticky_state_and_lets_block_recover — only
  unblock_task resolves the sticky state; subsequent circuit-breaker
  blocks recover normally.
* test_protocol_violation_loop_is_broken — full bug-shaped
  reproduction: block → tick → (would-be) crash + gave_up → next tick
  still blocked.  Without the fix this would loop indefinitely.

The seventh test from the original PR (legacy-DB init recovery) was
dropped during salvage — the schema-init half of #28712 is already
fixed on main by #28754 and #28781, and the contract is covered by
test_kanban_db.py::test_connect_migrates_legacy_db_before_optional_column_indexes.
2026-05-19 17:26:23 -07:00
xxxigm
34120a0ae2 fix(kanban): worker-initiated block must not be auto-promoted (#28712)
When a worker calls ``kanban_block(reason="review-required: ...")`` to
hand a task off for human review, the dispatcher's ``recompute_ready``
was treating the resulting ``blocked`` status as eligible for
auto-promotion — exactly the same as a circuit-breaker block.  On the
next tick the task flipped back to ``ready``, a fresh worker spawned,
found nothing to do (work already applied, review-required comment
already posted), exited cleanly, got recorded as ``protocol_violation``
→ ``gave_up`` → ``blocked``, and the dispatcher promoted again.
Infinite loop until manual ``hermes kanban reclaim`` + ``kanban block``.

Add ``_has_sticky_block`` which distinguishes the two block sources
using the cheapest available signal: the most recent
``"blocked"``/``"unblocked"`` event in ``task_events``.

* Worker / operator ``kanban_block`` emits ``"blocked"`` →
  ``_has_sticky_block`` returns True → ``recompute_ready`` skips the
  task entirely.  ``unblock_task`` emits ``"unblocked"`` which flips
  the predicate back, so the only legitimate exit is the documented
  human-in-the-loop path.
* Circuit-breaker ``_record_task_failure`` emits ``"gave_up"`` (not
  ``"blocked"``) → predicate stays False → original
  parent-completion-recovery semantics from #40c1decb3 are preserved.
* Tasks blocked purely by direct DB manipulation also recover, since
  they have no ``"blocked"`` event row at all — matches the existing
  ``test_recompute_ready_promotes_blocked_with_done_parents`` fixture
  behaviour.
2026-05-19 17:26:23 -07:00
Teknium
64a9a199bb
fix(xai-oauth): pin inference base_url to x.ai origin (#28952)
XAI_BASE_URL / HERMES_XAI_BASE_URL let users repoint the OAuth-authenticated
inference endpoint, but the env override was an unguarded credential-leak
vector: a tampered .env or hostile shell init setting
XAI_BASE_URL=https://attacker.example/v1 would silently ship the SuperGrok
OAuth bearer to a third party on every request.

Add _xai_validate_inference_base_url() that pins the host to x.ai or a
*.x.ai subdomain and rejects non-HTTPS. On rejection, fall back to the
default with a warning rather than raise — a bad env var should not
deadlock auth, but should never leak the bearer either.

Apply at all three sites that read the env override for xai-oauth:
- hermes_cli/auth.py resolve_xai_oauth_runtime_credentials (main path)
- hermes_cli/auth.py _xai_oauth_loopback_login (initial login)
- agent/auxiliary_client.py _resolve_xai_oauth_for_aux (aux client)

E2E validated against four scenarios: attacker.example, lookalike
api.x.ai.evil.com, http:// downgrade on api.x.ai, and legit custom.x.ai
subdomain (which still resolves correctly).

Discovered while comparing against the opencode-grok-auth plugin
(github.com/ysnock404/opencode-grok-auth), which highlighted the same
guard on the OpenCode side.
2026-05-19 14:51:21 -07:00
墨綠BG
c9d5ef28bf 🐛 fix(cli): handle missing remote tracking refs 2026-05-19 14:50:42 -07:00
墨綠BG
28ab420302 🐛 fix(cli): handle no-remote worktree cleanup 2026-05-19 14:50:42 -07:00
helix4u
d9829ab45f fix(model): match custom provider by active base url 2026-05-19 14:50:38 -07:00
Teknium
60bb98e003
fix(install.ps1): pin PortableGit instead of hitting rate-limited GitHub API (#28943)
The Windows installer fetched the latest git-for-windows release via
api.github.com/repos/git-for-windows/git/releases/latest, which is
rate-limited to 60 requests/hour/IP for unauthenticated callers. Users
behind CGNAT, corporate NAT, dorm WiFi, or shared ISP routinely hit the
limit, and the installer aborts asking them to install Git manually.

Switch to a pinned release tag (v2.54.0.windows.1) and a static
github.com/.../releases/download/<tag>/<asset> URL. Static download
URLs are served by GitHub's blob storage and are not subject to the
API rate limit.

Trade-offs:
- We have to bump the pin when we want a newer Git for Windows. The
  installer doesn't depend on Git features beyond 'works', so this is
  a once-a-year maintenance cost at most.
- Loses the (cosmetic) MB size display, since we no longer have asset
  metadata. Replaced with the version string in the 'Downloading ...'
  line instead.
2026-05-19 14:38:34 -07:00
dependabot[bot]
aa4e49275e
chore(deps): bump ws from 8.20.0 to 8.20.1 in /ui-tui
Bumps [ws](https://github.com/websockets/ws) from 8.20.0 to 8.20.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.20.0...8.20.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.20.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-19 21:37:58 +00:00
Teknium
544c31b50b
perf(agent-loop): cut 47% of per-conversation function calls via 3 targeted hot-path optimizations (#28866)
* perf(config): add load_config_readonly() fast path for hot agent loop

`load_config()` is called from the agent loop's per-API-call hot path via
`get_provider_request_timeout()` and `get_provider_stale_timeout()` —
both invoked once per turn from `_resolved_api_call_timeout()` in
run_agent.py.

Profiling a synthetic 20-tool-call agent run revealed:
- 21 invocations of `load_config()` cumulating 56ms (~17% of agent loop)
- 34,398 deepcopy calls totaling 37ms (config defensive deepcopy + chain)
- 8,652 `_expand_env_vars` invocations (~412 per turn)

Microbench (cache-hit, real config.yaml present):
  load_config()          265us/call  (125us deepcopy + 140us infra)
  load_config_readonly() 138us/call  (~48% faster)

`load_config_readonly()` returns the cached dict directly without the
defensive deepcopy. Documented contract: caller must not mutate. Returns
plain dict (not MappingProxyType) so downstream `isinstance(x, dict)`
guards keep working — caught during initial implementation when
MappingProxyType broke get_provider_request_timeout's guard logic.

Wired into hermes_cli/timeouts.py (the two functions called per agent
turn). load_config() is unchanged for the 263 other call sites that
mutate the result before save_config(), are not in the hot path, or
where the safety guarantee matters more than the perf.

Profile A/B (cached config, 21-turn agent loop):
                                BEFORE  AFTER   delta
  get_provider_request_timeout  55ms    16ms    -71%
  total function calls          399k    160k    -60%
  deepcopy calls (in hotspots)  34,398  ~0      ~elim

Verified:
- isinstance(load_config_readonly(), dict) is True
- timeout/stale resolutions correct
- load_config() still returns isolated mutable deepcopies
- tests/hermes_cli/test_config*.py / test_timeouts.py: 102/102 pass
- tests/cli/ + tests/agent/test_auxiliary_client.py: 883/883 pass

* perf(redact): substring pre-screens skip non-matching regex chains

Every log record passes through `RedactingFormatter.format` which calls
`redact_sensitive_text`, which historically ran ALL 13 secret-pattern
regexes against every line — including DB connection strings, JWTs,
Discord mentions, Signal phone numbers, etc. — even for typical clean
log records like 'INFO run_agent: API call completed'.

Add cheap substring pre-checks before each regex pass. False positives
still run the regex (which then matches nothing); false negatives are
impossible because every pattern requires the gated substring to match
its leading anchor:

- `_PREFIX_RE`        gated on any of 33 known credential prefix substrings
- `_ENV_ASSIGN_RE`    gated on `=` in text
- `_JSON_FIELD_RE`    gated on `:` and `"` in text
- `_AUTH_HEADER_RE`   gated on `uthorization`/`UTHORIZATION` in text
- `_TELEGRAM_RE`      gated on `:` in text
- `_PRIVATE_KEY_RE`   gated on `BEGIN` and `-----`
- `_DB_CONNSTR_RE`    gated on `://` in text
- `_JWT_RE`           gated on `eyJ` in text
- URL userinfo/query  gated on `://`
- `_redact_form_body` gated on `&` and `=`
- `_DISCORD_MENTION_RE` gated on `<@`
- `_SIGNAL_PHONE_RE`  gated on `+`

Microbench (5 typical log records, 20k iterations each):
                              BEFORE  AFTER  delta
  redact_sensitive_text per call  5.63us  1.79us  -68%

Real-world impact: ~244 log records emitted in a 30-turn agent loop, so
the chain saves ~1ms of CPU per conversation. Bigger win is the
reduction in regex execution and GC pressure during heavy logging
sessions (verbose logging, gateway message processing).

Security regression test: 30 secret-containing inputs (sk-/ghp_/JWT/DB
connstr/Auth-Bearer/private key/URL userinfo/Discord/Signal/etc.)
verified to produce identical redacted output before/after. All 75
existing tests/agent/test_redact.py cases pass.

The `?access_token=foo&code=bar` (bare query string, no scheme) case
that 'leaks' is pre-existing behavior — the URL query redaction
requires a well-formed URL with scheme+host. Not a regression.

* perf(run_agent): cache _needs_thinking_reasoning_pad result per (provider, model, base_url)

Profile of a 31-turn synthetic agent run shows `_needs_thinking_reasoning_pad`
fires 495 times (~16 per turn) and each call ran 3 helper methods, each
hitting `base_url_host_matches` 1-4 times via `urlparse`. Total cost:
3,342 base_url_host_matches calls + 3,373 urlparse calls accounting for
~36ms of agent-loop overhead (~7% of the entire post-network work).

Provider / model / base_url don't change during a conversation except via
`switch_model` and fallback activation — both of which already overwrite
those attributes atomically. Cache the result on a tuple key; since the
key is derived from the very fields that would change, the cache
auto-invalidates on the next read after a switch. No manual invalidation
needed in switch_model / _try_activate_fallback.

Profile A/B (31-turn cached-config agent run):
                                      BEFORE  AFTER  delta
  _needs_thinking_reasoning_pad cum    18ms    1ms    -94%
  _copy_reasoning_content_for_api cum  17ms    1ms    -94%
  base_url_host_matches calls          3,342   372    -89%
  urlparse calls                       3,373   403    -88%
  total function calls                 296k    223k   -25%

Verified:
- tests/run_agent/test_deepseek_reasoning_content_echo.py: 36/36 pass
- tests/run_agent/ (full): 1383/1383 pass + 3 skipped
2026-05-19 14:25:10 -07:00
Teknium
784febe1cf
perf(cli): defer openai._base_client import via sys.meta_path finder (#28864)
`cli.py` was eager-importing `openai._base_client` at module-load time
purely to monkeypatch `AsyncHttpxClientWrapper.__del__` (defense against
"Press ENTER to continue..." errors when AsyncOpenAI clients are GC'd
against dead event loops). That import cost ~166ms / ~30MB on every
cold CLI start because openai's type tree (responses/*, graders/*) is huge.

Replace with a `sys.meta_path` finder that intercepts the first import
of `openai._base_client` from anywhere in the codebase, lets the normal
load run, then applies the `__del__ = lambda self: None` patch before
control returns to the caller. Same correctness guarantee (patch
applies before any AsyncOpenAI instance can be constructed), zero cost
until the SDK is actually needed.

Hot path: every hermes chat / gateway boot / cron tick / subagent spawn.

A/B benchmark, 10 runs each, fresh subprocess:
                     BEFORE  AFTER   delta
  import cli wall    0.86s   0.62s   -28% (median)
  import cli wall    0.85s   0.59s   -31% (min)
  import cli RSS     91.2MB  74.0MB  -19% (median)

The `neuter_async_httpx_del` function in agent/auxiliary_client.py is
unchanged; its tests still pass and any future callers can still invoke
it directly.

Verified:
- import cli no longer pulls openai into sys.modules
- first 'from openai._base_client import AsyncHttpxClientWrapper'
  triggers the patch; __del__.__name__ == '<lambda>'
- tests/run_agent/test_async_httpx_del_neuter.py: 9/9 pass
- tests/agent/test_auxiliary_client.py: 159/159 pass
- tests/cli/: 715/715 pass
2026-05-19 14:24:53 -07:00
teknium1
6a159be7ca fix(runtime): treat 'ollama'/'vllm'/'llamacpp' aliases like 'custom' for base_url trust (#27132)
When config.yaml has provider: ollama (or vllm/llamacpp/llama-cpp) with a
non-loopback base_url, auth.py's resolve_provider() correctly normalises
the alias to 'custom' at the top level, but two sites in runtime_provider.py
were still comparing the *original* string against the literal 'custom':

  - _config_base_url_trustworthy_for_bare_custom() rejected non-loopback
    URLs because cfg_provider_norm was 'ollama', not 'custom'.
  - _resolve_openrouter_runtime() only entered the trust branch when
    requested_norm == 'custom'.

Both sites now consult resolve_provider() and treat any alias that
resolves to 'custom' identically. Result: provider: ollama + LAN IP no
longer silently falls through to OpenRouter (HTTP 401), matching the
behaviour of provider: custom with the same base_url.

E2E verified across 6 cases (ollama/vllm/llamacpp/custom + LAN; ollama +
loopback; openrouter + cloud) — all route to the configured endpoint;
'frobnicate' + LAN still rejects with AuthError as before.

Also adds scripts/release.py AUTHOR_MAP entry for @stepanov1975
(PR #22074 — wizard config picker preservation, cherry-picked into the
preceding commit).
2026-05-19 14:23:19 -07:00
stepanov1975
e13f242f01 fix(cli): preserve setup config picker writes
Resync the setup wizard's in-memory config after the shared model picker writes to disk so the wizard's final save does not overwrite auxiliary choices or other provider updates.\n\nAdds a regression test for auxiliary task choices saved by the picker.
2026-05-19 14:23:19 -07:00
Teknium
3f552568c1
docs(skills): document browse.sh source (#28939)
Add browse.sh (browse-sh) to the supported-sources table and
integrated-hubs section in user-guide/features/skills.md, and to the
--source notes in reference/cli-commands.md. Companion to the
BrowseShSource adapter merged in #28936.
2026-05-19 14:20:22 -07:00
teknium1
890b2ebd5b fix(browse-sh): fetch SKILL.md via /api/skills/{slug}+skillMdUrl
The catalog's sourceUrl points at github.com/browserbase/browse.sh,
whose underlying repository is not always public — most raw URLs derived
from it 404. Use the per-skill detail endpoint instead, which returns a
skillMdUrl CDN blob that reliably resolves to the SKILL.md text. Fall
back to a raw.githubusercontent.com sourceUrl if the detail call fails.

- tools/skills_hub.py: rewrite BrowseShSource.fetch() to resolve via
  /api/skills/{slug} -> skillMdUrl; drop the unreachable _to_raw_url
  helper; expose the resolved URL in bundle.metadata.skill_md_url.
- tests/tools/test_skills_hub_browse_sh.py: match the real catalog
  shape (name = task name, slug = host/task-id), exercise the
  detail-endpoint -> blob two-call flow, and add a fallback test.
- scripts/release.py: map kylejeong21@gmail.com -> Kylejeong2.
2026-05-19 14:17:38 -07:00
Kyle Jeong
90be1be501 fix: register browse-sh in per-source limits and --source choices
- Add 'browse-sh' to _PER_SOURCE_LIMIT in both do_browse() and
  browse_skills() with limit=500 (covers full 171-skill catalog)
- Add 'browse-sh' to --source argparse choices for both
  'hermes skills browse' and 'hermes skills search'

Without these, browse-sh fell back to the default cap of 50 results
and was not filterable via --source.
2026-05-19 14:17:38 -07:00
Kyle Jeong
57145ca146 feat: add BrowseShSource adapter for browse.sh skills catalog
Adds BrowseShSource — a new skill source adapter that integrates
Browserbase's browse.sh catalog (169+ site-specific SKILL.md files)
into the Hermes Skills Hub.

- BrowseShSource class in tools/skills_hub.py implementing SkillSource ABC
- Fetches browse.sh catalog API with 1h TTL cache
- Full-text search across name, title, description, hostname, category, tags
- fetch() downloads SKILL.md via sourceUrl (GitHub HTML -> raw URL conversion)
- Registered in create_source_router() after LobeHubSource
- Tests in tests/tools/test_skills_hub_browse_sh.py (7 tests, all passing)
2026-05-19 14:17:38 -07:00
ethernet
2b41f9d893
Merge pull request #28914 from justincc/fix/fix-blank-tool-names-at-msg-construction
fix blank tool_name entries in state.db and JSON session logs
2026-05-19 16:36:13 -04:00
adybag14-cyber
7c2ff742a4 fix(tui): termux-gate scrollback preservation, touch-friendly defaults
Adds a Termux runtime detection helper and gates three TUI defaults on it:

- Skip the startup scrollback clear on Termux so users can review/copy
  earlier output after reopening the app. Desktop keeps the existing
  \x1b[2J\x1b[H\x1b[3J slate (AlternateScreen takes over there anyway).
- Default INLINE_MODE on under Termux: primary-buffer rendering makes
  long-thread review and copy/paste much less fragile when users
  background/foreground the app. Override with HERMES_TUI_INLINE=0/1.
- Default mouse tracking off under Termux so touch selection isn't
  intercepted by terminal mouse protocols. Explicit override via
  HERMES_TUI_MOUSE_TRACKING=0/1; legacy HERMES_TUI_DISABLE_MOUSE still
  works on desktop.

Detection is purely env-based (TERMUX_VERSION or PREFIX path) with an
explicit opt-out HERMES_TUI_TERMUX_MODE=0 for debugging. Non-Termux
platforms keep every existing default.

Co-authored-by: adybag14-cyber <252811164+adybag14-cyber@users.noreply.github.com>
2026-05-19 12:49:23 -07:00
justincc
a61420952e fix(agent): set tool_name on tool-result messages at construction time
Introduces make_tool_result_message() in tool_dispatch_helpers.py as the
single place where tool-result message dicts are built. All six construction
sites in tool_executor.py, agent_runtime_helpers.py, and mini_swe_runner.py
now use it, so tool_name is set in memory from the moment a message is
created rather than relying on fallback logic in the flush paths.

Fixes blank tool_name in both state.db and JSON session logs.

Adds tests.
2026-05-19 20:49:11 +01:00
teknium1
a19eb54727 test(gateway-windows): make ctypes.windll monkeypatch tolerant on non-Windows
Linux/macOS CI runners don't have ctypes.windll, so the elevated-gateway
test fails at module load. Adding raising=False lets monkeypatch install
the mock attribute without first requiring it to exist.
2026-05-19 11:23:15 -07:00
nekwo
d948de39e9 fix(gateway): harden Windows gateway install lifecycle
Preserve Windows profile install decisions across UAC handoff, avoid visible console windows by launching via pythonw, make repeated install/start idempotent, recreate stale Scheduled Tasks, and separate start-now from login auto-start behavior. Add Windows gateway regression coverage and systemd setup tests for the shared install flow.
2026-05-19 11:23:15 -07:00
nekwo
95683c0283 fix(windows): hide local subprocess consoles
Apply Windows CREATE_NO_WINDOW flags to foreground local terminal subprocesses and tracked background processes so Hermes operations do not flash or steal focus with extra console windows.
2026-05-19 11:23:15 -07:00
nekwo
f007ef8ab5 fix(windows): hide cron script subprocess consoles
Apply CREATE_NO_WINDOW flags when the cron scheduler launches job scripts on Windows so gateway-managed no-agent cron jobs do not flash cmd or python console windows every tick.
2026-05-19 11:23:15 -07:00