feat(moa): per-preset fanout cadence — user_turn runs advisors once per user turn - #57591
Merged
Conversation
…er user turn 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.
lenardhuebner88-rgb
pushed a commit
to lenardhuebner88-rgb/hermes-agent
that referenced
this pull request
Jul 3, 2026
…er user turn (NousResearch#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. (cherry picked from commit 9e044cf)
13 tasks
abhibansal-sg
added a commit
to abhibansal-sg/hermes-agent-fork
that referenced
this pull request
Jul 5, 2026
* feat(egress): iron-proxy credential-injection firewall for sandboxes
Rebuilds the iron-proxy egress feature cleanly onto current main. The
original feat/iron-proxy branch had diverged from main with an
unmergeable history (no usable merge-base after main history motion),
so the feature's content diff was re-applied onto a fresh main cut and
the three config/docs conflicts (commands.py status/egress, config.py
proxy vs computer_use, slash-commands.md) resolved keeping main's
content plus the egress additions.
Optional, off-by-default TLS-intercepting egress proxy for remote
terminal sandboxes. Sandboxes hold opaque proxy tokens; iron-proxy
swaps them for real provider API keys at the network boundary.
Includes the full review-cycle hardening:
- P0/P1/P2 rounds (GodsBoy, stephenschoettler, arshkumarsingh,
annguyenNous, maxpetrusenko, sxuff findings)
- v0.39 schema realignment + Docker bridge-bind/listener-role fixes
- Docker UX/enforcement hardening
Salvaged security fixes folded in with credit:
- Three P0 gaps (version-probe env scrub, Bitwarden ImportError
fail-closed, container-reuse egress-boundary) + Docker v29.5.3
empty-label edge — kuangmi-bit (#48073)
- P1/P2 (fail-closed replace.require:true, NODE_OPTIONS CA-flag
conflict, GPG checksum verify, threat-model wording) — Bartok9 (#48076)
Co-authored-by: kuangmi-bit <kuangmi@deeparchi.com>
Co-authored-by: Bartok9 <danielrpike9@gmail.com>
* fix(egress): close GOOGLE_API_KEY coverage gap + config/mappings write TOCTOU
Two correctness gaps surfaced in the review thread (texasich) that
survived the prior rounds:
- GOOGLE_API_KEY was warn-only while GEMINI_API_KEY was fail-closed,
despite both authenticating the same generativelanguage LLM endpoint
(auth.py treats them as interchangeable). An operator with only
GOOGLE_API_KEY set + fail_on_uncovered_providers got false coverage.
Added it to _LLM_SPECIFIC_NON_BEARER_PROVIDERS.
- write_proxy_config / write_mappings chmod'd AFTER os.replace, leaving
the token-bearing files briefly world-readable under a slack umask
(the 0o700 state dir mitigates but same-uid race remained). chmod the
temp file BEFORE the atomic replace, matching the CA-key write path.
Tests: assert GOOGLE_API_KEY in blocked tier; assert proxy.yaml +
mappings.json land at 0o600.
* chore(egress): drop committed infographic PNG from tree
Infographics live at their hosted URL and are referenced from the PR
body — they are never committed to the repo (repo-cleanliness rule).
Removes the 1.8MB infographic/iron-proxy-egress/infographic.png that
the original PR added to the tree.
* feat(egress): smoother UX — restart command, auto-restart on setup, .env key discovery
- Add `hermes egress restart` (stop-then-start) so applying a config /
token / Bitwarden-rotation change is one command instead of the
stop+start dance.
- `hermes egress setup` now offers to restart a running daemon after
rewriting config/tokens (asks on a tty; `--restart` / `--no-restart`
for non-interactive control), so changes take effect without the
operator remembering a manual restart.
- `setup` discovers provider keys kept only in ~/.hermes/.env, not just
exported shell vars — no more confusing 'no provider keys found' when
the keys plainly exist.
- Tests + docs updated.
* fix(cli): reliable interrupts, bounded exit, and exit feedback (#57000)
Three CLI reliability fixes:
1. Interrupt reliability: chat() only re-queued the user's interrupt
message when the turn result carried interrupted=True. When the agent
thread raced past its last interrupt check (or finished) before the
interrupt landed, the message was silently dropped — and the stale
_interrupt_requested flag left on the agent instantly aborted the
NEXT turn. Un-acknowledged interrupt messages are now re-queued as
the next turn and the stale flag is cleared (only when the agent
thread actually exited). The clarify-race path also parks the message
in _pending_input instead of dropping it.
2. Slow exit (5+ min): stdlib ThreadPoolExecutor workers are non-daemon
and joined unconditionally by concurrent.futures' atexit hook — even
after shutdown(wait=False). One wedged tool worker (abandoned after
interrupt/timeout) held the process open forever. Promoted
async_delegation's daemon executor to a shared tools/daemon_pool
module and adopted it in tool_executor (concurrent tool batches),
memory_manager (background sync), delegate_tool (child timeout wrapper
+ batch fan-out), and skills_hub (source fan-out). Added a 30s exit
watchdog (HERMES_EXIT_WATCHDOG_S) armed at _run_cleanup start as a
backstop for wedged cleanup steps.
3. Exit jank: after prompt_toolkit tears down the input/status bars the
terminal sat silent for the whole cleanup window, looking hung. Print
'Shutting down… (finalizing session)' immediately at exit start.
E2E: live PTY interrupt of a foreground 'sleep 120' terminal tool now
aborts in ~1s and the typed message runs as the next turn; wedged-worker
+ wedged-cleanup subprocess exits in 5.8s (watchdog) instead of hanging.
* chore(release): map ai-lab@foxmail.com to CrazyBoyM
Adds the AUTHOR_MAP entry for CrazyBoyM (ai-lab@foxmail.com) so the
contributor-attribution CI check passes when PR #55828's commits are
rebase-merged with authorship preserved.
* fix(codex): extend stale timeout for gateway-scale tool payloads
Lower the openai-codex stale-timeout floor from 25k to 10k estimated
tokens so Telegram/gateway sessions (~20k tools+instructions) are not
aborted at the generic 90s cutoff while Codex is still prefilling.
* test(codex): cover gateway-scale stale timeout floor and TTFB gate
* docs(codex): clarify stale-floor docstring reflects the 10k gate
The helper docstring described the typical ~15-25k gateway payload but
read as if that were the trigger range; the floor actually engages above
10k tokens. Clarify the prose to match the gate.
* fix(browser): guard Camofox snapshot/vision/images on private pages
Follow-up to #56874, which added the Camofox private-page SSRF guard
(_camofox_current_page_private_url) but wired it only into the Camofox
eval path (_camofox_eval). The other Camofox content-read tools —
camofox_snapshot, camofox_get_images, and camofox_vision — still read the
current page's accessibility tree / images / screenshot without the
guard, so on a non-local Camofox backend they can return the content of
an intranet or cloud-metadata page (e.g. 169.254.169.254) that the
terminal itself can't reach.
Apply the same guard, gated on _eval_ssrf_guard_active (non-local
backend, not a local sidecar, allow_private_urls unset) and fail-open on
probe failure, matching the eval-path guard and the main-browser
snapshot/vision guards. camofox_back is intentionally not changed: its
target is unknown until navigation completes, and the subsequent content
read is already guarded.
Adds regression tests covering the three read tools blocking on a private
page, the public-page pass-through, and the guard-inactive no-probe path.
* feat(image-gen): support Codex image inputs
* test(image-gen): cap Codex reference inputs
* refactor(image-gen): reuse shared image sniffer + raster allowlist in codex backend
Replace the plugin-local _IMAGE_MAGIC_MIME table + _sniff_image_mime
body with a delegation to agent.image_routing._sniff_mime_from_bytes,
the canonical magic-byte sniffer already used across the codebase, then
gate its result to the raster formats gpt-image-2's Responses
input_image actually accepts (png/jpeg/gif/webp).
The shared sniffer also recognizes SVG/TIFF/ICO; without the allowlist
those would pass local validation and be rejected server-side with an
opaque HTTP 400. Gating locally fails them cleanly as invalid_image_input.
Adds a regression test for SVG rejection.
Follow-up on top of @CrazyBoyM's #55828.
* fix(whatsapp): resolve LID sender IDs to phone numbers in bridge message payload
WhatsApp has migrated to Linked Identity Device (LID) format for user
IDs (e.g. 244645917392975@lid instead of 18505551234@s.whatsapp.net).
The bridge already resolves LIDs to phone numbers for its own allowlist
check via buildLidMap(), but the senderId field in the message payload
sent to the gateway still contained the raw LID. This caused the
gateway's WHATSAPP_ALLOWED_USERS check to reject all messages as
unauthorized, since the LID numbers don't match the phone numbers in
the allowlist.
Fix: resolve LID → phone in the senderId, senderName, and chatName
fields of the event payload before sending to the gateway, using the
existing lidToPhone mapping.
* chore: add AUTHOR_MAP entry for @ajmeese7 (#3219 salvage)
* fix(status): label provider as custom when config.yaml model.base_url is set
Salvage of the surviving hunk of #3296 by @Mibayy. The PR's gateway
_handle_provider_command hunk targets code removed on main (/provider was
absorbed into /model + /status, which already read model.base_url); the
hermes status mislabel was the remaining live symptom:
_effective_provider_label() only checked the legacy OPENAI_BASE_URL env var,
so a custom endpoint configured canonically in config.yaml still displayed
as OpenRouter.
* feat(gateway): add 'log' option to display.tool_progress
Salvage of #3459 by @keslerm, reimplemented against the restructured
progress-callback block in gateway/run.py (resolve_display_setting,
needs_progress_queue, thinking-relay). Duplicate PR #3458 by @dlkakbs was
submitted 4 minutes earlier with the same feature — both credited.
Co-authored-by: Dilee <uzmpsk.dilekakbas@gmail.com>
tool_progress: log keeps the chat silent and appends timestamped tool-call
lines to ~/.hermes/logs/tool_calls.log via a dedicated queue drained by an
async writer (RotatingFileHandler 5MB x 3, RedactingFormatter so secrets
never land on disk). Gateway-only by design; thinking_progress relaying and
the webhook gate are unaffected. /verbose now cycles
off -> new -> all -> verbose -> log.
* fix(i18n): add gateway.verbose.mode_log to all locale catalogs
* feat(commands): /compact alias + --preview/--dry-run flags for /compress (#3243 salvage)
Salvaged from PR #3243 by @Mibayy, reimplemented against current main
(the original diff targeted a removed gateway/run.py handler).
- /compact is now a first-class alias of /compress (CLI, gateway,
Telegram/Slack/Discord command lists, autocomplete) — also fixes the
dangling '/compact' references in gateway error messages
(gateway/run.py context-exhausted banners).
- --preview / --dry-run: report what WOULD be compressed (message
counts, token estimate, 'here [N]' boundary) without touching the
transcript. Flags coexist with the existing 'here [N]' / focus-topic
args on both the CLI and gateway surfaces via shared pure helpers in
hermes_cli/partial_compress.py.
- --aggressive (LLM-free hard truncation) is intentionally NOT
implemented: it would need its own transcript-persistence branch
outside the guarded _compress_context rotation machinery (#44794
data-loss class). The flag is recognized and returns an explanatory
message pointing at '/compress here [N]' and /undo instead of being
mis-parsed as a focus topic.
- locales: gateway.compress.aggressive_unsupported added to all 16
catalogs (parity test enforced).
- release.py: AUTHOR_MAP entry for contributor credit.
* feat(api-server): per-client model routing via model_routes (#3176 salvage)
Adds a no-code routing layer to the OpenAI-compatible API server so one
Hermes deployment can map different API clients to different
model/provider backends. Clients pick a backend by sending a configured
alias as the OpenAI 'model' field; unmatched values fall back to the
global model. Configured aliases are listed by GET /v1/models.
Precedence (highest first): session /model override > model_routes
route > global config. Route provider credentials resolve through
_resolve_runtime_agent_kwargs_for_provider (same seam as
channel_overrides); per-route api_key/base_url are upstream provider
credential overrides — never caller auth, never logged.
Salvaged and rebased from PR #3176 by @Mibayy onto current main.
* feat(config): extra HTTP headers for LLM API calls (#3526 salvage)
Named providers / custom_providers entries in config.yaml now accept an
extra_headers dict scoped to that endpoint — for reverse proxies, API
gateways, and custom auth schemes (e.g. Cloudflare Access service tokens).
- hermes_cli/config.py: normalize extra_headers on provider entries
(_normalize_custom_provider_entry + providers-dict translation), add
get_custom_provider_extra_headers /
apply_custom_provider_extra_headers_to_client_kwargs helpers keyed on
base_url (case/trailing-slash insensitive, no substring bypass —
mirrors the TLS helpers)
- hermes_cli/runtime_provider.py: surface extra_headers in the resolved
runtime for named custom providers (providers dict, legacy
custom_providers list, and the credential-pool path)
- run_agent.py / agent/agent_init.py: merge per-provider extra_headers
onto the OpenAI client default_headers at construction and on every
_apply_client_headers_for_base_url re-application (credential swaps,
rebuilds), most-specific level wins; OpenAI-wire only (native
Anthropic/Bedrock scoped out)
- agent/auxiliary_client.py: accept model.extra_headers as an alias of
model.default_headers for the global variant
- cli-config.yaml.example: documented commented example
- Header values are treated as secrets and never logged
Salvaged from PR #3526 by @jneeee, reimplemented against current main.
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
* feat(gateway): persist per-session /model overrides across gateway restarts
Per-session /model overrides (_session_model_overrides) were in-memory only,
so a gateway restart silently reverted every session to the global default
model. Persist the non-secret parts (model/provider/base_url ONLY — never
api_key) into the session entry in sessions.json and lazily rehydrate them
on first use after a restart, re-resolving credentials through the normal
runtime provider resolution.
- gateway/session.py: SessionEntry.model_override field with
sanitize_model_override() (allowlist: model/provider/base_url) applied on
both serialization and deserialization; SessionStore.set_model_override /
get_model_override accessors. reset_session() already creates a fresh entry,
so /new keeps its clear-on-reset semantics — a restart cannot resurrect an
override the user reset away.
- gateway/slash_commands.py: write-through at both /model set sites (text
command + picker) after storing the in-memory override.
- gateway/run.py: _rehydrate_session_model_override() called from
_resolve_session_agent_runtime(); in-memory state always wins, credentials
are re-resolved per provider (credential-less fallback on failure). Session
expiry finalization also drops the persisted override.
- tests/gateway/test_session_model_override_persistence.py: restart
round-trip, /new clearing, api_key-never-serialized (including tampered
sessions.json), rehydration + live-state precedence + credential-failure
degradation.
Salvaged from #3659 by @Git-on-my-level, narrowed to the restart-persistence
gap confirmed in triage.
* fix(desktop): parse multiline slash commands + hand degenerate payloads back
parseSlashCommand used /^(\S+)\s*(.*)$/ where `.` can't cross a newline and
`$` anchors end-of-string, so any slash command whose arg contained a newline
(/goal <multi-line text>, a skill command with a long pasted context) failed
the whole match, parsed as an empty name, and rendered "empty slash command"
while the payload vanished — cleared from the composer and absent from the
Up-arrow history ring, which only derives from sent user messages.
- name now splits on any whitespace ([\s\S]* arg), matching the CLI and the
gateway's split(maxsplit=1); multiline args flow to slash.exec intact
- the residual empty-name branch (bare "/", "/ text") restores the submitted
text to the composer draft instead of eating it
Fixes #41323. Fixes #55510.
* fix(desktop): call checkUpdates() in startUpdatePoller so version pill auto-populates
startUpdatePoller() only called checkBackendUpdates() — never checkUpdates().
The statusbar version pill reads $updateStatus (set by checkUpdates()), so the
commit-behind counter stayed null after restart. It only appeared when the user
manually clicked the pill, which triggered checkUpdates() via openUpdateOverlayFor.
Added void checkUpdates() in three places alongside the existing
checkBackendUpdates() calls:
- On startup in startUpdatePoller()
- In the 30-minute setInterval callback
- In the onFocus handler
checkUpdates() uses the Electron IPC bridge (local git check), not the gateway,
so no mode gating is needed. The existing $updateChecking atom guard prevents
double-fire on overlap.
Fixes #53079
* fix(desktop): restore remote artifact rendering
* style(desktop): fix import ordering + padding lint in remote-artifact files
* feat(desktop): /journey opens the memory graph overlay instead of printing text
* style(desktop): fix pre-existing import-order lint in use-prompt-actions
* fix(desktop): restore remote file picker attachments
* fix(desktop): read attachment previews local-first in remote mode
attachImagePath fetched its thumbnail through readDesktopFileDataUrl, which in
remote mode routes every read to the gateway fs bridge. Paperclip picks,
clipboard saves, and OS drops always produce paths on the LOCAL machine, so the
gateway read 404s — toasting "image preview failed" and dropping the thumbnail
even though the attach itself works (upload reads local bytes via the Electron
bridge). Read the local bridge first and fall back to the remote facade, which
still serves in-app drags from the remote project tree. Local mode is
unchanged (the facade already reads locally there).
Follow-up to #56572, which restored the remote paperclip picker and made this
path reachable from the picker as well.
* fix(desktop): load remote model options before session
Both Desktop picker surfaces (status-bar model menu, settings/onboarding
dialog) only asked the connected gateway's model.options once a session
existed; before that they fell back to the Desktop REST/global options, which
can't see virtual providers a remote gateway exposes — including the MoA
presets from #53817. Centralize the fetch rule in requestModelOptions(): prefer
the connected gateway whenever one exists (no session_id needed — the RPC
resolves disk config), REST only when no gateway is connected.
The status-bar MoA preset section now renders from the same model.options
payload (the virtual `moa` provider row) instead of the local /api/model/moa
REST config, so remote presets appear correctly; the row is filtered out of
the main provider groups so presets don't list twice. Preset selection keeps
the persistent switchTo path from #56417 and drops the vestigial session gate —
like regular model rows, a pre-session pick ships on the next session.create.
Fixes #53817.
Rebased and reconciled with #56417 (persistent MoA selection), which landed
after this PR was opened and covered its one-shot-/moa half.
* fix(terminal): set MSYS_NO_PATHCONV for Windows Git Bash subprocesses
Git Bash mangles native Windows command flags (/FO, /TN, /Create) into
bogus paths. Hermes terminal and background spawns now opt out by default
so tasklist, schtasks, and wmic work without manual prefixes.
Fixes #56700.
* test(terminal): cover MSYS_NO_PATHCONV defaults on Windows env builders
* fix(terminal): also set MSYS2_ARG_CONV_EXCL for MSYS2/Cygwin bash fallback
MSYS_NO_PATHCONV is honored by Git for Windows bash only. _find_bash's
final shutil.which fallback can return MSYS2-proper or Cygwin bash,
which ignore it and honor MSYS2_ARG_CONV_EXCL instead. Set both so argv
path conversion stays disabled regardless of which bash flavor spawns.
Also subsumes the cmd /c mangling in #56147.
* feat(desktop): collapse profile rail to a select past 13 profiles (#57306)
The colored-square rail stops scaling once a user racks up many profiles:
tiny drag targets and an endless horizontal scroll strip. Past a threshold
(13) the rail swaps the squares for a compact select dropdown — same active
tint + initial glyph, minus the drag-reorder / long-press-recolor / per-row
context menu that only make sense at small counts. Two render paths behind
one flag; the left default↔all toggle, the "+" create button, and Manage
stay put in both. Rename/delete/color remain reachable via Manage.
* Prevent deleted profile skeleton revival
* feat(auth): make xAI Grok OAuth device-code-only, drop loopback login
Replace the loopback/PKCE-callback server and manual-paste fallback with
the RFC 8628 device-code flow as the only xAI Grok OAuth login path. The
flow works in headless/SSH/container sessions with no 127.0.0.1 listener,
shrinking the local attack surface.
- Poll the token endpoint with server-provided interval, honoring
slow_down and expires_in; store tokens with auth_mode
oauth_device_code.
- Adaptive proactive refresh skew for short-lived device-code JWTs;
rotated tokens sync back to auth.json, the global root store, and the
credential pool (no refresh-token replay).
- Clear source suppression on successful re-login (CLI + dashboard) and
drop the duplicate dashboard pool entry so exactly one seeded
device_code entry exists.
- Use the shared device_code source name for consistency with the
nous/codex device-code providers.
- Desktop: remove the loopback OAuth flow states and dead type variants;
pkce providers' sign-in URL selection is unchanged.
- Docs (EN + zh-Hans) rewritten for device-code login; drop the deleted
--manual-paste flag from documented commands.
* fix(auth): remove stale loopback_pkce reference in xAI quarantine removal list
The terminal-refresh quarantine filtered in-memory entries on
source == "device_code" but built removed_ids from the deleted
"loopback_pkce" source name, so the revoked device-code entry was
never pruned from the persisted pool in auth.json. Also restores the
_print_loopback_ssh_hint test suite scoped to Spotify (the helper's
remaining caller) instead of deleting it wholesale.
* fix(desktop): skip ensureBackend after profile-delete teardown to prevent respawn loop
When the renderer sends a DELETE /api/profiles/{name} request, the IPC
handler tears down the profile's pool backend (or primary backend) via
prepareProfileDeleteRequest. However, the very next line calls
ensureBackend(profile), which spawns a fresh pool backend for the just-
deleted profile. The new backend's startup path calls ensure_hermes_home(),
which recreates the profile directory — defeating the deletion and leaving
the process as a zombie.
On the next Desktop restart the cycle repeats: the profile directory exists,
the Desktop spawns a backend, the backend recreates the directory after
deletion, and PIDs accumulate indefinitely.
Fix: make prepareProfileDeleteRequest return the torn-down profile name.
The IPC handler uses this to route the DELETE to the primary backend
instead of spawning a new pool backend for the deleted profile.
Fixes #52279
* fix(desktop): refresh profile rail after deletion (#49289)
* fix(desktop,tui-gateway,zai): stop thinking-off from reverting to medium
A Z.ai desktop user reported thinking reverting to medium after one turn,
burning ~200% of a week's credits in 4 days despite reasoning_effort: false
in config.yaml. Four compounding bugs:
- _session_info reported reasoning_effort "" for disabled reasoning,
indistinguishable from unset — the desktop adopted it after the first
turn, wiping its sticky "thinking off" pick so every later chat
reverted to the default effort.
- config.set key=reasoning always wrote agent.reasoning_effort to global
config.yaml, so every desktop model-menu selection (preset.effort ??
'medium') clobbered the user's configured value. Now session-scoped
like the messaging gateway's /reasoning, landing on
create_reasoning_override so lazily-built sessions keep it too.
- YAML `reasoning_effort: false`/`off`/`no` (boolean False) was coerced
to "" by every loader's `str(x or "")`, silently re-enabling thinking.
parse_reasoning_effort now treats False/"false"/"disabled" as
{"enabled": False}; loaders (tui gateway, gateway, cli, cron,
delegate) pass the raw value through. The desktop config reader also
crashed on the boolean (false.trim()), aborting voice/STT settings.
- The zai provider profile never sent thinking on the wire, and GLM-4.5+
defaults to thinking ON server-side — so disabling reasoning was a
silent no-op on direct Z.ai, the actual token burner. The profile now
emits extra_body.thinking {"type": "enabled"|"disabled"} for
thinking-capable GLM models, mirroring the DeepSeek profile.
Also: /new (session reset) now carries reasoning_config across the
rebuild like model_override; config.get reasoning prefers the session's
live value and maps a config False to "none"; Settings shows "Off"
instead of a blank select for hand-written false.
* fix(cli): stop profile-bound backends before deleting so rmtree converges
delete_profile stopped only the process named in gateway.pid, but a Desktop
app spawns a headless `serve`/`dashboard` backend per profile that holds the
profile's SQLite connection open and keeps writing sessions/WAL/sandbox files.
That backend is never in gateway.pid, so a CLI `hermes profile delete` run
while the Desktop app is up left it writing into the tree — rmtree's final
rmdir then failed with ENOTEMPTY (#47368 "Bug 2"), and pre-guard it also
resurrected the directory.
- _profile_bound_backend_pids(): find running Hermes backends bound to this
profile via a `--profile <name>` selector or a HERMES_HOME env resolving to
the profile dir. Tightly scoped — current-user only, backend subcommands
(serve/dashboard/gateway) only so an interactive chat is never killed, and
never this process or its ancestors.
- _stop_profile_backends(): terminate them (graceful, then force), best-effort
so it can never make delete worse.
- _rmtree_with_retry(): a few spaced retries absorb the ENOTEMPTY / Windows
file-lock race from a just-terminated writer's in-flight -wal/-shm/sandbox
writes instead of failing the whole delete on a race the next attempt wins.
Complements the recreation guard (deleted profiles no longer reappear) and the
Desktop teardown-before-delete flow; this is the CLI-side convergence fix for a
delete run while a Desktop-managed backend is live.
Part of #47368.
* fix(tui_gateway): route setup.runtime_check and setup.status to RPC pool
setup.runtime_check and setup.status are polled by the Desktop frontend on
connect and periodically (use-status-snapshot → evaluateRuntimeReadiness), but
neither was in _LONG_HANDLERS — so dispatch() ran both inline on the WS reader
thread. Under GIL pressure from concurrent agent turns (terminal I/O, large
output, background-process completions) either can block for seconds:
- setup.runtime_check → resolve_runtime_provider() (config read, auth check,
may probe the provider endpoint)
- setup.status → _has_any_provider_configured() (provider config + credential
scan)
While either blocks the reader thread the WS read loop can't service later
requests; the frontend RPC timeout fires, the client drops the socket, and the
lost setup.runtime_check response reads as ready=false — a false "needs setup"
/ "Settings failed to load" even though the provider is configured.
Route both to the RPC pool (same precedent as #55545's session.list/pet.info/
process.list). The handlers are read-only and pool writes go through the
lock-guarded write_json, so there's no ordering or safety concern.
Test asserts all 5 frontend-polled RPCs are pool-routed.
Co-authored-by: izumi0uu <izumi0uu@gmail.com>
* chore(release): map yingliang-zhang in AUTHOR_MAP for #57335
* fix(usage): capture reasoning_tokens from completion_tokens_details on chat_completions (#57340)
normalize_usage only read output_tokens_details.reasoning_tokens (the
Responses API shape). Chat Completions providers — OpenAI, OpenRouter,
DeepSeek, and every OpenAI-compatible proxy — report it under
completion_tokens_details.reasoning_tokens, so reasoning_tokens was 0 for
every chat_completions reasoning model: hidden thinking was invisible in
session accounting, MoA traces, and the eval's per-task token columns.
Measured impact (HermesBench MoA run on deepseek-v4-flash, 4,828 advisor
calls): reasoning_tokens showed 0 everywhere while individual calls burned
up to 21.5K hidden thinking tokens to emit ~500 visible tokens. Verified
live against OpenRouter: deepseek-v4-flash returns
completion_tokens_details.reasoning_tokens=61 for a 74-completion-token
call; the field was simply never read.
Responses-shape reads are unchanged; the new read only fires when the
Responses shape yielded nothing.
* fix(slack): keep blank-line-separated ordered items in one rich_text_list
When a Markdown ordered list has blank lines between items (common in
LLM-authored content), the list run loop breaks on each blank line.
Slack numbers each rich_text_list independently, so N items produce N
lists each starting at 1.
Skip blank lines inside the list run as soft separators instead of
breaking, so ordered items stay in one rich_text_list and Slack renders
the correct numbering.
Fixes #57076
* fix(slack): guard blank-line list continuation on next-item lookahead
Refine the blank-line handling so a blank line only continues a list run
when the next non-blank line is another list item. This keeps a list ->
paragraph -> list sequence as three separate blocks and matches the
contiguous-list layout for mixed/nested lists (one rich_text block, split
into sub-lists by (indent, ordered)), rather than emitting a separate
block per item.
Adds regression tests for the mixed blank-separated layout and the
list->paragraph->list boundary.
* refactor(slack): extract _is_list_line helper for list-marker checks
Deduplicate the '_BULLET_RE.match or _ORDERED_RE.match' idiom used at the
list-run entry guard and the blank-line lookahead into a single helper, so
adding future marker types is a one-point change. Pure refactor, no
behavior change (22 block_kit tests still pass).
* fix: refresh NVIDIA featured models
* fix(agent): honor live vLLM context limits on local endpoints
Reconcile stale local disk cache against live vLLM/Ollama max_model_len
probes, probe local servers before the llama hardcoded default, parse
vLLM max_model_len overflow errors, and surface the non-agentic Hermes 3/4
warning at agent init on gateway/TUI.
Sub-64K live probes are returned for startup rejection but are not
persisted to the context cache — preserving the 64K minimum-context
contract instead of normalizing undersized windows as valid config.
(cherry picked from commit c3a02db4fd9d57b7b0eb2732de91f8334d311aa5)
* test(agent): cover local vLLM context-length resolution
Add regression tests for vLLM max_model_len error parsing, stale local
cache reconciliation, live probes over llama defaults, and the 64K minimum
guard on persistent cache writes.
(cherry picked from commit 1cb47ef437de7ce289cb358e8d6b89e9194b43ed)
* fix(gateway): close webhook sessions on delivery completion so prune can reap them
Webhook deliveries created a unique one-shot session (delivery_id baked into
the session key at gateway/platforms/webhook.py:668) but the adapter fired
handle_message via asyncio.create_task WITHOUT ever ending the session
(webhook.py:713, pre-fix). Nothing else closes it: the gateway caches/expires
the agent per session_key but never calls end_session for the webhook path,
and _end_session_on_close teardown doesn't run for these fire-and-forget tasks.
SessionDB.prune_sessions (hermes_state.py:4965) only deletes rows WHERE
ended_at IS NOT NULL. So every webhook session stayed with ended_at NULL ->
unprunable -> unbounded state.db growth. This was the primary driver of the
SQLite lock-contention gateway outage.
Fix: wrap the delivery in _run_delivery_and_close, which awaits
handle_message and then (in finally, so failures still reap) calls
_end_webhook_session -> SessionDB.end_session(session_id, 'webhook_complete').
This mirrors how cron closes its session with 'cron_complete'
(cron/scheduler.py:3065). end_session is first-reason-wins and no-ops on an
already-ended row, so it never clobbers a compression/agent_close reason.
Adds tests/gateway/test_webhook_session_close.py asserting the invariant
(a completed webhook session has ended_at set + is prunable), including the
error-path case, against a real SessionStore + SessionDB.
* chore: map gumclaw@gumroad.com in AUTHOR_MAP for PR #57322 salvage
* refactor(gateway): add SessionStore.peek_session_id public accessor for webhook close
Replace the webhook delivery-close path's direct reach into private
SessionStore._entries (which also bypassed the store lock) with a public,
lock-held peek_session_id(session_key) accessor. Mirrors the existing
lookup_by_session_id inverse helper. Keeps a getattr fallback for older
stores / test doubles. Adds a unit test for the accessor.
* fix(agent): resolve review findings on vLLM local-context salvage
Salvage review of #56431 surfaced one Critical + two Warning issues; fix
them on top of the contributor's cherry-picked commits:
1. Critical — duplicate non-agentic warning on the interactive CLI. The new
agent_init warning fires on every platform, but cli.py show_banner()
already warns on CLI (richer output + /model hint), so a CLI user saw the
warning twice per startup. Guard the agent_init emit to skip platform=="cli"
— it now fills exactly the gateway/TUI gap the PR intended, no duplication.
2. Warning — vLLM error-parse regex under-matched. The patterns required a
literal space before the number, so "max_model_len: 32768", "=32768",
"(32768)", and "... is 32768" all returned None. Broaden both patterns to
accept :/=/(/ 'is' delimiters. Add a parametrized test over all delimiter
variants.
3. Warning — per-call live probe latency on local endpoints. The new
reconcile-on-hit + pre-defaults step-7 probe made every local resolution
fire a synchronous network probe (banner + /model switch + compressor
update_model each within one startup). Add a 30s in-process TTL cache
keyed by (model, base_url) around _query_local_context_length so back-to-
back resolutions reuse one round-trip; not persisted to disk, so the
reconcile freshness contract (re-probe after restart) is preserved. Add an
autouse fixture clearing the cache between tests + TTL coverage.
Tests: 148 passed (was 138). ruff clean.
* chore(release): add infinitycrew39 to AUTHOR_MAP (#56431 salvage)
* chore: add trismegistus-wanderer to AUTHOR_MAP for PR #31856 salvage
* fix(dashboard): disable ws keepalive ping on loopback to survive event-loop stalls
Desktop/dashboard WebSocket connections drop during long agent operations
(delegate_task subagents, large model outputs) when the uvicorn event loop is
GIL-starved for minutes. Root cause: uvicorn's ws keepalive ping runs on the
SAME event loop as agent turns. A single synchronous GIL-holding call on a
worker thread (a regex/scrub over a large output, or a long subagent turn)
freezes the loop, so it cannot process the incoming pong within ws_ping_timeout
and uvicorn closes an otherwise-healthy connection (#53773: 'event loop stalled
226.3s'; #48445/#50005). Loosening the timeout only raises the threshold — a
multi-minute stall sails past any finite window.
The keepalive ping exists to detect half-open connections (reverse-proxy 524,
dropped tunnels), which cannot happen on loopback: there is no network or proxy
in the path, and a dead local client tears the socket down with a real FIN/RST
that starlette surfaces as WebSocketDisconnect regardless of the ping. So on
loopback the ping provides ~no liveness value while actively killing
recoverable stalls — disable it entirely (ws_ping_interval/timeout=None).
Non-loopback (public) binds sit behind a Cloudflare Tunnel where half-open IS a
real failure mode, so the ping stays at 20/20 to detect it.
Empirically verified (real uvicorn + websockets peer): with ws_ping=None the
server never closes a silent peer during an 8s window; with the pre-fix 2s/2s
window uvicorn closes it. A genuinely-dead client still fires the
WebSocketDisconnect reap path regardless of the ping.
Note: this fixes the local Desktop case (the OP's scenario). A remote Desktop
over an authenticated public dashboard route (McCalebTheSecond's comment) keeps
the ping and needs the deeper GIL-hotspot fix — tracked separately.
Closes #53773
* fix(agent): self-review follow-ups on vLLM local-context salvage
Self-review (ruff+ty lint diff = 0 net-new; 2-agent deep review) surfaced one
Warning + comment-accuracy nits; no Critical:
- W1: the local-probe TTL cache memoized None (probe failure) for 30s, so a
probe that failed during a startup race would suppress a legit retry once
the server came up. Cache only positive results — still fully bounds the
hot-path probe rate (reachable servers cache their value) while an
unreachable one re-probes on the next call. Add a regression test asserting
a None result is NOT cached (retry re-probes); mutation-verified.
- Tighten the platform-guard comment: gateway/TUI/cron already construct with
quiet_mode=True (gated by `not agent.quiet_mode`), so the guard's active job
is CLI dedup vs show_banner, not "filling the gateway/TUI gap" as originally
worded.
Verified not-issues (per review): positive-value 30s cache does not break the
reconcile-after-restart freshness contract (restart = fresh process, empty
cache); cache key is collision-safe; platform guard is correct in both
directions (no runtime path leaves platform None on a non-CLI surface).
Tests: 149 passed. ruff clean; ty 0 net-new vs base.
* fix(gateway): keep idle cached agents alive until session actually expires
The idle-TTL sweep (_sweep_idle_cached_agents) was evicting agents
as soon as they passed _AGENT_CACHE_IDLE_TTL_SECS, even when the
session hadn't expired yet. In daily-reset mode the reset can fire
hours after the last user message — evicting the agent early means
the session-expiry watcher has no agent in cache to call
on_session_end() with, so memory providers miss the live transcript.
Now the sweep checks the session store before evicting: if the
session still exists and hasn't expired, the agent stays in cache
so the expiry watcher can tear it down properly later.
When the session store is unavailable or throws, falls back to the
original eviction behavior (safe default).
Fixes: #11205
* fix(gateway): complete on_session_end coverage across all eviction paths
Follow-up to the cherry-picked #31856 fix. The contributor's guard defers
idle-TTL eviction until the session store reports the session expired, so the
expiry watcher can tear the agent down and fire MemoryProvider.on_session_end()
with the live transcript. Two gaps remained:
1. Memory-leak regression for mode='none' sessions. _is_session_expired()
returns False forever for the 'none' reset policy, so the naive guard would
never idle-evict those agents — reopening the unbounded-cache leak the idle
sweep (#11565) exists to relieve. Added SessionStore.is_session_finalizable()
(a public predicate: will the expiry watcher EVER finalize this session?) and
gate the deferral on it. mode='none' agents fall through to soft eviction as
before.
2. on_session_end still dropped on the LRU-cap path. Both cache-pressure paths
(_enforce_agent_cache_cap and _sweep_idle_cached_agents) soft-evict via
_release_evicted_agent_soft, which by design does NOT fire on_session_end.
If cache pressure evicts a finalizable-but-not-yet-expired agent before it
expires, the watcher later finds no cached agent and the hook is skipped.
Added _commit_memory_before_soft_evict(): at LRU eviction, if the session is
finalizable and not yet expired, commit end-of-session extraction via the
live agent's own (fully-scoped) memory manager using commit_memory_session()
— extraction WITHOUT provider teardown, so the eviction stays soft and a
resumed turn keeps working. Skipped for mode='none' (no missed boundary to
compensate) and expired sessions (the watcher tears those down directly).
This closes #11205 for ALL eviction paths and reset policies, not just the
idle-sweep + finite-policy case, while preserving the soft-eviction
resumability contract (never calls close() on a live session).
Tests: 5 new cases in test_agent_cache.py (mode='none' still reaped, LRU-cap
commits for finalizable / skips for none, real is_session_finalizable
predicate); all mutation-checked. Contributor's original 2 tests updated to
assert the finalizable path explicitly.
* fix(providers): pass extra headers to model discovery
* refactor(providers): dedupe extra_headers normalizer + key picker groups by headers
Follow-up to @helix4u's #57336 salvage. Two review findings:
- W1: model-picker grouped custom-provider rows by
(api_url, credential, api_mode) but NOT extra_headers. Entries sharing a
URL+credential+api_mode yet declaring different headers (e.g. per-tenant
routing behind one proxy) collapsed into one row and probed /models with
whichever header set was seen first (order-dependent). Fold a canonical
header identity into group_key so distinct header-authed endpoints stay
separate; drops the now-dead first-non-empty merge branch.
- W2: the extra_headers stringify+None-filter comprehension existed in 5
copies (config.py x2, runtime_provider.py, model_switch.py, models.py).
Extract one shared hermes_cli.config.normalize_extra_headers primitive;
all sites now call it.
Tests: +normalize_extra_headers unit tests, +regression test proving two
same-endpoint entries with different headers stay distinct and each probes
with its own headers. 223 targeted tests pass; ruff clean.
* fix(desktop): let settings content use full pane width
Remove the max-w-4xl wrapper from SettingsContent so every settings
page can use the available overlay width.
* fix(desktop): extend profile startup REST timeouts (#48504)
* fix(desktop): extend startup long-timeout to the whole boot data burst
Broadens Tranquil-Flow's profile-startup timeout fix (#48518) from getProfiles
+ refreshActiveProfile to the rest of the calls the desktop fires during
connect: /api/config, /api/config/defaults, /api/model/info, /api/model/options,
/api/cron/jobs. On a profile-heavy or remote install any of these can exceed
the 15s DEFAULT_FETCH_TIMEOUT_MS while the backend is alive-but-busy (e.g.
list_profiles walks the skill tree per profile), surfacing as the spurious
"Timed out connecting to Hermes backend after 15000ms" that hangs the UI
(#48504).
Uses the surgical per-call mechanism (renamed STARTUP_PROFILE_REQUEST_TIMEOUT_MS
→ STARTUP_REQUEST_TIMEOUT_MS) rather than raising the global default (the
alternative in #48526): the liveness poll /api/status and all interactive/
runtime calls keep the short default, so a genuinely-dead backend is still
detected fast and the boot readiness probe (waitForHermes) is untouched.
Supersedes #48518 (carried as the base commit) and #48526 (global-default
raise). Fixes #48504.
Co-authored-by: YapBi <129007007+HeLLGURD@users.noreply.github.com>
Co-authored-by: Tranquil-Flow <66773372+Tranquil-Flow@users.noreply.github.com>
* fix(webhook): close per-delivery session at the true end of the run (#57423)
The merged webhook session-close fix (#57370, salvaging #57322) wrapped
handle_message in a try/finally — but BasePlatformAdapter.handle_message
is fire-and-forget: it spawns _process_message_background and returns
before the agent run starts. The finally-close therefore ran BEFORE
get_or_create_session created the session row, found no session_id, and
silently no-op'd — the ghost-session leak persisted on the real path.
(The shipped test masked this by stubbing handle_message with a fake
that created the row synchronously.)
Move the close to an on_processing_complete override — the lifecycle
hook the base class fires at the TRUE end of the run, on the success,
failure, and cancellation paths alike. Empirically verified through the
real fire-and-forget pipeline: before, ended_at stayed NULL; after,
ended_at is set with end_reason=webhook_complete and the row is
prunable.
Tests now stub only the runner-side _message_handler (the seam the live
gateway injects) so handle_message / _process_message_background /
on_processing_complete all run for real; adds an AsyncSessionDB-facade
coverage test for the coroutine-await branch.
* fix(dump): flag API keys visible only to the shell, not the managed backend
hermes debug share reads os.getenv — the invoking terminal's environment — but
launchd/systemd and the desktop-spawned `serve` backend load credentials from
~/.hermes/.env, not the login shell. A key exported in the shell but absent
from .env is invisible to the backend, yet the dump printed a bare "set",
sending support down a phantom "the key is configured" path.
This was the actual trap behind a "Desktop has no web_search / no tools"
report: FIRECRAWL_API_KEY was a shell export (so `debug share` in a terminal
read "firecrawl set") but not in .env, so the launchd backend's
check_web_api_key returned False and web_search was gated off — which a
contributor then misdiagnosed as a missing `desktop` platform registration.
The dump now annotates any key set in-process but missing from ~/.hermes/.env
with "(shell only — not in .env; managed/desktop backend may not see it)" so
the mismatch is obvious instead of hidden behind "set".
* feat(skills): add security/unbroker (autonomous data-broker removal)
unbroker finds where a consenting person's info is exposed across data
brokers and people-search sites and files the removals, running as far as
each site allows and handing only genuinely human-only steps (hard CAPTCHA,
gov-ID, phone, fax) back as an end-of-run digest.
- Deterministic stdlib CLI (scripts/pdd.py) owns config, dossiers+consent,
the broker DB, tier planning, the ledger, email, and the autonomous
action queue; the agent scans/submits with native tools (web_extract,
browser_*, delegate_task, cronjob, terminal).
- Verify-before-disclose, least-disclosure (never volunteers SSN), consent
gate, opaque ids, optional age-at-rest encryption, file-locked ledger.
- Jurisdiction-aware (CCPA/CPRA, GDPR, generic); CA DROP one-shot covers
the state registry (~545) in a single request; BADBOOL + curated
people-search coverage; scheduled re-scan for re-listing.
- No CAPTCHA-solving services or anti-bot bypass; browser email mode needs
no stored password.
- 85 hermetic tests (tests/skills/test_unbroker_skill.py; SMTP/IMAP via
injected fakes, registry via CSV fixtures). Ships placeholder data only.
Broker dataset adapted from BADBOOL (Yael Grauer, CC BY-NC-SA 4.0).
* docs(unbroker): point README image link at hermes-agent; sync test count (85)
* feat(desktop): cap overlay inner-page width at 75rem
Add a shared PAGE_MAX_W (1200px) and center OverlayMain within its pane
so settings and command center bodies stay readable instead of sprawling
on wide/ultrawide displays.
* fix(desktop): stop macOS Tahoe misplacing the traffic lights
On macOS Tahoe (Darwin 25+), a nonzero titleBarOverlay height makes
setWindowButtonPosition() miscalculate the native traffic-light position
(electron#49183), shoving the lights into the left titlebar tools. Pass
height 0 there so the lights land at the configured inset; the renderer
paints its own drag strips, so nothing is lost. Pre-Tahoe is unchanged.
Gate on the truthful Darwin kernel major (25 = Tahoe) rather than the
product version, which macOS reports as 16 or 26 depending on build SDK.
* fix(desktop): clear stale active todos on turn end AND on rehydration
A turn that ends without a final `todo` update left the composer "Tasks N/M"
panel pinned with its last item stuck pending/in_progress, and it survived
restarts because the panel is read back from stored session history.
Two coupled fixes (the first alone is undone by the second path):
- Turn end: clear a still-active todo list on `message.complete` and on a
terminal `error` (new `clearActiveSessionTodos` — active lists only; a
finished list keeps its short linger so the last checkmark still lands).
- Rehydration: `hydrateFromStoredSession` runs *after* a turn completes, so an
"active" stored list is stale, not in-flight. It now restores only a
*finished* list (via new `todosForHydration`) and drops anything still
active — otherwise it re-pinned the panel right after the turn-end clear and
resurrected it on every restart.
Salvages #52996 (@0disoft): the fix shape (clearActiveSessionTodos on turn
completion, preserving the finished-list linger) is carried forward and ported
onto the current use-message-stream/ folder split (gateway-event.ts), then
extended to the rehydration path per review.
Co-authored-by: 0disoft <rodisoft1@gmail.com>
* fix(unbroker): suppress-first for PeopleConnect (deletion undoes suppression)
PeopleConnect is the exception to deletion-beats-suppression: "DELETE MY
USER DATA" also deletes suppressions on file, and deletion does not stop the
people-search sites from showing you (public records re-list). Suppression
is the effective lever and must be maintained.
- intelius.json: deletion.prefer=false; playbook/quirks/notes rewritten with
the verbatim privacy-center language; delete is the data-purge-only path.
- autopilot: honor deletion.prefer -> prefer_suppression when false.
- methods.md / SKILL.md / README: exception called out.
- tests updated + prefer-flag routing test (86 tests).
* fix(file-tools): preserve container paths for docker file ops (#56637)
* fix(config): preserve owner on atomic writes (#56644)
* fix(desktop): use gift codicon for update-available toast
Add optional notification icon override and use codicon-gift on the
update-ready toast so it reads as a present rather than generic info.
* fix(desktop): use symbol-namespace codicon for Model settings nav
* fix(desktop): cancel downloads triggered by link-title fetch window
The hidden BrowserWindow used by fetchLinkTitle to scrape page titles
had no will-download handler on its session. When a link artifact URL
responds with Content-Disposition: attachment, Electron fires will-download
and the file is saved for real — explaining the spurious download on the
Artifacts page.
Add guardLinkTitleSession() (parallel to the existing audio-mute guard for
#49505) that installs a will-download handler which immediately cancels
every download item on the hermes:link-titles session. Call it from
getLinkTitleSession() right after the request-type blocklist is wired up.
* fix(slack): MPIMs (group DMs) obey shared-surface mention gating + reaction guard
Group DMs (MPIMs) were classified as DMs and thereby exempted from every
operator control that shared surfaces are supposed to honor: allowed_channels,
require_mention, strict_mention, free_response_channels, and the reaction
guard. Symptom: the bot added :eyes:/:white_check_mark: to unmentioned MPIM
messages and still invoked the agent (which then returned NO_REPLY) instead of
the gateway dropping the event before model execution. Removing an MPIM from
allowed_channels did not disable it.
Root cause is the DM classification at adapter.py:
is_dm = channel_type in {"im", "mpim"}
used for BOTH routing exemptions and reaction gating. An MPIM is a shared
surface (multiple humans can see and trigger the bot), not a private 1:1 DM,
so it must be gated like a channel.
This behavior was introduced/reinforced by a trail of Slack group-DM PRs:
- #4633 fix(slack): treat group DMs (mpim) like DMs + reaction guard
- #54632 fix(slack): subscribe to message.mpim + mpim scopes so group DMs work
- #54663 fix(slack): group DMs work OOTB + reinstall nudge
#54632/#54663 correctly made MPIM messages *reachable*; #4633 over-reached by
giving them the DM mention/reaction *exemptions*. This corrects only that
over-reach.
Fix (minimal): introduce `is_one_to_one_dm = channel_type == "im"` and key the
two EXEMPTION sites off it instead of `is_dm`:
- mention/allowlist gating block (`if not is_one_to_one_dm and bot_uid:`)
- reaction guard (`(is_one_to_one_dm or is_mentioned)`)
`is_dm` is intentionally retained for session/thread scoping and chat_type
labeling, where treating an MPIM as a persistent multi-party conversation is
correct — only the mention/reaction exemptions were wrong.
Docs: slack.md now distinguishes 1:1 DMs (mention-exempt) from group DMs
(shared surface; obey require_mention/strict_mention/allowed_channels/
free_response_channels; reactions only when @mentioned).
Tests: +7 in test_slack_mention.py (MPIM unmentioned dropped under
require_mention and strict_mention; MPIM mentioned processed; MPIM off
allowed_channels dropped; MPIM in free_response opted in; 1:1 IM still exempt;
reaction guard drops unmentioned MPIM). Updated _would_process to model the
is_one_to_one_dm gating + strict_mention. 72 passed.
* 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.
* 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
* 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
* 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.
* 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.
* 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.
* 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
* fix(browser): retry next candidate when debug launch exits early
* 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
* 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.
* 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>
* 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.
* 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…
zebadee2kk
added a commit
to zebadee2kk/hermes-agent
that referenced
this pull request
Jul 5, 2026
…_server.enabled is false (#3) * fix(desktop): guard configured-cwd override against active sessions Follow-up to the #39227 salvage: config refreshes fire mid-session too (gateway events, settings saves), so applying terminal.cwd unconditionally would yank the workspace out from under an attached session. Gate the override on activeSessionIdRef like the sibling reasoning/tier settings, keep branch refresh on the live cwd, and add coverage for the active-session path. Also lint-polish the new test file (typed config mock, prettier formatting). * chore: add AUTHOR_MAP entry for @sahibzada-allahyar (#39227 salvage) * fix(terminal): stop stripping CLAUDE_CODE_OAUTH_TOKEN from spawned subprocesses (#56935) CLAUDE_CODE_OAUTH_TOKEN is set and owned by the user's Claude Code install (subscription OAuth), not a Hermes-managed inference credential — Claude subscription auth is not a working Hermes provider path. Blocklisting it broke agent-spawned claude CLIs: with no token in the child env, claude fell through to the shared macOS Keychain / ~/.claude/.credentials.json store and, on auth failure, cleared it — logging the user out of their interactive Claude sessions and the desktop app. Exempt it from _HERMES_PROVIDER_ENV_BLOCKLIST (it arrives via the anthropic registry entry, so discard explicitly with rationale). ANTHROPIC_API_KEY / ANTHROPIC_TOKEN and every other provider credential remain stripped, and the GHSA-rhgp-j443-p4rf fail-closed passthrough guard is unchanged for everything still on the blocklist. Fixes #55878 * feat(delegation): unify concurrency caps — deprecate max_async_children (#56955) delegation.max_concurrent_children is now the single cap for both a batch's parallelism and concurrent background delegation units. - _get_max_async_children() delegates to _get_max_concurrent_children(); a leftover max_async_children key logs a one-time deprecation warning - config v32→33 migration removes the stale key, folding a raised max_async_children into max_concurrent_children (max wins, no lost headroom) - capacity error messages now point at max_concurrent_children - pool-at-capacity sync fallback now attaches an explanatory note so the model/user know why the call blocked instead of dispatching async Previously users who raised max_concurrent_children (e.g. to 15) still hit the invisible default-3 async cap: the 4th background delegate_task silently ran inline, blocking the turn with no signal. * fix(webhook): remove unused payload from delivery state * fix(webhook): remove unused payload from delivery state * chore: add AUTHOR_MAP entry for @VolodymyrBg (#2861 salvage) * fix(config): accept 'on' as truthy for env flags via shared env_var_enabled helper Salvage of #2863 by @aydnOktay, reimplemented against current main using the existing utils.env_var_enabled / TRUTHY_STRINGS helper instead of per-site tuple edits. Covers the 7 gateway/config.py env-flag sites that still rejected 'on' (WHATSAPP_ENABLED, SIGNAL_IGNORE_STORIES, MATRIX_ENCRYPTION, API_SERVER_ENABLED, WEBHOOK_ENABLED, MSGRAPH_WEBHOOK_ENABLED, BLUEBUBBLES_SEND_READ_RECEIPTS) plus HERMES_DESKTOP gating in read_terminal/close_terminal. The PR's approval.py HERMES_YOLO_MODE portion is already on main via is_truthy_value. * test: env-flag 'on' truthy behavior contract (#2863 follow-up) * feat(gateway): per-channel model and system prompt overrides (Fixes #1955) - config: ChannelOverride + PlatformConfig.channel_overrides - run: _resolve_model_for_channel, _get_system_prompt_for_channel, channel provider runtime - tests: channel overrides + config guard for bare runner; conftest asyncio fix; slack/whatsapp warning filters Made-with: Cursor * feat(gateway): per-channel model and system prompt overrides (Fixes #1955) - ChannelOverride + channel_overrides on PlatformConfig - Resolve model/runtime: session /model, then channel_overrides, then global - Thread/parent channel lookup; bridge discord.channel_overrides from YAML - Drop unrelated test and delegate_tool changes from PR scope * feat(gateway): per-channel model and system prompt overrides (Fixes #1955) - ChannelOverride + channel_overrides; session /model > channel > global - Thread/parent lookup; YAML bridge for discord.channel_overrides - Guard channel_overrides when config lacks platforms (test mocks) - Add sampiyonyus@gmail.com to AUTHOR_MAP * fix(email): harden adapter against malformed IMAP responses Salvage of #2794 by @CharmingGroot, ported to the relocated plugins/platforms/email/adapter.py: - Guard raw_email = msg_data[0][1] against IndexError/TypeError and non-bytes payloads. UIDs are added to _seen_uids before fetch, so an exception mid-batch permanently skipped every remaining message in the batch — now the bad message is logged and skipped instead. - Message-ID domain generation falls back to 'localhost' when EMAIL_ADDRESS lacks '@' (now via a shared _message_id_domain() helper covering all 3 send paths; the PR fixed 2 of 3). * feat(api-server): inline MEDIA: image tags as base64 data URLs for remote frontends Salvage of the surviving piece of #2696 by @tarunravi. The PR's other two changes (tool progress streaming, SSE None-sentinel fix) were independently superseded on main by the structured hermes.tool.progress SSE events and the rewritten queue-drain loop. Remote OpenAI-compatible frontends can't read server-local file paths, so MEDIA:<path> tags (browser screenshots, generated images) were dead text. _resolve_media_to_data_urls() now inlines small (<=5MB) local images as markdown data URLs across all four response surfaces: chat completions (non-streaming), session chat, session chat stream final event, and the Responses API. Non-image, missing, or oversized paths pass through untouched. * fix(cli): reliable interrupts, bounded exit, and exit feedback (#57000) Three CLI reliability fixes: 1. Interrupt reliability: chat() only re-queued the user's interrupt message when the turn result carried interrupted=True. When the agent thread raced past its last interrupt check (or finished) before the interrupt landed, the message was silently dropped — and the stale _interrupt_requested flag left on the agent instantly aborted the NEXT turn. Un-acknowledged interrupt messages are now re-queued as the next turn and the stale flag is cleared (only when the agent thread actually exited). The clarify-race path also parks the message in _pending_input instead of dropping it. 2. Slow exit (5+ min): stdlib ThreadPoolExecutor workers are non-daemon and joined unconditionally by concurrent.futures' atexit hook — even after shutdown(wait=False). One wedged tool worker (abandoned after interrupt/timeout) held the process open forever. Promoted async_delegation's daemon executor to a shared tools/daemon_pool module and adopted it in tool_executor (concurrent tool batches), memory_manager (background sync), delegate_tool (child timeout wrapper + batch fan-out), and skills_hub (source fan-out). Added a 30s exit watchdog (HERMES_EXIT_WATCHDOG_S) armed at _run_cleanup start as a backstop for wedged cleanup steps. 3. Exit jank: after prompt_toolkit tears down the input/status bars the terminal sat silent for the whole cleanup window, looking hung. Print 'Shutting down… (finalizing session)' immediately at exit start. E2E: live PTY interrupt of a foreground 'sleep 120' terminal tool now aborts in ~1s and the typed message runs as the next turn; wedged-worker + wedged-cleanup subprocess exits in 5.8s (watchdog) instead of hanging. * chore(release): map ai-lab@foxmail.com to CrazyBoyM Adds the AUTHOR_MAP entry for CrazyBoyM (ai-lab@foxmail.com) so the contributor-attribution CI check passes when PR #55828's commits are rebase-merged with authorship preserved. * fix(codex): extend stale timeout for gateway-scale tool payloads Lower the openai-codex stale-timeout floor from 25k to 10k estimated tokens so Telegram/gateway sessions (~20k tools+instructions) are not aborted at the generic 90s cutoff while Codex is still prefilling. * test(codex): cover gateway-scale stale timeout floor and TTFB gate * docs(codex): clarify stale-floor docstring reflects the 10k gate The helper docstring described the typical ~15-25k gateway payload but read as if that were the trigger range; the floor actually engages above 10k tokens. Clarify the prose to match the gate. * fix(browser): guard Camofox snapshot/vision/images on private pages Follow-up to #56874, which added the Camofox private-page SSRF guard (_camofox_current_page_private_url) but wired it only into the Camofox eval path (_camofox_eval). The other Camofox content-read tools — camofox_snapshot, camofox_get_images, and camofox_vision — still read the current page's accessibility tree / images / screenshot without the guard, so on a non-local Camofox backend they can return the content of an intranet or cloud-metadata page (e.g. 169.254.169.254) that the terminal itself can't reach. Apply the same guard, gated on _eval_ssrf_guard_active (non-local backend, not a local sidecar, allow_private_urls unset) and fail-open on probe failure, matching the eval-path guard and the main-browser snapshot/vision guards. camofox_back is intentionally not changed: its target is unknown until navigation completes, and the subsequent content read is already guarded. Adds regression tests covering the three read tools blocking on a private page, the public-page pass-through, and the guard-inactive no-probe path. * feat(image-gen): support Codex image inputs * test(image-gen): cap Codex reference inputs * refactor(image-gen): reuse shared image sniffer + raster allowlist in codex backend Replace the plugin-local _IMAGE_MAGIC_MIME table + _sniff_image_mime body with a delegation to agent.image_routing._sniff_mime_from_bytes, the canonical magic-byte sniffer already used across the codebase, then gate its result to the raster formats gpt-image-2's Responses input_image actually accepts (png/jpeg/gif/webp). The shared sniffer also recognizes SVG/TIFF/ICO; without the allowlist those would pass local validation and be rejected server-side with an opaque HTTP 400. Gating locally fails them cleanly as invalid_image_input. Adds a regression test for SVG rejection. Follow-up on top of @CrazyBoyM's #55828. * fix(whatsapp): resolve LID sender IDs to phone numbers in bridge message payload WhatsApp has migrated to Linked Identity Device (LID) format for user IDs (e.g. 244645917392975@lid instead of 18505551234@s.whatsapp.net). The bridge already resolves LIDs to phone numbers for its own allowlist check via buildLidMap(), but the senderId field in the message payload sent to the gateway still contained the raw LID. This caused the gateway's WHATSAPP_ALLOWED_USERS check to reject all messages as unauthorized, since the LID numbers don't match the phone numbers in the allowlist. Fix: resolve LID → phone in the senderId, senderName, and chatName fields of the event payload before sending to the gateway, using the existing lidToPhone mapping. * chore: add AUTHOR_MAP entry for @ajmeese7 (#3219 salvage) * fix(status): label provider as custom when config.yaml model.base_url is set Salvage of the surviving hunk of #3296 by @Mibayy. The PR's gateway _handle_provider_command hunk targets code removed on main (/provider was absorbed into /model + /status, which already read model.base_url); the hermes status mislabel was the remaining live symptom: _effective_provider_label() only checked the legacy OPENAI_BASE_URL env var, so a custom endpoint configured canonically in config.yaml still displayed as OpenRouter. * feat(gateway): add 'log' option to display.tool_progress Salvage of #3459 by @keslerm, reimplemented against the restructured progress-callback block in gateway/run.py (resolve_display_setting, needs_progress_queue, thinking-relay). Duplicate PR #3458 by @dlkakbs was submitted 4 minutes earlier with the same feature — both credited. Co-authored-by: Dilee <uzmpsk.dilekakbas@gmail.com> tool_progress: log keeps the chat silent and appends timestamped tool-call lines to ~/.hermes/logs/tool_calls.log via a dedicated queue drained by an async writer (RotatingFileHandler 5MB x 3, RedactingFormatter so secrets never land on disk). Gateway-only by design; thinking_progress relaying and the webhook gate are unaffected. /verbose now cycles off -> new -> all -> verbose -> log. * fix(i18n): add gateway.verbose.mode_log to all locale catalogs * feat(commands): /compact alias + --preview/--dry-run flags for /compress (#3243 salvage) Salvaged from PR #3243 by @Mibayy, reimplemented against current main (the original diff targeted a removed gateway/run.py handler). - /compact is now a first-class alias of /compress (CLI, gateway, Telegram/Slack/Discord command lists, autocomplete) — also fixes the dangling '/compact' references in gateway error messages (gateway/run.py context-exhausted banners). - --preview / --dry-run: report what WOULD be compressed (message counts, token estimate, 'here [N]' boundary) without touching the transcript. Flags coexist with the existing 'here [N]' / focus-topic args on both the CLI and gateway surfaces via shared pure helpers in hermes_cli/partial_compress.py. - --aggressive (LLM-free hard truncation) is intentionally NOT implemented: it would need its own transcript-persistence branch outside the guarded _compress_context rotation machinery (#44794 data-loss class). The flag is recognized and returns an explanatory message pointing at '/compress here [N]' and /undo instead of being mis-parsed as a focus topic. - locales: gateway.compress.aggressive_unsupported added to all 16 catalogs (parity test enforced). - release.py: AUTHOR_MAP entry for contributor credit. * feat(api-server): per-client model routing via model_routes (#3176 salvage) Adds a no-code routing layer to the OpenAI-compatible API server so one Hermes deployment can map different API clients to different model/provider backends. Clients pick a backend by sending a configured alias as the OpenAI 'model' field; unmatched values fall back to the global model. Configured aliases are listed by GET /v1/models. Precedence (highest first): session /model override > model_routes route > global config. Route provider credentials resolve through _resolve_runtime_agent_kwargs_for_provider (same seam as channel_overrides); per-route api_key/base_url are upstream provider credential overrides — never caller auth, never logged. Salvaged and rebased from PR #3176 by @Mibayy onto current main. * feat(config): extra HTTP headers for LLM API calls (#3526 salvage) Named providers / custom_providers entries in config.yaml now accept an extra_headers dict scoped to that endpoint — for reverse proxies, API gateways, and custom auth schemes (e.g. Cloudflare Access service tokens). - hermes_cli/config.py: normalize extra_headers on provider entries (_normalize_custom_provider_entry + providers-dict translation), add get_custom_provider_extra_headers / apply_custom_provider_extra_headers_to_client_kwargs helpers keyed on base_url (case/trailing-slash insensitive, no substring bypass — mirrors the TLS helpers) - hermes_cli/runtime_provider.py: surface extra_headers in the resolved runtime for named custom providers (providers dict, legacy custom_providers list, and the credential-pool path) - run_agent.py / agent/agent_init.py: merge per-provider extra_headers onto the OpenAI client default_headers at construction and on every _apply_client_headers_for_base_url re-application (credential swaps, rebuilds), most-specific level wins; OpenAI-wire only (native Anthropic/Bedrock scoped out) - agent/auxiliary_client.py: accept model.extra_headers as an alias of model.default_headers for the global variant - cli-config.yaml.example: documented commented example - Header values are treated as secrets and never logged Salvaged from PR #3526 by @jneeee, reimplemented against current main. Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com> * feat(gateway): persist per-session /model overrides across gateway restarts Per-session /model overrides (_session_model_overrides) were in-memory only, so a gateway restart silently reverted every session to the global default model. Persist the non-secret parts (model/provider/base_url ONLY — never api_key) into the session entry in sessions.json and lazily rehydrate them on first use after a restart, re-resolving credentials through the normal runtime provider resolution. - gateway/session.py: SessionEntry.model_override field with sanitize_model_override() (allowlist: model/provider/base_url) applied on both serialization and deserialization; SessionStore.set_model_override / get_model_override accessors. reset_session() already creates a fresh entry, so /new keeps its clear-on-reset semantics — a restart cannot resurrect an override the user reset away. - gateway/slash_commands.py: write-through at both /model set sites (text command + picker) after storing the in-memory override. - gateway/run.py: _rehydrate_session_model_override() called from _resolve_session_agent_runtime(); in-memory state always wins, credentials are re-resolved per provider (credential-less fallback on failure). Session expiry finalization also drops the persisted override. - tests/gateway/test_session_model_override_persistence.py: restart round-trip, /new clearing, api_key-never-serialized (including tampered sessions.json), rehydration + live-state precedence + credential-failure degradation. Salvaged from #3659 by @Git-on-my-level, narrowed to the restart-persistence gap confirmed in triage. * fix(desktop): parse multiline slash commands + hand degenerate payloads back parseSlashCommand used /^(\S+)\s*(.*)$/ where `.` can't cross a newline and `$` anchors end-of-string, so any slash command whose arg contained a newline (/goal <multi-line text>, a skill command with a long pasted context) failed the whole match, parsed as an empty name, and rendered "empty slash command" while the payload vanished — cleared from the composer and absent from the Up-arrow history ring, which only derives from sent user messages. - name now splits on any whitespace ([\s\S]* arg), matching the CLI and the gateway's split(maxsplit=1); multiline args flow to slash.exec intact - the residual empty-name branch (bare "/", "/ text") restores the submitted text to the composer draft instead of eating it Fixes #41323. Fixes #55510. * fix(desktop): call checkUpdates() in startUpdatePoller so version pill auto-populates startUpdatePoller() only called checkBackendUpdates() — never checkUpdates(). The statusbar version pill reads $updateStatus (set by checkUpdates()), so the commit-behind counter stayed null after restart. It only appeared when the user manually clicked the pill, which triggered checkUpdates() via openUpdateOverlayFor. Added void checkUpdates() in three places alongside the existing checkBackendUpdates() calls: - On startup in startUpdatePoller() - In the 30-minute setInterval callback - In the onFocus handler checkUpdates() uses the Electron IPC bridge (local git check), not the gateway, so no mode gating is needed. The existing $updateChecking atom guard prevents double-fire on overlap. Fixes #53079 * fix(desktop): restore remote artifact rendering * style(desktop): fix import ordering + padding lint in remote-artifact files * feat(desktop): /journey opens the memory graph overlay instead of printing text * style(desktop): fix pre-existing import-order lint in use-prompt-actions * fix(desktop): restore remote file picker attachments * fix(desktop): read attachment previews local-first in remote mode attachImagePath fetched its thumbnail through readDesktopFileDataUrl, which in remote mode routes every read to the gateway fs bridge. Paperclip picks, clipboard saves, and OS drops always produce paths on the LOCAL machine, so the gateway read 404s — toasting "image preview failed" and dropping the thumbnail even though the attach itself works (upload reads local bytes via the Electron bridge). Read the local bridge first and fall back to the remote facade, which still serves in-app drags from the remote project tree. Local mode is unchanged (the facade already reads locally there). Follow-up to #56572, which restored the remote paperclip picker and made this path reachable from the picker as well. * fix(desktop): load remote model options before session Both Desktop picker surfaces (status-bar model menu, settings/onboarding dialog) only asked the connected gateway's model.options once a session existed; before that they fell back to the Desktop REST/global options, which can't see virtual providers a remote gateway exposes — including the MoA presets from #53817. Centralize the fetch rule in requestModelOptions(): prefer the connected gateway whenever one exists (no session_id needed — the RPC resolves disk config), REST only when no gateway is connected. The status-bar MoA preset section now renders from the same model.options payload (the virtual `moa` provider row) instead of the local /api/model/moa REST config, so remote presets appear correctly; the row is filtered out of the main provider groups so presets don't list twice. Preset selection keeps the persistent switchTo path from #56417 and drops the vestigial session gate — like regular model rows, a pre-session pick ships on the next session.create. Fixes #53817. Rebased and reconciled with #56417 (persistent MoA selection), which landed after this PR was opened and covered its one-shot-/moa half. * fix(terminal): set MSYS_NO_PATHCONV for Windows Git Bash subprocesses Git Bash mangles native Windows command flags (/FO, /TN, /Create) into bogus paths. Hermes terminal and background spawns now opt out by default so tasklist, schtasks, and wmic work without manual prefixes. Fixes #56700. * test(terminal): cover MSYS_NO_PATHCONV defaults on Windows env builders * fix(terminal): also set MSYS2_ARG_CONV_EXCL for MSYS2/Cygwin bash fallback MSYS_NO_PATHCONV is honored by Git for Windows bash only. _find_bash's final shutil.which fallback can return MSYS2-proper or Cygwin bash, which ignore it and honor MSYS2_ARG_CONV_EXCL instead. Set both so argv path conversion stays disabled regardless of which bash flavor spawns. Also subsumes the cmd /c mangling in #56147. * feat(desktop): collapse profile rail to a select past 13 profiles (#57306) The colored-square rail stops scaling once a user racks up many profiles: tiny drag targets and an endless horizontal scroll strip. Past a threshold (13) the rail swaps the squares for a compact select dropdown — same active tint + initial glyph, minus the drag-reorder / long-press-recolor / per-row context menu that only make sense at small counts. Two render paths behind one flag; the left default↔all toggle, the "+" create button, and Manage stay put in both. Rename/delete/color remain reachable via Manage. * Prevent deleted profile skeleton revival * feat(auth): make xAI Grok OAuth device-code-only, drop loopback login Replace the loopback/PKCE-callback server and manual-paste fallback with the RFC 8628 device-code flow as the only xAI Grok OAuth login path. The flow works in headless/SSH/container sessions with no 127.0.0.1 listener, shrinking the local attack surface. - Poll the token endpoint with server-provided interval, honoring slow_down and expires_in; store tokens with auth_mode oauth_device_code. - Adaptive proactive refresh skew for short-lived device-code JWTs; rotated tokens sync back to auth.json, the global root store, and the credential pool (no refresh-token replay). - Clear source suppression on successful re-login (CLI + dashboard) and drop the duplicate dashboard pool entry so exactly one seeded device_code entry exists. - Use the shared device_code source name for consistency with the nous/codex device-code providers. - Desktop: remove the loopback OAuth flow states and dead type variants; pkce providers' sign-in URL selection is unchanged. - Docs (EN + zh-Hans) rewritten for device-code login; drop the deleted --manual-paste flag from documented commands. * fix(auth): remove stale loopback_pkce reference in xAI quarantine removal list The terminal-refresh quarantine filtered in-memory entries on source == "device_code" but built removed_ids from the deleted "loopback_pkce" source name, so the revoked device-code entry was never pruned from the persisted pool in auth.json. Also restores the _print_loopback_ssh_hint test suite scoped to Spotify (the helper's remaining caller) instead of deleting it wholesale. * fix(desktop): skip ensureBackend after profile-delete teardown to prevent respawn loop When the renderer sends a DELETE /api/profiles/{name} request, the IPC handler tears down the profile's pool backend (or primary backend) via prepareProfileDeleteRequest. However, the very next line calls ensureBackend(profile), which spawns a fresh pool backend for the just- deleted profile. The new backend's startup path calls ensure_hermes_home(), which recreates the profile directory — defeating the deletion and leaving the process as a zombie. On the next Desktop restart the cycle repeats: the profile directory exists, the Desktop spawns a backend, the backend recreates the directory after deletion, and PIDs accumulate indefinitely. Fix: make prepareProfileDeleteRequest return the torn-down profile name. The IPC handler uses this to route the DELETE to the primary backend instead of spawning a new pool backend for the deleted profile. Fixes #52279 * fix(desktop): refresh profile rail after deletion (#49289) * fix(desktop,tui-gateway,zai): stop thinking-off from reverting to medium A Z.ai desktop user reported thinking reverting to medium after one turn, burning ~200% of a week's credits in 4 days despite reasoning_effort: false in config.yaml. Four compounding bugs: - _session_info reported reasoning_effort "" for disabled reasoning, indistinguishable from unset — the desktop adopted it after the first turn, wiping its sticky "thinking off" pick so every later chat reverted to the default effort. - config.set key=reasoning always wrote agent.reasoning_effort to global config.yaml, so every desktop model-menu selection (preset.effort ?? 'medium') clobbered the user's configured value. Now session-scoped like the messaging gateway's /reasoning, landing on create_reasoning_override so lazily-built sessions keep it too. - YAML `reasoning_effort: false`/`off`/`no` (boolean False) was coerced to "" by every loader's `str(x or "")`, silently re-enabling thinking. parse_reasoning_effort now treats False/"false"/"disabled" as {"enabled": False}; loaders (tui gateway, gateway, cli, cron, delegate) pass the raw value through. The desktop config reader also crashed on the boolean (false.trim()), aborting voice/STT settings. - The zai provider profile never sent thinking on the wire, and GLM-4.5+ defaults to thinking ON server-side — so disabling reasoning was a silent no-op on direct Z.ai, the actual token burner. The profile now emits extra_body.thinking {"type": "enabled"|"disabled"} for thinking-capable GLM models, mirroring the DeepSeek profile. Also: /new (session reset) now carries reasoning_config across the rebuild like model_override; config.get reasoning prefers the session's live value and maps a config False to "none"; Settings shows "Off" instead of a blank select for hand-written false. * fix(cli): stop profile-bound backends before deleting so rmtree converges delete_profile stopped only the process named in gateway.pid, but a Desktop app spawns a headless `serve`/`dashboard` backend per profile that holds the profile's SQLite connection open and keeps writing sessions/WAL/sandbox files. That backend is never in gateway.pid, so a CLI `hermes profile delete` run while the Desktop app is up left it writing into the tree — rmtree's final rmdir then failed with ENOTEMPTY (#47368 "Bug 2"), and pre-guard it also resurrected the directory. - _profile_bound_backend_pids(): find running Hermes backends bound to this profile via a `--profile <name>` selector or a HERMES_HOME env resolving to the profile dir. Tightly scoped — current-user only, backend subcommands (serve/dashboard/gateway) only so an interactive chat is never killed, and never this process or its ancestors. - _stop_profile_backends(): terminate them (graceful, then force), best-effort so it can never make delete worse. - _rmtree_with_retry(): a few spaced retries absorb the ENOTEMPTY / Windows file-lock race from a just-terminated writer's in-flight -wal/-shm/sandbox writes instead of failing the whole delete on a race the next attempt wins. Complements the recreation guard (deleted profiles no longer reappear) and the Desktop teardown-before-delete flow; this is the CLI-side convergence fix for a delete run while a Desktop-managed backend is live. Part of #47368. * fix(tui_gateway): route setup.runtime_check and setup.status to RPC pool setup.runtime_check and setup.status are polled by the Desktop frontend on connect and periodically (use-status-snapshot → evaluateRuntimeReadiness), but neither was in _LONG_HANDLERS — so dispatch() ran both inline on the WS reader thread. Under GIL pressure from concurrent agent turns (terminal I/O, large output, background-process completions) either can block for seconds: - setup.runtime_check → resolve_runtime_provider() (config read, auth check, may probe the provider endpoint) - setup.status → _has_any_provider_configured() (provider config + credential scan) While either blocks the reader thread the WS read loop can't service later requests; the frontend RPC timeout fires, the client drops the socket, and the lost setup.runtime_check response reads as ready=false — a false "needs setup" / "Settings failed to load" even though the provider is configured. Route both to the RPC pool (same precedent as #55545's session.list/pet.info/ process.list). The handlers are read-only and pool writes go through the lock-guarded write_json, so there's no ordering or safety concern. Test asserts all 5 frontend-polled RPCs are pool-routed. Co-authored-by: izumi0uu <izumi0uu@gmail.com> * chore(release): map yingliang-zhang in AUTHOR_MAP for #57335 * fix(usage): capture reasoning_tokens from completion_tokens_details on chat_completions (#57340) normalize_usage only read output_tokens_details.reasoning_tokens (the Responses API shape). Chat Completions providers — OpenAI, OpenRouter, DeepSeek, and every OpenAI-compatible proxy — report it under completion_tokens_details.reasoning_tokens, so reasoning_tokens was 0 for every chat_completions reasoning model: hidden thinking was invisible in session accounting, MoA traces, and the eval's per-task token columns. Measured impact (HermesBench MoA run on deepseek-v4-flash, 4,828 advisor calls): reasoning_tokens showed 0 everywhere while individual calls burned up to 21.5K hidden thinking tokens to emit ~500 visible tokens. Verified live against OpenRouter: deepseek-v4-flash returns completion_tokens_details.reasoning_tokens=61 for a 74-completion-token call; the field was simply never read. Responses-shape reads are unchanged; the new read only fires when the Responses shape yielded nothing. * fix(slack): keep blank-line-separated ordered items in one rich_text_list When a Markdown ordered list has blank lines between items (common in LLM-authored content), the list run loop breaks on each blank line. Slack numbers each rich_text_list independently, so N items produce N lists each starting at 1. Skip blank lines inside the list run as soft separators instead of breaking, so ordered items stay in one rich_text_list and Slack renders the correct numbering. Fixes #57076 * fix(slack): guard blank-line list continuation on next-item lookahead Refine the blank-line handling so a blank line only continues a list run when the next non-blank line is another list item. This keeps a list -> paragraph -> list sequence as three separate blocks and matches the contiguous-list layout for mixed/nested lists (one rich_text block, split into sub-lists by (indent, ordered)), rather than emitting a separate block per item. Adds regression tests for the mixed blank-separated layout and the list->paragraph->list boundary. * refactor(slack): extract _is_list_line helper for list-marker checks Deduplicate the '_BULLET_RE.match or _ORDERED_RE.match' idiom used at the list-run entry guard and the blank-line lookahead into a single helper, so adding future marker types is a one-point change. Pure refactor, no behavior change (22 block_kit tests still pass). * fix: refresh NVIDIA featured models * fix(agent): honor live vLLM context limits on local endpoints Reconcile stale local disk cache against live vLLM/Ollama max_model_len probes, probe local servers before the llama hardcoded default, parse vLLM max_model_len overflow errors, and surface the non-agentic Hermes 3/4 warning at agent init on gateway/TUI. Sub-64K live probes are returned for startup rejection but are not persisted to the context cache — preserving the 64K minimum-context contract instead of normalizing undersized windows as valid config. (cherry picked from commit c3a02db4fd9d57b7b0eb2732de91f8334d311aa5) * test(agent): cover local vLLM context-length resolution Add regression tests for vLLM max_model_len error parsing, stale local cache reconciliation, live probes over llama defaults, and the 64K minimum guard on persistent cache writes. (cherry picked from commit 1cb47ef437de7ce289cb358e8d6b89e9194b43ed) * fix(gateway): close webhook sessions on delivery completion so prune can reap them Webhook deliveries created a unique one-shot session (delivery_id baked into the session key at gateway/platforms/webhook.py:668) but the adapter fired handle_message via asyncio.create_task WITHOUT ever ending the session (webhook.py:713, pre-fix). Nothing else closes it: the gateway caches/expires the agent per session_key but never calls end_session for the webhook path, and _end_session_on_close teardown doesn't run for these fire-and-forget tasks. SessionDB.prune_sessions (hermes_state.py:4965) only deletes rows WHERE ended_at IS NOT NULL. So every webhook session stayed with ended_at NULL -> unprunable -> unbounded state.db growth. This was the primary driver of the SQLite lock-contention gateway outage. Fix: wrap the delivery in _run_delivery_and_close, which awaits handle_message and then (in finally, so failures still reap) calls _end_webhook_session -> SessionDB.end_session(session_id, 'webhook_complete'). This mirrors how cron closes its session with 'cron_complete' (cron/scheduler.py:3065). end_session is first-reason-wins and no-ops on an already-ended row, so it never clobbers a compression/agent_close reason. Adds tests/gateway/test_webhook_session_close.py asserting the invariant (a completed webhook session has ended_at set + is prunable), including the error-path case, against a real SessionStore + SessionDB. * chore: map gumclaw@gumroad.com in AUTHOR_MAP for PR #57322 salvage * refactor(gateway): add SessionStore.peek_session_id public accessor for webhook close Replace the webhook delivery-close path's direct reach into private SessionStore._entries (which also bypassed the store lock) with a public, lock-held peek_session_id(session_key) accessor. Mirrors the existing lookup_by_session_id inverse helper. Keeps a getattr fallback for older stores / test doubles. Adds a unit test for the accessor. * fix(agent): resolve review findings on vLLM local-context salvage Salvage review of #56431 surfaced one Critical + two Warning issues; fix them on top of the contributor's cherry-picked commits: 1. Critical — duplicate non-agentic warning on the interactive CLI. The new agent_init warning fires on every platform, but cli.py show_banner() already warns on CLI (richer output + /model hint), so a CLI user saw the warning twice per startup. Guard the agent_init emit to skip platform=="cli" — it now fills exactly the gateway/TUI gap the PR intended, no duplication. 2. Warning — vLLM error-parse regex under-matched. The patterns required a literal space before the number, so "max_model_len: 32768", "=32768", "(32768)", and "... is 32768" all returned None. Broaden both patterns to accept :/=/(/ 'is' delimiters. Add a parametrized test over all delimiter variants. 3. Warning — per-call live probe latency on local endpoints. The new reconcile-on-hit + pre-defaults step-7 probe made every local resolution fire a synchronous network probe (banner + /model switch + compressor update_model each within one startup). Add a 30s in-process TTL cache keyed by (model, base_url) around _query_local_context_length so back-to- back resolutions reuse one round-trip; not persisted to disk, so the reconcile freshness contract (re-probe after restart) is preserved. Add an autouse fixture clearing the cache between tests + TTL coverage. Tests: 148 passed (was 138). ruff clean. * chore(release): add infinitycrew39 to AUTHOR_MAP (#56431 salvage) * chore: add trismegistus-wanderer to AUTHOR_MAP for PR #31856 salvage * fix(dashboard): disable ws keepalive ping on loopback to survive event-loop stalls Desktop/dashboard WebSocket connections drop during long agent operations (delegate_task subagents, large model outputs) when the uvicorn event loop is GIL-starved for minutes. Root cause: uvicorn's ws keepalive ping runs on the SAME event loop as agent turns. A single synchronous GIL-holding call on a worker thread (a regex/scrub over a large output, or a long subagent turn) freezes the loop, so it cannot process the incoming pong within ws_ping_timeout and uvicorn closes an otherwise-healthy connection (#53773: 'event loop stalled 226.3s'; #48445/#50005). Loosening the timeout only raises the threshold — a multi-minute stall sails past any finite window. The keepalive ping exists to detect half-open connections (reverse-proxy 524, dropped tunnels), which cannot happen on loopback: there is no network or proxy in the path, and a dead local client tears the socket down with a real FIN/RST that starlette surfaces as WebSocketDisconnect regardless of the ping. So on loopback the ping provides ~no liveness value while actively killing recoverable stalls — disable it entirely (ws_ping_interval/timeout=None). Non-loopback (public) binds sit behind a Cloudflare Tunnel where half-open IS a real failure mode, so the ping stays at 20/20 to detect it. Empirically verified (real uvicorn + websockets peer): with ws_ping=None the server never closes a silent peer during an 8s window; with the pre-fix 2s/2s window uvicorn closes it. A genuinely-dead client still fires the WebSocketDisconnect reap path regardless of the ping. Note: this fixes the local Desktop case (the OP's scenario). A remote Desktop over an authenticated public dashboard route (McCalebTheSecond's comment) keeps the ping and needs the deeper GIL-hotspot fix — tracked separately. Closes #53773 * fix(agent): self-review follow-ups on vLLM local-context salvage Self-review (ruff+ty lint diff = 0 net-new; 2-agent deep review) surfaced one Warning + comment-accuracy nits; no Critical: - W1: the local-probe TTL cache memoized None (probe failure) for 30s, so a probe that failed during a startup race would suppress a legit retry once the server came up. Cache only positive results — still fully bounds the hot-path probe rate (reachable servers cache their value) while an unreachable one re-probes on the next call. Add a regression test asserting a None result is NOT cached (retry re-probes); mutation-verified. - Tighten the platform-guard comment: gateway/TUI/cron already construct with quiet_mode=True (gated by `not agent.quiet_mode`), so the guard's active job is CLI dedup vs show_banner, not "filling the gateway/TUI gap" as originally worded. Verified not-issues (per review): positive-value 30s cache does not break the reconcile-after-restart freshness contract (restart = fresh process, empty cache); cache key is collision-safe; platform guard is correct in both directions (no runtime path leaves platform None on a non-CLI surface). Tests: 149 passed. ruff clean; ty 0 net-new vs base. * fix(gateway): keep idle cached agents alive until session actually expires The idle-TTL sweep (_sweep_idle_cached_agents) was evicting agents as soon as they passed _AGENT_CACHE_IDLE_TTL_SECS, even when the session hadn't expired yet. In daily-reset mode the reset can fire hours after the last user message — evicting the agent early means the session-expiry watcher has no agent in cache to call on_session_end() with, so memory providers miss the live transcript. Now the sweep checks the session store before evicting: if the session still exists and hasn't expired, the agent stays in cache so the expiry watcher can tear it down properly later. When the session store is unavailable or throws, falls back to the original eviction behavior (safe default). Fixes: #11205 * fix(gateway): complete on_session_end coverage across all eviction paths Follow-up to the cherry-picked #31856 fix. The contributor's guard defers idle-TTL eviction until the session store reports the session expired, so the expiry watcher can tear the agent down and fire MemoryProvider.on_session_end() with the live transcript. Two gaps remained: 1. Memory-leak regression for mode='none' sessions. _is_session_expired() returns False forever for the 'none' reset policy, so the naive guard would never idle-evict those agents — reopening the unbounded-cache leak the idle sweep (#11565) exists to relieve. Added SessionStore.is_session_finalizable() (a public predicate: will the expiry watcher EVER finalize this session?) and gate the deferral on it. mode='none' agents fall through to soft eviction as before. 2. on_session_end still dropped on the LRU-cap path. Both cache-pressure paths (_enforce_agent_cache_cap and _sweep_idle_cached_agents) soft-evict via _release_evicted_agent_soft, which by design does NOT fire on_session_end. If cache pressure evicts a finalizable-but-not-yet-expired agent before it expires, the watcher later finds no cached agent and the hook is skipped. Added _commit_memory_before_soft_evict(): at LRU eviction, if the session is finalizable and not yet expired, commit end-of-session extraction via the live agent's own (fully-scoped) memory manager using commit_memory_session() — extraction WITHOUT provider teardown, so the eviction stays soft and a resumed turn keeps working. Skipped for mode='none' (no missed boundary to compensate) and expired sessions (the watcher tears those down directly). This closes #11205 for ALL eviction paths and reset policies, not just the idle-sweep + finite-policy case, while preserving the soft-eviction resumability contract (never calls close() on a live session). Tests: 5 new cases in test_agent_cache.py (mode='none' still reaped, LRU-cap commits for finalizable / skips for none, real is_session_finalizable predicate); all mutation-checked. Contributor's original 2 tests updated to assert the finalizable path explicitly. * fix(providers): pass extra headers to model discovery * refactor(providers): dedupe extra_headers normalizer + key picker groups by headers Follow-up to @helix4u's #57336 salvage. Two review findings: - W1: model-picker grouped custom-provider rows by (api_url, credential, api_mode) but NOT extra_headers. Entries sharing a URL+credential+api_mode yet declaring different headers (e.g. per-tenant routing behind one proxy) collapsed into one row and probed /models with whichever header set was seen first (order-dependent). Fold a canonical header identity into group_key so distinct header-authed endpoints stay separate; drops the now-dead first-non-empty merge branch. - W2: the extra_headers stringify+None-filter comprehension existed in 5 copies (config.py x2, runtime_provider.py, model_switch.py, models.py). Extract one shared hermes_cli.config.normalize_extra_headers primitive; all sites now call it. Tests: +normalize_extra_headers unit tests, +regression test proving two same-endpoint entries with different headers stay distinct and each probes with its own headers. 223 targeted tests pass; ruff clean. * fix(desktop): let settings content use full pane width Remove the max-w-4xl wrapper from SettingsContent so every settings page can use the available overlay width. * fix(desktop): extend profile startup REST timeouts (#48504) * fix(desktop): extend startup long-timeout to the whole boot data burst Broadens Tranquil-Flow's profile-startup timeout fix (#48518) from getProfiles + refreshActiveProfile to the rest of the calls the desktop fires during connect: /api/config, /api/config/defaults, /api/model/info, /api/model/options, /api/cron/jobs. On a profile-heavy or remote install any of these can exceed the 15s DEFAULT_FETCH_TIMEOUT_MS while the backend is alive-but-busy (e.g. list_profiles walks the skill tree per profile), surfacing as the spurious "Timed out connecting to Hermes backend after 15000ms" that hangs the UI (#48504). Uses the surgical per-call mechanism (renamed STARTUP_PROFILE_REQUEST_TIMEOUT_MS → STARTUP_REQUEST_TIMEOUT_MS) rather than raising the global default (the alternative in #48526): the liveness poll /api/status and all interactive/ runtime calls keep the short default, so a genuinely-dead backend is still detected fast and the boot readiness probe (waitForHermes) is untouched. Supersedes #48518 (carried as the base commit) and #48526 (global-default raise). Fixes #48504. Co-authored-by: YapBi <129007007+HeLLGURD@users.noreply.github.com> Co-authored-by: Tranquil-Flow <66773372+Tranquil-Flow@users.noreply.github.com> * fix(webhook): close per-delivery session at the true end of the run (#57423) The merged webhook session-close fix (#57370, salvaging #57322) wrapped handle_message in a try/finally — but BasePlatformAdapter.handle_message is fire-and-forget: it spawns _process_message_background and returns before the agent run starts. The finally-close therefore ran BEFORE get_or_create_session created the session row, found no session_id, and silently no-op'd — the ghost-session leak persisted on the real path. (The shipped test masked this by stubbing handle_message with a fake that created the row synchronously.) Move the close to an on_processing_complete override — the lifecycle hook the base class fires at the TRUE end of the run, on the success, failure, and cancellation paths alike. Empirically verified through the real fire-and-forget pipeline: before, ended_at stayed NULL; after, ended_at is set with end_reason=webhook_complete and the row is prunable. Tests now stub only the runner-side _message_handler (the seam the live gateway injects) so handle_message / _process_message_background / on_processing_complete all run for real; adds an AsyncSessionDB-facade coverage test for the coroutine-await branch. * fix(dump): flag API keys visible only to the shell, not the managed backend hermes debug share reads os.getenv — the invoking terminal's environment — but launchd/systemd and the desktop-spawned `serve` backend load credentials from ~/.hermes/.env, not the login shell. A key exported in the shell but absent from .env is invisible to the backend, yet the dump printed a bare "set", sending support down a phantom "the key is configured" path. This was the actual trap behind a "Desktop has no web_search / no tools" report: FIRECRAWL_API_KEY was a shell export (so `debug share` in a terminal read "firecrawl set") but not in .env, so the launchd backend's check_web_api_key returned False and web_search was gated off — which a contributor then misdiagnosed as a missing `desktop` platform registration. The dump now annotates any key set in-process but missing from ~/.hermes/.env with "(shell only — not in .env; managed/desktop backend may not see it)" so the mismatch is obvious instead of hidden behind "set". * feat(skills): add security/unbroker (autonomous data-broker removal) unbroker finds where a consenting person's info is exposed across data brokers and people-search sites and files the removals, running as far as each site allows and handing only genuinely human-only steps (hard CAPTCHA, gov-ID, phone, fax) back as an end-of-run digest. - Deterministic stdlib CLI (scripts/pdd.py) owns config, dossiers+consent, the broker DB, tier planning, the ledger, email, and the autonomous action queue; the agent scans/submits with native tools (web_extract, browser_*, delegate_task, cronjob, terminal). - Verify-before-disclose, least-disclosure (never volunteers SSN), consent gate, opaque ids, optional age-at-rest encryption, file-locked ledger. - Jurisdiction-aware (CCPA/CPRA, GDPR, generic); CA DROP one-shot covers the state registry (~545) in a single request; BADBOOL + curated people-search coverage; scheduled re-scan for re-listing. - No CAPTCHA-solving services or anti-bot bypass; browser email mode needs no stored password. - 85 hermetic tests (tests/skills/test_unbroker_skill.py; SMTP/IMAP via injected fakes, registry via CSV fixtures). Ships placeholder data only. Broker dataset adapted from BADBOOL (Yael Grauer, CC BY-NC-SA 4.0). * docs(unbroker): point README image link at hermes-agent; sync test count (85) * feat(desktop): cap overlay inner-page width at 75rem Add a shared PAGE_MAX_W (1200px) and center OverlayMain within its pane so settings and command center bodies stay readable instead of sprawling on wide/ultrawide displays. * fix(desktop): stop macOS Tahoe misplacing the traffic lights On macOS Tahoe (Darwin 25+), a nonzero titleBarOverlay height makes setWindowButtonPosition() miscalculate the native traffic-light position (electron#49183), shoving the lights into the left titlebar tools. Pass height 0 there so the lights land at the configured inset; the renderer paints its own drag strips, so nothing is lost. Pre-Tahoe is unchanged. Gate on the truthful Darwin kernel major (25 = Tahoe) rather than the product version, which macOS reports as 16 or 26 depending on build SDK. * fix(desktop): clear stale active todos on turn end AND on rehydration A turn that ends without a final `todo` update left the composer "Tasks N/M" panel pinned with its last item stuck pending/in_progress, and it survived restarts because the panel is read back from stored session history. Two coupled fixes (the first alone is undone by the second path): - Turn end: clear a still-active todo list on `message.complete` and on a terminal `error` (new `clearActiveSessionTodos` — active lists only; a finished list keeps its short linger so the last checkmark still lands). - Rehydration: `hydrateFromStoredSession` runs *after* a turn completes, so an "active" stored list is stale, not in-flight. It now restores only a *finished* list (via new `todosForHydration`) and drops anything still active — otherwise it re-pinned the panel right after the turn-end clear and resurrected it on every restart. Salvages #52996 (@0disoft): the fix shape (clearActiveSessionTodos on turn completion, preserving the finished-list linger) is carried forward and ported onto the current use-message-stream/ folder split (gateway-event.ts), then extended to the rehydration path per review. Co-authored-by: 0disoft <rodisoft1@gmail.com> * fix(unbroker): suppress-first for PeopleConnect (deletion undoes suppression) PeopleConnect is the exception to deletion-beats-suppression: "DELETE MY USER DATA" also deletes suppressions on file, and deletion does not stop the people-search sites from showing you (public records re-list). Suppression is the effective lever and must be maintained. - intelius.json: deletion.prefer=false; playbook/quirks/notes rewritten with the verbatim privacy-center language; delete is the data-purge-only path. - autopilot: honor deletion.prefer -> prefer_suppression when false. - methods.md / SKILL.md / README: exception called out. - tests updated + prefer-flag routing test (86 tests). * fix(file-tools): preserve container paths for docker file ops (#56637) * fix(config): preserve owner on atomic writes (#56644) * fix(desktop): use gift codicon for update-available toast Add optional notification icon override and use codicon-gift on the update-ready toast so it reads as a present rather than generic info. * fix(desktop): use symbol-namespace codicon for Model settings nav * fix(desktop): cancel downloads triggered by link-title fetch window The hidden BrowserWindow used by fetchLinkTitle to scrape page titles had no will-download handler on its session. When a link artifact URL responds with Content-Disposition: attachment, Electron fires will-download and the file is saved for real — explaining the spurious download on the Artifacts page. Add guardLinkTitleSession() (parallel to the existing audio-mute guard for #49505) that installs a will-download handler which immediately cancels every download item on the hermes:link-titles session. Call it from getLinkTitleSession() right after the request-type blocklist is wired up. * fix(slack): MPIMs (group DMs) obey shared-surface mention gating + reaction guard Group DMs (MPIMs) were classified as DMs and thereby exempted from every operator control that shared surfaces are supposed to honor: allowed_channels, require_mention, strict_mention, free_response_channels, and the reaction guard. Symptom: the bot added :eyes:/:white_check_mark: to unmentioned MPIM messages and still invoked the agent (which then returned NO_REPLY) instead of the gateway dropping the event before model execution. Removing an MPIM from allowed_channels did not disable it. Root cause is the DM classification at adapter.py: is_dm = channel_type in {"im", "mpim"} used for BOTH routing exemptions and reaction gating. An MPIM is a shared surface (multiple humans can see and trigger the bot), not a private 1:1 DM, so it must be gated like a channel. This behavior was introduced/reinforced by a trail of Slack group-DM PRs: - #4633 fix(slack): treat group DMs (mpim) like DMs + reaction guard - #54632 fix(slack): subscribe to message.mpim + mpim scopes so group DMs work - #54663 fix(slack): group DMs work OOTB + reinstall nudge #54632/#54663 correctly made MPIM messages *reachable*; #4633 over-reached by giving them the DM mention/reaction *exemptions*. This corrects only that over-reach. Fix (minimal): introduce `is_one_to_one_dm = channel_type == "im"` and key the two EXEMPTION sites off it instead of `is_dm`: - mention/allowlist gating block (`if not is_one_to_one_dm and bot_uid:`) - reaction guard (`(is_one_to_one_dm or is_mentioned)`) `is_dm` is intentionally retained for session/thread scoping and chat_type labeling, where treating an MPIM as a persistent multi-party conversation is correct — only the mention/reaction exemptions were wrong. Docs: slack.md now distinguishes 1:1 DMs (mention-exempt) from group DMs (shared surface; obey require_mention/strict_mention/allowed_channels/ free_response_channels; reactions only when @mentioned). Tests: +7 in test_slack_mention.py (MPIM unmentioned dropped under require_mention and strict_mention; MPIM mentioned processed; MPIM off allowed_channels dropped; MPIM in free_response opted in; 1:1 IM still exempt; reaction guard drops unmentioned MPIM). Updated _would_process to model the is_one_to_one_dm gating + strict_mention. 72 passed. * 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. * 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 * 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 * 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. * 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. * 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. * 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 * fix(browser): retry next candidate when debug launch exits early * 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 * 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…
zebadee2kk
added a commit
to zebadee2kk/hermes-agent
that referenced
this pull request
Jul 5, 2026
…sticky active_profile (#4) * chore: add AUTHOR_MAP entry for @sahibzada-allahyar (#39227 salvage) * fix(terminal): stop stripping CLAUDE_CODE_OAUTH_TOKEN from spawned subprocesses (#56935) CLAUDE_CODE_OAUTH_TOKEN is set and owned by the user's Claude Code install (subscription OAuth), not a Hermes-managed inference credential — Claude subscription auth is not a working Hermes provider path. Blocklisting it broke agent-spawned claude CLIs: with no token in the child env, claude fell through to the shared macOS Keychain / ~/.claude/.credentials.json store and, on auth failure, cleared it — logging the user out of their interactive Claude sessions and the desktop app. Exempt it from _HERMES_PROVIDER_ENV_BLOCKLIST (it arrives via the anthropic registry entry, so discard explicitly with rationale). ANTHROPIC_API_KEY / ANTHROPIC_TOKEN and every other provider credential remain stripped, and the GHSA-rhgp-j443-p4rf fail-closed passthrough guard is unchanged for everything still on the blocklist. Fixes #55878 * feat(delegation): unify concurrency caps — deprecate max_async_children (#56955) delegation.max_concurrent_children is now the single cap for both a batch's parallelism and concurrent background delegation units. - _get_max_async_children() delegates to _get_max_concurrent_children(); a leftover max_async_children key logs a one-time deprecation warning - config v32→33 migration removes the stale key, folding a raised max_async_children into max_concurrent_children (max wins, no lost headroom) - capacity error messages now point at max_concurrent_children - pool-at-capacity sync fallback now attaches an explanatory note so the model/user know why the call blocked instead of dispatching async Previously users who raised max_concurrent_children (e.g. to 15) still hit the invisible default-3 async cap: the 4th background delegate_task silently ran inline, blocking the turn with no signal. * fix(webhook): remove unused payload from delivery state * fix(webhook): remove unused payload from delivery state * chore: add AUTHOR_MAP entry for @VolodymyrBg (#2861 salvage) * fix(config): accept 'on' as truthy for env flags via shared env_var_enabled helper Salvage of #2863 by @aydnOktay, reimplemented against current main using the existing utils.env_var_enabled / TRUTHY_STRINGS helper instead of per-site tuple edits. Covers the 7 gateway/config.py env-flag sites that still rejected 'on' (WHATSAPP_ENABLED, SIGNAL_IGNORE_STORIES, MATRIX_ENCRYPTION, API_SERVER_ENABLED, WEBHOOK_ENABLED, MSGRAPH_WEBHOOK_ENABLED, BLUEBUBBLES_SEND_READ_RECEIPTS) plus HERMES_DESKTOP gating in read_terminal/close_terminal. The PR's approval.py HERMES_YOLO_MODE portion is already on main via is_truthy_value. * test: env-flag 'on' truthy behavior contract (#2863 follow-up) * feat(gateway): per-channel model and system prompt overrides (Fixes #1955) - config: ChannelOverride + PlatformConfig.channel_overrides - run: _resolve_model_for_channel, _get_system_prompt_for_channel, channel provider runtime - tests: channel overrides + config guard for bare runner; conftest asyncio fix; slack/whatsapp warning filters Made-with: Cursor * feat(gateway): per-channel model and system prompt overrides (Fixes #1955) - ChannelOverride + channel_overrides on PlatformConfig - Resolve model/runtime: session /model, then channel_overrides, then global - Thread/parent channel lookup; bridge discord.channel_overrides from YAML - Drop unrelated test and delegate_tool changes from PR scope * feat(gateway): per-channel model and system prompt overrides (Fixes #1955) - ChannelOverride + channel_overrides; session /model > channel > global - Thread/parent lookup; YAML bridge for discord.channel_overrides - Guard channel_overrides when config lacks platforms (test mocks) - Add sampiyonyus@gmail.com to AUTHOR_MAP * fix(email): harden adapter against malformed IMAP responses Salvage of #2794 by @CharmingGroot, ported to the relocated plugins/platforms/email/adapter.py: - Guard raw_email = msg_data[0][1] against IndexError/TypeError and non-bytes payloads. UIDs are added to _seen_uids before fetch, so an exception mid-batch permanently skipped every remaining message in the batch — now the bad message is logged and skipped instead. - Message-ID domain generation falls back to 'localhost' when EMAIL_ADDRESS lacks '@' (now via a shared _message_id_domain() helper covering all 3 send paths; the PR fixed 2 of 3). * feat(api-server): inline MEDIA: image tags as base64 data URLs for remote frontends Salvage of the surviving piece of #2696 by @tarunravi. The PR's other two changes (tool progress streaming, SSE None-sentinel fix) were independently superseded on main by the structured hermes.tool.progress SSE events and the rewritten queue-drain loop. Remote OpenAI-compatible frontends can't read server-local file paths, so MEDIA:<path> tags (browser screenshots, generated images) were dead text. _resolve_media_to_data_urls() now inlines small (<=5MB) local images as markdown data URLs across all four response surfaces: chat completions (non-streaming), session chat, session chat stream final event, and the Responses API. Non-image, missing, or oversized paths pass through untouched. * fix(cli): reliable interrupts, bounded exit, and exit feedback (#57000) Three CLI reliability fixes: 1. Interrupt reliability: chat() only re-queued the user's interrupt message when the turn result carried interrupted=True. When the agent thread raced past its last interrupt check (or finished) before the interrupt landed, the message was silently dropped — and the stale _interrupt_requested flag left on the agent instantly aborted the NEXT turn. Un-acknowledged interrupt messages are now re-queued as the next turn and the stale flag is cleared (only when the agent thread actually exited). The clarify-race path also parks the message in _pending_input instead of dropping it. 2. Slow exit (5+ min): stdlib ThreadPoolExecutor workers are non-daemon and joined unconditionally by concurrent.futures' atexit hook — even after shutdown(wait=False). One wedged tool worker (abandoned after interrupt/timeout) held the process open forever. Promoted async_delegation's daemon executor to a shared tools/daemon_pool module and adopted it in tool_executor (concurrent tool batches), memory_manager (background sync), delegate_tool (child timeout wrapper + batch fan-out), and skills_hub (source fan-out). Added a 30s exit watchdog (HERMES_EXIT_WATCHDOG_S) armed at _run_cleanup start as a backstop for wedged cleanup steps. 3. Exit jank: after prompt_toolkit tears down the input/status bars the terminal sat silent for the whole cleanup window, looking hung. Print 'Shutting down… (finalizing session)' immediately at exit start. E2E: live PTY interrupt of a foreground 'sleep 120' terminal tool now aborts in ~1s and the typed message runs as the next turn; wedged-worker + wedged-cleanup subprocess exits in 5.8s (watchdog) instead of hanging. * chore(release): map ai-lab@foxmail.com to CrazyBoyM Adds the AUTHOR_MAP entry for CrazyBoyM (ai-lab@foxmail.com) so the contributor-attribution CI check passes when PR #55828's commits are rebase-merged with authorship preserved. * fix(codex): extend stale timeout for gateway-scale tool payloads Lower the openai-codex stale-timeout floor from 25k to 10k estimated tokens so Telegram/gateway sessions (~20k tools+instructions) are not aborted at the generic 90s cutoff while Codex is still prefilling. * test(codex): cover gateway-scale stale timeout floor and TTFB gate * docs(codex): clarify stale-floor docstring reflects the 10k gate The helper docstring described the typical ~15-25k gateway payload but read as if that were the trigger range; the floor actually engages above 10k tokens. Clarify the prose to match the gate. * fix(browser): guard Camofox snapshot/vision/images on private pages Follow-up to #56874, which added the Camofox private-page SSRF guard (_camofox_current_page_private_url) but wired it only into the Camofox eval path (_camofox_eval). The other Camofox content-read tools — camofox_snapshot, camofox_get_images, and camofox_vision — still read the current page's accessibility tree / images / screenshot without the guard, so on a non-local Camofox backend they can return the content of an intranet or cloud-metadata page (e.g. 169.254.169.254) that the terminal itself can't reach. Apply the same guard, gated on _eval_ssrf_guard_active (non-local backend, not a local sidecar, allow_private_urls unset) and fail-open on probe failure, matching the eval-path guard and the main-browser snapshot/vision guards. camofox_back is intentionally not changed: its target is unknown until navigation completes, and the subsequent content read is already guarded. Adds regression tests covering the three read tools blocking on a private page, the public-page pass-through, and the guard-inactive no-probe path. * feat(image-gen): support Codex image inputs * test(image-gen): cap Codex reference inputs * refactor(image-gen): reuse shared image sniffer + raster allowlist in codex backend Replace the plugin-local _IMAGE_MAGIC_MIME table + _sniff_image_mime body with a delegation to agent.image_routing._sniff_mime_from_bytes, the canonical magic-byte sniffer already used across the codebase, then gate its result to the raster formats gpt-image-2's Responses input_image actually accepts (png/jpeg/gif/webp). The shared sniffer also recognizes SVG/TIFF/ICO; without the allowlist those would pass local validation and be rejected server-side with an opaque HTTP 400. Gating locally fails them cleanly as invalid_image_input. Adds a regression test for SVG rejection. Follow-up on top of @CrazyBoyM's #55828. * fix(whatsapp): resolve LID sender IDs to phone numbers in bridge message payload WhatsApp has migrated to Linked Identity Device (LID) format for user IDs (e.g. 244645917392975@lid instead of 18505551234@s.whatsapp.net). The bridge already resolves LIDs to phone numbers for its own allowlist check via buildLidMap(), but the senderId field in the message payload sent to the gateway still contained the raw LID. This caused the gateway's WHATSAPP_ALLOWED_USERS check to reject all messages as unauthorized, since the LID numbers don't match the phone numbers in the allowlist. Fix: resolve LID → phone in the senderId, senderName, and chatName fields of the event payload before sending to the gateway, using the existing lidToPhone mapping. * chore: add AUTHOR_MAP entry for @ajmeese7 (#3219 salvage) * fix(status): label provider as custom when config.yaml model.base_url is set Salvage of the surviving hunk of #3296 by @Mibayy. The PR's gateway _handle_provider_command hunk targets code removed on main (/provider was absorbed into /model + /status, which already read model.base_url); the hermes status mislabel was the remaining live symptom: _effective_provider_label() only checked the legacy OPENAI_BASE_URL env var, so a custom endpoint configured canonically in config.yaml still displayed as OpenRouter. * feat(gateway): add 'log' option to display.tool_progress Salvage of #3459 by @keslerm, reimplemented against the restructured progress-callback block in gateway/run.py (resolve_display_setting, needs_progress_queue, thinking-relay). Duplicate PR #3458 by @dlkakbs was submitted 4 minutes earlier with the same feature — both credited. Co-authored-by: Dilee <uzmpsk.dilekakbas@gmail.com> tool_progress: log keeps the chat silent and appends timestamped tool-call lines to ~/.hermes/logs/tool_calls.log via a dedicated queue drained by an async writer (RotatingFileHandler 5MB x 3, RedactingFormatter so secrets never land on disk). Gateway-only by design; thinking_progress relaying and the webhook gate are unaffected. /verbose now cycles off -> new -> all -> verbose -> log. * fix(i18n): add gateway.verbose.mode_log to all locale catalogs * feat(commands): /compact alias + --preview/--dry-run flags for /compress (#3243 salvage) Salvaged from PR #3243 by @Mibayy, reimplemented against current main (the original diff targeted a removed gateway/run.py handler). - /compact is now a first-class alias of /compress (CLI, gateway, Telegram/Slack/Discord command lists, autocomplete) — also fixes the dangling '/compact' references in gateway error messages (gateway/run.py context-exhausted banners). - --preview / --dry-run: report what WOULD be compressed (message counts, token estimate, 'here [N]' boundary) without touching the transcript. Flags coexist with the existing 'here [N]' / focus-topic args on both the CLI and gateway surfaces via shared pure helpers in hermes_cli/partial_compress.py. - --aggressive (LLM-free hard truncation) is intentionally NOT implemented: it would need its own transcript-persistence branch outside the guarded _compress_context rotation machinery (#44794 data-loss class). The flag is recognized and returns an explanatory message pointing at '/compress here [N]' and /undo instead of being mis-parsed as a focus topic. - locales: gateway.compress.aggressive_unsupported added to all 16 catalogs (parity test enforced). - release.py: AUTHOR_MAP entry for contributor credit. * feat(api-server): per-client model routing via model_routes (#3176 salvage) Adds a no-code routing layer to the OpenAI-compatible API server so one Hermes deployment can map different API clients to different model/provider backends. Clients pick a backend by sending a configured alias as the OpenAI 'model' field; unmatched values fall back to the global model. Configured aliases are listed by GET /v1/models. Precedence (highest first): session /model override > model_routes route > global config. Route provider credentials resolve through _resolve_runtime_agent_kwargs_for_provider (same seam as channel_overrides); per-route api_key/base_url are upstream provider credential overrides — never caller auth, never logged. Salvaged and rebased from PR #3176 by @Mibayy onto current main. * feat(config): extra HTTP headers for LLM API calls (#3526 salvage) Named providers / custom_providers entries in config.yaml now accept an extra_headers dict scoped to that endpoint — for reverse proxies, API gateways, and custom auth schemes (e.g. Cloudflare Access service tokens). - hermes_cli/config.py: normalize extra_headers on provider entries (_normalize_custom_provider_entry + providers-dict translation), add get_custom_provider_extra_headers / apply_custom_provider_extra_headers_to_client_kwargs helpers keyed on base_url (case/trailing-slash insensitive, no substring bypass — mirrors the TLS helpers) - hermes_cli/runtime_provider.py: surface extra_headers in the resolved runtime for named custom providers (providers dict, legacy custom_providers list, and the credential-pool path) - run_agent.py / agent/agent_init.py: merge per-provider extra_headers onto the OpenAI client default_headers at construction and on every _apply_client_headers_for_base_url re-application (credential swaps, rebuilds), most-specific level wins; OpenAI-wire only (native Anthropic/Bedrock scoped out) - agent/auxiliary_client.py: accept model.extra_headers as an alias of model.default_headers for the global variant - cli-config.yaml.example: documented commented example - Header values are treated as secrets and never logged Salvaged from PR #3526 by @jneeee, reimplemented against current main. Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com> * feat(gateway): persist per-session /model overrides across gateway restarts Per-session /model overrides (_session_model_overrides) were in-memory only, so a gateway restart silently reverted every session to the global default model. Persist the non-secret parts (model/provider/base_url ONLY — never api_key) into the session entry in sessions.json and lazily rehydrate them on first use after a restart, re-resolving credentials through the normal runtime provider resolution. - gateway/session.py: SessionEntry.model_override field with sanitize_model_override() (allowlist: model/provider/base_url) applied on both serialization and deserialization; SessionStore.set_model_override / get_model_override accessors. reset_session() already creates a fresh entry, so /new keeps its clear-on-reset semantics — a restart cannot resurrect an override the user reset away. - gateway/slash_commands.py: write-through at both /model set sites (text command + picker) after storing the in-memory override. - gateway/run.py: _rehydrate_session_model_override() called from _resolve_session_agent_runtime(); in-memory state always wins, credentials are re-resolved per provider (credential-less fallback on failure). Session expiry finalization also drops the persisted override. - tests/gateway/test_session_model_override_persistence.py: restart round-trip, /new clearing, api_key-never-serialized (including tampered sessions.json), rehydration + live-state precedence + credential-failure degradation. Salvaged from #3659 by @Git-on-my-level, narrowed to the restart-persistence gap confirmed in triage. * fix(desktop): parse multiline slash commands + hand degenerate payloads back parseSlashCommand used /^(\S+)\s*(.*)$/ where `.` can't cross a newline and `$` anchors end-of-string, so any slash command whose arg contained a newline (/goal <multi-line text>, a skill command with a long pasted context) failed the whole match, parsed as an empty name, and rendered "empty slash command" while the payload vanished — cleared from the composer and absent from the Up-arrow history ring, which only derives from sent user messages. - name now splits on any whitespace ([\s\S]* arg), matching the CLI and the gateway's split(maxsplit=1); multiline args flow to slash.exec intact - the residual empty-name branch (bare "/", "/ text") restores the submitted text to the composer draft instead of eating it Fixes #41323. Fixes #55510. * fix(desktop): call checkUpdates() in startUpdatePoller so version pill auto-populates startUpdatePoller() only called checkBackendUpdates() — never checkUpdates(). The statusbar version pill reads $updateStatus (set by checkUpdates()), so the commit-behind counter stayed null after restart. It only appeared when the user manually clicked the pill, which triggered checkUpdates() via openUpdateOverlayFor. Added void checkUpdates() in three places alongside the existing checkBackendUpdates() calls: - On startup in startUpdatePoller() - In the 30-minute setInterval callback - In the onFocus handler checkUpdates() uses the Electron IPC bridge (local git check), not the gateway, so no mode gating is needed. The existing $updateChecking atom guard prevents double-fire on overlap. Fixes #53079 * fix(desktop): restore remote artifact rendering * style(desktop): fix import ordering + padding lint in remote-artifact files * feat(desktop): /journey opens the memory graph overlay instead of printing text * style(desktop): fix pre-existing import-order lint in use-prompt-actions * fix(desktop): restore remote file picker attachments * fix(desktop): read attachment previews local-first in remote mode attachImagePath fetched its thumbnail through readDesktopFileDataUrl, which in remote mode routes every read to the gateway fs bridge. Paperclip picks, clipboard saves, and OS drops always produce paths on the LOCAL machine, so the gateway read 404s — toasting "image preview failed" and dropping the thumbnail even though the attach itself works (upload reads local bytes via the Electron bridge). Read the local bridge first and fall back to the remote facade, which still serves in-app drags from the remote project tree. Local mode is unchanged (the facade already reads locally there). Follow-up to #56572, which restored the remote paperclip picker and made this path reachable from the picker as well. * fix(desktop): load remote model options before session Both Desktop picker surfaces (status-bar model menu, settings/onboarding dialog) only asked the connected gateway's model.options once a session existed; before that they fell back to the Desktop REST/global options, which can't see virtual providers a remote gateway exposes — including the MoA presets from #53817. Centralize the fetch rule in requestModelOptions(): prefer the connected gateway whenever one exists (no session_id needed — the RPC resolves disk config), REST only when no gateway is connected. The status-bar MoA preset section now renders from the same model.options payload (the virtual `moa` provider row) instead of the local /api/model/moa REST config, so remote presets appear correctly; the row is filtered out of the main provider groups so presets don't list twice. Preset selection keeps the persistent switchTo path from #56417 and drops the vestigial session gate — like regular model rows, a pre-session pick ships on the next session.create. Fixes #53817. Rebased and reconciled with #56417 (persistent MoA selection), which landed after this PR was opened and covered its one-shot-/moa half. * fix(terminal): set MSYS_NO_PATHCONV for Windows Git Bash subprocesses Git Bash mangles native Windows command flags (/FO, /TN, /Create) into bogus paths. Hermes terminal and background spawns now opt out by default so tasklist, schtasks, and wmic work without manual prefixes. Fixes #56700. * test(terminal): cover MSYS_NO_PATHCONV defaults on Windows env builders * fix(terminal): also set MSYS2_ARG_CONV_EXCL for MSYS2/Cygwin bash fallback MSYS_NO_PATHCONV is honored by Git for Windows bash only. _find_bash's final shutil.which fallback can return MSYS2-proper or Cygwin bash, which ignore it and honor MSYS2_ARG_CONV_EXCL instead. Set both so argv path conversion stays disabled regardless of which bash flavor spawns. Also subsumes the cmd /c mangling in #56147. * feat(desktop): collapse profile rail to a select past 13 profiles (#57306) The colored-square rail stops scaling once a user racks up many profiles: tiny drag targets and an endless horizontal scroll strip. Past a threshold (13) the rail swaps the squares for a compact select dropdown — same active tint + initial glyph, minus the drag-reorder / long-press-recolor / per-row context menu that only make sense at small counts. Two render paths behind one flag; the left default↔all toggle, the "+" create button, and Manage stay put in both. Rename/delete/color remain reachable via Manage. * Prevent deleted profile skeleton revival * feat(auth): make xAI Grok OAuth device-code-only, drop loopback login Replace the loopback/PKCE-callback server and manual-paste fallback with the RFC 8628 device-code flow as the only xAI Grok OAuth login path. The flow works in headless/SSH/container sessions with no 127.0.0.1 listener, shrinking the local attack surface. - Poll the token endpoint with server-provided interval, honoring slow_down and expires_in; store tokens with auth_mode oauth_device_code. - Adaptive proactive refresh skew for short-lived device-code JWTs; rotated tokens sync back to auth.json, the global root store, and the credential pool (no refresh-token replay). - Clear source suppression on successful re-login (CLI + dashboard) and drop the duplicate dashboard pool entry so exactly one seeded device_code entry exists. - Use the shared device_code source name for consistency with the nous/codex device-code providers. - Desktop: remove the loopback OAuth flow states and dead type variants; pkce providers' sign-in URL selection is unchanged. - Docs (EN + zh-Hans) rewritten for device-code login; drop the deleted --manual-paste flag from documented commands. * fix(auth): remove stale loopback_pkce reference in xAI quarantine removal list The terminal-refresh quarantine filtered in-memory entries on source == "device_code" but built removed_ids from the deleted "loopback_pkce" source name, so the revoked device-code entry was never pruned from the persisted pool in auth.json. Also restores the _print_loopback_ssh_hint test suite scoped to Spotify (the helper's remaining caller) instead of deleting it wholesale. * fix(desktop): skip ensureBackend after profile-delete teardown to prevent respawn loop When the renderer sends a DELETE /api/profiles/{name} request, the IPC handler tears down the profile's pool backend (or primary backend) via prepareProfileDeleteRequest. However, the very next line calls ensureBackend(profile), which spawns a fresh pool backend for the just- deleted profile. The new backend's startup path calls ensure_hermes_home(), which recreates the profile directory — defeating the deletion and leaving the process as a zombie. On the next Desktop restart the cycle repeats: the profile directory exists, the Desktop spawns a backend, the backend recreates the directory after deletion, and PIDs accumulate indefinitely. Fix: make prepareProfileDeleteRequest return the torn-down profile name. The IPC handler uses this to route the DELETE to the primary backend instead of spawning a new pool backend for the deleted profile. Fixes #52279 * fix(desktop): refresh profile rail after deletion (#49289) * fix(desktop,tui-gateway,zai): stop thinking-off from reverting to medium A Z.ai desktop user reported thinking reverting to medium after one turn, burning ~200% of a week's credits in 4 days despite reasoning_effort: false in config.yaml. Four compounding bugs: - _session_info reported reasoning_effort "" for disabled reasoning, indistinguishable from unset — the desktop adopted it after the first turn, wiping its sticky "thinking off" pick so every later chat reverted to the default effort. - config.set key=reasoning always wrote agent.reasoning_effort to global config.yaml, so every desktop model-menu selection (preset.effort ?? 'medium') clobbered the user's configured value. Now session-scoped like the messaging gateway's /reasoning, landing on create_reasoning_override so lazily-built sessions keep it too. - YAML `reasoning_effort: false`/`off`/`no` (boolean False) was coerced to "" by every loader's `str(x or "")`, silently re-enabling thinking. parse_reasoning_effort now treats False/"false"/"disabled" as {"enabled": False}; loaders (tui gateway, gateway, cli, cron, delegate) pass the raw value through. The desktop config reader also crashed on the boolean (false.trim()), aborting voice/STT settings. - The zai provider profile never sent thinking on the wire, and GLM-4.5+ defaults to thinking ON server-side — so disabling reasoning was a silent no-op on direct Z.ai, the actual token burner. The profile now emits extra_body.thinking {"type": "enabled"|"disabled"} for thinking-capable GLM models, mirroring the DeepSeek profile. Also: /new (session reset) now carries reasoning_config across the rebuild like model_override; config.get reasoning prefers the session's live value and maps a config False to "none"; Settings shows "Off" instead of a blank select for hand-written false. * fix(cli): stop profile-bound backends before deleting so rmtree converges delete_profile stopped only the process named in gateway.pid, but a Desktop app spawns a headless `serve`/`dashboard` backend per profile that holds the profile's SQLite connection open and keeps writing sessions/WAL/sandbox files. That backend is never in gateway.pid, so a CLI `hermes profile delete` run while the Desktop app is up left it writing into the tree — rmtree's final rmdir then failed with ENOTEMPTY (#47368 "Bug 2"), and pre-guard it also resurrected the directory. - _profile_bound_backend_pids(): find running Hermes backends bound to this profile via a `--profile <name>` selector or a HERMES_HOME env resolving to the profile dir. Tightly scoped — current-user only, backend subcommands (serve/dashboard/gateway) only so an interactive chat is never killed, and never this process or its ancestors. - _stop_profile_backends(): terminate them (graceful, then force), best-effort so it can never make delete worse. - _rmtree_with_retry(): a few spaced retries absorb the ENOTEMPTY / Windows file-lock race from a just-terminated writer's in-flight -wal/-shm/sandbox writes instead of failing the whole delete on a race the next attempt wins. Complements the recreation guard (deleted profiles no longer reappear) and the Desktop teardown-before-delete flow; this is the CLI-side convergence fix for a delete run while a Desktop-managed backend is live. Part of #47368. * fix(tui_gateway): route setup.runtime_check and setup.status to RPC pool setup.runtime_check and setup.status are polled by the Desktop frontend on connect and periodically (use-status-snapshot → evaluateRuntimeReadiness), but neither was in _LONG_HANDLERS — so dispatch() ran both inline on the WS reader thread. Under GIL pressure from concurrent agent turns (terminal I/O, large output, background-process completions) either can block for seconds: - setup.runtime_check → resolve_runtime_provider() (config read, auth check, may probe the provider endpoint) - setup.status → _has_any_provider_configured() (provider config + credential scan) While either blocks the reader thread the WS read loop can't service later requests; the frontend RPC timeout fires, the client drops the socket, and the lost setup.runtime_check response reads as ready=false — a false "needs setup" / "Settings failed to load" even though the provider is configured. Route both to the RPC pool (same precedent as #55545's session.list/pet.info/ process.list). The handlers are read-only and pool writes go through the lock-guarded write_json, so there's no ordering or safety concern. Test asserts all 5 frontend-polled RPCs are pool-routed. Co-authored-by: izumi0uu <izumi0uu@gmail.com> * chore(release): map yingliang-zhang in AUTHOR_MAP for #57335 * fix(usage): capture reasoning_tokens from completion_tokens_details on chat_completions (#57340) normalize_usage only read output_tokens_details.reasoning_tokens (the Responses API shape). Chat Completions providers — OpenAI, OpenRouter, DeepSeek, and every OpenAI-compatible proxy — report it under completion_tokens_details.reasoning_tokens, so reasoning_tokens was 0 for every chat_completions reasoning model: hidden thinking was invisible in session accounting, MoA traces, and the eval's per-task token columns. Measured impact (HermesBench MoA run on deepseek-v4-flash, 4,828 advisor calls): reasoning_tokens showed 0 everywhere while individual calls burned up to 21.5K hidden thinking tokens to emit ~500 visible tokens. Verified live against OpenRouter: deepseek-v4-flash returns completion_tokens_details.reasoning_tokens=61 for a 74-completion-token call; the field was simply never read. Responses-shape reads are unchanged; the new read only fires when the Responses shape yielded nothing. * fix(slack): keep blank-line-separated ordered items in one rich_text_list When a Markdown ordered list has blank lines between items (common in LLM-authored content), the list run loop breaks on each blank line. Slack numbers each rich_text_list independently, so N items produce N lists each starting at 1. Skip blank lines inside the list run as soft separators instead of breaking, so ordered items stay in one rich_text_list and Slack renders the correct numbering. Fixes #57076 * fix(slack): guard blank-line list continuation on next-item lookahead Refine the blank-line handling so a blank line only continues a list run when the next non-blank line is another list item. This keeps a list -> paragraph -> list sequence as three separate blocks and matches the contiguous-list layout for mixed/nested lists (one rich_text block, split into sub-lists by (indent, ordered)), rather than emitting a separate block per item. Adds regression tests for the mixed blank-separated layout and the list->paragraph->list boundary. * refactor(slack): extract _is_list_line helper for list-marker checks Deduplicate the '_BULLET_RE.match or _ORDERED_RE.match' idiom used at the list-run entry guard and the blank-line lookahead into a single helper, so adding future marker types is a one-point change. Pure refactor, no behavior change (22 block_kit tests still pass). * fix: refresh NVIDIA featured models * fix(agent): honor live vLLM context limits on local endpoints Reconcile stale local disk cache against live vLLM/Ollama max_model_len probes, probe local servers before the llama hardcoded default, parse vLLM max_model_len overflow errors, and surface the non-agentic Hermes 3/4 warning at agent init on gateway/TUI. Sub-64K live probes are returned for startup rejection but are not persisted to the context cache — preserving the 64K minimum-context contract instead of normalizing undersized windows as valid config. (cherry picked from commit c3a02db4fd9d57b7b0eb2732de91f8334d311aa5) * test(agent): cover local vLLM context-length resolution Add regression tests for vLLM max_model_len error parsing, stale local cache reconciliation, live probes over llama defaults, and the 64K minimum guard on persistent cache writes. (cherry picked from commit 1cb47ef437de7ce289cb358e8d6b89e9194b43ed) * fix(gateway): close webhook sessions on delivery completion so prune can reap them Webhook deliveries created a unique one-shot session (delivery_id baked into the session key at gateway/platforms/webhook.py:668) but the adapter fired handle_message via asyncio.create_task WITHOUT ever ending the session (webhook.py:713, pre-fix). Nothing else closes it: the gateway caches/expires the agent per session_key but never calls end_session for the webhook path, and _end_session_on_close teardown doesn't run for these fire-and-forget tasks. SessionDB.prune_sessions (hermes_state.py:4965) only deletes rows WHERE ended_at IS NOT NULL. So every webhook session stayed with ended_at NULL -> unprunable -> unbounded state.db growth. This was the primary driver of the SQLite lock-contention gateway outage. Fix: wrap the delivery in _run_delivery_and_close, which awaits handle_message and then (in finally, so failures still reap) calls _end_webhook_session -> SessionDB.end_session(session_id, 'webhook_complete'). This mirrors how cron closes its session with 'cron_complete' (cron/scheduler.py:3065). end_session is first-reason-wins and no-ops on an already-ended row, so it never clobbers a compression/agent_close reason. Adds tests/gateway/test_webhook_session_close.py asserting the invariant (a completed webhook session has ended_at set + is prunable), including the error-path case, against a real SessionStore + SessionDB. * chore: map gumclaw@gumroad.com in AUTHOR_MAP for PR #57322 salvage * refactor(gateway): add SessionStore.peek_session_id public accessor for webhook close Replace the webhook delivery-close path's direct reach into private SessionStore._entries (which also bypassed the store lock) with a public, lock-held peek_session_id(session_key) accessor. Mirrors the existing lookup_by_session_id inverse helper. Keeps a getattr fallback for older stores / test doubles. Adds a unit test for the accessor. * fix(agent): resolve review findings on vLLM local-context salvage Salvage review of #56431 surfaced one Critical + two Warning issues; fix them on top of the contributor's cherry-picked commits: 1. Critical — duplicate non-agentic warning on the interactive CLI. The new agent_init warning fires on every platform, but cli.py show_banner() already warns on CLI (richer output + /model hint), so a CLI user saw the warning twice per startup. Guard the agent_init emit to skip platform=="cli" — it now fills exactly the gateway/TUI gap the PR intended, no duplication. 2. Warning — vLLM error-parse regex under-matched. The patterns required a literal space before the number, so "max_model_len: 32768", "=32768", "(32768)", and "... is 32768" all returned None. Broaden both patterns to accept :/=/(/ 'is' delimiters. Add a parametrized test over all delimiter variants. 3. Warning — per-call live probe latency on local endpoints. The new reconcile-on-hit + pre-defaults step-7 probe made every local resolution fire a synchronous network probe (banner + /model switch + compressor update_model each within one startup). Add a 30s in-process TTL cache keyed by (model, base_url) around _query_local_context_length so back-to- back resolutions reuse one round-trip; not persisted to disk, so the reconcile freshness contract (re-probe after restart) is preserved. Add an autouse fixture clearing the cache between tests + TTL coverage. Tests: 148 passed (was 138). ruff clean. * chore(release): add infinitycrew39 to AUTHOR_MAP (#56431 salvage) * chore: add trismegistus-wanderer to AUTHOR_MAP for PR #31856 salvage * fix(dashboard): disable ws keepalive ping on loopback to survive event-loop stalls Desktop/dashboard WebSocket connections drop during long agent operations (delegate_task subagents, large model outputs) when the uvicorn event loop is GIL-starved for minutes. Root cause: uvicorn's ws keepalive ping runs on the SAME event loop as agent turns. A single synchronous GIL-holding call on a worker thread (a regex/scrub over a large output, or a long subagent turn) freezes the loop, so it cannot process the incoming pong within ws_ping_timeout and uvicorn closes an otherwise-healthy connection (#53773: 'event loop stalled 226.3s'; #48445/#50005). Loosening the timeout only raises the threshold — a multi-minute stall sails past any finite window. The keepalive ping exists to detect half-open connections (reverse-proxy 524, dropped tunnels), which cannot happen on loopback: there is no network or proxy in the path, and a dead local client tears the socket down with a real FIN/RST that starlette surfaces as WebSocketDisconnect regardless of the ping. So on loopback the ping provides ~no liveness value while actively killing recoverable stalls — disable it entirely (ws_ping_interval/timeout=None). Non-loopback (public) binds sit behind a Cloudflare Tunnel where half-open IS a real failure mode, so the ping stays at 20/20 to detect it. Empirically verified (real uvicorn + websockets peer): with ws_ping=None the server never closes a silent peer during an 8s window; with the pre-fix 2s/2s window uvicorn closes it. A genuinely-dead client still fires the WebSocketDisconnect reap path regardless of the ping. Note: this fixes the local Desktop case (the OP's scenario). A remote Desktop over an authenticated public dashboard route (McCalebTheSecond's comment) keeps the ping and needs the deeper GIL-hotspot fix — tracked separately. Closes #53773 * fix(agent): self-review follow-ups on vLLM local-context salvage Self-review (ruff+ty lint diff = 0 net-new; 2-agent deep review) surfaced one Warning + comment-accuracy nits; no Critical: - W1: the local-probe TTL cache memoized None (probe failure) for 30s, so a probe that failed during a startup race would suppress a legit retry once the server came up. Cache only positive results — still fully bounds the hot-path probe rate (reachable servers cache their value) while an unreachable one re-probes on the next call. Add a regression test asserting a None result is NOT cached (retry re-probes); mutation-verified. - Tighten the platform-guard comment: gateway/TUI/cron already construct with quiet_mode=True (gated by `not agent.quiet_mode`), so the guard's active job is CLI dedup vs show_banner, not "filling the gateway/TUI gap" as originally worded. Verified not-issues (per review): positive-value 30s cache does not break the reconcile-after-restart freshness contract (restart = fresh process, empty cache); cache key is collision-safe; platform guard is correct in both directions (no runtime path leaves platform None on a non-CLI surface). Tests: 149 passed. ruff clean; ty 0 net-new vs base. * fix(gateway): keep idle cached agents alive until session actually expires The idle-TTL sweep (_sweep_idle_cached_agents) was evicting agents as soon as they passed _AGENT_CACHE_IDLE_TTL_SECS, even when the session hadn't expired yet. In daily-reset mode the reset can fire hours after the last user message — evicting the agent early means the session-expiry watcher has no agent in cache to call on_session_end() with, so memory providers miss the live transcript. Now the sweep checks the session store before evicting: if the session still exists and hasn't expired, the agent stays in cache so the expiry watcher can tear it down properly later. When the session store is unavailable or throws, falls back to the original eviction behavior (safe default). Fixes: #11205 * fix(gateway): complete on_session_end coverage across all eviction paths Follow-up to the cherry-picked #31856 fix. The contributor's guard defers idle-TTL eviction until the session store reports the session expired, so the expiry watcher can tear the agent down and fire MemoryProvider.on_session_end() with the live transcript. Two gaps remained: 1. Memory-leak regression for mode='none' sessions. _is_session_expired() returns False forever for the 'none' reset policy, so the naive guard would never idle-evict those agents — reopening the unbounded-cache leak the idle sweep (#11565) exists to relieve. Added SessionStore.is_session_finalizable() (a public predicate: will the expiry watcher EVER finalize this session?) and gate the deferral on it. mode='none' agents fall through to soft eviction as before. 2. on_session_end still dropped on the LRU-cap path. Both cache-pressure paths (_enforce_agent_cache_cap and _sweep_idle_cached_agents) soft-evict via _release_evicted_agent_soft, which by design does NOT fire on_session_end. If cache pressure evicts a finalizable-but-not-yet-expired agent before it expires, the watcher later finds no cached agent and the hook is skipped. Added _commit_memory_before_soft_evict(): at LRU eviction, if the session is finalizable and not yet expired, commit end-of-session extraction via the live agent's own (fully-scoped) memory manager using commit_memory_session() — extraction WITHOUT provider teardown, so the eviction stays soft and a resumed turn keeps working. Skipped for mode='none' (no missed boundary to compensate) and expired sessions (the watcher tears those down directly). This closes #11205 for ALL eviction paths and reset policies, not just the idle-sweep + finite-policy case, while preserving the soft-eviction resumability contract (never calls close() on a live session). Tests: 5 new cases in test_agent_cache.py (mode='none' still reaped, LRU-cap commits for finalizable / skips for none, real is_session_finalizable predicate); all mutation-checked. Contributor's original 2 tests updated to assert the finalizable path explicitly. * fix(providers): pass extra headers to model discovery * refactor(providers): dedupe extra_headers normalizer + key picker groups by headers Follow-up to @helix4u's #57336 salvage. Two review findings: - W1: model-picker grouped custom-provider rows by (api_url, credential, api_mode) but NOT extra_headers. Entries sharing a URL+credential+api_mode yet declaring different headers (e.g. per-tenant routing behind one proxy) collapsed into one row and probed /models with whichever header set was seen first (order-dependent). Fold a canonical header identity into group_key so distinct header-authed endpoints stay separate; drops the now-dead first-non-empty merge branch. - W2: the extra_headers stringify+None-filter comprehension existed in 5 copies (config.py x2, runtime_provider.py, model_switch.py, models.py). Extract one shared hermes_cli.config.normalize_extra_headers primitive; all sites now call it. Tests: +normalize_extra_headers unit tests, +regression test proving two same-endpoint entries with different headers stay distinct and each probes with its own headers. 223 targeted tests pass; ruff clean. * fix(desktop): let settings content use full pane width Remove the max-w-4xl wrapper from SettingsContent so every settings page can use the available overlay width. * fix(desktop): extend profile startup REST timeouts (#48504) * fix(desktop): extend startup long-timeout to the whole boot data burst Broadens Tranquil-Flow's profile-startup timeout fix (#48518) from getProfiles + refreshActiveProfile to the rest of the calls the desktop fires during connect: /api/config, /api/config/defaults, /api/model/info, /api/model/options, /api/cron/jobs. On a profile-heavy or remote install any of these can exceed the 15s DEFAULT_FETCH_TIMEOUT_MS while the backend is alive-but-busy (e.g. list_profiles walks the skill tree per profile), surfacing as the spurious "Timed out connecting to Hermes backend after 15000ms" that hangs the UI (#48504). Uses the surgical per-call mechanism (renamed STARTUP_PROFILE_REQUEST_TIMEOUT_MS → STARTUP_REQUEST_TIMEOUT_MS) rather than raising the global default (the alternative in #48526): the liveness poll /api/status and all interactive/ runtime calls keep the short default, so a genuinely-dead backend is still detected fast and the boot readiness probe (waitForHermes) is untouched. Supersedes #48518 (carried as the base commit) and #48526 (global-default raise). Fixes #48504. Co-authored-by: YapBi <129007007+HeLLGURD@users.noreply.github.com> Co-authored-by: Tranquil-Flow <66773372+Tranquil-Flow@users.noreply.github.com> * fix(webhook): close per-delivery session at the true end of the run (#57423) The merged webhook session-close fix (#57370, salvaging #57322) wrapped handle_message in a try/finally — but BasePlatformAdapter.handle_message is fire-and-forget: it spawns _process_message_background and returns before the agent run starts. The finally-close therefore ran BEFORE get_or_create_session created the session row, found no session_id, and silently no-op'd — the ghost-session leak persisted on the real path. (The shipped test masked this by stubbing handle_message with a fake that created the row synchronously.) Move the close to an on_processing_complete override — the lifecycle hook the base class fires at the TRUE end of the run, on the success, failure, and cancellation paths alike. Empirically verified through the real fire-and-forget pipeline: before, ended_at stayed NULL; after, ended_at is set with end_reason=webhook_complete and the row is prunable. Tests now stub only the runner-side _message_handler (the seam the live gateway injects) so handle_message / _process_message_background / on_processing_complete all run for real; adds an AsyncSessionDB-facade coverage test for the coroutine-await branch. * fix(dump): flag API keys visible only to the shell, not the managed backend hermes debug share reads os.getenv — the invoking terminal's environment — but launchd/systemd and the desktop-spawned `serve` backend load credentials from ~/.hermes/.env, not the login shell. A key exported in the shell but absent from .env is invisible to the backend, yet the dump printed a bare "set", sending support down a phantom "the key is configured" path. This was the actual trap behind a "Desktop has no web_search / no tools" report: FIRECRAWL_API_KEY was a shell export (so `debug share` in a terminal read "firecrawl set") but not in .env, so the launchd backend's check_web_api_key returned False and web_search was gated off — which a contributor then misdiagnosed as a missing `desktop` platform registration. The dump now annotates any key set in-process but missing from ~/.hermes/.env with "(shell only — not in .env; managed/desktop backend may not see it)" so the mismatch is obvious instead of hidden behind "set". * feat(skills): add security/unbroker (autonomous data-broker removal) unbroker finds where a consenting person's info is exposed across data brokers and people-search sites and files the removals, running as far as each site allows and handing only genuinely human-only steps (hard CAPTCHA, gov-ID, phone, fax) back as an end-of-run digest. - Deterministic stdlib CLI (scripts/pdd.py) owns config, dossiers+consent, the broker DB, tier planning, the ledger, email, and the autonomous action queue; the agent scans/submits with native tools (web_extract, browser_*, delegate_task, cronjob, terminal). - Verify-before-disclose, least-disclosure (never volunteers SSN), consent gate, opaque ids, optional age-at-rest encryption, file-locked ledger. - Jurisdiction-aware (CCPA/CPRA, GDPR, generic); CA DROP one-shot covers the state registry (~545) in a single request; BADBOOL + curated people-search coverage; scheduled re-scan for re-listing. - No CAPTCHA-solving services or anti-bot bypass; browser email mode needs no stored password. - 85 hermetic tests (tests/skills/test_unbroker_skill.py; SMTP/IMAP via injected fakes, registry via CSV fixtures). Ships placeholder data only. Broker dataset adapted from BADBOOL (Yael Grauer, CC BY-NC-SA 4.0). * docs(unbroker): point README image link at hermes-agent; sync test count (85) * feat(desktop): cap overlay inner-page width at 75rem Add a shared PAGE_MAX_W (1200px) and center OverlayMain within its pane so settings and command center bodies stay readable instead of sprawling on wide/ultrawide displays. * fix(desktop): stop macOS Tahoe misplacing the traffic lights On macOS Tahoe (Darwin 25+), a nonzero titleBarOverlay height makes setWindowButtonPosition() miscalculate the native traffic-light position (electron#49183), shoving the lights into the left titlebar tools. Pass height 0 there so the lights land at the configured inset; the renderer paints its own drag strips, so nothing is lost. Pre-Tahoe is unchanged. Gate on the truthful Darwin kernel major (25 = Tahoe) rather than the product version, which macOS reports as 16 or 26 depending on build SDK. * fix(desktop): clear stale active todos on turn end AND on rehydration A turn that ends without a final `todo` update left the composer "Tasks N/M" panel pinned with its last item stuck pending/in_progress, and it survived restarts because the panel is read back from stored session history. Two coupled fixes (the first alone is undone by the second path): - Turn end: clear a still-active todo list on `message.complete` and on a terminal `error` (new `clearActiveSessionTodos` — active lists only; a finished list keeps its short linger so the last checkmark still lands). - Rehydration: `hydrateFromStoredSession` runs *after* a turn completes, so an "active" stored list is stale, not in-flight. It now restores only a *finished* list (via new `todosForHydration`) and drops anything still active — otherwise it re-pinned the panel right after the turn-end clear and resurrected it on every restart. Salvages #52996 (@0disoft): the fix shape (clearActiveSessionTodos on turn completion, preserving the finished-list linger) is carried forward and ported onto the current use-message-stream/ folder split (gateway-event.ts), then extended to the rehydration path per review. Co-authored-by: 0disoft <rodisoft1@gmail.com> * fix(unbroker): suppress-first for PeopleConnect (deletion undoes suppression) PeopleConnect is the exception to deletion-beats-suppression: "DELETE MY USER DATA" also deletes suppressions on file, and deletion does not stop the people-search sites from showing you (public records re-list). Suppression is the effective lever and must be maintained. - intelius.json: deletion.prefer=false; playbook/quirks/notes rewritten with the verbatim privacy-center language; delete is the data-purge-only path. - autopilot: honor deletion.prefer -> prefer_suppression when false. - methods.md / SKILL.md / README: exception called out. - tests updated + prefer-flag routing test (86 tests). * fix(file-tools): preserve container paths for docker file ops (#56637) * fix(config): preserve owner on atomic writes (#56644) * fix(desktop): use gift codicon for update-available toast Add optional notification icon override and use codicon-gift on the update-ready toast so it reads as a present rather than generic info. * fix(desktop): use symbol-namespace codicon for Model settings nav * fix(desktop): cancel downloads triggered by link-title fetch window The hidden BrowserWindow used by fetchLinkTitle to scrape page titles had no will-download handler on its session. When a link artifact URL responds with Content-Disposition: attachment, Electron fires will-download and the file is saved for real — explaining the spurious download on the Artifacts page. Add guardLinkTitleSession() (parallel to the existing audio-mute guard for #49505) that installs a will-download handler which immediately cancels every download item on the hermes:link-titles session. Call it from getLinkTitleSession() right after the request-type blocklist is wired up. * fix(slack): MPIMs (group DMs) obey shared-surface mention gating + reaction guard Group DMs (MPIMs) were classified as DMs and thereby exempted from every operator control that shared surfaces are supposed to honor: allowed_channels, require_mention, strict_mention, free_response_channels, and the reaction guard. Symptom: the bot added :eyes:/:white_check_mark: to unmentioned MPIM messages and still invoked the agent (which then returned NO_REPLY) instead of the gateway dropping the event before model execution. Removing an MPIM from allowed_channels did not disable it. Root cause is the DM classification at adapter.py: is_dm = channel_type in {"im", "mpim"} used for BOTH routing exemptions and reaction gating. An MPIM is a shared surface (multiple humans can see and trigger the bot), not a private 1:1 DM, so it must be gated like a channel. This behavior was introduced/reinforced by a trail of Slack group-DM PRs: - #4633 fix(slack): treat group DMs (mpim) like DMs + reaction guard - #54632 fix(slack): subscribe to message.mpim + mpim scopes so group DMs work - #54663 fix(slack): group DMs work OOTB + reinstall nudge #54632/#54663 correctly made MPIM messages *reachable*; #4633 over-reached by giving them the DM mention/reaction *exemptions*. This corrects only that over-reach. Fix (minimal): introduce `is_one_to_one_dm = channel_type == "im"` and key the two EXEMPTION sites off it instead of `is_dm`: - mention/allowlist gating block (`if not is_one_to_one_dm and bot_uid:`) - reaction guard (`(is_one_to_one_dm or is_mentioned)`) `is_dm` is intentionally retained for session/thread scoping and chat_type labeling, where treating an MPIM as a persistent multi-party conversation is correct — only the mention/reaction exemptions were wrong. Docs: slack.md now distinguishes 1:1 DMs (mention-exempt) from group DMs (shared surface; obey require_mention/strict_mention/allowed_channels/ free_response_channels; reactions only when @mentioned). Tests: +7 in test_slack_mention.py (MPIM unmentioned dropped under require_mention and strict_mention; MPIM mentioned processed; MPIM off allowed_channels dropped; MPIM in free_response opted in; 1:1 IM still exempt; reaction guard drops unmentioned MPIM). Updated _would_process to model the is_one_to_one_dm gating + strict_mention. 72 passed. * 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. * 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 * 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 * 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. * 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. * 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. * 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 * fix(browser): retry next candidate when debug launch exits early * 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 * 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. * fix(desktop): poll messaging sessions so platform traffic appears live Inbound Telegram/WeChat/Discord messages are written by the background gat…
hughmadden
pushed a commit
to hughmadden/hermes-agent
that referenced
this pull request
Jul 8, 2026
…er user turn (NousResearch#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. (cherry picked from commit 9e044cf)
habarmc1223-sudo
pushed a commit
to habarmc1223-sudo/hermes-agent-fluxmem
that referenced
this pull request
Jul 8, 2026
…er user turn (NousResearch#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.
santhreal
pushed a commit
to santhreal/hermes-agent
that referenced
this pull request
Jul 13, 2026
…er user turn (NousResearch#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.
1 task
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
…er user turn (NousResearch#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.
gabrielcosi
pushed a commit
to gabrielcosi/home-ops
that referenced
this pull request
Jul 21, 2026
…7.7 ➔ v2026.7.20) (#10)
This PR contains the following updates:
| Package | Update | Change |
|---|---|---|
| [ghcr.io/gabrielcosi/hermes-agent](https://github.com/NousResearch/hermes-agent) | patch | `v2026.7.7` → `v2026.7.20` |
---
### Release Notes
<details>
<summary>NousResearch/hermes-agent (ghcr.io/gabrielcosi/hermes-agent)</summary>
### [`v2026.7.20`](https://github.com/NousResearch/hermes-agent/releases/tag/v2026.7.20): Hermes Agent v0.19.0 (2026.7.20) — The Quicksilver Release
[Compare Source](https://github.com/NousResearch/hermes-agent/compare/v2026.7.7...v2026.7.20)
##### Hermes Agent v0.19.0 (v2026.7.20)
**Release Date:** July 20, 2026
**Since v0.18.0:** \~2,245 commits · \~1,065 merged PRs · \~2,465 files changed · \~300,000 insertions · \~36,000 deletions · **\~3,300 issues closed** · **450+ community contributors**
> **The Quicksilver Release.** Hermes is the messenger god, and this window we made him move like it. First-turn time-to-first-token dropped **\~80% on every platform**, reasoning streams live by default, the desktop app got a \~20-PR speed overhaul (14× faster streaming markdown, virtualized diffs, snappy session switching), and the TUI renders markdown incrementally. Around that speed spine: you can now **manage your Nous subscription without leaving the terminal**, plug **Bitwarden and 1Password** straight into Hermes, let **smart approvals** judge flagged commands for you by default, **watch your subagents work live**, and trust that a finished response **survives a gateway crash** thanks to a durable delivery ledger. This release also rolls up everything from the v0.18.1 and v0.18.2 infrastructure patch tags — those windows are fully documented here.
***
##### ✨ Highlights
- **Hermes got dramatically faster — first token in a fraction of the time** — Cold-start "Initializing agent..." used to eat \~4.3 seconds before your first turn even reached the model; it's now \~0.9s, an \~80% cut that applies to the CLI, gateway, TUI, desktop, and cron alike. Round 2 attacked what you *see* while waiting: reasoning models now stream their thinking live by default (no more staring at a spinner for 30 seconds), and the response box paints per token instead of per line. If Hermes ever felt like it took a deep breath before answering, that breath is gone. ([#​59332](https://github.com/NousResearch/hermes-agent/pull/59332), [#​59389](https://github.com/NousResearch/hermes-agent/pull/59389) — [@​teknium1](https://github.com/teknium1))
- **The desktop app speed wave — 20+ targeted perf PRs** — Long replies used to cost 14× more CPU in the markdown splitter than they do now; giant diffs froze the review pane until we virtualized it; switching sessions thrashes layout no more. Streaming no longer re-renders the sidebar and every tool row per token, profile backends pre-warm on hover intent, and boot-hidden panes mount at idle instead of on the cold-start critical path. The net effect: the desktop app feels like a native app under load, even with huge transcripts and busy agents. ([#​67154](https://github.com/NousResearch/hermes-agent/pull/67154), [#​67818](https://github.com/NousResearch/hermes-agent/pull/67818), [#​65898](https://github.com/NousResearch/hermes-agent/pull/65898), [#​66033](https://github.com/NousResearch/hermes-agent/pull/66033), [#​66747](https://github.com/NousResearch/hermes-agent/pull/66747), [#​67742](https://github.com/NousResearch/hermes-agent/pull/67742) and more — [@​OutThisLife](https://github.com/OutThisLife))
- **Manage your Nous plan from the terminal — `/subscription` and `/topup`** — Changing your subscription used to mean a trip to the billing website. Now `/subscription` opens a full flow right in the TUI or classic CLI: see your plan and remaining allowance, preview exactly what an upgrade costs ("Pay $46.30 & upgrade now") or when a downgrade takes effect, and apply it — with scheduled-change banners and undo. The desktop app got a matching billing settings tab. Your wallet never has to leave the keyboard. ([#​51639](https://github.com/NousResearch/hermes-agent/pull/51639), [#​61054](https://github.com/NousResearch/hermes-agent/pull/61054), [#​61067](https://github.com/NousResearch/hermes-agent/pull/61067) — [@​alt-glitch](https://github.com/alt-glitch))
- **Smart approvals are now the default** — When Hermes wants to run a flagged command, an LLM reviewer now assesses it independently instead of asking you to approve every single one — and each verdict covers only that exact command, so a later command matching the same pattern gets its own review. Combined with the new **user-defined deny rules** (which block commands even under yolo mode) and `/deny <reason>` (which tells the agent *why* you refused so it course-corrects), day-to-day approval fatigue drops sharply without giving up control. ([#​62661](https://github.com/NousResearch/hermes-agent/pull/62661), [#​59164](https://github.com/NousResearch/hermes-agent/pull/59164), [#​54518](https://github.com/NousResearch/hermes-agent/pull/54518) — [@​teknium1](https://github.com/teknium1))
- **Plug your password manager into Hermes — Bitwarden & 1Password secret sources** — API keys no longer have to live in a plaintext `.env`. A new pluggable `SecretSource` interface lets Hermes fetch secrets from Bitwarden and 1Password (`op://` references) at load time, with multiple vaults enabled simultaneously, deterministic precedence, conflict warnings, and per-variable provenance. This consolidated eleven competing community PRs into one orchestrated interface — future vault providers drop in as plugins. ([#​59498](https://github.com/NousResearch/hermes-agent/pull/59498) — [@​teknium1](https://github.com/teknium1), 1Password provider salvaged from [@​hwrdprkns](https://github.com/hwrdprkns))
- **Watch your subagents work — live transcripts + durable background delegation** — `delegate_task` dispatches now return live transcript files you can `tail -f` the moment the subagents launch: every tool call, result, and streamed reply, one human-readable log per child. And background delegation completions are now **durable** — if the process restarts mid-run, results are restored and delivered through an ownership-checked ledger instead of vanishing. Fan out a fleet, watch any worker live, and never lose the results. ([#​67479](https://github.com/NousResearch/hermes-agent/pull/67479), [#​63494](https://github.com/NousResearch/hermes-agent/pull/63494) — [@​teknium1](https://github.com/teknium1))
- **A finished answer can no longer be lost — the delivery-obligation ledger** — If the gateway died between generating your response and confirming the platform actually delivered it, that answer used to be silently gone (and you'd paid for the turn). Final responses are now recorded in a durable ledger in `state.db` around the platform send and **redelivered on the next boot** — closing a P1 silent-loss window for Telegram, Discord, Slack, and every other channel. ([#​67181](https://github.com/NousResearch/hermes-agent/pull/67181) — [@​teknium1](https://github.com/teknium1))
- **One gateway, many profiles — profile-based message routing** — A single multiplexed gateway sharing one bot token can now route specific guilds, channels, or threads to different profiles — each with fully isolated config, skills, memory, and secrets. Point your work Discord server at the `work` profile and your hobby server at `personal`, from one bot. A second multiplex hardening wave means one misconfigured profile can no longer take down the whole gateway. ([#​64835](https://github.com/NousResearch/hermes-agent/pull/64835) salvaging [@​Burgunthy](https://github.com/Burgunthy), [#​65700](https://github.com/NousResearch/hermes-agent/pull/65700), [#​60589](https://github.com/NousResearch/hermes-agent/pull/60589) — [@​teknium1](https://github.com/teknium1), [@​benbarclay](https://github.com/benbarclay) + six salvaged contributors)
- **New providers and the newest frontier models** — Fireworks AI and DeepInfra land as first-class providers (Fireworks with cost estimation and a [#​2](https://github.com/NousResearch/hermes-agent/issues/2) slot in the provider picker), Upstage Solar joins via salvage, and the model catalogs picked up **GPT-5.6 (Sol/Terra/Luna + Pro variants, wired end-to-end across every route)**, **grok-4.5 (GA)**, **moonshotai/kimi-k3**, **claude-fable-5 / claude-sonnet-5**, and GA **tencent/hy3** — plus LM Studio JIT model loading for local setups. ([#​62593](https://github.com/NousResearch/hermes-agent/pull/62593), [#​63969](https://github.com/NousResearch/hermes-agent/pull/63969), [#​61616](https://github.com/NousResearch/hermes-agent/pull/61616) — [@​kshitijk4poor](https://github.com/kshitijk4poor) completing [@​rob-maron](https://github.com/rob-maron)'s [#​61578](https://github.com/NousResearch/hermes-agent/issues/61578), [#​60887](https://github.com/NousResearch/hermes-agent/pull/60887), [#​65913](https://github.com/NousResearch/hermes-agent/pull/65913), [#​64541](https://github.com/NousResearch/hermes-agent/pull/64541), [#​65472](https://github.com/NousResearch/hermes-agent/pull/65472))
- **Crank the thinking to max — new reasoning effort tiers and per-model control** — Reasoning effort gained `max` and `ultra` levels (GPT-5.6 and Codex's top tiers), selectable everywhere from the CLI to the desktop, with sane clamping on providers with smaller scales. You can now also pin **per-model reasoning-effort overrides** in config, set **per-slot effort in MoA presets** (your advisors think hard, your synthesizer stays fast), and per-task effort for auxiliary models. Thinking depth is now a dial, not a global switch. ([#​62650](https://github.com/NousResearch/hermes-agent/pull/62650), [#​64458](https://github.com/NousResearch/hermes-agent/pull/64458), [#​64631](https://github.com/NousResearch/hermes-agent/pull/64631), [#​64597](https://github.com/NousResearch/hermes-agent/pull/64597) — [@​teknium1](https://github.com/teknium1))
- **Your sessions, your data — export everything** — `hermes sessions export` now writes Markdown, Quarto, HTML, prompt-only, and even Hugging Face-ready trace formats, with the full filter surface (age, workspace, platform), an opt-in `--redact` secret-scrubbing pass, and compacted-session lineage stitched into one logical export. Pair with the new prune filters and bulk archive to keep your session store tidy. Your conversation history is a real dataset now, not a black box. ([#​60186](https://github.com/NousResearch/hermes-agent/pull/60186) salvaging [@​web3blind](https://github.com/web3blind), [#​60492](https://github.com/NousResearch/hermes-agent/pull/60492), [#​60507](https://github.com/NousResearch/hermes-agent/pull/60507), [#​59327](https://github.com/NousResearch/hermes-agent/pull/59327) — [@​teknium1](https://github.com/teknium1))
- **Security hardening round** — This window closed a long list of credential-surface gaps: Vertex credentials scoped away from subprocess env and through profile secret scopes, media/vision/image-gen local-file reads routed through one shared credential-read guard, a webhook body-size-cap sweep across every aiohttp server, bot-token redaction in Telegram transport errors, Fireworks token prefixes added to the redactor, six P1 browser/MEDIA/.env hardening PRs salvaged in one pass, and CI hardened against untrusted-ref interpolation. ([#​57660](https://github.com/NousResearch/hermes-agent/pull/57660), [#​58709](https://github.com/NousResearch/hermes-agent/pull/58709), [#​59215](https://github.com/NousResearch/hermes-agent/pull/59215), [#​56582](https://github.com/NousResearch/hermes-agent/pull/56582), [#​57842](https://github.com/NousResearch/hermes-agent/pull/57842) — [@​teknium1](https://github.com/teknium1), [@​srojk34](https://github.com/srojk34), [@​kshitijk4poor](https://github.com/kshitijk4poor), [@​jquesnelle](https://github.com/jquesnelle))
***
##### ⚡ Performance — the speed spine
##### First-turn latency (all platforms)
- **\~80% TTFT cut** — Discord capability detection off the critical path (token-keyed 24h disk cache + background refresh), Ollama probe skipped for known non-Ollama providers, agent-init blocking work removed; cold submit→dispatch \~4.3s → \~0.9s ([#​59332](https://github.com/NousResearch/hermes-agent/pull/59332) — [@​teknium1](https://github.com/teknium1))
- **Perceived-latency round 2** — `display.show_reasoning` default ON (watch the model think instead of a spinner), per-token response-box painting with width-aware force-flush, prompt-build caching, mtime-cached timezone resolution ([#​59389](https://github.com/NousResearch/hermes-agent/pull/59389) — [@​teknium1](https://github.com/teknium1))
- Segment mixed tool batches to recover lost concurrency; drop per-call base64 re-serialization from request-size estimates ([#​64460](https://github.com/NousResearch/hermes-agent/pull/64460), [#​67788](https://github.com/NousResearch/hermes-agent/pull/67788) — [@​teknium1](https://github.com/teknium1), [@​OutThisLife](https://github.com/OutThisLife))
##### Desktop speed wave
- 14× less splitter CPU via incremental block lexing for streaming markdown; virtualized review-pane diffs (no more full-Shiki freeze); snappy session switching on large transcripts; killed the layout-thrash cascade on session switch ([#​67154](https://github.com/NousResearch/hermes-agent/pull/67154), [#​67818](https://github.com/NousResearch/hermes-agent/pull/67818), [#​65898](https://github.com/NousResearch/hermes-agent/pull/65898), [#​66033](https://github.com/NousResearch/hermes-agent/pull/66033) — [@​OutThisLife](https://github.com/OutThisLife))
- Cut startup serialization + per-turn REST amplification; pre-warm profile backends and gateway sockets on hover intent; idle-mount boot-hidden panes; fast model picker + dialogs ([#​66747](https://github.com/NousResearch/hermes-agent/pull/66747), [#​66347](https://github.com/NousResearch/hermes-agent/pull/66347), [#​67857](https://github.com/NousResearch/hermes-agent/pull/67857), [#​66470](https://github.com/NousResearch/hermes-agent/pull/66470) — [@​OutThisLife](https://github.com/OutThisLife))
- Stop per-token sidebar + tool-row re-renders during streaming; stop eager JSON.stringify of every tool's args/result; scope tool-diff subscriptions; batch sidebar session slices into one profile-DB pass; targeted file-tree revalidation; rAF-coalesced sash resizes ([#​67742](https://github.com/NousResearch/hermes-agent/pull/67742), [#​67842](https://github.com/NousResearch/hermes-agent/pull/67842), [#​67195](https://github.com/NousResearch/hermes-agent/pull/67195), [#​67245](https://github.com/NousResearch/hermes-agent/pull/67245), [#​67824](https://github.com/NousResearch/hermes-agent/pull/67824), [#​67838](https://github.com/NousResearch/hermes-agent/pull/67838), [#​67844](https://github.com/NousResearch/hermes-agent/pull/67844) — [@​OutThisLife](https://github.com/OutThisLife))
- Systematized perf benchmark harness with trustworthy cold-start + first-token measurement, replacing 12 one-off scripts ([#​67466](https://github.com/NousResearch/hermes-agent/pull/67466), [#​67697](https://github.com/NousResearch/hermes-agent/pull/67697) — [@​OutThisLife](https://github.com/OutThisLife))
##### Everywhere else
- TUI renders streamed markdown incrementally per block ([#​67236](https://github.com/NousResearch/hermes-agent/pull/67236) — [@​OutThisLife](https://github.com/OutThisLife))
- Skill discovery cached by scan signature; snapshot manifest builds \~5× faster; text prefilter before AST parse in tool discovery ([#​61414](https://github.com/NousResearch/hermes-agent/pull/61414), [#​61131](https://github.com/NousResearch/hermes-agent/pull/61131), [#​63941](https://github.com/NousResearch/hermes-agent/pull/63941) — [@​kshitijk4poor](https://github.com/kshitijk4poor), [@​ethernet8023](https://github.com/ethernet8023))
- Copy-on-write message prep instead of full deepcopy; model-metadata probe-cache cluster; gateway `session.resume` model + display history from one SELECT ([#​61133](https://github.com/NousResearch/hermes-agent/pull/61133), [#​61368](https://github.com/NousResearch/hermes-agent/pull/61368), [#​67247](https://github.com/NousResearch/hermes-agent/pull/67247) — [@​kshitijk4poor](https://github.com/kshitijk4poor), [@​OutThisLife](https://github.com/OutThisLife))
- `hermes update` skips npm install when Node manifests are unchanged; dashboard session-list payloads trimmed + messages paginated ([#​61580](https://github.com/NousResearch/hermes-agent/pull/61580), [#​60883](https://github.com/NousResearch/hermes-agent/pull/60883) — [@​kshitijk4poor](https://github.com/kshitijk4poor))
- Byte-stable gateway system prompts — pinned session-context render keeps the prompt cache alive across turns ([#​67403](https://github.com/NousResearch/hermes-agent/pull/67403) — [@​kshitijk4poor](https://github.com/kshitijk4poor))
##### 🏗️ Core Agent & Architecture
##### Providers & models
- **Fireworks AI provider** with cost estimation + cached picker price columns, promoted to [#​2](https://github.com/NousResearch/hermes-agent/issues/2) in provider pickers ([#​62593](https://github.com/NousResearch/hermes-agent/pull/62593), [#​65476](https://github.com/NousResearch/hermes-agent/pull/65476), [#​65214](https://github.com/NousResearch/hermes-agent/pull/65214) — [@​teknium1](https://github.com/teknium1))
- **DeepInfra** hardened integration; **Upstage Solar** provider ([#​42231](https://github.com/NousResearch/hermes-agent/issues/42231) salvage) ([#​63969](https://github.com/NousResearch/hermes-agent/pull/63969), [#​64541](https://github.com/NousResearch/hermes-agent/pull/64541) — [@​kshitijk4poor](https://github.com/kshitijk4poor))
- **GPT-5.6 (Sol/Terra/Luna + Pro) end-to-end** — context lengths, native/Codex catalogs, pricing, compaction caps across every route ([#​61616](https://github.com/NousResearch/hermes-agent/pull/61616) — [@​kshitijk4poor](https://github.com/kshitijk4poor), building on [@​rob-maron](https://github.com/rob-maron))
- grok-4.5 (GA) catalog + reasoning allowlist; kimi-k3 on Nous Portal + OpenRouter (kimi-k2.x retired) + K3 discovery on the Kimi Coding endpoint; claude-fable-5 / claude-sonnet-5 / fugu-ultra curated; GA tencent/hy3 ([#​60887](https://github.com/NousResearch/hermes-agent/pull/60887), [#​65913](https://github.com/NousResearch/hermes-agent/pull/65913), [#​65922](https://github.com/NousResearch/hermes-agent/pull/65922), [#​56617](https://github.com/NousResearch/hermes-agent/pull/56617), [#​60943](https://github.com/NousResearch/hermes-agent/pull/60943) — [@​teknium1](https://github.com/teknium1))
- Catalog-labeled silent default (GLM-5.2) + bare-provider `/model` cost-safe routing; LM Studio JIT load mode; adaptive thinking for Kimi-family Anthropic endpoints ([#​64771](https://github.com/NousResearch/hermes-agent/pull/64771), [#​65472](https://github.com/NousResearch/hermes-agent/pull/65472), [#​67606](https://github.com/NousResearch/hermes-agent/pull/67606) — [@​teknium1](https://github.com/teknium1), [@​kshitijk4poor](https://github.com/kshitijk4poor))
- GLM-5.2 native reasoning\_effort controls; Gemini request-context improvements; extra HTTP headers for LLM API calls; per-client model routing on the API server ([#​58884](https://github.com/NousResearch/hermes-agent/pull/58884), [#​61873](https://github.com/NousResearch/hermes-agent/pull/61873) — [@​vishal-dharm](https://github.com/vishal-dharm), [#​57038](https://github.com/NousResearch/hermes-agent/pull/57038), [#​57028](https://github.com/NousResearch/hermes-agent/pull/57028) — [@​teknium1](https://github.com/teknium1))
- **Claude Sonnet 5 fully wired** — curated lists, intro pricing, and metadata across every route ([#​67932](https://github.com/NousResearch/hermes-agent/pull/67932) — [@​teknium1](https://github.com/teknium1))
- **Hide providers you don't use** — `enabled: false` per-provider flag + `excluded_providers` config scrub unwanted providers from `/model` pickers and built-in resolution ([#​67971](https://github.com/NousResearch/hermes-agent/pull/67971) — [@​teknium1](https://github.com/teknium1))
- Bedrock catalog wave: real context-window probing from the live endpoint, 1M-context rows for current-gen Claude + Fable, geo-prefix parity, versioned profile-ID pricing, Opus 4.8/4.7 rows ([#​68007](https://github.com/NousResearch/hermes-agent/pull/68007), [#​67977](https://github.com/NousResearch/hermes-agent/pull/67977), [#​68005](https://github.com/NousResearch/hermes-agent/pull/68005), [#​67976](https://github.com/NousResearch/hermes-agent/pull/67976) — [@​teknium1](https://github.com/teknium1))
- kimi-k3 rollout completed across Kimi-direct catalog surfaces with 1M context on canonical Kimi Coding endpoints ([#​68108](https://github.com/NousResearch/hermes-agent/pull/68108) — [@​teknium1](https://github.com/teknium1))
- Provider pickers: Qwen providers folded into one group row; collapsible provider groups in the desktop model picker; friendlier TUI model display grouping same-endpoint providers ([#​67758](https://github.com/NousResearch/hermes-agent/pull/67758), [#​67904](https://github.com/NousResearch/hermes-agent/pull/67904), [#​67908](https://github.com/NousResearch/hermes-agent/pull/67908) — [@​teknium1](https://github.com/teknium1))
##### Reasoning & MoA
- `max` + `ultra` effort levels across every surface and route ([#​62650](https://github.com/NousResearch/hermes-agent/pull/62650) — [@​teknium1](https://github.com/teknium1))
- Per-model reasoning\_effort overrides via a unified resolution chokepoint; per-task auxiliary effort; per-slot MoA preset effort; session-scoped `/reasoning` in the CLI ([#​64458](https://github.com/NousResearch/hermes-agent/pull/64458), [#​64597](https://github.com/NousResearch/hermes-agent/pull/64597), [#​64631](https://github.com/NousResearch/hermes-agent/pull/64631), [#​67946](https://github.com/NousResearch/hermes-agent/pull/67946) — [@​teknium1](https://github.com/teknium1))
- MoA: `reference_max_tokens` to cap advisor output and cut latency; per-preset fanout cadence (`user_turn` runs advisors once per user turn); stale presets surfaced without retries; half-filled preset saves rejected at the API boundary; aggregator resolves reasoning like an acting model ([#​56756](https://github.com/NousResearch/hermes-agent/pull/56756), [#​57591](https://github.com/NousResearch/hermes-agent/pull/57591), [#​64756](https://github.com/NousResearch/hermes-agent/pull/64756) — [@​teknium1](https://github.com/teknium1))
##### Delegation, approvals & the agent loop
- Live subagent transcripts + durable background completions (see Highlights) ([#​67479](https://github.com/NousResearch/hermes-agent/pull/67479), [#​63494](https://github.com/NousResearch/hermes-agent/pull/63494) — [@​teknium1](https://github.com/teknium1))
- Smart approvals default; user-defined deny rules (block even under yolo); `/deny <reason>` relays the denial reason; plugin `pre_tool_call` approve action escalates to a human gate (re-landed with rule keys) ([#​62661](https://github.com/NousResearch/hermes-agent/pull/62661), [#​59164](https://github.com/NousResearch/hermes-agent/pull/59164), [#​54518](https://github.com/NousResearch/hermes-agent/pull/54518), [#​60504](https://github.com/NousResearch/hermes-agent/pull/60504) — [@​teknium1](https://github.com/teknium1), [@​kshitijk4poor](https://github.com/kshitijk4poor))
- Unified delegation concurrency caps (`max_async_children` deprecated); explain long provider waits on the live status line; deterministic tool-output risk exposure ([#​56955](https://github.com/NousResearch/hermes-agent/pull/56955), [#​64775](https://github.com/NousResearch/hermes-agent/pull/64775), [#​61793](https://github.com/NousResearch/hermes-agent/pull/61793) — [@​teknium1](https://github.com/teknium1))
- Codex: live TUI/desktop tool cards for the app-server runtime, commentary streamed as visible interim messages, compaction routed through `thread/compact/start`, max-output truncation recovery, oversized message ids dropped on replay, banked usage-limit resets via `/usage reset` ([#​66514](https://github.com/NousResearch/hermes-agent/pull/66514), [#​66115](https://github.com/NousResearch/hermes-agent/pull/66115), [#​60114](https://github.com/NousResearch/hermes-agent/pull/60114), [#​58155](https://github.com/NousResearch/hermes-agent/pull/58155), [#​62225](https://github.com/NousResearch/hermes-agent/pull/62225) — [@​teknium1](https://github.com/teknium1), [@​kshitijk4poor](https://github.com/kshitijk4poor), [@​JoaoMarcos44](https://github.com/JoaoMarcos44), [#​64280](https://github.com/NousResearch/hermes-agent/pull/64280) — [@​teknium1](https://github.com/teknium1))
- Hooks: oversized hook-injected context spills to disk ([#​20468](https://github.com/NousResearch/hermes-agent/pull/20468) — [@​teknium1](https://github.com/teknium1))
- Vibe reactions — floating hearts on affection across CLI/TUI/desktop, token-free core detection ([#​62016](https://github.com/NousResearch/hermes-agent/pull/62016) — [@​OutThisLife](https://github.com/OutThisLife))
##### Secrets & config
- Pluggable `SecretSource` interface + Bitwarden & 1Password providers (see Highlights) ([#​59498](https://github.com/NousResearch/hermes-agent/pull/59498) — [@​teknium1](https://github.com/teknium1), [@​hwrdprkns](https://github.com/hwrdprkns))
- `hermes config get` / `unset`; warn on unknown root config keys + doctor deprecated-key reporting; `display.timestamp_format` ([#​65540](https://github.com/NousResearch/hermes-agent/pull/65540), [#​67370](https://github.com/NousResearch/hermes-agent/pull/67370), [#​40622](https://github.com/NousResearch/hermes-agent/pull/40622) — [@​teknium1](https://github.com/teknium1))
- Auxiliary model usage recorded per task in session accounting; conversation-scoped Nous Portal usage tags across aux/MoA/delegate calls; `--usage-file` JSON report for `hermes -z` ([#​65537](https://github.com/NousResearch/hermes-agent/pull/65537), [#​65468](https://github.com/NousResearch/hermes-agent/pull/65468), [#​59615](https://github.com/NousResearch/hermes-agent/pull/59615) — [@​teknium1](https://github.com/teknium1))
##### Sessions & compression
- Sessions export: Markdown/QMD/HTML/prompt-only/trace formats, HF upload, `--redact`, unified filters; full prune filter surface + bulk archive; CLI workspace filter + restore-cwd-on-resume ([#​60186](https://github.com/NousResearch/hermes-agent/pull/60186), [#​60492](https://github.com/NousResearch/hermes-agent/pull/60492), [#​60507](https://github.com/NousResearch/hermes-agent/pull/60507), [#​59327](https://github.com/NousResearch/hermes-agent/pull/59327), [#​63091](https://github.com/NousResearch/hermes-agent/pull/63091) — [@​teknium1](https://github.com/teknium1), [@​web3blind](https://github.com/web3blind))
- Compression: preserve human intent and durable handoffs; retain prompt cache when memory is unchanged; flatten multimodal content for the summarizer keeping image handles; gateway compression routing integrity ([#​67275](https://github.com/NousResearch/hermes-agent/pull/67275), [#​67916](https://github.com/NousResearch/hermes-agent/pull/67916), [#​65046](https://github.com/NousResearch/hermes-agent/pull/65046), [#​56868](https://github.com/NousResearch/hermes-agent/pull/56868) — [@​kshitijk4poor](https://github.com/kshitijk4poor), [@​teknium1](https://github.com/teknium1))
- Gateway session metadata consolidated into state.db; routing index moved to state.db (sessions.json now an optional legacy mirror); exact API bytes persisted in an `api_content` sidecar ([#​58899](https://github.com/NousResearch/hermes-agent/pull/58899), [#​59203](https://github.com/NousResearch/hermes-agent/pull/59203), [#​67274](https://github.com/NousResearch/hermes-agent/pull/67274) — [@​teknium1](https://github.com/teknium1), [@​kshitijk4poor](https://github.com/kshitijk4poor))
##### 🌐 Gateway, Fleet & Relay
- **Durable delivery-obligation ledger** for final responses (see Highlights) ([#​67181](https://github.com/NousResearch/hermes-agent/pull/67181) — [@​teknium1](https://github.com/teknium1))
- **Profile-based routing for inbound messages** + multiplex hardening wave 2 + `GATEWAY_MULTIPLEX_PROFILES` override (see Highlights) ([#​64835](https://github.com/NousResearch/hermes-agent/pull/64835), [#​65700](https://github.com/NousResearch/hermes-agent/pull/65700), [#​60589](https://github.com/NousResearch/hermes-agent/pull/60589) — [@​teknium1](https://github.com/teknium1), [@​benbarclay](https://github.com/benbarclay) + salvaged contributors)
- Per-session turn lease + conversation-scope funnel; unified session reset boundaries (reset sessions stay reset); truthful runtime readiness checks; per-channel model and system prompt overrides; per-session `/model` overrides persist across restarts ([#​67401](https://github.com/NousResearch/hermes-agent/pull/67401), [#​65783](https://github.com/NousResearch/hermes-agent/pull/65783), [#​62645](https://github.com/NousResearch/hermes-agent/pull/62645), [#​56967](https://github.com/NousResearch/hermes-agent/pull/56967), [#​57030](https://github.com/NousResearch/hermes-agent/pull/57030) — [@​teknium1](https://github.com/teknium1))
- Session auto-reset default off; `/sessions search <query>`; webhook payload filters + route scripts; platform HTTP event callback routing; configurable long-running status phrases ([#​60194](https://github.com/NousResearch/hermes-agent/pull/60194), [#​57685](https://github.com/NousResearch/hermes-agent/pull/57685), [#​60944](https://github.com/NousResearch/hermes-agent/pull/60944), [#​65702](https://github.com/NousResearch/hermes-agent/pull/65702), [#​58872](https://github.com/NousResearch/hermes-agent/pull/58872) — [@​teknium1](https://github.com/teknium1))
- Relay: generic OIDC client-credentials provisioning (NAS-free), routed profile carried from the connector wire source, channel context consumed from the connector; Nous auth forensics + `nous_session_valid` on `/api/status` for hosted self-heal; Docker re-seeds a terminally-dead Nous bootstrap session on boot ([#​60730](https://github.com/NousResearch/hermes-agent/pull/60730), [#​60586](https://github.com/NousResearch/hermes-agent/pull/60586), [#​64649](https://github.com/NousResearch/hermes-agent/pull/64649), [#​59976](https://github.com/NousResearch/hermes-agent/pull/59976), [#​59969](https://github.com/NousResearch/hermes-agent/pull/59969), [#​59983](https://github.com/NousResearch/hermes-agent/pull/59983) — [@​benbarclay](https://github.com/benbarclay))
##### 📱 Messaging Platforms
- **Inline choice pickers** for `/reasoning` and `/fast` on Telegram, Discord, and Matrix — one-tap native buttons instead of typing ([#​65799](https://github.com/NousResearch/hermes-agent/pull/65799) — [@​teknium1](https://github.com/teknium1))
- WhatsApp: native Baileys polls (clarify renders as a poll), locations, rich inbound metadata; dashboard pairing flow ([#​58865](https://github.com/NousResearch/hermes-agent/pull/58865), [#​60571](https://github.com/NousResearch/hermes-agent/pull/60571) — [@​teknium1](https://github.com/teknium1))
- Discord: recover messages missed during reconnect; auto-created threads renamed to generated session titles; configurable interactive view timeout; opt-in owner mentions on exec-approval prompts; optional admin-only gate for approval buttons ([#​66149](https://github.com/NousResearch/hermes-agent/pull/66149), [#​60187](https://github.com/NousResearch/hermes-agent/pull/60187), [#​60230](https://github.com/NousResearch/hermes-agent/pull/60230), [#​60493](https://github.com/NousResearch/hermes-agent/pull/60493), [#​51751](https://github.com/NousResearch/hermes-agent/pull/51751) — [@​teknium1](https://github.com/teknium1))
- Slack: live per-tool status line ([#​67080](https://github.com/NousResearch/hermes-agent/pull/67080) — [@​teknium1](https://github.com/teknium1), salvaging [#​62007](https://github.com/NousResearch/hermes-agent/issues/62007))
- Telegram: per-topic free-response allowlist; Google Chat clarify prompts rendered as cards ([#​65543](https://github.com/NousResearch/hermes-agent/pull/65543), [#​65546](https://github.com/NousResearch/hermes-agent/pull/65546) — [@​teknium1](https://github.com/teknium1))
- Voice: `stt.echo_transcripts` toggle; MEDIA: captions attached to the media bubble on standalone sends; `display.tool_progress: log` option ([#​58859](https://github.com/NousResearch/hermes-agent/pull/58859), [#​61415](https://github.com/NousResearch/hermes-agent/pull/61415), [#​57014](https://github.com/NousResearch/hermes-agent/pull/57014) — [@​teknium1](https://github.com/teknium1), [@​kshitijk4poor](https://github.com/kshitijk4poor))
##### 🖥️ Hermes Desktop App
- **Contribution-driven shell on a layout-tree model** — panes, zones, and layouts as data; plugin-scoped i18n locale bundles followed ([#​60638](https://github.com/NousResearch/hermes-agent/pull/60638), [#​67303](https://github.com/NousResearch/hermes-agent/pull/67303) — [@​OutThisLife](https://github.com/OutThisLife))
- **Capabilities page** — Skills/Tools/MCP + Hub in one place, with responsive overlay nav; CLI/dashboard parity for skills hub, MCP test/toggle/catalog, maintenance ops, log filters; five UX fixes from live testing ([#​57590](https://github.com/NousResearch/hermes-agent/pull/57590), [#​57441](https://github.com/NousResearch/hermes-agent/pull/57441), [#​67482](https://github.com/NousResearch/hermes-agent/pull/67482) — [@​OutThisLife](https://github.com/OutThisLife), [@​teknium1](https://github.com/teknium1))
- **Hermes Cloud connection mode** (salvage of [#​55402](https://github.com/NousResearch/hermes-agent/issues/55402)); soft gateway switch + gateway-settings polish; terminal execution backend picker with health probes ([#​61912](https://github.com/NousResearch/hermes-agent/pull/61912), [#​61916](https://github.com/NousResearch/hermes-agent/pull/61916), [#​67203](https://github.com/NousResearch/hermes-agent/pull/67203) — [@​OutThisLife](https://github.com/OutThisLife), [@​teknium1](https://github.com/teknium1))
- Keybind hint tooltips + keybinds settings tab + unified worktree dialog; base-branch picker for new worktrees; green unread dot for background-finished sessions; background-task sidebar indicators; grouped tool calls across text-less messages; auto-scrolling window for long tool-call runs ([#​65204](https://github.com/NousResearch/hermes-agent/pull/65204), [#​62243](https://github.com/NousResearch/hermes-agent/pull/62243), [#​65109](https://github.com/NousResearch/hermes-agent/pull/65109), [#​65174](https://github.com/NousResearch/hermes-agent/pull/65174), [#​61147](https://github.com/NousResearch/hermes-agent/pull/61147), [#​57913](https://github.com/NousResearch/hermes-agent/pull/57913) — [@​ethernet8023](https://github.com/ethernet8023), [@​OutThisLife](https://github.com/OutThisLife))
- Session + project color system (inherit from project, per-session override, shared across sidebar/tabs); unified active-project identity in chat status; workspace path status action ([#​67469](https://github.com/NousResearch/hermes-agent/pull/67469), [#​67681](https://github.com/NousResearch/hermes-agent/pull/67681), [#​67282](https://github.com/NousResearch/hermes-agent/pull/67282), [#​63086](https://github.com/NousResearch/hermes-agent/pull/63086) — [@​OutThisLife](https://github.com/OutThisLife))
- Declarative memory-provider panel + full-config modal; config-defined TTS/STT providers + xAI TTS params; custom endpoint settings; per-job cron model picker; profile-aware approval mode control; UI scale setting; Ctrl/Cmd+wheel zoom; chat backdrop toggle; `/journey` opens the memory graph overlay ([#​67206](https://github.com/NousResearch/hermes-agent/pull/67206) salvaging [@​erosika](https://github.com/erosika), [#​67209](https://github.com/NousResearch/hermes-agent/pull/67209), [#​67759](https://github.com/NousResearch/hermes-agent/pull/67759) — [@​austinpickett](https://github.com/austinpickett), [#​67472](https://github.com/NousResearch/hermes-agent/pull/67472), [#​63520](https://github.com/NousResearch/hermes-agent/pull/63520), [#​60457](https://github.com/NousResearch/hermes-agent/pull/60457), [#​67029](https://github.com/NousResearch/hermes-agent/pull/67029), [#​64598](https://github.com/NousResearch/hermes-agent/pull/64598), [#​57267](https://github.com/NousResearch/hermes-agent/pull/57267) — [@​teknium1](https://github.com/teknium1), [@​OutThisLife](https://github.com/OutThisLife))
- Full TypeScript conversion of the desktop tree ([#​57855](https://github.com/NousResearch/hermes-agent/pull/57855) — [@​ethernet8023](https://github.com/ethernet8023))
##### 📊 Web Dashboard
- Memory provider switching; safe session import flow; WhatsApp pairing; Discord-specific toolsets editable from the web UI; clarified manual Telegram bot setup ([#​60569](https://github.com/NousResearch/hermes-agent/pull/60569), [#​63699](https://github.com/NousResearch/hermes-agent/pull/63699), [#​60571](https://github.com/NousResearch/hermes-agent/pull/60571), [#​65361](https://github.com/NousResearch/hermes-agent/pull/65361), [#​64636](https://github.com/NousResearch/hermes-agent/pull/64636) — [@​teknium1](https://github.com/teknium1), [@​kshitijk4poor](https://github.com/kshitijk4poor), [@​shannonsands](https://github.com/shannonsands))
- Terminal keep-alive + reattach for dashboard chat sessions; heavy turns isolated in a compute host; paste/drop images into Chat; `browser.headed` schema toggle; profile + gateway topology on `/api/status`; mobile/hosted OpenAI OAuth login ([#​60515](https://github.com/NousResearch/hermes-agent/pull/60515), [#​65895](https://github.com/NousResearch/hermes-agent/pull/65895), [#​61929](https://github.com/NousResearch/hermes-agent/pull/61929), [#​67046](https://github.com/NousResearch/hermes-agent/pull/67046), [#​60537](https://github.com/NousResearch/hermes-agent/pull/60537), [#​61330](https://github.com/NousResearch/hermes-agent/pull/61330) — [@​teknium1](https://github.com/teknium1), [@​OutThisLife](https://github.com/OutThisLife), [@​benbarclay](https://github.com/benbarclay))
- `hermes serve` is a true headless backend (no web UI build/mount) ([#​55923](https://github.com/NousResearch/hermes-agent/pull/55923) — [@​OutThisLife](https://github.com/OutThisLife))
##### 🧰 CLI & TUI
- `/subscription` + `/topup` terminal billing (see Highlights) ([#​51639](https://github.com/NousResearch/hermes-agent/pull/51639) — [@​alt-glitch](https://github.com/alt-glitch))
- **`/model --once`** — one-turn model override that reverts automatically ([#​67113](https://github.com/NousResearch/hermes-agent/pull/67113) — [@​teknium1](https://github.com/teknium1), salvaging [#​29923](https://github.com/NousResearch/hermes-agent/issues/29923))
- **Stacked slash-skill invocations** — `/skill-a /skill-b do XYZ` loads both skills in order (Claude Code port), with autocomplete + ghost text ([#​57987](https://github.com/NousResearch/hermes-agent/pull/57987), [#​58763](https://github.com/NousResearch/hermes-agent/pull/58763) — [@​teknium1](https://github.com/teknium1))
- `--safe-mode` troubleshooting flag; uninstall dry-run; TLS failures fail fast with fix hints; `/compact` alias + preview flags; pip/Homebrew installs warned unsupported ([#​45300](https://github.com/NousResearch/hermes-agent/pull/45300), [#​60111](https://github.com/NousResearch/hermes-agent/pull/60111), [#​57992](https://github.com/NousResearch/hermes-agent/pull/57992), [#​57029](https://github.com/NousResearch/hermes-agent/pull/57029), [#​57225](https://github.com/NousResearch/hermes-agent/pull/57225) — [@​teknium1](https://github.com/teknium1), [@​ethernet8023](https://github.com/ethernet8023))
- TUI: model picker refresh support; custom skill bundles dispatched as agent turns; banner sizes skills display to terminal width ([#​59782](https://github.com/NousResearch/hermes-agent/pull/59782) — [@​helix4u](https://github.com/helix4u), [#​62859](https://github.com/NousResearch/hermes-agent/pull/62859) — [@​Adolanium](https://github.com/Adolanium), [#​40624](https://github.com/NousResearch/hermes-agent/pull/40624) — [@​teknium1](https://github.com/teknium1))
- Hermes Console REPL + perf follow-ups; `hermes curator usage` all-skills view; entry-point plugins surfaced in `hermes plugins list` ([#​57781](https://github.com/NousResearch/hermes-agent/pull/57781) — [@​kshitijk4poor](https://github.com/kshitijk4poor), [#​36727](https://github.com/NousResearch/hermes-agent/pull/36727), [#​40623](https://github.com/NousResearch/hermes-agent/pull/40623) — [@​teknium1](https://github.com/teknium1))
##### 🔧 Tool System, Skills & MCP
- MCP: `mcp__server__tool` naming convention; server log notifications surfaced in agent.log; hosted OAuth completed across Dashboard + Desktop; configurable `redirect_uri`/`redirect_host` for proxied/WAF setups; OAuth callback port races closed; Blender added to the MCP catalog with a curated 4-tool default ([#​52750](https://github.com/NousResearch/hermes-agent/pull/52750), [#​57416](https://github.com/NousResearch/hermes-agent/pull/57416), [#​66151](https://github.com/NousResearch/hermes-agent/pull/66151), [#​65610](https://github.com/NousResearch/hermes-agent/pull/65610), [#​65622](https://github.com/NousResearch/hermes-agent/pull/65622), [#​64463](https://github.com/NousResearch/hermes-agent/pull/64463) — [@​teknium1](https://github.com/teknium1), [@​benbarclay](https://github.com/benbarclay))
- Skills: `security/unbroker` (autonomous data-broker removal) + blind opt-out hardening; `unreal-mcp` companion skill; blender-mcp reworked around the catalog entry; humanizer pattern expansion; `mcp-oauth-remote-gateway` optional skill ([#​57438](https://github.com/NousResearch/hermes-agent/pull/57438), [#​57902](https://github.com/NousResearch/hermes-agent/pull/57902), [#​65989](https://github.com/NousResearch/hermes-agent/pull/65989), [#​64715](https://github.com/NousResearch/hermes-agent/pull/64715) — [@​SHL0MS](https://github.com/SHL0MS), [#​65066](https://github.com/NousResearch/hermes-agent/pull/65066), [#​65486](https://github.com/NousResearch/hermes-agent/pull/65486) — [@​teknium1](https://github.com/teknium1))
- Browser: full snapshots stored on truncation, eval denylist opt-in; computer\_use follows cua-driver's verify→escalate ladder ([#​65923](https://github.com/NousResearch/hermes-agent/pull/65923), [#​67123](https://github.com/NousResearch/hermes-agent/pull/67123) — [@​teknium1](https://github.com/teknium1))
- Kanban: modal create-task dialog + editable board project directory; Done-card results made obvious; grab-to-pan board scrolling; attachment toolset + CLI with SSRF-guarded URL fetch; project directory captured at board creation ([#​66333](https://github.com/NousResearch/hermes-agent/pull/66333), [#​63638](https://github.com/NousResearch/hermes-agent/pull/63638), [#​60226](https://github.com/NousResearch/hermes-agent/pull/60226), [#​65698](https://github.com/NousResearch/hermes-agent/pull/65698), [#​63249](https://github.com/NousResearch/hermes-agent/pull/63249) — [@​teknium1](https://github.com/teknium1))
- Cron: durable execution audit history; one-shot stale-removal race fixed; run-claim TTL derived from HERMES\_CRON\_TIMEOUT ([#​61791](https://github.com/NousResearch/hermes-agent/pull/61791) — [@​teknium1](https://github.com/teknium1), [#​62014](https://github.com/NousResearch/hermes-agent/pull/62014) — [@​PRATHAMESH75](https://github.com/PRATHAMESH75), [#​59567](https://github.com/NousResearch/hermes-agent/pull/59567))
- mem0: self-hosted dashboard backend + recall tuning + setup-wizard mode ([#​56943](https://github.com/NousResearch/hermes-agent/pull/56943), [#​60494](https://github.com/NousResearch/hermes-agent/pull/60494) — [@​kshitijk4poor](https://github.com/kshitijk4poor), [@​teknium1](https://github.com/teknium1))
- Image gen: Codex image inputs; unsupported Codex image accounts classified; tool args recursively normalized by schema (cline port) ([#​57017](https://github.com/NousResearch/hermes-agent/pull/57017), [#​63627](https://github.com/NousResearch/hermes-agent/pull/63627), [#​52220](https://github.com/NousResearch/hermes-agent/pull/52220) — [@​teknium1](https://github.com/teknium1), [@​kshitijk4poor](https://github.com/kshitijk4poor))
##### 🔒 Security & Reliability
- Vertex: credential/project/region resolution through the profile secret scope; `VERTEX_CREDENTIALS_PATH`/`GOOGLE_APPLICATION_CREDENTIALS` stripped from subprocess env ([#​56680](https://github.com/NousResearch/hermes-agent/pull/56680), [#​56582](https://github.com/NousResearch/hermes-agent/pull/56582) — [@​srojk34](https://github.com/srojk34))
- Six P1 hardening PRs salvaged in one pass — browser guards, MEDIA anchoring, .env lockdown, delegate ACP transport ([#​57660](https://github.com/NousResearch/hermes-agent/pull/57660) — [@​teknium1](https://github.com/teknium1))
- Media/vision/image-gen local-file reads routed through the shared credential-read guard; native image routing guarded by file-safety policy; unified image-source resolver + terminal-backend confinement ([#​58709](https://github.com/NousResearch/hermes-agent/pull/58709), [#​58752](https://github.com/NousResearch/hermes-agent/pull/58752), [#​57890](https://github.com/NousResearch/hermes-agent/pull/57890) — [@​teknium1](https://github.com/teknium1))
- Webhook body-cap sweep: explicit `client_max_size` on 3 uncapped aiohttp servers + completion sweep; Raft chunked-request body limit; timestamp-bound V2 webhook signatures ([#​59180](https://github.com/NousResearch/hermes-agent/pull/59180), [#​59215](https://github.com/NousResearch/hermes-agent/pull/59215), [#​58902](https://github.com/NousResearch/hermes-agent/pull/58902), [#​58508](https://github.com/NousResearch/hermes-agent/pull/58508) — [@​teknium1](https://github.com/teknium1), [@​srojk34](https://github.com/srojk34))
- Redaction: Fireworks token prefixes + Telegram transport errors; env-lookup false positives fixed for KEY=value and JSON/YAML config fields; bot tokens scrubbed from Telegram connect/send errors ([#​58501](https://github.com/NousResearch/hermes-agent/pull/58501), [#​58534](https://github.com/NousResearch/hermes-agent/pull/58534), [#​58915](https://github.com/NousResearch/hermes-agent/pull/58915), [#​58893](https://github.com/NousResearch/hermes-agent/pull/58893) — [@​teknium1](https://github.com/teknium1))
- computer-use: subprocess env sanitized across all five cua-driver spawn sites ([#​58889](https://github.com/NousResearch/hermes-agent/pull/58889), [#​59165](https://github.com/NousResearch/hermes-agent/pull/59165) — [@​teknium1](https://github.com/teknium1))
- Dashboard: managed-files credential guard widened past .env + dir-tree gap closed; OAuth token TOCTOU closed with atomic 0o600 writes; stale dashboards can't recreate deleted profiles ([#​58222](https://github.com/NousResearch/hermes-agent/pull/58222) — [@​kshitijk4poor](https://github.com/kshitijk4poor), [#​60236](https://github.com/NousResearch/hermes-agent/pull/60236) — [@​teknium1](https://github.com/teknium1), [#​49435](https://github.com/NousResearch/hermes-agent/pull/49435) — [@​LeonSGP43](https://github.com/LeonSGP43))
- CI: untrusted refs passed through env, not `run:` interpolation; JS/TS tests wired into CI with source-regex tests banned; js-autofix pushes via PR instead of direct-to-main ([#​57842](https://github.com/NousResearch/hermes-agent/pull/57842) — [@​jquesnelle](https://github.com/jquesnelle), [#​60707](https://github.com/NousResearch/hermes-agent/pull/60707), [#​65186](https://github.com/NousResearch/hermes-agent/pull/65186) — [@​ethernet8023](https://github.com/ethernet8023))
- Docker: terminal network toggle with full-path coverage; Git Bash Mandatory-ASLR install failures detected; Windows updater console hidden during handoff ([#​59149](https://github.com/NousResearch/hermes-agent/pull/59149) — [@​teknium1](https://github.com/teknium1), [#​64651](https://github.com/NousResearch/hermes-agent/pull/64651), [#​66040](https://github.com/NousResearch/hermes-agent/pull/66040) — [@​helix4u](https://github.com/helix4u))
- Anthropic: request-local clients so the stale/interrupt watchdog never corrupts SQLite; per-profile OAuth file; OAuth login 429 fixed (UA must not be claude-code/) ([#​67238](https://github.com/NousResearch/hermes-agent/pull/67238) — [@​OutThisLife](https://github.com/OutThisLife), [#​59339](https://github.com/NousResearch/hermes-agent/pull/59339), [#​58178](https://github.com/NousResearch/hermes-agent/pull/58178) — [@​teknium1](https://github.com/teknium1))
- Gateway/agent: tool\_call\_id deduplicated across pre-API sanitizers; background review inherits parent reasoning\_config for Anthropic cache parity; `/new` memory extraction moved off the command path ([#​58350](https://github.com/NousResearch/hermes-agent/pull/58350), [#​64379](https://github.com/NousResearch/hermes-agent/pull/64379), [#​61139](https://github.com/NousResearch/hermes-agent/pull/61139) — [@​teknium1](https://github.com/teknium1), [@​kshitijk4poor](https://github.com/kshitijk4poor))
##### 🔁 Reverted in this window (for the record)
- iron-proxy credential-injection egress firewall ([#​30179](https://github.com/NousResearch/hermes-agent/issues/30179) → reverted in [#​58489](https://github.com/NousResearch/hermes-agent/pull/58489)) — not shipping in this release
- dynamic-workflow orchestration skill (landed, then reverted) — not shipping
- memory provider-actions extension point (landed, then reverted) — not shipping
- Note: the plugin `pre_tool_call` approve escalation was reverted mid-window but **re-landed** in [#​60504](https://github.com/NousResearch/hermes-agent/pull/60504) and ships in this release.
##### 👥 Contributors
**450+ people** contributed to this release (via commits, co-author trailers, and salvaged PRs) — the biggest contributor window yet. Thank you, all of you.
##### Core team
- [@​teknium1](https://github.com/teknium1) — release lead; TTFT perf wave, delivery + delegation durability, smart approvals, SecretSource, gateway multiplex + profile routing, sessions export, security round, and a \~290-PR community salvage burn
- [@​OutThisLife](https://github.com/OutThisLife) — desktop app (the speed wave, layout-tree shell, Capabilities page, session colors, vibe reactions, TUI incremental markdown, perf harness)
- [@​kshitijk4poor](https://github.com/kshitijk4poor) — GPT-5.6 end-to-end, DeepInfra + Upstage Solar providers, perf cluster, compression integrity, mem0, dashboard guards
- [@​ethernet8023](https://github.com/ethernet8023) — CI overhaul (JS/TS tests wired in, autofix-via-PR, python speedups), desktop keybinds/worktrees/status indicators, full desktop TypeScript conversion
- [@​benbarclay](https://github.com/benbarclay) — relay OIDC provisioning, gateway multiplex override, Nous auth self-heal, hosted MCP OAuth groundwork
- [@​alt-glitch](https://github.com/alt-glitch) — terminal billing (`/subscription`, `/topup`), desktop billing tab
- [@​helix4u](https://github.com/helix4u) — desktop provider/model UX, TUI model picker refresh, Windows install/updater hardening
- [@​austinpickett](https://github.com/austinpickett) — desktop custom endpoint settings
- [@​SHL0MS](https://github.com/SHL0MS) — unbroker + unreal-mcp skills, humanizer expansion
##### Top community contributors
- [@​srojk34](https://github.com/srojk34) — security hardening: Vertex credential/project/region scoping through the profile secret scope, subprocess env stripping, Raft chunked-request body limits
- [@​HexLab98](https://github.com/HexLab98) — 11 fixes across MCP capability gating, Windows installer PATH, desktop cron editing, gateway systemd warnings
- [@​UnathiCodex](https://github.com/UnathiCodex) — desktop stability: zoom across display moves, LaTeX rendering, resume-stall and runtime-readiness fixes
- [@​xxxigm](https://github.com/xxxigm) — `<think>` leak fix after thinking-only retry flush, dashboard auth/theme/PTY fixes
- [@​erosika](https://github.com/erosika) — desktop declarative memory-provider panel + honcho recall/timeout correctness
- [@​Frowtek](https://github.com/Frowtek) — credential security: master stores never mounted into skill sandboxes, live-transcript redaction, dashboard api\_key precedence
- [@​necoweb3](https://github.com/necoweb3) — browser private-page CDP guard, cron one-shot liveness, gateway compression fail-closed
- [@​DavidMetcalfe](https://github.com/DavidMetcalfe) — desktop updater version pill, Local/custom endpoint exposure, sidebar collapse behavior
- [@​shannonsands](https://github.com/shannonsands) — dashboard: mobile channel setup, Discord toolsets from web UI, Telegram setup clarity
- [@​vishal-dharm](https://github.com/vishal-dharm) — Gemini request-context improvements
- [@​PRATHAMESH75](https://github.com/PRATHAMESH75) — cron one-shot stale-removal race, dashboard multiplex port-binding guard
- [@​alelpoan](https://github.com/alelpoan), [@​embwl0x](https://github.com/embwl0x), [@​Adolanium](https://github.com/Adolanium), [@​giggling-ginger](https://github.com/giggling-ginger), [@​Drexuxux](https://github.com/Drexuxux), [@​frizikk](https://github.com/frizikk), [@​JoaoMarcos44](https://github.com/JoaoMarcos44), [@​wesleysimplici](https://github.com/wesleysimplici), [@​LeonSGP43](https://github.com/LeonSGP43), [@​pierrenode](https://github.com/pierrenode), [@​simpolism](https://github.com/simpolism), [@​MorAlekss](https://github.com/MorAlekss), [@​r266-tech](https://github.com/r266-tech), [@​WadydX](https://github.com/WadydX), [@​nv-kasikritc](https://github.com/nv-kasikritc) — targeted fixes across desktop, TUI, gateway, cron, webhook, nix, and browser surfaces
- Salvaged-work authors whose PRs were cherry-picked with credit this window: [@​Burgunthy](https://github.com/Burgunthy) (profile routing), [@​web3blind](https://github.com/web3blind) (sessions export), [@​hwrdprkns](https://github.com/hwrdprkns) (1Password), [@​Christopher-Schulze](https://github.com/Christopher-Schulze), [@​Ahmett101](https://github.com/Ahmett101), [@​sjiangtao2024](https://github.com/sjiangtao2024), and many more — see the salvage PR bodies for full attribution
##### All contributors
[@​0-CYBERDYNE-SYSTEMS-0](https://github.com/0-CYBERDYNE-SYSTEMS-0), [@​0disoft](https://github.com/0disoft), [@​0xbyt4](https://github.com/0xbyt4), [@​100yenadmin](https://github.com/100yenadmin), [@​17324393074](https://github.com/17324393074), [@​2751738943](https://github.com/2751738943), [@​8294](https://github.com/8294), [@​abhibansal-sg](https://github.com/abhibansal-sg),
[@​adambiggs](https://github.com/adambiggs), [@​Adolanium](https://github.com/Adolanium), [@​aeyeopsdev](https://github.com/aeyeopsdev), [@​aguung](https://github.com/aguung), [@​AhmetArif0](https://github.com/AhmetArif0), [@​Ahmett101](https://github.com/Ahmett101), [@​ai-ag2026](https://github.com/ai-ag2026), [@​AIalliAI](https://github.com/AIalliAI), [@​ajzrva-sys](https://github.com/ajzrva-sys),
[@​alastraz](https://github.com/alastraz), [@​alelpoan](https://github.com/alelpoan), [@​alex-fireworks](https://github.com/alex-fireworks), [@​alex-heritier](https://github.com/alex-heritier), [@​alex107ivanov](https://github.com/alex107ivanov), [@​AlexFucuson9](https://github.com/AlexFucuson9), [@​Alix-007](https://github.com/Alix-007),
[@​allenliang2022](https://github.com/allenliang2022), [@​Almurat123](https://github.com/Almurat123), [@​AlsayedHoota](https://github.com/AlsayedHoota), [@​alt-glitch](https://github.com/alt-glitch), [@​alvarosanchez](https://github.com/alvarosanchez), [@​amanning3390](https://github.com/amanning3390), [@​AmAzing129](https://github.com/AmAzing129),
[@​AndreasHiltner](https://github.com/AndreasHiltner), [@​andrewhomeyer](https://github.com/andrewhomeyer), [@​annguyenNous](https://github.com/annguyenNous), [@​ansel-f](https://github.com/ansel-f), [@​antydizajn](https://github.com/antydizajn), [@​arminanton](https://github.com/arminanton), [@​arnispiekus](https://github.com/arnispiekus), [@​asimons81](https://github.com/asimons81),
[@​asscan](https://github.com/asscan), [@​ats3v](https://github.com/ats3v), [@​austinlaw076](https://github.com/austinlaw076), [@​austinpickett](https://github.com/austinpickett), [@​avifenesh](https://github.com/avifenesh), [@​aydnOktay](https://github.com/aydnOktay), [@​Bartok9](https://github.com/Bartok9), [@​bautrey](https://github.com/bautrey), [@​bbednarski9](https://github.com/bbednarski9),
[@​bbopen](https://github.com/bbopen), [@​benbarclay](https://github.com/benbarclay), [@​bigstar0920](https://github.com/bigstar0920), [@​binhnt92](https://github.com/binhnt92), [@​bird](https://github.com/bird), [@​Black0Fox0](https://github.com/Black0Fox0), [@​BlackishGreen33](https://github.com/BlackishGreen33), [@​bo](https://github.com/bo).fu, [@​brendandebeasi](https://github.com/brendandebeasi),
[@​briandevans](https://github.com/briandevans), [@​BROCCOLO1D](https://github.com/BROCCOLO1D), [@​Bruce-anle](https://github.com/Bruce-anle), [@​brunz-me](https://github.com/brunz-me), [@​Burgunthy](https://github.com/Burgunthy), [@​bytesnail](https://github.com/bytesnail), [@​catbearlove1-lang](https://github.com/catbearlove1-lang), [@​Cdddo](https://github.com/Cdddo),
[@​cgarwood82](https://github.com/cgarwood82), [@​CharmingGroot](https://github.com/CharmingGroot), [@​chouqin](https://github.com/chouqin), [@​Christopher-Schulze](https://github.com/Christopher-Schulze), [@​claudlos](https://github.com/claudlos), [@​CocaKova](https://github.com/CocaKova), [@​Code-suphub](https://github.com/Code-suphub), [@​CodeForgeNet](https://github.com/CodeForgeNet),
[@​craigdfrench](https://github.com/craigdfrench), [@​CrazyBoyM](https://github.com/CrazyBoyM), [@​crazywriter1](https://github.com/crazywriter1), [@​cresslank](https://github.com/cresslank), [@​cruzanstx](https://github.com/cruzanstx), [@​cyrkstudios](https://github.com/cyrkstudios), [@​danilofalcao](https://github.com/danilofalcao),
[@​datachainsystems](https://github.com/datachainsystems), [@​DatTheMaster](https://github.com/DatTheMaster), [@​davidb73-hub](https://github.com/davidb73-hub), [@​davidgut1982](https://github.com/davidgut1982), [@​DavidMetcalfe](https://github.com/DavidMetcalfe), [@​davidrobertson](https://github.com/davidrobertson),
[@​deacon-botdoctor](https://github.com/deacon-botdoctor), [@​DECK6](https://github.com/DECK6), [@​deepujain](https://github.com/deepujain), [@​derek2000139](https://github.com/derek2000139), [@​designnotdrum](https://github.com/designnotdrum), [@​deusyu](https://github.com/deusyu), [@​devatnull](https://github.com/devatnull), [@​devorun](https://github.com/devorun),
[@​dexhunter](https://github.com/dexhunter), [@​dfein38347g](https://github.com/dfein38347g), [@​Dhravya](https://github.com/Dhravya), [@​DictatorBacon](https://github.com/DictatorBacon), [@​digitalbase](https://github.com/digitalbase), [@​dlkakbs](https://github.com/dlkakbs), [@​dmabry](https://github.com/dmabry), [@​DNAlec](https://github.com/DNAlec), [@​dodo-reach](https://github.com/dodo-reach),
[@​doncazper](https://github.com/doncazper), [@​dorokuma](https://github.com/dorokuma), [@​doxe0x](https://github.com/doxe0x), [@​Drexuxux](https://github.com/Drexuxux), [@​dschnurbusch](https://github.com/dschnurbusch), [@​Dusk1e](https://github.com/Dusk1e), [@​EdderTalmor](https://github.com/EdderTalmor), [@​egilewski](https://github.com/egilewski), [@​elashera](https://github.com/elashera),
[@​Elektrofussel](https://github.com/Elektrofussel), [@​eliteworkstation94-ai](https://github.com/eliteworkstation94-ai), [@​embwl0x](https://github.com/embwl0x), [@​emo-eth](https://github.com/emo-eth), [@​emozilla](https://github.com/emozilla), [@​enzo-adami](https://github.com/enzo-adami), [@​Epoxidex](https://github.com/Epoxidex), [@​ErnestHysa](https://github.com/ErnestHysa),
[@​Erosika](https://github.com/Erosika), [@​esthonjr](https://github.com/esthonjr), [@​ethernet8023](https://github.com/ethernet8023), [@​evefromwayback](https://github.com/evefromwayback), [@​evelynburger](https://github.com/evelynburger), [@​F4TB0Yz](https://github.com/F4TB0Yz), [@​falkoro](https://github.com/falkoro), [@​fanyangCS](https://github.com/fanyangCS), [@​firefly](https://github.com/firefly),
[@​fjlaowan1983](https://github.com/fjlaowan1983), [@​flewe](https://github.com/flewe), [@​flo1t](https://github.com/flo1t), [@​flow-digital-ny](https://github.com/flow-digital-ny), [@​floze-the-genius](https://github.com/floze-the-genius), [@​frizikk](https://github.com/frizikk), [@​Frowtek](https://github.com/Frowtek), [@​FuryMartin](https://github.com/FuryMartin),
[@​fyzanshaik](https://github.com/fyzanshaik), [@​gauravsaxena1997](https://github.com/gauravsaxena1997), [@​geoffreybutler94](https://github.com/geoffreybutler94), [@​georgedrury](https://github.com/georgedrury), [@​gigakun3030](https://github.com/gigakun3030), [@​giggling-ginger](https://github.com/giggling-ginger),
[@​Git-on-my-level](https://github.com/Git-on-my-level), [@​gitcommit90](https://github.com/gitcommit90), [@​githubespresso407](https://github.com/githubespresso407), [@​gnodet](https://github.com/gnodet), [@​GottZ](https://github.com/G…
leewenjie
pushed a commit
to leewenjie/hermes-agent
that referenced
this pull request
Aug 7, 2026
…er user turn (NousResearch#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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
New per-preset
fanoutkey:user_turnruns the MoA reference fan-out 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, single acting model). Defaultper_iterationis byte-for-byte unchanged behavior.Motivation: benchmark data shows advisor generation dominates MoA's wall/cost multiplier (median per-turn wall 10s solo vs 52s MoA on opus-4.8; ~9x cost), and per-iteration divergent process advice measurably hurts strict-procedure tasks.
user_turnis the cheapest lever: advisors shape the plan, the aggregator executes it uninterrupted.Changes
hermes_cli/moa_config.py:fanoutpreset key normalized via_coerce_fanout(per_iteration|user_turn; unknown → default), included in_default_presetand the flattened compat view.agent/moa_loop.py: inuser_turnmode the existing turn-scoped reference cache signature hashes only the prefix up to the LAST user message — mid-turn advisory-view growth no longer changes the key, so iteration 2+ is a cache HIT (advice reused, zero repeat advisor spend, no duplicate trace). A new user message changes the prefix and re-triggers the fan-out.Validation
test_moa_config.py,test_moa_loop_mode.py,test_moa_trace_streamed_capture.pyall pass; signature-stability verified E2E with real imports.Infographic