Commit graph

14322 commits

Author SHA1 Message Date
Sabin Iacob
e4105a2ffd test(web_tools): regression for plugin-registered provider availability
A plugin-registered WebSearchProvider with no built-in provider credentials
must light up web_search / web_extract and be discoverable by the backend
selectors. Covers check_web_api_key(), _get_backend(), _is_backend_available()
registry delegation, per-capability extract selection (#32698), and that the
web_search / web_extract tool registry entries are not filtered out.

Tests contributed by @m0n5t3r (PR #28652, issue #28651).
2026-07-03 20:14:27 +05:30
kshitijk4poor
0a9d42ce40 fix(web_tools): delegate backend availability to provider registry
Plugin-registered web providers (registered via agent.web_search_registry)
were invisible to the tool-availability gate: _is_backend_available() was a
hardcoded env-var if-chain that returned False for any name outside the eight
built-in backends. Because check_web_api_key() is the check_fn for both
web_search and web_extract, a working custom provider with no built-in creds
left both tools filtered out of the toolset entirely.

Fix at the single chokepoint: _is_backend_available() now delegates non-legacy
backend names to the registered provider's is_available(), falling back to the
legacy built-in probes for known names and unregistered providers. Because
_get_backend(), _get_capability_backend(), and check_web_api_key() all resolve
availability through this one function, the fix cascades to every caller —
including the per-capability extract selection that produced a dead-end
'search-only' error (#32698). The two remaining hardcoded whitelist
early-returns (_get_backend, check_web_api_key) now also accept registered
names, and both walk registered providers as a final fallback so a custom
backend still resolves when no built-in has credentials.

Built-in backend priority is preserved unchanged: the registry is consulted
only for names outside _LEGACY_WEB_BACKENDS.

Fixes #28651
Fixes #31873
Fixes #32698
2026-07-03 20:14:27 +05:30
kshitijk4poor
def6d6fe1b test(cron): regression test for run_one_job secret scope
Asserts the behavior contract that run_one_job installs a profile secret
scope around run_job under multiplexing (so resolve_runtime_provider's
get_secret does not fail-close with UnscopedSecretError) and tears it
down afterward. Mutation-verified: fails on unmodified main with the
exact UnscopedSecretError, passes with the fix.
2026-07-03 19:32:52 +05:30
Jonny Kovacs
fdab380a1a fix(cron): run jobs under the profile secret scope
Once profile isolation is active (multiple gateway profiles or room->profile
multiplexing), get_secret() fails closed outside an installed scope. The cron
ticker fires jobs from a thread with no per-turn scope, so run_job() died in
resolve_runtime_provider() with UnscopedSecretError (e.g. for
OPENROUTER_BASE_URL / CUSTOM_BASE_URL) before model selection - every cron
job failed while interactive turns worked fine.

Wrap run_job() in set_secret_scope(build_profile_secret_scope(...)) with a
finally-reset, mirroring the proven per-turn pattern in gateway/run.py
(_profile_runtime_scope). Single-profile installs are unaffected (the scope
is just the profile's own .env).

tests/cron: 611 passed, 1 pre-existing unrelated failure
(TestRoutingIntents::test_all_token_case_insensitive fails identically on
unmodified main in a full-suite run and passes in isolation).
2026-07-03 19:32:52 +05:30
kshitijk4poor
104232979d fix(xai): route video-gen local inputs through the shared read guard
Fold the xAI video credential-read guard into the same shared
agent.file_safety.raise_if_read_blocked chokepoint this PR introduces for
the image providers, so the whole image+video bug class is covered by one
enforced boundary. Consolidates the parallel salvage of #57695 (xAI
image+video) into this PR; #57727 is now redundant and will be closed.

- video_gen/xai: guard _image_ref_to_xai_url and _video_ref_to_xai_url
  (the video image + video byte-read chokepoints) via the shared helper.
- Regression tests: symlinked auth.json with .png/.mp4 names are blocked
  across both video read paths (mutation-checked).
2026-07-03 18:47:53 +05:30
kshitijk4poor
c1826e2690 fix(image-gen): route local-input credential guard through one shared chokepoint + cover xai (#57698)
Follow-up to the per-provider guards. Three improvements from review:

1. Extract agent.file_safety.raise_if_read_blocked() as a single shared
   chokepoint and route the OpenAI, OpenRouter, and (newly) xAI image
   providers through it, replacing the 3x-duplicated inline try/except.
   Fixes the whole bug class: xai/_xai_image_field read a model-supplied
   local path via open() with no guard — the same vulnerability the PR
   fixed for OpenAI/OpenRouter, in a sibling provider it missed.
2. Strengthen the regression tests from pass-on-any-ValueError to true
   security invariants: spy open()/read_bytes() and assert the blocked
   credential is NEVER read; add negative controls (legit local image
   still loads; remote/data: URIs pass through unguarded) so a
   block-everything regression can't pass.
3. Guard is best-effort by design (defense-in-depth, not a security
   boundary) — documented on the shared helper.

- agent/file_safety.py: raise_if_read_blocked()
- plugins/image_gen/{openai,openrouter,xai}: route through helper
- tests: no-read spies + negative controls across all three providers
2026-07-03 18:47:53 +05:30
dsad
587be5b5b4 fix(image-gen): guard local provider inputs against credential reads 2026-07-03 18:47:53 +05:30
kshitij
203b5d4cea
Merge pull request #57728 from kshitijk4poor/chore/author-map-cocakova
chore: add AUTHOR_MAP entry for PR #57692 salvage (CocaKova)
2026-07-03 18:43:32 +05:30
kshitijk4poor
ad3261bc77 chore: add AUTHOR_MAP entry for PR #57692 salvage (CocaKova) 2026-07-03 18:38:12 +05:30
Teknium
22c5048d9c
fix(moa): restore prompt caching for the aggregator and advisors (#57675)
Two caching holes made MoA re-bill essentially its entire input stream:

1. AGGREGATOR: anthropic_prompt_cache_policy() judged the agent's own
   model/provider — on the MoA path those are the virtual preset name and
   'moa', which match no caching branch, so _use_prompt_caching was False
   and the acting aggregator (Claude on OpenRouter) ran with ZERO
   cache_control breakpoints. Measured on identical opus-4.8 sessions:
   85% cache share solo vs 2% via MoA — ~30M re-billed input tokens on one
   132-task benchmark run. Fix: when provider == 'moa', resolve the policy
   from the preset's real aggregator slot (provider/model/base_url/api_mode
   via resolve_runtime_provider).

2. ADVISORS: _run_reference never applied cache_control at all, and
   Anthropic caching is opt-in per request — Claude advisors served 0
   cache reads across 1,227 benchmark calls (11.5M re-billed input tokens)
   even though the advisory view is append-only across iterations (stable
   prefix; the synthetic end marker is last so it never pollutes it). Fix:
   _maybe_apply_advisor_cache_control() reuses the SAME policy function and
   SAME system_and_3 layout as the main loop, judged on the advisor slot's
   own resolved runtime — advisor requests are now decorated exactly like
   an acting agent on that provider. Auto-caching routes (OpenAI-family)
   are left untouched by policy.

Live-verified on the wire (per-iteration opus+gpt5.5 preset, 4 fan-outs):
claude advisor fan-out 2-3 cache_write=2161/2344, fan-out 4
cache_read=2206 / fresh_in=2; aggregator session cache share 84%/77%
(vs 2%/0% before). Sub-1024-token prompts correctly stay uncached
(Anthropic minimum).
2026-07-03 04:08:48 -07:00
Teknium
87ae4ae94b
fix(update): harden #57659 follow-ups — task restore on failure, --force-venv split, trampoline detection, managed-install health (#57680)
Five follow-ups to #57659 from post-merge review:

1. install.ps1: gateway scheduled-task re-enable now runs in a finally
   (a thrown Remove-Item/uv venv failure previously stranded the user's
   gateway autostart disabled), and tasks that were already disabled
   before the install are no longer blindly re-enabled.
2. The venv-python holder guard is no longer bypassed by plain --force
   (which the desktop bootstrap passes on every update while its lock
   probe only checks hermes.exe/app.asar). New explicit --force-venv is
   the escape hatch; --force keeps bypassing only the hermes.exe shim
   guard.
3. _detect_venv_python_processes now also catches uv/base-interpreter
   trampolines whose exe is outside the venv, via cmdline (venv path or
   '-m hermes_cli.main' tied to this install root) and cwd.
4. Missing venv python is now UNHEALTHY on managed installs
   (.hermes-bootstrap-complete / .update-incomplete markers) so the
   repair lane runs instead of 'Already up to date!'; the repair branch
   recreates the venv first when it's gone entirely. Dev checkouts keep
   reporting healthy.
5. install.ps1 comment no longer claims a Startup-folder disarm the
   code doesn't perform (logon-only, not a mid-install respawner).
2026-07-03 04:08:37 -07:00
teknium1
0e9136cb27 chore: add suninrain086 to AUTHOR_MAP for salvaged #50685 2026-07-03 03:54:01 -07:00
teknium1
0ad4dd60e9 test(vision): adapt salvaged config-priority tests to async _handle_vision_analyze
The salvaged tests from #53754 predate _handle_vision_analyze becoming
async and the native fast path; await the handler and force the legacy
aux path so the model-resolution assertion is actually exercised.
2026-07-03 03:54:01 -07:00
liuhao1024
149641485c fix(vision): read auxiliary model from config.yaml before env var
_handlers for vision_analyze and video_analyze read model name from
config.yaml (auxiliary.vision.model / auxiliary.video.model) before
falling back to AUXILIARY_VISION_MODEL / AUXILIARY_VIDEO_MODEL env
vars.  Matches the existing config-first pattern for timeout and
temperature in the same file.

Fixes #53749
2026-07-03 03:54:01 -07:00
Jacky Zeng
25aa626cb4 fix(vision): forward custom-endpoint credentials in vision auto-detect
A custom:<name> main provider resolves at runtime to the bare provider id
"custom". In the vision auto-detect chain, the main-provider branch called
resolve_provider_client("custom", ...) WITHOUT explicit_base_url/api_key,
so it returned (None, None) ("no endpoint credentials found") and the whole
chain fell through to OpenRouter/Nous. A user on a custom endpoint with no
aggregator configured then got "No LLM provider configured for task=vision
provider=auto" on every image, even though their main model fully supports
vision.

Recover the live endpoint that set_runtime_main() records each turn
(_RUNTIME_MAIN_BASE_URL/_API_KEY/_API_MODE) and forward it to Step 1, with
a fallback to _resolve_custom_runtime() for non-gateway callers. Mirrors the
existing explicit-base_url branch directly above.

Adds TestResolveVisionCustomProvider covering custom, custom:<name>, and the
no-runtime fallback path.
2026-07-03 03:54:01 -07:00
Jiahui-Gu
8bf797f1c2 fix(agent): prefer native vision over auxiliary fallback in auto mode (#29135) 2026-07-03 03:43:35 -07:00
brooklyn!
b19e32c702
Merge pull request #57665 from NousResearch/bb/tts-managed-model-coerce
fix(tts): coerce direct-only OpenAI model on the managed audio gateway
2026-07-03 05:35:38 -05:00
teknium1
25d1a07746 test(gateway): accept kwargs in _decide_image_input_mode stub after #36055 signature change 2026-07-03 03:33:06 -07:00
LeonSGP43
f6a3d2e900 fix(model): preserve named custom provider slug 2026-07-03 03:33:06 -07:00
Maxim Esipov
769469a703 fix: route gateway images by session model override
(cherry picked from commit 7702071c01db4df67469397118d9561d2e55eb92)
2026-07-03 03:33:06 -07:00
liuhao1024
5e11628546 fix(image_routing): check stripped custom:<name> provider key for vision override
When model.provider is set to custom:<name>, _supports_vision_override()
previously tried only the runtime provider key ('custom') and the raw
config value ('custom:my-proxy'). It did not try the stripped name
('my-proxy'), which is the actual key under providers: in config.yaml.

This caused native image routing to fall back to text mode even when the
user explicitly declared supports_vision: true on the named provider's
model entry.

Fixes #39963
2026-07-03 03:33:06 -07:00
Brooklyn Nicholson
b53ba0e188 fix(tts): coerce direct-only OpenAI model on the managed audio gateway
A user with tts.openai.model set to a direct-OpenAI model (e.g. tts-1-hd)
but no VOICE_TOOLS_OPENAI_KEY/OPENAI_API_KEY (or with tts.use_gateway)
routes TTS through the managed Nous audio gateway, which only proxies
gpt-4o-mini-tts. The request 400s with:

  VALIDATION_ERROR: Unsupported managed OpenAI speech model
  {'model': 'tts-1-hd', 'supportedModels': ['gpt-4o-mini-tts']}

_resolve_openai_audio_client_config now reports whether it resolved the
managed gateway; _generate_openai_tts coerces the model to a
managed-supported one (logging a warning that points at the direct-key
escape hatch) unless the user redirected base_url to their own endpoint.
Direct-key users keep their tts-1/tts-1-hd preference unchanged.
2026-07-03 05:30:16 -05:00
teknium1
7485fe0605 fix(dashboard): make .env sensitive-file guard case-insensitive
Follow-up to #57507: .ENV / .Env.local on case-insensitive filesystem
mounts slipped past the guard. Lowercase the name before matching and
add a regression test. Addresses egilewski's open review note.
2026-07-03 03:27:47 -07:00
Que0x
62882b8e6f fix(matrix): isolate per-event failures in _dispatch_sync gather
`_dispatch_sync` gathers the mautrix per-event handler tasks with a bare
`asyncio.gather(*tasks)`. Without `return_exceptions=True`, the first handler
that raises aborts the gather, so the sibling events in the same sync response
are dropped unprocessed — the exception propagates up to the sync loop, which
logs a single "sync error" and moves on. The invite/redaction gathers a few
lines above already use `return_exceptions=True`.

Use `return_exceptions=True` and log each failing handler, so one bad event no
longer takes out the rest of its batch and per-event failures stay visible.

Regression test: a batch with one failing and one succeeding handler no longer
raises, the good handler still runs, and the failure is logged (mutation-
verified — reverting re-raises RuntimeError out of _dispatch_sync).
2026-07-03 03:27:47 -07:00
Eugeniusz Gilewski
e4dbb67bf5 fix(security): remove model-controlled delegate ACP transport
Source: https://github.com/NousResearch/hermes-agent/pull/52346
Related prior work: https://github.com/NousResearch/hermes-agent/pull/39462
Related prior work: https://github.com/NousResearch/hermes-agent/pull/27426
Maintainer direction: https://github.com/NousResearch/hermes-agent/pull/52346#issuecomment-4854881612

Remove acp_command and acp_args from the model-facing delegate_task schema and
dispatch paths. Child agents can still use ACP subprocess transport when it
comes from trusted delegation config or parent inheritance, but a model tool
call can no longer choose the command or arguments that reach child
construction.

This is salvageable because the risky boundary is model control over child ACP
transport, not ACP itself. The patch follows the maintainer direction from the
source discussion by preserving trusted ACP configuration and prior integration
work while removing the untrusted tool-call fields from both top-level and
per-task delegate inputs.

Reproduced on main by passing acp_command through delegate_task and observing it
reach _build_child_agent. Verified after the fix that model dispatch strips the
hidden top-level fields and per-task hidden fields are ignored before child
construction.

Co-authored-by: Carlosian <claudlos@agentmail.to>
Co-authored-by: ssiweifnag <120658181+ssiweifnag@users.noreply.github.com>
Co-authored-by: nikshepsvn <23241247+nikshepsvn@users.noreply.github.com>
2026-07-03 03:27:47 -07:00
liuhao1024
1bcc52c14e fix(dashboard): use pattern match for .env sensitive file guard
Replace the exact-filename frozenset with _is_sensitive_filename()
that matches .env plus any .env.<suffix> variant.  This covers
shorthand suffixes like .env.prod that the previous enumeration
missed.

Add test_sensitive_env_suffix_variants_blocked regression test
covering .env.prod, .env.dev, .env.staging.local, and .env.ci.

Addresses review feedback from egilewski on PR #57507.
2026-07-03 03:27:47 -07:00
liuhao1024
bc55c201c7 fix(dashboard): block .env files from managed-files API
The dashboard Files tab could list, read, and download .env files
containing API keys when running with a bind-mounted Hermes home
directory (e.g. docker run -v ~/.hermes:/opt/data).

Add _SENSITIVE_FILENAMES frozenset and filter these from
list_managed_files(), read_managed_file(), and download_managed_file().
Return 403 for direct read/download attempts on sensitive files.

Fixes #57505
2026-07-03 03:27:47 -07:00
srojk34
16332af60b security(gateway): anchor api_server MEDIA tag resolution to safe paths
_resolve_media_to_data_urls's ad-hoc _MEDIA_TAG_RE matched any bare
token after MEDIA: (no absolute-path anchor) and read the resolved
path directly with no denylist. A relative/traversal path like
MEDIA:../../../../etc/passwd.png slipped through, and any image-
suffixed file the process could read (including under ~/.ssh, ~/.aws,
etc.) was base64-inlined into the API response if its path merely
appeared in the model's own final reply text.

Every other platform adapter's MEDIA: handling already goes through
two shared primitives in gateway/platforms/base.py:
  - MEDIA_TAG_CLEANUP_RE, which anchors the path to ~/, /, or a
    Windows drive letter plus a known deliverable extension.
  - validate_media_delivery_path, which resolves symlinks and rejects
    paths under the credential/system-path denylist.

Reuse both here instead of the local unanchored pattern and naive
Path().expanduser() resolution.
2026-07-03 03:27:47 -07:00
srojk34
47764f19f4 fix(browser): apply private-page guard to browser_cdp frame_id routing
browser_cdp's frame_id (OOPIF) path returned early via
_browser_cdp_via_supervisor before _browser_cdp_private_guard ever ran,
unlike the stateless path a few lines below. A model that navigated a
cloud browser to a private/internal URL could still read page content
by passing frame_id, bypassing the same SSRF/private-page boundary
already enforced on Runtime.evaluate, Page.navigate, and other raw CDP
calls.

Apply the same guard call used by the stateless path before dispatching
to the supervisor, so both routing modes share one boundary.
2026-07-03 03:27:47 -07:00
dsad
4470d957cb fix(browser): block Camofox input on private pages 2026-07-03 03:27:47 -07:00
Teknium
b14d75f8af
fix(update): prevent and self-heal half-updated venvs on Windows (#57659)
Root-causes the July 2026 Windows incident chain (locked _brotlicffi.pyd /
_sodium.pyd during install, then 'No module named annotated_doc' with
'hermes update' insisting 'Already up to date!'):

- hermes update: probe venv core imports even when the checkout is current;
  a half-updated venv (dep sync killed mid-flight by a locked .pyd) is now
  detected and repaired instead of being reported as up to date
- hermes update (Windows): after pausing gateways, refuse to mutate the venv
  while other processes run from the venv interpreter (the Desktop backend
  runs as python.exe so the hermes.exe shim guard never saw it); --force
  keeps the old behavior
- install.ps1 venv stage: disarm gateway autostart Scheduled Tasks before
  the kill sweep (they respawn the gateway inside the kill->delete window),
  make the sweep a bounded loop requiring 3 clean passes, and rename-then-
  delete the old venv (a rename succeeds even with mapped DLLs) with stale-
  dir cleanup on the next run
- desktop updater: 'venv shim still locked after 15s' now ABORTS the update
  hand-off (restarting our backend, surfacing the holder to the user)
  instead of 'proceeding anyway (force)' into guaranteed venv corruption;
  the unlock wait also re-kills respawned backends each poll tick
2026-07-03 03:24:08 -07:00
teknium1
741bd9ba42 fix(gateway): resolve queued follow-up session key before native-image buffering
The cherry-picked #48919 fix resolved next_session_key AFTER
_prepare_inbound_message_text had already buffered native image paths
under the stale key. Reorder so the write key and the consume key are
the same resolved key.
2026-07-03 03:21:09 -07:00
LeonSGP43
bb24ac6f20 fix(gateway): preserve queued native image attachments 2026-07-03 03:21:09 -07:00
tt-a1i
e880396488 fix(gateway): key native image handoff by session 2026-07-03 03:21:09 -07:00
brooklyn!
44cb0ea9e6
Merge pull request #57658 from NousResearch/bb/readtitle-race
fix(desktop): guard link-title readTitle against destroyed windows
2026-07-03 05:16:23 -05:00
Brooklyn Nicholson
359518beac fix(desktop): guard link-title readTitle against destroyed windows
Grace and timeout timers in runRenderTitleJob can call getTitle after
finish() tears down the hidden BrowserWindow, throwing in the main
process when the Artifacts page resolves many link titles concurrently.
2026-07-03 05:13:23 -05:00
brooklyn!
662426ec3d
Merge pull request #57636 from NousResearch/bb/desktop-messaging-poll
fix(desktop): poll messaging sessions so platform traffic appears live
2026-07-03 05:04:58 -05:00
Brooklyn Nicholson
c1e825399c test(gateway): stub get_compression_tip in stale-guard db mock
The routing-heal added to get_or_create_session calls
SessionDB.get_compression_tip; the stale-guard suite's bare MagicMock db
returned a Mock the heal then assigned as session_id, failing JSON
serialization. Model the real contract (a non-compressed session's tip is
itself) so the heal is a correct no-op.
2026-07-03 04:46:01 -05:00
Brooklyn Nicholson
dfb28cc631 refactor(desktop): only poll the transcript when it's a messaging session
The active-transcript poll armed a 5 s timer for every selected session
and no-op'd inside the tick for local chats (already live over the
websocket). Derive activeIsMessaging and gate the effect on it so local
chats never spin an idle timer.
2026-07-03 04:38:45 -05:00
Brooklyn Nicholson
52d0d671e7 fix(desktop): poll messaging sessions so platform traffic appears live
Inbound Telegram/WeChat/Discord messages are written by the background
gateway, not the desktop websocket that drives local chats. Without
explicit polling the messaging sidebar and the open transcript stay
frozen until the user manually refreshes.

Desktop:
- MESSAGING_POLL_INTERVAL_MS (10 s): interval poll of the messaging
  session list so new platform sessions surface automatically.
- ACTIVE_MESSAGING_SESSION_POLL_INTERVAL_MS (5 s): poll the currently-
  viewed messaging transcript and re-hydrate the chat state when the
  FNV-1a signature changes (hash covers role + timestamp + content).
- sameCronSignature now compares lineage_root_id / source / profile /
  preview / message_count / last_active / ended_at so stale previews
  and activity times are no longer silently ignored.
- sessionMatchesStoredId helper de-dups the id / _lineage_root_id check.
- refreshMessagingSessions exposed from useSessionListActions so the
  controller can use it in the poll effect.

Gateway:
- SessionStore._compression_tip_for_session_id: look up the latest
  compression continuation for a session id.
- SessionStore._heal_compression_tip_locked: rewrite a stale entry to
  the compression child before returning it, so a restart or failed send
  no longer leaves the store pinned to the compressed parent.

Co-authored-by: lawyer112 <lawyer112@users.noreply.github.com>
2026-07-03 04:29:22 -05:00
Teknium
1c4cc00f73
fix(moa): user_turn fanout — synthetic advisory marker must not count as a user turn (#57598)
The advisory view appends a synthetic user marker when it ends on an
assistant turn (Anthropic end-on-user rule) — i.e. on every tool iteration
after the first. The user_turn prefix hash treated that marker as the last
user message, so the hashed prefix included the grown mid-turn context and
the signature changed every iteration: advisors re-ran per iteration,
silently defeating the once-per-turn cadence (live smoke test: 2 fan-outs
for a 2-iteration task; expected 1). Hoist the marker to a module constant
and skip it when locating the last REAL user message. Verified: iteration-2
signature now equals iteration-1 (cache HIT); a new real user message still
re-triggers the fan-out.
2026-07-03 01:24:58 -07:00
teknium1
eb99f82ce4 fix(browser): surface launch diagnostics when debug browser never opens the CDP port
Follow-up to the salvaged early-exit retry fix (#35617): the debug-browser
launch path was fire-and-forget (stderr to DEVNULL, no logging), so every
platform failure — Windows singleton forward to an existing instance, bad
profile dir, missing shared libraries, policy blocks — collapsed into the
same unactionable 'port 9222 isn't responding yet' message and debug
reports contained nothing.

- launch_chrome_debug() returns a structured ChromeDebugLaunch with
  per-candidate attempts (state, exit code, stderr tail)
- browser stderr is captured to <hermes_home>/chrome-debug/launch-stderr.log
- clean exit (code 0) without the port opening is detected as Chromium's
  single-instance forward and produces a targeted user hint to close all
  running instances of that browser
- crash exits surface the stderr tail (e.g. missing libnspr4.so)
- every spawn/exit is logged to agent.log so hermes debug share captures it
- CLI (/browser connect) and TUI/desktop (browser.manage) both print the hint
2026-07-03 01:05:22 -07:00
LeonSGP43
c74f093523 fix(browser): retry next candidate when debug launch exits early 2026-07-03 01:05:22 -07:00
Teknium
c7103c637c
feat(desktop): CLI/dashboard parity — skills hub, MCP test/toggle/catalog, maintenance ops, log filters (#57441)
* feat(desktop): CLI/dashboard parity — skills hub browser, MCP test/toggle/catalog, maintenance ops, log filters

Brings desktop GUI to parity with hermes skills/mcp/doctor/backup/debug-share/
curator/memory CLI commands and the dashboard's System + Skills-hub pages:

- Skills page: new Browse Hub tab (search official/GitHub/community sources,
  preview SKILL.md, security scan verdicts, install/update with live action log)
- MCP settings: connection test (tool listing), per-server enable/disable
  toggle, and a Catalog tab installing Nous-approved MCP servers with env prompts
- Command Center: new Maintenance section (doctor, security audit, backup,
  debug share links, curator status/pause/run, memory file status + reset)
- Command Center system logs: file (agent/errors/gateway/desktop), level, and
  substring filters instead of a fixed agent.log tail
- hermes.ts API client + types for all the above; en/zh locale strings (ja and
  zh-hant inherit via defineLocale)

* feat(desktop): backend model catalogs in toolset config — hermes tools parity

Completes the `hermes tools` parity gap: after picking an image/video
generation backend the CLI runs a model picker (e.g. FAL's multi-model
catalog with speed/strengths/price); the desktop toolset drawer now has the
same flow as a radio-card list.

- web_server: GET /api/tools/toolsets/{name}/models (catalog + current +
  default for the active or named provider row) and PUT .../model
  (validated write to image_gen.model / video_gen.model), reusing the CLI's
  plugin catalog helpers so GUI and `hermes tools` stay in lockstep
- desktop: ModelCatalogPicker in ToolsetConfigPanel — per-model cards with
  speed/strengths/price, in-use + default badges, disabled until the
  backend is the active one; provider selection now mirrors is_active
  locally so the catalog unlocks without a refetch
- tests: 3 backend endpoint tests (catalog shape invariants, persist +
  validation), 2 component tests, 2 API-contract tests; en/zh strings
2026-07-03 01:02:47 -07:00
Teknium
9e044cf795
feat(moa): per-preset fanout cadence — user_turn runs advisors once per user turn (#57591)
New preset key 'fanout': 'per_iteration' (default, unchanged behavior)
re-runs the reference fan-out whenever the advisory view changes — every
tool iteration. 'user_turn' runs the advisors ONCE per user turn and lets
the aggregator act alone for the rest of the tool loop — the original MoA
shape (upfront multi-model synthesis, then a single acting model), and the
obvious lever on MoA's wall/cost multiplier (advisor generation dominates
per-turn latency).

Implementation reuses the existing turn-scoped reference cache: in
user_turn mode the cache signature hashes only the prefix up to the LAST
user message, so mid-turn advisory-view growth doesn't change the key and
iteration 2+ is a cache HIT (advice reused, zero advisor spend, no
re-trace). A new user message changes the prefix and re-triggers the
fan-out. Unknown fanout values normalize to per_iteration.
2026-07-03 01:02:44 -07:00
Teknium
6eb39c2bbe
fix(opencode-go): heal stripped /v1 base_url so non-minimax models stop 404ing (#57585)
OpenCode Go serves minimax/qwen via Anthropic Messages (base URL without
/v1 — the SDK appends /v1/messages) and glm/kimi/deepseek/mimo via OpenAI
chat completions (base URL WITH /v1). The runtime stripped /v1 for
anthropic-routed models, and the TUI/desktop + gateway persisted that
stripped URL to model.base_url. Every later chat_completions model then
POSTed to https://opencode.ai/zen/go/chat/completions — a 404 (the
marketing site). Result: only minimax worked; glm/deepseek/kimi all 404ed.

- New normalize_opencode_base_url(): symmetric /v1 normalization —
  strip for anthropic_messages, re-append for chat_completions /
  codex_responses on opencode.ai hosts (heals persisted stripped URLs;
  custom proxy overrides untouched)
- Applied at all three former one-way strip sites (resolve_runtime_provider
  x2, switch_model)
- opencode_model_api_mode: all Qwen models on Go AND Zen now route via
  /v1/messages per current published endpoint tables (previously only
  qwen3.7-max on Go — qwen3.6-plus etc. would 404 the same way)
- Catalog refresh: Go gains deepseek-v4-pro/flash, glm-5.2,
  kimi-k2.7-code, minimax-m3, qwen3.7-plus; Zen gains glm-5.2,
  kimi-k2.7-code, minimax-m3, qwen3.7-plus

Reported by IndieSuperhuman on X: opencode-go 404s for any model other
than minimax.
2026-07-03 00:46:45 -07:00
Teknium
372f8195c7
fix(moa): default temperatures to unset — provider default, like single-model agents (#57440)
A single-model Hermes agent never sends temperature; the provider default
applies. MoA hardcoded reference_temperature=0.6 / aggregator_temperature=0.4,
and the coercion float(preset.get(key, 0.6) or 0.6) made unset IMPOSSIBLE to
express: absent, null, empty, and even an explicit 0 all collapsed to the
baked-in default. Every MoA advisor and aggregator therefore ran at 0.6/0.4
while the same model running solo used the provider default — silently
skewing solo-vs-MoA comparisons and overriding provider-tuned defaults.

- moa_config normalization: temperatures coerce to None when absent/blank/
  invalid (new _coerce_float_or_none); explicit values incl. 0 honored.
- moa_loop: _preset_temperature() resolves preset values; None flows to
  call_llm, which already omits the parameter when None (same contract as
  max_tokens). Aggregator still inherits the acting agent's own configured
  temperature when the preset doesn't pin one.
- conversation_loop (context-mode MoA): same resolution, no more hardcoded
  0.6/0.4 at the call site.
- DEFAULT_CONFIG preset + web_server payload models + docs updated: unset
  is the default, pinning stays available.
2026-07-03 00:22:49 -07:00
kshitijk4poor
e1a1dac848 fix(agent): enforce marker-strip invariant with a single terminal sweep (#57491)
Follow-up to the per-site strips from the review gate. The two copy-site
strips are correct but positional — a copy site added after the assembly
loops would re-leak _db_persisted into the child-session flush. Add a single
terminal sweep (_strip_persistence_markers) run once on the fully-assembled
compressed list so the invariant 'no compacted message leaves compress()
carrying a persistence marker' is structural, not dependent on copy-site order.

- agent/context_compressor.py: _strip_persistence_markers() called before
  compress() returns; helper docstring notes the sweep is the authoritative guard
- tests/agent/test_context_compressor.py: structural regression — neuter the
  per-site helper to a leaking copy, assert the terminal sweep still strips
- tests/run_agent/test_compression_persistence.py: pin the fixture assumption
  behind the exact-equality row-count assertion
2026-07-03 12:51:12 +05:30
nankingjing
3e204bd771 fix(agent): strip _db_persisted when assembling rotation compression transcript (#57491)
Shallow messages[i].copy() during context compression propagated the
_db_persisted marker from cached gateway incremental flushes into the
post-rotation compressed list. _flush_messages_to_session_db then skipped
every row when writing to the new child session, so gateway restarts
lost the compacted transcript (severe amnesia).

Strip the marker in _fresh_compaction_message_copy() and add regression
tests for rotation flush + compressor assembly.

Fixes #57491
2026-07-03 12:51:12 +05:30
kshitijk4poor
5e2b051e60 test(slack): give the MPIM reaction-guard test real teeth
The reaction-guard regression test defined a local _should_react lambda and
asserted it against itself — a tautology that would stay green even if the
production guard at _handle_slack_message reverted to (is_dm or is_mentioned),
re-introducing the unmentioned-MPIM reaction spam this PR fixes.

Replace it with a shared _reaction_guard helper plus a source-introspection
test that pins the production expression: asserts (is_one_to_one_dm or
is_mentioned) is present and (is_dm or is_mentioned) is absent. Mutation-checked
— reverting the adapter guard now fails the test.

Follow-up self-review finding on the salvage of #57339.
2026-07-03 12:34:53 +05:30