feat(gateway): webhook payload filters and route scripts (salvage #57544) - #60944
Merged
Conversation
…AP entry - run_route_script shells out with subprocess.run (up to 30s timeout); wrap the call in asyncio.to_thread so a slow script can't stall every other webhook and gateway task on the loop. - scripts/release.py: map grace@weeb.onl -> evelynburger for the salvaged contributor commit.
tonydwb
reviewed
Jul 8, 2026
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Comment (high surface area — 12 files, 767 additions, new webhook routing logic)
Scope
Webhook payload filters and route scripts. Adds a scripts/ directory with Python filter/route logic, plus gateway configuration for Todoist and generic webhook integrations.
Observations
Security:
_INSECURE_NO_AUTHsentinel value used in webhook configs — appropriate for development endpoints, ensure production configs override with real secrets.- CLI prints guidance on HMAC-SHA256 signature validation — helpful.
Code Quality:
- 12 files touched. Clean separation between filter/route modules under
scripts/. - Route registration pattern is extensible.
Testing:
- No dedicated test files for the new webhook routing logic.
Note: Appears to be a salvage of #57544 — link/reference to the original would help reviewers.
Recommendation
Feature additions with webhook security logic warrant human reviewer sign-off before merge.
Reviewed by Hermes Agent
This was referenced Jul 8, 2026
teknium1
added a commit
that referenced
this pull request
Jul 8, 2026
…urfaces (#60983) Follow-up to #60944 (webhook payload filters and route scripts): - reference/cli-commands.md (en+zh): document the new --script option on 'hermes webhook subscribe' - zh-Hans user-guide webhooks.md: mirror the Payload Filters and Script Filters/Transforms sections plus the filters/script route properties (the salvage shipped English-only docs) - hermes-agent skill webhooks reference: teach the agent the filters/ script surface so agent-driven subscriptions can use them
DaveVoyles
added a commit
to DaveVoyles/hermes-agent
that referenced
this pull request
Jul 10, 2026
* feat(desktop): add UI scale setting to appearance settings
* chore(desktop): drop PR screenshot assets from tree
* fix(agent): add cross-turn stream-stale circuit breaker (#58962)
A session wedged against an unresponsive OpenAI-compatible provider can hit the stale-stream detector on every turn and loop forever, burning the full 180s x retries each turn with no response. Issue #58962 reports 494 consecutive failures over 3+ days on a single session.
The streaming retry path already caps retries WITHIN a turn (HERMES_STREAM_RETRIES, default 2) but has no cross-turn cap. Once a session's conversation state makes every turn stale, it retries indefinitely across turns and never notifies the user.
Add a per-session consecutive-stale-stream counter on the agent:
- incremented on every stale-stream kill in the outer poll loop;
- reset to 0 only when a stream actually completes;
- when it reaches HERMES_STREAM_STALE_GIVEUP (default 5), the next turn aborts immediately with a clear, actionable RuntimeError instead of spending 180s x retries again.
This is distinct from the existing stale-stream work (local-provider hard ceiling #44938, backoff/parse-error #60031): those bound a single hung stream, while this bounds repeated cross-turn staleness and surfaces a user-visible error.
Adds tests/run_agent/test_stream_stale_circuit_breaker.py covering the short-circuit, the success-reset, and the increment.
* fix: reset stream-stale breaker on model switch and fallback activation
Follow-up for the salvaged #60332 circuit breaker. The breaker latches:
once the streak trips, interruptible_streaming_api_call raises before any
stream is attempted, so the on-success reset can never run again. The
error text tells the user to switch models and retry — but neither
switch_model() nor try_activate_fallback() cleared the streak, so a
freshly selected healthy provider kept short-circuiting forever (only
/new recovered), and the automatic fallback chain was wedged the same way.
Reset the streak at both swap sites (after a successful rebuild only;
rollback/exhaustion paths keep the latch). 4 tests.
* docs: document HERMES_STREAM_STALE_GIVEUP alongside sibling stream knobs
* fix: widen stale circuit breaker to non-streaming path + all provider-swap resets
Review findings on the salvaged #60332 breaker, fixed as follow-ups:
- restore_primary_runtime() now resets the streak (third provider-swap
path; without it a recovered primary was short-circuited before a
single attempt and could never be re-proven healthy except via /model).
- interruptible_api_call (non-streaming) now carries the same breaker
(guard at entry, bump on stale_call_kill, reset on success). Quiet-mode
/ subagent / headless sessions — the profile most like #58962's
unattended 494-failure session — take this path and had the identical
infinite stale-retry class.
- Partial-stream stub return now resets the streak (chunks were received,
provider demonstrably responsive).
- Consolidated the triple-duplicated counter arithmetic into shared
helpers (_stale_streak/_bump_stale_streak/_reset_stale_streak/
_check_stale_giveup) with one canonical comment block; error message
now says 'consecutive stale attempts' (the counter counts kills, not
turns — a single turn can produce several).
4 new tests (restore resets / no-op restore keeps latch / non-streaming
short-circuit / non-streaming success reset).
* fix(bedrock): route non-Claude auxiliary models through Converse API
Auxiliary Bedrock resolution always used the Anthropic Bedrock SDK, which
only works for Claude foundation-model IDs. Non-Claude models such as
openai.gpt-oss-20b-1:0 now use a Bedrock Converse adapter, matching the
main agent's bedrock_converse transport.
* test(bedrock): cover auxiliary Converse routing for non-Claude models
Assert gpt-oss Bedrock IDs resolve to BedrockAuxiliaryClient while Claude
IDs keep the Anthropic SDK path, including async mode.
* fix: normalize string stop + surface dropped stream/tool_choice in Converse shim
Review findings on the salvaged shim: (a) OpenAI callers may pass stop as
a bare string but Converse's stopSequences requires a list — normalize;
(b) call_llm(stream=True) (MoA aggregator) can reach this client and the
shim silently returned a complete response — keep that behavior (the
streaming consumer's got-final-object path downgrades gracefully) but log
it, and log dropped tool_choice, instead of silently ignoring both.
+2 regression tests.
Follow-up to the salvage of #60217 by @xxxigm.
* feat(mem0): self-hosted dashboard backend + recall tuning (salvage #55614)
Salvage of #55614 by @kartik-mem0 (mem0 maintainer). Adds a SelfHostedBackend
that talks to a self-hosted Mem0 Docker server over httpx (X-API-Key auth,
/search + /memories routes), gated behind `host`. Also folds in the mem0
research-team recall tuning that rides with it: rerank defaults to false across
all modes, the mem0_list tool is removed (5->4 tools), search guidance is
de-shouted, and self-hosted get_all reports the true stored total (#52921).
Supersedes the self-hosted portion of #52487 (@liuhao1024, first-submitted).
Closes #52478
Fixes #52921
* fix(mem0): make prompt label + platform setup honor host routing precedence
Follow-up on the salvaged #55614. The PR added host-based routing to
_create_backend (precedence: oss > host > platform) but two sibling surfaces
didn't mirror it:
- system_prompt_block() checked host before oss, so an oss+host config ran
OSS but told the model it was self-hosted HTTP. Reordered to match routing.
- Platform setup (hermes memory setup mem0 --mode platform) left a stale host
in mem0.json; since host beats platform, the user kept routing to the
self-hosted server. save_config merges (no delete), so clear host to ""
rather than pop() so the merge actually overwrites it.
Adds regression tests for both (mutation-checked).
* fix(mem0): prune dead get_all, wire rerank config default, warn on MEM0_HOST env override
Review follow-ups on the salvage:
- get_all() pruned from the ABC and all three backends: mem0_list (its
only caller) was removed by the recall-tuning commit, leaving new,
tested, unreachable code — including SelfHostedBackend's _MAX_TOP_K
over-fetch workaround. Tests for it dropped; fake-class stubs remain
harmlessly. (The #52921 true-total fix lives on in the PR history if
a lister ever returns.)
- The persisted rerank config key was write-only (setup prompted for it,
nothing read it). initialize() now parses it into _rerank_default and
mem0_search uses it when the model doesn't pass rerank explicitly;
per-call args still win. Guard test added.
- Platform-mode setup now warns when MEM0_HOST is set in the environment:
the json host-clear can't help there (_load_config seeds host from the
env var, docs tell users to put it in .env) — the user would silently
keep routing to the self-hosted server.
- SelfHostedBackend: connect-level retries (httpx.HTTPTransport(retries=2))
so a single transient blip doesn't count toward the provider breaker;
transport now injectable and the test helper uses the real __init__
instead of mirroring it via __new__.
- plugin.yaml description no longer leads with reranking (off by default,
platform-only); docs em-dash typo fixed.
* feat: add prompt-only session export
* feat(cli): add standalone HTML session export with sidebar navigation
Implements a professional, standalone HTML export feature for Hermes sessions.
Key changes:
- Adds 'hermes sessions export <file>.html' support to the CLI.
- Implements a dark-mode-first, responsive HTML generator in 'hermes_cli/session_export_html.py'.
- Single session export features a focused, centered 90% width layout.
- Multi-session export adds a fixed sidebar with session switching and real-time search filtering.
- ZERO external dependencies; all styles and JS are embedded for offline portability.
* feat(cli): include system prompts in HTML export
* feat(cli): redesign system prompt display as dedicated header section
* feat(cli): expand system prompt by default in HTML export
* fix(cli): fix layout width bug and ensure system prompt header is used
* style(export): restore width: 0 for multi-session flex layout
* feat(cli): filter internal session_meta messages from HTML export
* feat(sessions): wire html + prompt-only formats into 'sessions export'
Salvage follow-up integrating PR #30481 (@simplast) and PR #57683
(@catbearlove1-lang) into the unified export surface:
- --format html: standalone self-contained HTML transcript (single
session or multi-session with sidebar), works with all shared filters
and --redact; requires a file output path.
- --only user-prompts: prompt-only export (jsonl records or md sections)
via the shared session_export renderer; the separate export-prompts
subcommand from the original PR is subsumed by this flag.
- AUTHOR_MAP entries for both contributors; docs EN + zh-Hans.
* feat(discord): optionally mention approval owners on exec prompts
Opt-in discord.approval_mentions (config.yaml, bridged to
DISCORD_APPROVAL_MENTIONS) prepends <@id> mentions for numeric
allowlist entries to exec-approval prompts, with a scoped
AllowedMentions override (users only). Default off - no surprise
pings. Reapplied onto the content-mirror layout from #60245: mentions
prepend to the visible content block and its truncation budget.
Original implementation from PR #39719; commits arrived bot-authored,
re-attributed to the contributor.
* chore: add alex107ivanov to AUTHOR_MAP
* fix: restore cli-config.yaml.example from main (stale-branch version leaked into salvage)
* fix(web-server): close OAuth token TOCTOU by writing 0o600 atomically
`_save_anthropic_oauth_creds` wrote the Anthropic OAuth token file with
`os.replace(tmp, path)` followed by a post-hoc `chmod(0o600)`. Between the
rename and the chmod the token file existed at the default umask (0o644 on most
hosts) — a window in which another local user could read the access/refresh
tokens.
Write via `utils.atomic_json_write(..., mode=0o600)`, which creates the temp
with mode 0o600 *before* any content is written, fsyncs, atomically replaces,
preserves the existing file's owner, and cleans up its temp on failure. This
matches the `atomic_json_write(mode=0o600)` call already used elsewhere in this
module for the credential-pool write, and #56644's owner preservation.
Tests updated for the new mechanism, plus a check that the write goes through
`atomic_json_write(mode=0o600)` (mutation-verified).
* feat(mem0): add self-hosted mode to the setup wizard
The salvaged SelfHostedBackend made self-hosted servers reachable via
mem0.json / MEM0_HOST, but the setup wizard still offered only Platform
and OSS — exactly the gap users hit (Discord report: 'At memory setup
there's only 2 options'). Adds a third wizard mode:
- interactive picker: Platform / Self-hosted server / Open Source
- non-interactive: hermes memory setup mem0 --mode selfhosted
--host http://... [--api-key ...] [--dry-run]
- host -> mem0.json (behavioral), API key -> .env as MEM0_API_KEY
(secret), optional key for AUTH_DISABLED servers
- best-effort reachability check against the server, non-fatal
- README + memory-providers docs updated with the wizard path
* feat(sessions): trace export + HF upload via 'sessions export --format trace' (#60507)
* feat(trace): upload sessions to HF Agent Trace Viewer
Salvage trace upload as a smaller CLI-first feature: deterministic Claude Code JSONL export, fail-closed redaction, lazy Hugging Face dependency, and no gateway slash-command wiring.
* chore(trace): drop external porting references from docstrings
Describe the trace-upload design in Hermes' own terms.
* feat(sessions): fold trace upload into 'sessions export --format trace'
Integrates the HF Agent Trace Viewer exporter (PR #36145) onto the
unified export surface instead of a separate 'hermes trace' subcommand:
- --format trace: Claude Code JSONL to stdout/file, or one
<id>.trace.jsonl per session for filtered bulk export; defaults to
the most recent session when no --session-id/filters given.
- --upload pushes to the user's private HF traces dataset (--public to
opt out of private); reads HF_TOKEN with guided setup when missing.
- traces are secret-redacted by default (force mode); --no-redact opts
out after review; redaction failure blocks export (fail closed).
- hermes_cli/trace.py + subcommands/trace.py removed; agent/trace_upload.py
is the single engine. Docs EN + zh-Hans; 4 new CLI tests.
* fix: limit desktop model pickers to explicit providers
* chore: map Ronald contributor email
* fix: harden explicit-provider gate for stale env-seeded pool entries + non-desktop picker opt-ins
Follow-up on the #56966 salvage:
- is_provider_explicitly_configured(): an env-seeded credential-pool entry
only counts as explicit while its env var still resolves to a usable
secret. A stale auth.json entry left behind after the user deletes the
var no longer keeps the provider in the picker forever (#55790).
- TUI modelPicker + dashboard ModelPickerDialog/api.getModelOptions pass
include_unconfigured=true explicitly, preserving their full-universe
setup-affordance behavior now that the backend defaults to the
configured subset.
- desktop lib/model-options.ts routes explicit_only through the shared
requestModelOptions() helper (added on main after the PR branched).
- regression tests for ambient (gh_cli) pool sources, explicit manual/
device-code sources, and stale vs live env-seeded entries.
* feat(plugins): pass approve rule keys to approval gate
* fix(approval): wire gateway notify round-trip into the plugin escalation gate
_run_approval_gate's gateway branch only queued via submit_pending, so
plugin-escalated approvals never sent the interactive embed+buttons on
Discord/Telegram/Slack (#59413) - the user was never notified and the
action stayed silently blocked. Mirror check_dangerous_command's path:
when a session notify callback is registered, run the blocking
_await_gateway_decision round-trip (redacted payload, once/session/
always persistence, deny/timeout produce definitive BLOCKED outcomes);
fall back to submit_pending only when no callback exists.
Fixes #59413.
* chore: add doncazper to AUTHOR_MAP
* feat(pty): RingBuffer for keep-alive output capture
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(pty): PtySession drain/attach/detach with EOF close 4410
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(pty): PtySessionRegistry with reap + capacity
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(chat): reattach /api/pty sessions via ?attach= token
Keep-alive path when ?attach=<token> is present: PTY outlives the socket
via PTY_REGISTRY, reattaches on reconnect. No token = unchanged legacy
pump (_legacy_pump). detach (not close) on disconnect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(pty): periodic reaper wired into dashboard lifespan
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(chat): persist attach token, reconnect on transient close
ChatPage sends ?attach=<localStorage token> so /chat reattaches to its
live PTY across refresh. onclose: 4410=process-exit (session ended),
4409=superseded (quiet), else transient -> auto-reconnect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(mcp): reap orphaned subprocesses before spawning new ones on retry
When an MCP stdio subprocess fails to connect (token expiry, port
contention, timeout), the run() reconnect loop retries with backoff.
Each retry calls _run_stdio() which spawns a new process pair, but the
previous failed pair was only detected as orphaned (added to
_orphan_stdio_pids) — never actually killed. This caused rapid zombie
accumulation: 5 failed attempts × 2 procs each = 10 orphans competing
for the same port.
Add a _kill_orphaned_mcp_children() call at the top of _run_stdio(),
before the _snapshot_child_pids() baseline, so any orphans from prior
failed attempts are reaped before a new subprocess is spawned.
Fixes #57355
* fix(mcp): reap stdio orphans before reconnect
* fix(mcp): unify reconnect orphan reaping + move off the event loop
Merge the two cherry-picked reap call sites into one unscoped sweep at
the top of _run_stdio (the unscoped sweep is a superset of the
per-server one), and run it via asyncio.to_thread so the 2s
SIGTERM->SIGKILL escalation cannot stall the shared MCP event loop.
* fix(mcp): bound stdio initialize handshake to stop subprocess/FD leak
A stdio MCP server that never completes `initialize` (e.g. emits a
non-JSON-RPC frame and then blocks on stdin) leaks a child process plus its
stdio pipes/pidfd on every discovery-retry cycle — unbounded, until the
gateway hits EMFILE and every new open()/spawn fails (#59349).
Root cause (confirmed by instrumenting the live repro, and different from the
issue's own hypothesis): the spawned child IS captured in `new_pids`, so the
report's "new_pids empty at finally" guess is not it. The real cause is that
`session.initialize()` hangs forever on the garbage stream. `connect_timeout`
only bounds the caller's `.result()` wait on the foreground thread — it does
NOT cancel the `_run_stdio` coroutine on the background MCP loop. So the
coroutine is stuck at `await session.initialize()` permanently, its cleanup
`finally` never runs, the child is never reaped, and it stays invisible to the
orphan-reaper (whose `_orphan_stdio_pids` set never gets populated).
Fix: wrap `session.initialize()` in `asyncio.wait_for(..., connect_timeout)`
so a stalled handshake fails instead of hanging. The TimeoutError unwinds
through the SDK context managers (closing the child's stdin -> EOF -> exit)
and lets the existing `finally` reap any straggler. Cross-platform — no
signals/pgid/proc.
Scope: stdio only. The HTTP path has the same `await session.initialize()`
shape but spawns no subprocess (so it can't cause this leak) and already has
httpx transport timeouts.
Verified: the reporter's repro goes from unbounded growth to draining to zero;
added a hermetic regression test (fake transport whose `initialize()` hangs,
asserts the connect is bounded by connect_timeout) that fails on the pre-fix
code and passes on the fix; 566 existing MCP tests pass; ruff clean.
Repro confirmed on macOS (pipe FDs); the Linux-specific pidfd growth in the
report should be equivalent — the reporter offered to validate on Linux.
Closes #59349
* fix(mcp): widen #59349 handshake bound to HTTP transports + cancel abandoned start() task
Sibling sites of the same bug class as the salvaged stdio fix:
- SSE, streamable-HTTP (new + deprecated API) initialize() calls are now
bounded by the same connect_timeout, so an endpoint that accepts the
connection but never answers the handshake cannot park the run() task
forever.
- start() now cancels its ensure_future'd run() task when the caller's
connect timeout cancels start() itself — the orphaned-task leak was
the root mechanism behind #59349, and this closes the class for any
future pre-ready hang.
* fix(mcp): reap orphaned stdio MCP children on ungraceful parent death
A stdio MCP server (e.g. `npx -y mcp-remote <url>`) is spawned as a direct
child of the Hermes process. Existing teardown (MCPServerTask.shutdown() /
_kill_orphaned_mcp_children()) reaps it correctly on a clean exit, but a
kill -9 / crash / force-quit of the Hermes process skips that path entirely
-- the child (and its own descendants, e.g. mcp-remote's spawned node
process) is orphaned and keeps running. Repeated ungraceful restarts pile up
N orphaned processes racing to hold the same upstream SSE session, producing
errors like 'Invalid request parameters' on legitimate reconnects.
macOS/Linux have no portable equivalent of prctl(PR_SET_PDEATHSIG) at the
Python subprocess level, so this adds a thin supervisor
(tools/mcp_stdio_watchdog.py) that:
- execs the real command as its own child in its own process group
- passes stdin/stdout/stderr through untouched (MCP stdio protocol
talks directly over those streams)
- polls the original spawning PID with the same orphan-detection
algorithm already proven in tui_gateway/slash_worker.py (ppid
comparison + psutil creation-time guard against PID reuse)
- SIGTERM-then-SIGKILL's the child's process group the moment the
original parent is gone
Wired into _run_stdio via a new _wrap_command_with_watchdog() helper,
POSIX-only (matches the existing killpg-based cleanup's platform scope),
fails open (any error resolving pid/create-time falls back to the
unwrapped command) so this can never be the reason a working MCP server
stops starting.
Verified: reproduced the exact orphan scenario standalone (fake parent
process spawns watchdog + fake long-running MCP child, kill -9 the fake
parent, confirm the watchdog reaps the child within its poll window with
zero leaked processes). Updated test_mcp_tool_issue_948.py's resolved-path
assertion to check the watchdog-wrapped command instead of the raw
resolved binary. Full test_mcp_tool.py + test_mcp_stability.py +
test_mcp_tool_issue_948.py suite: 232 passed. Full -k mcp sweep across the
whole test tree: 1003 passed, 2 skipped, 0 failed.
* fix(mcp): watchdog wrap after OSV preflight + forward SIGTERM to child group
Two fixes on top of the salvaged parent-death watchdog:
- Apply the watchdog wrap AFTER the OSV malware preflight so the check
inspects the real npx/uvx package instead of the python wrapper
(the wrap previously made the preflight a silent no-op for every
stdio server).
- The real server runs in its own process group under the watchdog, so
the graceful-shutdown killpg no longer reached it; the watchdog now
forwards SIGTERM/SIGINT to the child's group, keeping wedged servers
killable on clean shutdown.
* Recycle idle MCP stdio servers
* Handle minimal MCP server fakes
* chore(release): map rainbowgore + thestudionorth in AUTHOR_MAP for MCP leak salvages
* docs(mcp): document idle_timeout_seconds / max_lifetime_seconds recycle keys + handshake-bound note
* test(mcp): unblock recycle-reconnect test from the parked self-probe wait
The salvaged test predates the parked-server self-probe
(_PARKED_RETRY_INTERVAL, landed on main after the PR branched): after the
final failed retry, run() parks in a real asyncio.wait that the patched
asyncio.sleep doesn't cover, stalling the test 300s. Signal shutdown once
the retry budget is exhausted so the park exits immediately.
* fix(mcp): guard POSIX-only kill primitives in stdio watchdog for the Windows footgun linter
signal.SIGKILL / os.killpg don't exist on Windows. The watchdog is only
spawned on POSIX (wrap site gates on os.name), but guard via getattr with
a plain terminate/kill fallback so an accidental Windows import can't
AttributeError.
* feat(dashboard): report profile + gateway topology in /api/status (#60537)
/api/status (loopback/insecure binds only) now includes:
- profiles: every profile on the host (default + named)
- gateway_mode: none | single | multiple | multiplex
- gateways: one entry per live gateway with the host ports its
port-binding platforms listen on, plus served_profiles when the
default gateway is multiplexing
Ports resolve from each profile's config.yaml (top-level platforms:
wins over gateway.platforms:, matching load_gateway_config precedence)
with adapter defaults as fallback. Topology enumeration runs in an
executor so the profile scan + process-table probes stay off the event
loop, and the whole block is gated behind the same loopback-only split
as hermes_home/gateway_pid so gated binds leak nothing new.
* fix(tools): enable platform-native toolsets when their composite is explicitly configured (#35527)
When a user explicitly configures a platform with its native composite
(e.g. platform_toolsets.discord: [hermes-discord]), the discord and
discord_admin toolsets were silently stripped by _DEFAULT_OFF_TOOLSETS
even though the composite contains those tools. The strip could not tell
an explicit composite opt-in apart from the unconfigured default.
Track whether the platform was explicitly configured and, when it was,
exempt toolsets that are both default-off and platform-restricted to the
current platform from the strip. Only discord/discord_admin are affected
(the sole entries in both _DEFAULT_OFF_TOOLSETS and
_TOOLSET_PLATFORM_RESTRICTIONS). Unconfigured and empty-list platforms
keep the security default-off behaviour.
* docs(sessions): unify export docs under one overview section (#60554)
Restructures the five parallel export sections into a single 'Export
Sessions' section: a format table (jsonl/md/qmd/html/trace + --only
user-prompts), one shared-filters paragraph covering all formats, and
per-format subsections nested beneath. EN + zh-Hans.
* fix(discord): honor pairing grants for message auth
* fix(discord): explain fail-closed allowlist default
Log a one-shot structured warning when Discord denies traffic because
no allowlist/policy is configured, and correct the setup wizard's
inverted warning text. The fail-closed default itself is unchanged.
Fixes #58682.
* docs(discord): troubleshoot silent fail-closed denials
Docs portion of PR #57067: 'bot connects but never replies' section
pointing at the gateway.log warning and the allowlist/policy knobs.
Co-authored-by: ooovenenoso <120500656+ooovenenoso@users.noreply.github.com>
* fix(gateway): only session-discover channel targets for connected platforms (#60574)
Session-based channel discovery resurrected historical origins for
platforms with no connected adapter, exposing stale send_message
targets that can no longer deliver. Gate both the enum loop and the
plugin-registry loop on the live adapter set.
Surgical reapply of the channel-directory portion of PR #25959 (branch
was 6.5k commits stale; the text-batching delay changes bundled there
were dropped - separate concern, defaults have since been retuned on
main).
Co-authored-by: Marco-Olivier Lavoie <marcolivier@gmail.com>
* Fix delegation config precedence
* Use read-only config loader and honor HERMES_IGNORE_USER_CONFIG in delegation config
* fix(dashboard): advertise truecolor to the embedded chat TUI (#60576)
Headless/hosted deploys run the dashboard server without COLORTERM in
the process environment, so chalk inside the PTY-spawned TUI child
downgraded every skin hex color to the xterm 256 palette — the default
skin's bronze banner border (#CD7F32) snapped to palette 173 (#D7875F,
salmon red) and the gold caduceus rendered red/yellow on fresh cloud
instances. Local launches never reproduced it because the operator's
interactive terminal leaks COLORTERM=truecolor into the server env.
xterm.js always renders 24-bit RGB, so the dashboard PTY child should
always advertise truecolor: backfill COLORTERM=truecolor in
_resolve_chat_argv via setdefault (an explicit operator value wins).
Verified with a clean-env PTY probe of the real TUI binary:
no COLORTERM -> 0 truecolor SGRs / 165 palette-256 (salmon 38;5;173);
with the backfill -> 166 truecolor SGRs, exact bronze 38;2;205;127;50.
* feat(dashboard): expose profile names + gateway_mode on gated /api/status (#60585)
The profile+gateway topology added in #60537 sits entirely behind the
loopback/--insecure auth gate. But a hosted agent (Hermes Cloud) binds
non-loopback with OAuth, so should_require_auth is True, and NAS reads
/api/status over the network (fly-provider.ts getInstanceRuntimeStatus)
with no session token. On that gated path the whole topology block was
omitted, so the Portal could never render the profile list.
Split the topology readout by sensitivity:
- profile NAMES (profiles) + gateway_mode are low-sensitivity product
surface and now ride the always-public status body, surviving the auth
gate so NAS/the Portal can enumerate profiles.
- the per-gateway detail (gateways[], carrying host ports) is deployment
recon and stays gated alongside hermes_home / config_path / env_path /
gateway_pid / gateway_health_url.
The collector now runs unconditionally (still in the executor, off the
event loop). No new fields; only the gate placement changes.
* feat(relay): carry routed profile from the connector wire source (#60586)
The multiplex machinery already routes an inbound message to a profile via
SessionSource.profile (build_session_key namespacing + the per-turn
config/credential scope in SessionStore._resolve_profile_for_key). But the
relay path never populated it: _event_from_wire rebuilt the SessionSource
field-by-field and dropped any 'profile' the connector sent, so a
Team-Gateway (connector + relay) message could not be routed to a specific
profile the way the /p/<profile>/ HTTP prefix and per-credential polling
adapters already can.
Stamp source.profile from the wire payload in _event_from_wire. This is the
last missing link for NAS-driven per-profile routing over the relay in
multiplex mode; the connector populating the field ships separately
(gateway-gateway contract adds the optional wire field).
Back-compat: absent 'profile' → None → legacy agent:main namespace,
byte-identical to today for every single-profile gateway.
* Add dashboard memory provider switching
* fix: validate memory provider names before filesystem lookup and setup commands
Strict charset allowlist (alnum + - _, max 64) on the {name} path param of
the memory-provider config/setup endpoints. Prevents traversal-shaped names
from reaching find_provider_dir(), and setup now 404s when neither a
loadable provider nor a plugin manifest exists, so the command-running path
is only reachable for discoverable plugins. Adds regression tests.
* feat(gateway): GATEWAY_MULTIPLEX_PROFILES env override for multiplex flag (#60589)
The connector now depends on the single multiplexed gateway for per-profile
relay routing, so hosted deployments need to FORCE multiplexing on regardless
of the image's config.yaml. gateway.multiplex_profiles was config.yaml-only,
which a user could leave unset or flip off.
Add GATEWAY_MULTIPLEX_PROFILES as a standard operator override on top of the
existing config key — the same 'config.yaml is canonical, env is the operator
override' pattern the Telegram/Signal require_mention bridges use:
env (recognized token) > config.yaml (top-level or nested gateway.*) > False
- gateway/config.py: _env_multiplex_profiles_override() resolves the env var
tri-state — recognized truthy/falsy token → bool; unset/blank/unrecognized
→ None (fall through to config). Blank is deliberately None, not False, so a
provisioned-but-unpopulated Fly secret ('') can't shadow a config.yaml opt-in
(the empty-secret trap). Wired into GatewayConfig.from_dict so every consumer
(run.py, session.py via self.config) sees the resolved value.
- hermes_cli/gateway.py: the named-profile-start guard
(_guard_named_profile_under_multiplexer) reads config.yaml directly, so it
gets the SAME env precedence — otherwise env-forced multiplex would leave the
guard blind and someone could start a conflicting per-profile gateway that
double-binds a bot token. Env-forced-on trips the guard even with no
config.yaml key; env-forced-off disables it over a config opt-in.
Tests: full 3-tier precedence in test_config.py (incl. the discriminating
env-overrides-config cases + the empty/whitespace/unrecognized fall-through
trap + resolver tri-state), mutation-verified (flipping precedence fails
exactly the two env-wins tests); guard env cases in test_multiplex_lifecycle.py.
Force-on is safe on a single-profile instance: session keys stay byte-identical
(agent:main) and the _run_agent wrapper installs the per-turn secret scope, so
the fail-closed get_secret() path is satisfied.
* Fix dashboard chat model profile scoping
* fix: pass profile-scoped SessionDB to _session_latest_descendant in dashboard chat PTY resume
The chat PTY launch path landed on main after PR #50558 and still called
_session_latest_descendant() with the old one-arg signature. Open the
requested profile's state DB (matching the REST endpoint) so profile-scoped
resume resolves descendants in the right database.
* Add WhatsApp dashboard pairing flow
* chore: release v0.18.1 (2026.7.7) (#60595)
* fix(whatsapp): unpin Baileys from git commit, use published 7.0.0-rc13 (#60643)
The April 2026 pin to WhiskeySockets/Baileys#01047deb existed only to
pick up the abprops bad-request fix (Baileys PR #2473) before it was
released. That fix shipped in v7.0.0-rc11 (May 2026); our pinned commit
is now 48 commits behind rc13.
The git pin forced npm to clone the repo and compile Baileys from
TypeScript source on every fresh install (~3 min), which blew past the
dashboard pairing flow's timeout. Registry install takes ~3s.
Validation: all 9 bridge.js imports present in rc13, bridge.native.test.mjs
passes (13/13), live bridge boot renders pairing QR against real WA servers.
* chore: release v0.18.2 (2026.7.7.2) (#60651)
* feat(install): warn pip/Homebrew installs are unsupported (CLI, TUI, desktop) (#57225)
* feat(install): warn pip/Homebrew installs are unsupported (CLI, TUI, desktop)
pip and Homebrew are now Unsupported install methods per
website/docs/getting-started/platform-support.md. Surface a
warn-don't-block deprecation notice everywhere the install method is
already shown, pointing at the platform-support docs and noting these
installs will not receive further updates. NixOS (Tier 2) is untouched.
- hermes_cli/config.py: shared is_unsupported_install_method() /
format_unsupported_install_warning() helpers so the wording and docs
link stay consistent across every surface.
- hermes_cli/banner.py: generalize the existing pip-only banner
warning to also cover Homebrew.
- hermes_cli/main.py: hermes update and hermes update --check print
the warning before proceeding (still update; warn, don't block).
- tui_gateway/server.py: session.info gains install_warning.
- ui-tui: SessionPanel renders install_warning alongside the existing
'N commits behind' notice.
- apps/desktop: SessionRuntimeInfo/GatewayEventPayload gain
install_warning; applyRuntimeInfo + the live session.info event fire
a snoozable warning toast via a new reportInstallMethodWarning(),
mirroring the existing backend-contract-skew toast pattern. i18n
strings added for en/zh/zh-hant/ja.
- Tests: updated pip banner assertions for the new wording, added a
Homebrew banner test, and two tui_gateway session_info tests
(install_warning present for pip, absent for git).
* fix(nix): make `hermes` in developement environment actually work
install modules as editable overlay with uv
* feat: print install method when running --version
* fix: correct detect install method when running from a subtree
* Fail closed on invalid JSON/YAML/TOML writes instead of writing then reporting
write_file() previously called _atomic_write() first and only ran the
JSON/YAML/TOML/Python syntax check afterward as an informational lint
delta -- a parse failure never set the top-level `error` key, so a
corrupt structured-data write still landed on disk (and file_tools.py's
files_modified gating, which keys off `error`, silently reported it as
a successful modification).
Move the in-process syntax check for JSON/YAML/TOML ahead of
_atomic_write() and refuse the write outright on a parse failure: no
temp file, no rename, nothing touches disk, and the result carries a
top-level `error` so callers correctly see it as unmodified.
Deliberately scoped to _FAIL_CLOSED_INPROC_EXTS (JSON/YAML/TOML), not
all of LINTERS_INPROC -- .py is excluded because this codebase's own
test fixtures (TestPatchReplacePostWriteVerification et al.) write
arbitrary non-Python text through *.py paths purely to exercise
write-mechanics; a hard block there broke 3 previously-passing tests
during development. Python keeps its pre-existing non-blocking
lint-delta report.
Adds tests/tools/test_write_file_syntax_gate.py: invalid JSON/YAML/YML/
TOML refused with nothing written (new file) and nothing modified
(existing file); valid JSON/YAML still written byte-for-byte; a
non-linted extension with garbage content is unaffected; invalid Python
is confirmed NOT hard-refused (still just reported).
* fix(tools): make the YAML write gate syntax-only so multi-doc/tagged YAML isn't refused
safe_load() raises ComposerError on multi-document streams (k8s manifests)
and ConstructorError on application-defined tags (CloudFormation !Sub,
Ansible !vault) — both valid YAML syntax. Now that the linter's verdict is
a fail-closed write gate, those false positives would refuse legitimate
writes outright. Switch to yaml.parse() (scanner+parser only), which still
catches real syntax failures.
* chore: add AUTHOR_MAP entry for neoguyverx (PR #60526 salvage)
* fix(gateway): drain in-flight cron jobs before shutdown tool kill
/update and other shutdown paths only waited on gateway session agents,
so active cron tool work was killed immediately in final-cleanup while
the scheduler could still mark the job successful (#60432).
* test(gateway): cover cron drain during gateway shutdown (#60432)
* fix(gateway,cron): make shutdown drain visible to in-flight cron work
Cron jobs run through cron/scheduler.py's own ThreadPoolExecutor via a
standalone AIAgent (run_job/run_one_job), entirely outside
GatewayRunner._running_agents -- the dict _drain_active_agents() and
every other active-work check on that class reads. A gateway shutdown
(/update, /restart, and SIGUSR1 all funnel through the same stop())
could log active_at_start=0 and immediately kill tool subprocesses
while a cron job's terminal command was still running, with no wait
and no indication anything was interrupted.
Real-world impact (from the issue): a scheduled daily briefing cron
job was in flight during /update, its tool subprocess got killed
by the unconditional shutdown cleanup, and the job was never marked
failed -- it simply never completed or delivered, with no error
surfaced anywhere. A repro with a 30-minute `sleep` cron job in flight
during /update reproduced the same pattern: subprocess killed at
+0.22s of drain (active_at_start=0), the job's agent thread continued
in-process and produced a plausible-looking final response from the
truncated tool output, and the scheduler marked the run successful.
Root cause is layered, not a single line:
1. GatewayRunner._drain_active_agents() only waits on _running_agents.
Cron work was invisible to it, so drain returned instantly whenever
the only active work was a cron job.
2. Even with visibility, the shutdown's final tool-subprocess kill
(process_registry.kill_all()) is a global, unconditional sweep with
no per-job targeting -- a long-running cron job that outlives the
drain timeout still gets its subprocess killed.
3. cron/scheduler.py had no way to detect that a job's tool subprocess
was killed out from under it mid-run; the agent thread kept going
and its eventual (often degraded but plausible-looking) response
got reported as a normal successful completion.
Fix, three parts:
- cron/scheduler.py: expose get_running_job_ids() (thread-safe
snapshot of the existing _running_job_ids set, already used to
prevent double-dispatch) so the gateway can read cron's in-flight
state without reaching into private module internals.
- gateway/run.py: GatewayRunner._active_cron_job_count() reads that
snapshot. _drain_active_agents() now waits on
(_running_agents OR active cron jobs), so a cron-only workload gets
the same bounded wait chat sessions already get instead of an
instant active_at_start=0. Shutdown drain logging gains
cron_active_at_start/cron_active_now fields alongside the existing
ones (unchanged, for compat).
- cron/scheduler.py: mark_running_jobs_interrupted(reason), called by
gateway/run.py's _kill_tool_subprocesses() right after
process_registry.kill_all(), marks every job still in
_running_job_ids at that instant as failed/interrupted via the
existing mark_job_run() -- and records the job IDs in
_interrupted_job_ids BEFORE writing, so run_one_job()'s own
eventual completion for the same run (racing in its own thread)
checks that flag and skips its normal write instead of clobbering
the interrupted status with a false "ok" produced from the
now-truncated tool output. This does not attempt to correlate a
killed PID to a specific job ID (process_registry tracks PIDs, not
job IDs) -- any job still dispatched at the moment of a forced kill
is treated as interrupted, matching the existing coarser precedent
set by _interrupt_running_agents(), which interrupts every entry in
_running_agents on a drain timeout without per-agent correlation
either.
Deliberately out of scope (flagged in the issue as a separate,
lower-priority concern): startup-time reconciliation of cron runs that
started but never reached a terminal status.
Testing:
- tests/cron/test_shutdown_interrupt.py (12 tests): get_running_job_ids
snapshot semantics, mark_running_jobs_interrupted marking/no-op/
partial-failure behavior, and -- the core race guard -- run_one_job
skipping its own last_status write (both the success path and the
exception path) when the shutdown path already marked the run
interrupted, with a control test proving ordinary un-interrupted
completions are unaffected.
- tests/gateway/test_cron_active_work_drain.py (9 tests):
_active_cron_job_count reading cron state and failing closed (0) if
the cron module is unavailable; _drain_active_agents waiting for an
in-flight cron job the same way it waits for chat sessions, timing
out if the job outruns the window, and leaving existing chat-session
drain behavior unchanged; a full runner.stop() integration test
(drain-timeout path) proving mark_running_jobs_interrupted actually
fires with the right job ID when a tool subprocess is force-killed,
plus a no-op control when nothing cron-related is in flight.
- tests/gateway/test_shutdown_cache_cleanup.py: added
_active_cron_job_count() to that file's hand-rolled _FakeGateway test
double, which stop() now calls -- without it those 8 pre-existing
tests AttributeError (caught by fail-then-pass below, not a
production bug).
Fail-then-pass: reverted gateway/run.py + cron/scheduler.py, all 21
new tests fail (fixture/attribute errors -- the feature doesn't exist
yet); restored, all 21 pass.
Regression check: ran the full plausibly-affected surface --
tests/gateway/{test_gateway_shutdown,test_restart_drain,
test_restart_notification,test_restart_redelivery_dedup,
test_restart_resume_pending,test_restart_service_detection,
test_shutdown_cache_cleanup,test_stuck_loop,test_clean_shutdown_marker,
test_external_drain_control,test_session_state_cleanup,
test_update_command,test_update_streaming}.py plus tests/cron/ (944
tests) -- against a clean upstream/main checkout and against this
branch. Diffed the two FAILED lists: identical, 20 pre-existing
failures on both sides (Windows-locale/cp1252 file-encoding issues and
Unix-permission-bit assertions that don't apply on this Windows dev
box), zero new failures, zero fixed-by-accident. The 8
test_shutdown_cache_cleanup.py failures found mid-development were
from the _FakeGateway gap above, fixed in the same commit and
confirmed clean on the final rerun (diff against baseline: exit 0).
Fixes #60432
* fix(cron): stop interrupted jobs from delivering their pre-kill output
Follow-up to the previous commit on #60432. The status-write guard
(_consume_interrupted_flag, checked right before mark_job_run) closes
the false-success bookkeeping gap, but run_one_job delivers its result
BEFORE that check: delivery happens right after run_job() returns,
mark_job_run happens at the very end. A job whose tool subprocess was
killed mid-flight can still produce a plausible-looking final_response
from the truncated output, and that response would reach the user via
_deliver_result before the interrupted flag was ever consulted --
correct status in jobs.json, wrong message already sent.
Adds _is_interrupted(), a non-destructive peek at the same
_interrupted_job_ids set (_consume_interrupted_flag stays as the
consuming, authoritative check right before the status write -- this
needed a peek instead since the flag has to still be visible there).
Checked right after save_job_output, before the deliver_content
decision: if the run looked successful but was flagged interrupted,
force success=False with an explicit interruption message. This
routes delivery through the existing _summarize_cron_failure_for_delivery
path (the same one a real failure already uses) instead of the raw
final_response, so the user gets an honest "this run was interrupted"
instead of a truncated/misleading result.
Testing: 4 new tests in tests/cron/test_shutdown_interrupt.py --
_is_interrupted peek semantics (false/true/does-not-clear, as opposed
to the consuming _consume_interrupted_flag), and the delivery-gate
test itself, which mocks run_job to return a normal-looking success
with a "plausible final response" while the job is pre-marked
interrupted, and asserts _deliver_result receives the failure summary
("This run was interrupted.") instead, with the summarizer's error
argument confirmed to mention the interruption.
Fail-then-pass: reverted cron/scheduler.py only, the 4 new tests fail
(3 on the missing _is_interrupted attribute, 1 -- the delivery-gate
test -- on _summarize_cron_failure_for_delivery never being called,
i.e. the raw response would have gone out); restored, all 16 tests in
the file pass.
Regression: tests/cron/ (683 tests) + test_cron_active_work_drain.py +
test_gateway_shutdown.py + test_shutdown_cache_cleanup.py -- 11
pre-existing failures (Unix file-permission-bit and path-tilde
assertions that don't apply on this Windows dev box), matching the
same set already established as pre-existing in the prior commit's
regression check. Zero new failures.
Continues #60432
* fix(gateway,cron): reconcile #60612 + #60631 onto one drain surface
Keep #60631's get_running_job_ids() snapshot + _active_cron_job_count()
(import-guarded for minimal test doubles) as the single read path, and
retarget #60612's drain tests at it. Drops the redundant
cron_jobs_in_flight() helper so there is one surface, not two.
* fix(tui): prevent ws_orphan_reap from ending gateway-originated sessions
Guard _finalize_session's db.end_session() call against gateway-owned
sessions (telegram, bluebubbles, discord, etc.). The TUI is a viewer
for these sessions, not the lifecycle owner. Unconditionally ending
them in state.db creates a Groundhog Day routing loop: the gateway's
#54878 self-heal detects the stale entry, recovers to the parent
session, context compression splits back to the reaped child, and the
cycle repeats on every inbound message — causing complete conversational
context amnesia.
Fixes #60609
* fix(tui): derive gateway-owned sources from the Platform enum, not a hardcoded list
The salvaged guard used a hand-maintained frozenset of 14 platform names —
several of which (line, wechat, facebook, imessage, googlechat) aren't
actual Hermes Platform values, while real ones (whatsapp_cloud, feishu,
wecom, dingtalk, qqbot, yuanbao, plugin platforms like irc) were missing.
Resolve the source through gateway.config.Platform instead (built-ins +
registered plugin platforms via _missing_), with an explicit exclusion set
for self-owned/local sources. Adds tests for the guard and both reap paths.
* feat(gateway): generic OIDC client-credentials relay provisioning (NAS-free) (#60730)
For air-gapped / self-hosted-IdP deploys with NO Nous Portal, let the gateway
obtain its caller-identity bearer from a generic OAuth2 client_credentials grant
against the operator's own IdP (e.g. Microsoft Entra ID) instead of only
resolve_nous_access_token(). The connector's OIDC tenant resolver reads a claim
(default tid) off that token as the tenant.
- gateway/relay: new canonical _resolve_relay_identity_token() — client_credentials
when gateway.idp.token_url (or GATEWAY_RELAY_IDP_* env) is set, else Nous Portal
(unchanged default). Wired into self_provision_relay().
- hermes_cli/gateway_enroll: _resolve_identity_token() delegates to the canonical
resolver so the enroll CLI and the runtime self-provision path share ONE impl.
Config via gateway.idp.{token_url,client_id,client_secret,scope} in config.yaml
(env override GATEWAY_RELAY_IDP_*). No behaviour change when unset.
Tests: tests/gateway/relay/test_identity_token_resolver.py (6 — mode selection,
request shape, config/env precedence, fail-closed). Relay suite 162 pass.
Validated via the cross-repo gateway<->connector live E2E (provision, managed
self-provision, inbound round-trip, /link) against a connector running the OIDC
tenant resolver with zero NAS config.
* Fix slow Z.AI startup by caching auto-detected endpoint to disk
(cherry picked from commit 6ed884933a178d5540f02d80e3fe9e678ca844eb)
* chore: add veradim to AUTHOR_MAP for PR #41201 salvage
* fix: don't flip active_provider when caching Z.AI probe result
_save_provider_state() sets auth_store['active_provider'] as a side effect.
The Z.AI endpoint probe runs from credential-pool env seeding for any user
with a Z.AI key in env — persisting the probe cache must not silently make
zai the active provider. Use _store_provider_state(set_active=False).
Follow-up to PR #41201 salvage.
* fix: Z.AI endpoint persist failure must not break URL resolution
Review findings (hermes-pr-review Phase 2, 3-angle):
- _save_auth_store() does real filesystem I/O (mkdir, O_EXCL create, fsync,
atomic replace) and can raise on disk-full/permissions/lock-timeout. The
persist ran bare in the success path, so a persist failure aborted
_resolve_zai_base_url() after detection had already succeeded. Wrap the
persist in try/except: log a warning and still return the detected URL
(worst case: next start re-probes).
- Readability: stage the payload in a local detected_endpoint instead of
writing through the stale pre-lock 'state' dict, which is no longer what
gets persisted.
* fix(cron): stop the ticker from stalling forever on a wedged jobs lock (#60703) (#60855)
Three fixes for the silent post-restart ticker stall:
1. _jobs_lock() bounds its cross-process flock: LOCK_NB polled against a
30s deadline instead of an unbounded LOCK_EX taken while holding the
process-wide RLock. On timeout it logs at ERROR and degrades to
in-process-only locking (the existing fallback path), so a sibling
process wedged while holding .jobs.lock can no longer freeze every
cron function - including the ticker's get_due_jobs() and thus the
heartbeat - forever with zero logging.
2. fire_claim/run_claim freshness checks are bounded on both sides
(0 <= age < ttl): a claim stamped in the future (clock/TZ skew across
a restart) was previously fresh forever, making the job permanently
unfireable and every manual run report 'already being fired'.
3. _execute_job_now distinguishes paused/disabled/missing jobs from a
genuinely held claim instead of mislabeling them all as 'already
being fired'.
* fix(tui_gateway): back off notification poller when session is busy
The busy-session branch of _notification_poller_loop re-queued the
completion event and immediately re-polled it with no sleep, spinning
at full speed (100% CPU, ~1100 futex/s of GIL churn) for as long as
the session stayed running. This starved the dashboard asyncio loop:
/api/status went from 0.14s to 3-6s with 10s timeouts.
Sleep 0.25s outside history_lock before re-polling, mirroring the
0.1s back-off already used for foreign-session events.
* chore: add SiteupAgencia to AUTHOR_MAP for #57435 salvage
* test(tools): add unit tests for skill_gist
* fix(agent): tag desktop chat sessions as desktop
The desktop app's chat panel reuses tui_gateway as its backend, so every chat session was stamped platform="tui". That made the agent read terminal-specific platform guidance while running in the graphical desktop chat surface.
Resolve the misclassification at its source: tui_gateway now picks platform="desktop" when HERMES_DESKTOP=1 and HERMES_DESKTOP_TERMINAL is unset, and keeps platform="tui" for the embedded terminal pane and standalone TUI. Add a PLATFORM_HINTS["desktop"] entry describing the actual chat surface (full GFM markdown, MEDIA: intercept, inline images). Move the embedded-pane clarifier to the platform-hint resolution site so it appends only to the tui hint under HERMES_DESKTOP_TERMINAL=1. Delete the now-dead desktop-hint block from build_environment_hints() that competed with the platform hint.
Standalone TUI sessions produce byte-identical prompts as before; the new desktop hint and clarifier are assembled once per session in the stable tier, so prompt caching is preserved.
* fix(delegation): route async results to origin session
Carry the live TUI session id with async delegation completion events and prefer the commissioning UI session when desktop pollers share the completion queue. Resolve compressed session keys to their continuation before treating events as orphaned, and capture the live parent agent session id for TUI/ACP dispatch.
* fix(delegation): fail-closed orphan handling + session-scoped delegation lifecycle
Two invariants layered on the origin-routing commit (#55578):
1. Fail closed on orphaned async-delegation payloads. The poller's
belongs-elsewhere check handles events owned by another LIVE session,
but an event whose owner is gone previously fell through and was
adopted by whichever poller saw it - injecting one chat's delegation
output into another chat. Delegation completions are now injected
only into a session that PROVABLY owns them (origin UI id, or
session-key/lineage match via the compression chain); unowned
payloads are dropped from injection with a WARNING (the subagent's
output is already persisted in the delegation records, so nothing is
lost). The shutdown drain applies the same rule. Non-delegation
events keep the historical adopt-orphans behavior.
2. A session's in-flight async delegations end with the session.
_finalize_session now calls interrupt_for_session(): delegations
commissioned by the closing UI session are interrupted always;
key-matched delegations only when the TUI owns the session lifecycle,
so closing a viewer tab on a live gateway session never kills the
gateway's own background work.
* feat(models): swap curated Tencent Hy3 Preview for GA tencent/hy3, drop owl-alpha (#60943)
- OPENROUTER_MODELS: remove openrouter/owl-alpha (free) and
tencent/hy3-preview{,:free}; add tencent/hy3 and tencent/hy3:free
- _PROVIDER_MODELS[nous]: tencent/hy3-preview -> tencent/hy3
- run_agent.py reasoning-prefix list: tencent/hy3-preview -> tencent/hy3
(prefix match still covers -preview if pinned)
- model_metadata: register hy3 context length (262144) alongside hy3-preview
- regenerate website/static/api/model-catalog.json
- update tokenhub curated-list tests to the new IDs
The tencent-tokenhub direct provider still serves hy3-preview and is
intentionally unchanged.
* docs(i18n): align translated CONTRIBUTING files with pyproject Python range (3.11-3.13)
* docs(i18n): align translated CONTRIBUTING files with pyproject Python range (3.11-3.13)
* fix(delegation): route async delegate_task results back to originating session
The completion event already carries the dispatching session's session_key
(captured at dispatch time in delegate_tool.py:2798), but the delivery
router ignored it — results landed in whatever session was active at
completion time instead of the session that dispatched the subagent.
Changes:
- drain_notifications() in process_registry.py: optional session_key
filter. Non-matching async_delegation events are re-queued instead of
consumed, so they remain available for the correct session's drain.
- cli.py process_loop: passes active session_key to drain_notifications()
- tui_gateway/server.py post-turn drain: passes session_key from the
TUI session dict
- gateway/run.py _build_process_event_source: logs warning when routing
metadata is unresolvable (previously silent drop)
- Regression tests verifying session-scoped drain filtering
Fixes #58684
* fix(delegation): positive-proof ownership for the post-turn drain
Extends the salvaged session_key filter with the same fail-closed,
compression-chain-aware ownership gate the poller uses (#55578):
- drain_notifications() accepts an owns_event callback; when provided,
an async-delegation event is consumed ONLY on positive proof of
ownership, and a broken callback re-queues (never leaks). Bare key
equality remains for single-session callers (CLI); no filter remains
legacy behavior.
- The TUI post-turn drain passes _session_owns_notification_event, so
it can't adopt another session's (or an orphan's) delegation payload,
while a post-compression session still claims its own pre-compression
dispatches - the gap bare key equality left open.
* fix(desktop): register /compress command in TUI gateway dispatch so Desktop can invoke it
* fix(tui): route /compress and /compact past the slash worker to command.dispatch
Ported from #60834 (same author) — pending-input routing so clients that
fail the slash.exec->dispatch fallback still reach the new compress handler.
* fix(whatsapp): use windows_detach_popen_kwargs to prevent console window flash on Windows
* fix(cli): preserve chat -q answer by gating exit-summary screen clear (#53009)
In single-query (-q) mode, the assistant's final answer was printed and
then immediately erased by _print_exit_summary() — which unconditionally
called _clear_terminal_on_exit() (ESC[3J ESC[2J ESC[H]). The answer was
present in the session store but invisible in the terminal.
The clear is only needed for interactive TUI teardown (#38928) where
prompt_toolkit chrome must be cleaned up. Add a clear_screen parameter
to _print_exit_summary() (default True, preserving interactive behavior)
and pass False from the single-query call site so the answer stays
visible above the exit summary.
Regression tests cover:
- clear_screen=True (default) calls _clear_terminal_on_exit()
- clear_screen=False skips the clear
- Single-query -q path passes False end-to-end
- Interactive path still clears (preserving #38928)
* test(cli): update FakeCLI._print_exit_summary for new clear_screen kwarg
* perf(yuanbao): bounded-concurrency inbound media resolve
* feat(Yuanbao) optimizes media resource processing speed: parallel download
* fix(delegate): pin async completion to spawning parent session (#57498)
Background delegate_task completions only carried session_key. When multiple
active sessions shared a routing peer, get_or_create_session could recover the
latest ended_at IS NULL row and inject the subagent result into the wrong
session.
Capture parent_agent.session_id at dispatch time, include it on async-delegation
completion events, and pin gateway routing via switch_session when the
synthetic completion message is handled.
Fixes #57498
* fix(gateway): never resurrect ended sessions for delegation completions; /new severs in-flight delegations
Completes the session-binding class on the gateway surface (#55578),
matching the TUI rules:
1. Fail-closed pinning: switch_session() re-opens ended sessions, so
pinning a completion to a spawning session that has since ENDED
(user /new, closed rotation) would resurrect a conversation the user
explicitly ended and inject into it. The injection path now checks
the pinned row's ended_at first and drops the injection with a
WARNING when the spawning session is dead or unknown - the result
stays in the delegation records.
2. /new ends the old conversation's delegations: _handle_reset_command
calls interrupt_for_session() with the expiring durable session id
(matching the parent_session_id pin stamped at dispatch) plus the
routing key as fallback, so a reset can't leave dangling subagents
whose completions have no live owner.
interrupt_for_session() gains the parent_session_id selector because a
gateway chat's session_key (the platform conversation key) survives a
reset while the session id rotates - key-based matching alone could
never sever a gateway conversation's delegations.
* feat(gateway): add webhook payload filters
* fix(gateway): run webhook route scripts off the event loop + AUTHOR_MAP entry
- run_route_script shells out with subprocess.run (up to 30s timeout); wrap
the call in asyncio.to_thread so a slow script can't stall every other
webhook and gateway task on the loop.
- scripts/release.py: map grace@weeb.onl -> evelynburger for the salvaged
contributor commit.
* fix(desktop): continue the selected stored session instead of minting a new one (#55578) (#60874)
Two client-side halves of the #55578 session split:
1. Submit with a null activeSessionId but a SELECTED stored session now
resumes that stored session instead of falling straight through to
createBackendSessionForSend - which silently forked the user's
conversation into a brand-new session that then got orphan-reaped.
New-chat drafts (no stored selection) still create sessions as before.
2. prompt.submit recovery now also fires on gateway request timeouts,
not only 'session not found'. A starved backend loop (the async-
delegation poller spin) rejects the submit with 'request timed out'
even though the stored session is fine; previously that surfaced an
error, left the binding cleared, and set up the split on the next
send.
Fail-then-pass: 2 new tests fail with production code reverted.
* fix(compression): stop compaction thrash — 75% trigger floor under 512K, no summary output cap, reasoning-trace exclusion (#60989)
Sessions on sub-512K-context models were spending most of their wall-clock
re-summarizing: the 50% trigger left too little post-compaction headroom
(the incompressible floor — system prompt, tool schemas, protected tail,
rolling summary — ate most of the reclaimed space), so compaction re-fired
every 1-2 turns. Three compounding defects fixed:
- Threshold floor: models with context windows below 512K now trigger at
>=75% of the window (raise-only — a higher configured value or per-model
autoraise like Codex gpt-5.5's 85% always wins). Re-derived on
update_model() in both directions.
- No max_tokens on the summary call: the summary budget is prompt guidance
only ("Target ~N tokens"). The wire cap truncated summaries mid-section
on the Anthropic Messages / NVIDIA NIM paths (thinking models burn the
cap on reasoning first), yielding truncated or thinking-only summaries
and compaction loops. Summary token ceiling lowered 12K -> 10K to keep
the guidance within the intended 1K-10K envelope.
- Reasoning traces excluded end-to-end: inline <think>/<reasoning> blocks
are now stripped from assistant content before serialization to the
summarizer, and from the summarizer's own output before the summary is
stored (previously a thinking summarizer model's trace was persisted in
_previous_summary and re-fed into every iterative update, compounding
bloat). Native reasoning fields were already excluded.
Verified E2E with real imports against a temp HERMES_HOME: threshold table
across 64K-1M windows, override interactions (user 0.85 wins, spark 0.70
raised, gpt-5.5 0.85 kept), full compress() round-trip with a thinking
summarizer, and wire-kwargs capture proving no max_tokens is sent.
* docs(webhook): complete filters + route-scripts coverage across doc surfaces (#60983)
Follow-up to #60944 (webhook payload filters and route scripts):
- reference/cli-commands.md (en+zh): document the new --script option on
'hermes webhook subscribe'
- zh-Hans user-guide webhooks.md: mirror the Payload Filters and Script
Filters/Transforms sections plus the filters/script route properties
(the salvage shipped English-only docs)
- hermes-agent skill webhooks reference: teach the agent the filters/
script surface so agent-driven subscriptions can use them
* feat(xai): add grok-4.5 (GA) to model catalog, context lengths, and reasoning-effort allowlist (#60887)
* feat(xai): add grok-4.5 (early access) to catalog, context lengths, and reasoning-effort allowlist
- hermes_cli/models.py: grok-4.5 in _XAI_CURATED_EXTRAS (callable but absent
from models.dev) and _XAI_STATIC_FALLBACK, so the /model picker and
validation surface it on both xai and xai-oauth.
- agent/model_metadata.py: context lengths grok-4.5 -> 500K (per model card)
and grok-build-latest -> 500K (alias); grok-4.5 added to
_GROK_EFFORT_CAPABLE_PREFIXES.
Verified live against api.x.ai /v1/responses (2026-07-08): effort
low/medium/high accepted (server default: high), "none" rejected,
function calling works, full agent turn with terminal tool succeeded.
* feat(xai): grok-4.5 GA — add aggregator catalog entries, refresh comments
grok-4.5 is now GA: models.dev lists it (500K context, effort
low/medium/high) and both OpenRouter and Nous serve x-ai/grok-4.5.
Add it to the OpenRouter fallback snapshot and the Nous static list,
and update the early-access comments.
* chore: regenerate model-catalog.json for x-ai/grok-4.5
* f…
DaveVoyles
added a commit
to DaveVoyles/hermes-agent
that referenced
this pull request
Jul 10, 2026
* fix(discord): widen expired-defer handling to /thread slash command
Same 10062 degrade-gracefully pattern as _run_simple_slash: create the
thread anyway, skip the ephemeral followups that need a live
interaction token. Non-expiry defer errors still raise.
* feat(desktop): add UI scale setting to appearance settings
* chore(desktop): drop PR screenshot assets from tree
* fix(agent): add cross-turn stream-stale circuit breaker (#58962)
A session wedged against an unresponsive OpenAI-compatible provider can hit the stale-stream detector on every turn and loop forever, burning the full 180s x retries each turn with no response. Issue #58962 reports 494 consecutive failures over 3+ days on a single session.
The streaming retry path already caps retries WITHIN a turn (HERMES_STREAM_RETRIES, default 2) but has no cross-turn cap. Once a session's conversation state makes every turn stale, it retries indefinitely across turns and never notifies the user.
Add a per-session consecutive-stale-stream counter on the agent:
- incremented on every stale-stream kill in the outer poll loop;
- reset to 0 only when a stream actually completes;
- when it reaches HERMES_STREAM_STALE_GIVEUP (default 5), the next turn aborts immediately with a clear, actionable RuntimeError instead of spending 180s x retries again.
This is distinct from the existing stale-stream work (local-provider hard ceiling #44938, backoff/parse-error #60031): those bound a single hung stream, while this bounds repeated cross-turn staleness and surfaces a user-visible error.
Adds tests/run_agent/test_stream_stale_circuit_breaker.py covering the short-circuit, the success-reset, and the increment.
* fix: reset stream-stale breaker on model switch and fallback activation
Follow-up for the salvaged #60332 circuit breaker. The breaker latches:
once the streak trips, interruptible_streaming_api_call raises before any
stream is attempted, so the on-success reset can never run again. The
error text tells the user to switch models and retry — but neither
switch_model() nor try_activate_fallback() cleared the streak, so a
freshly selected healthy provider kept short-circuiting forever (only
/new recovered), and the automatic fallback chain was wedged the same way.
Reset the streak at both swap sites (after a successful rebuild only;
rollback/exhaustion paths keep the latch). 4 tests.
* docs: document HERMES_STREAM_STALE_GIVEUP alongside sibling stream knobs
* fix: widen stale circuit breaker to non-streaming path + all provider-swap resets
Review findings on the salvaged #60332 breaker, fixed as follow-ups:
- restore_primary_runtime() now resets the streak (third provider-swap
path; without it a recovered primary was short-circuited before a
single attempt and could never be re-proven healthy except via /model).
- interruptible_api_call (non-streaming) now carries the same breaker
(guard at entry, bump on stale_call_kill, reset on success). Quiet-mode
/ subagent / headless sessions — the profile most like #58962's
unattended 494-failure session — take this path and had the identical
infinite stale-retry class.
- Partial-stream stub return now resets the streak (chunks were received,
provider demonstrably responsive).
- Consolidated the triple-duplicated counter arithmetic into shared
helpers (_stale_streak/_bump_stale_streak/_reset_stale_streak/
_check_stale_giveup) with one canonical comment block; error message
now says 'consecutive stale attempts' (the counter counts kills, not
turns — a single turn can produce several).
4 new tests (restore resets / no-op restore keeps latch / non-streaming
short-circuit / non-streaming success reset).
* fix(bedrock): route non-Claude auxiliary models through Converse API
Auxiliary Bedrock resolution always used the Anthropic Bedrock SDK, which
only works for Claude foundation-model IDs. Non-Claude models such as
openai.gpt-oss-20b-1:0 now use a Bedrock Converse adapter, matching the
main agent's bedrock_converse transport.
* test(bedrock): cover auxiliary Converse routing for non-Claude models
Assert gpt-oss Bedrock IDs resolve to BedrockAuxiliaryClient while Claude
IDs keep the Anthropic SDK path, including async mode.
* fix: normalize string stop + surface dropped stream/tool_choice in Converse shim
Review findings on the salvaged shim: (a) OpenAI callers may pass stop as
a bare string but Converse's stopSequences requires a list — normalize;
(b) call_llm(stream=True) (MoA aggregator) can reach this client and the
shim silently returned a complete response — keep that behavior (the
streaming consumer's got-final-object path downgrades gracefully) but log
it, and log dropped tool_choice, instead of silently ignoring both.
+2 regression tests.
Follow-up to the salvage of #60217 by @xxxigm.
* feat(mem0): self-hosted dashboard backend + recall tuning (salvage #55614)
Salvage of #55614 by @kartik-mem0 (mem0 maintainer). Adds a SelfHostedBackend
that talks to a self-hosted Mem0 Docker server over httpx (X-API-Key auth,
/search + /memories routes), gated behind `host`. Also folds in the mem0
research-team recall tuning that rides with it: rerank defaults to false across
all modes, the mem0_list tool is removed (5->4 tools), search guidance is
de-shouted, and self-hosted get_all reports the true stored total (#52921).
Supersedes the self-hosted portion of #52487 (@liuhao1024, first-submitted).
Closes #52478
Fixes #52921
* fix(mem0): make prompt label + platform setup honor host routing precedence
Follow-up on the salvaged #55614. The PR added host-based routing to
_create_backend (precedence: oss > host > platform) but two sibling surfaces
didn't mirror it:
- system_prompt_block() checked host before oss, so an oss+host config ran
OSS but told the model it was self-hosted HTTP. Reordered to match routing.
- Platform setup (hermes memory setup mem0 --mode platform) left a stale host
in mem0.json; since host beats platform, the user kept routing to the
self-hosted server. save_config merges (no delete), so clear host to ""
rather than pop() so the merge actually overwrites it.
Adds regression tests for both (mutation-checked).
* fix(mem0): prune dead get_all, wire rerank config default, warn on MEM0_HOST env override
Review follow-ups on the salvage:
- get_all() pruned from the ABC and all three backends: mem0_list (its
only caller) was removed by the recall-tuning commit, leaving new,
tested, unreachable code — including SelfHostedBackend's _MAX_TOP_K
over-fetch workaround. Tests for it dropped; fake-class stubs remain
harmlessly. (The #52921 true-total fix lives on in the PR history if
a lister ever returns.)
- The persisted rerank config key was write-only (setup prompted for it,
nothing read it). initialize() now parses it into _rerank_default and
mem0_search uses it when the model doesn't pass rerank explicitly;
per-call args still win. Guard test added.
- Platform-mode setup now warns when MEM0_HOST is set in the environment:
the json host-clear can't help there (_load_config seeds host from the
env var, docs tell users to put it in .env) — the user would silently
keep routing to the self-hosted server.
- SelfHostedBackend: connect-level retries (httpx.HTTPTransport(retries=2))
so a single transient blip doesn't count toward the provider breaker;
transport now injectable and the test helper uses the real __init__
instead of mirroring it via __new__.
- plugin.yaml description no longer leads with reranking (off by default,
platform-only); docs em-dash typo fixed.
* feat: add prompt-only session export
* feat(cli): add standalone HTML session export with sidebar navigation
Implements a professional, standalone HTML export feature for Hermes sessions.
Key changes:
- Adds 'hermes sessions export <file>.html' support to the CLI.
- Implements a dark-mode-first, responsive HTML generator in 'hermes_cli/session_export_html.py'.
- Single session export features a focused, centered 90% width layout.
- Multi-session export adds a fixed sidebar with session switching and real-time search filtering.
- ZERO external dependencies; all styles and JS are embedded for offline portability.
* feat(cli): include system prompts in HTML export
* feat(cli): redesign system prompt display as dedicated header section
* feat(cli): expand system prompt by default in HTML export
* fix(cli): fix layout width bug and ensure system prompt header is used
* style(export): restore width: 0 for multi-session flex layout
* feat(cli): filter internal session_meta messages from HTML export
* feat(sessions): wire html + prompt-only formats into 'sessions export'
Salvage follow-up integrating PR #30481 (@simplast) and PR #57683
(@catbearlove1-lang) into the unified export surface:
- --format html: standalone self-contained HTML transcript (single
session or multi-session with sidebar), works with all shared filters
and --redact; requires a file output path.
- --only user-prompts: prompt-only export (jsonl records or md sections)
via the shared session_export renderer; the separate export-prompts
subcommand from the original PR is subsumed by this flag.
- AUTHOR_MAP entries for both contributors; docs EN + zh-Hans.
* feat(discord): optionally mention approval owners on exec prompts
Opt-in discord.approval_mentions (config.yaml, bridged to
DISCORD_APPROVAL_MENTIONS) prepends <@id> mentions for numeric
allowlist entries to exec-approval prompts, with a scoped
AllowedMentions override (users only). Default off - no surprise
pings. Reapplied onto the content-mirror layout from #60245: mentions
prepend to the visible content block and its truncation budget.
Original implementation from PR #39719; commits arrived bot-authored,
re-attributed to the contributor.
* chore: add alex107ivanov to AUTHOR_MAP
* fix: restore cli-config.yaml.example from main (stale-branch version leaked into salvage)
* fix(web-server): close OAuth token TOCTOU by writing 0o600 atomically
`_save_anthropic_oauth_creds` wrote the Anthropic OAuth token file with
`os.replace(tmp, path)` followed by a post-hoc `chmod(0o600)`. Between the
rename and the chmod the token file existed at the default umask (0o644 on most
hosts) — a window in which another local user could read the access/refresh
tokens.
Write via `utils.atomic_json_write(..., mode=0o600)`, which creates the temp
with mode 0o600 *before* any content is written, fsyncs, atomically replaces,
preserves the existing file's owner, and cleans up its temp on failure. This
matches the `atomic_json_write(mode=0o600)` call already used elsewhere in this
module for the credential-pool write, and #56644's owner preservation.
Tests updated for the new mechanism, plus a check that the write goes through
`atomic_json_write(mode=0o600)` (mutation-verified).
* feat(mem0): add self-hosted mode to the setup wizard
The salvaged SelfHostedBackend made self-hosted servers reachable via
mem0.json / MEM0_HOST, but the setup wizard still offered only Platform
and OSS — exactly the gap users hit (Discord report: 'At memory setup
there's only 2 options'). Adds a third wizard mode:
- interactive picker: Platform / Self-hosted server / Open Source
- non-interactive: hermes memory setup mem0 --mode selfhosted
--host http://... [--api-key ...] [--dry-run]
- host -> mem0.json (behavioral), API key -> .env as MEM0_API_KEY
(secret), optional key for AUTH_DISABLED servers
- best-effort reachability check against the server, non-fatal
- README + memory-providers docs updated with the wizard path
* feat(sessions): trace export + HF upload via 'sessions export --format trace' (#60507)
* feat(trace): upload sessions to HF Agent Trace Viewer
Salvage trace upload as a smaller CLI-first feature: deterministic Claude Code JSONL export, fail-closed redaction, lazy Hugging Face dependency, and no gateway slash-command wiring.
* chore(trace): drop external porting references from docstrings
Describe the trace-upload design in Hermes' own terms.
* feat(sessions): fold trace upload into 'sessions export --format trace'
Integrates the HF Agent Trace Viewer exporter (PR #36145) onto the
unified export surface instead of a separate 'hermes trace' subcommand:
- --format trace: Claude Code JSONL to stdout/file, or one
<id>.trace.jsonl per session for filtered bulk export; defaults to
the most recent session when no --session-id/filters given.
- --upload pushes to the user's private HF traces dataset (--public to
opt out of private); reads HF_TOKEN with guided setup when missing.
- traces are secret-redacted by default (force mode); --no-redact opts
out after review; redaction failure blocks export (fail closed).
- hermes_cli/trace.py + subcommands/trace.py removed; agent/trace_upload.py
is the single engine. Docs EN + zh-Hans; 4 new CLI tests.
* fix: limit desktop model pickers to explicit providers
* chore: map Ronald contributor email
* fix: harden explicit-provider gate for stale env-seeded pool entries + non-desktop picker opt-ins
Follow-up on the #56966 salvage:
- is_provider_explicitly_configured(): an env-seeded credential-pool entry
only counts as explicit while its env var still resolves to a usable
secret. A stale auth.json entry left behind after the user deletes the
var no longer keeps the provider in the picker forever (#55790).
- TUI modelPicker + dashboard ModelPickerDialog/api.getModelOptions pass
include_unconfigured=true explicitly, preserving their full-universe
setup-affordance behavior now that the backend defaults to the
configured subset.
- desktop lib/model-options.ts routes explicit_only through the shared
requestModelOptions() helper (added on main after the PR branched).
- regression tests for ambient (gh_cli) pool sources, explicit manual/
device-code sources, and stale vs live env-seeded entries.
* feat(plugins): pass approve rule keys to approval gate
* fix(approval): wire gateway notify round-trip into the plugin escalation gate
_run_approval_gate's gateway branch only queued via submit_pending, so
plugin-escalated approvals never sent the interactive embed+buttons on
Discord/Telegram/Slack (#59413) - the user was never notified and the
action stayed silently blocked. Mirror check_dangerous_command's path:
when a session notify callback is registered, run the blocking
_await_gateway_decision round-trip (redacted payload, once/session/
always persistence, deny/timeout produce definitive BLOCKED outcomes);
fall back to submit_pending only when no callback exists.
Fixes #59413.
* chore: add doncazper to AUTHOR_MAP
* feat(pty): RingBuffer for keep-alive output capture
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(pty): PtySession drain/attach/detach with EOF close 4410
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(pty): PtySessionRegistry with reap + capacity
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(chat): reattach /api/pty sessions via ?attach= token
Keep-alive path when ?attach=<token> is present: PTY outlives the socket
via PTY_REGISTRY, reattaches on reconnect. No token = unchanged legacy
pump (_legacy_pump). detach (not close) on disconnect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(pty): periodic reaper wired into dashboard lifespan
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(chat): persist attach token, reconnect on transient close
ChatPage sends ?attach=<localStorage token> so /chat reattaches to its
live PTY across refresh. onclose: 4410=process-exit (session ended),
4409=superseded (quiet), else transient -> auto-reconnect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(mcp): reap orphaned subprocesses before spawning new ones on retry
When an MCP stdio subprocess fails to connect (token expiry, port
contention, timeout), the run() reconnect loop retries with backoff.
Each retry calls _run_stdio() which spawns a new process pair, but the
previous failed pair was only detected as orphaned (added to
_orphan_stdio_pids) — never actually killed. This caused rapid zombie
accumulation: 5 failed attempts × 2 procs each = 10 orphans competing
for the same port.
Add a _kill_orphaned_mcp_children() call at the top of _run_stdio(),
before the _snapshot_child_pids() baseline, so any orphans from prior
failed attempts are reaped before a new subprocess is spawned.
Fixes #57355
* fix(mcp): reap stdio orphans before reconnect
* fix(mcp): unify reconnect orphan reaping + move off the event loop
Merge the two cherry-picked reap call sites into one unscoped sweep at
the top of _run_stdio (the unscoped sweep is a superset of the
per-server one), and run it via asyncio.to_thread so the 2s
SIGTERM->SIGKILL escalation cannot stall the shared MCP event loop.
* fix(mcp): bound stdio initialize handshake to stop subprocess/FD leak
A stdio MCP server that never completes `initialize` (e.g. emits a
non-JSON-RPC frame and then blocks on stdin) leaks a child process plus its
stdio pipes/pidfd on every discovery-retry cycle — unbounded, until the
gateway hits EMFILE and every new open()/spawn fails (#59349).
Root cause (confirmed by instrumenting the live repro, and different from the
issue's own hypothesis): the spawned child IS captured in `new_pids`, so the
report's "new_pids empty at finally" guess is not it. The real cause is that
`session.initialize()` hangs forever on the garbage stream. `connect_timeout`
only bounds the caller's `.result()` wait on the foreground thread — it does
NOT cancel the `_run_stdio` coroutine on the background MCP loop. So the
coroutine is stuck at `await session.initialize()` permanently, its cleanup
`finally` never runs, the child is never reaped, and it stays invisible to the
orphan-reaper (whose `_orphan_stdio_pids` set never gets populated).
Fix: wrap `session.initialize()` in `asyncio.wait_for(..., connect_timeout)`
so a stalled handshake fails instead of hanging. The TimeoutError unwinds
through the SDK context managers (closing the child's stdin -> EOF -> exit)
and lets the existing `finally` reap any straggler. Cross-platform — no
signals/pgid/proc.
Scope: stdio only. The HTTP path has the same `await session.initialize()`
shape but spawns no subprocess (so it can't cause this leak) and already has
httpx transport timeouts.
Verified: the reporter's repro goes from unbounded growth to draining to zero;
added a hermetic regression test (fake transport whose `initialize()` hangs,
asserts the connect is bounded by connect_timeout) that fails on the pre-fix
code and passes on the fix; 566 existing MCP tests pass; ruff clean.
Repro confirmed on macOS (pipe FDs); the Linux-specific pidfd growth in the
report should be equivalent — the reporter offered to validate on Linux.
Closes #59349
* fix(mcp): widen #59349 handshake bound to HTTP transports + cancel abandoned start() task
Sibling sites of the same bug class as the salvaged stdio fix:
- SSE, streamable-HTTP (new + deprecated API) initialize() calls are now
bounded by the same connect_timeout, so an endpoint that accepts the
connection but never answers the handshake cannot park the run() task
forever.
- start() now cancels its ensure_future'd run() task when the caller's
connect timeout cancels start() itself — the orphaned-task leak was
the root mechanism behind #59349, and this closes the class for any
future pre-ready hang.
* fix(mcp): reap orphaned stdio MCP children on ungraceful parent death
A stdio MCP server (e.g. `npx -y mcp-remote <url>`) is spawned as a direct
child of the Hermes process. Existing teardown (MCPServerTask.shutdown() /
_kill_orphaned_mcp_children()) reaps it correctly on a clean exit, but a
kill -9 / crash / force-quit of the Hermes process skips that path entirely
-- the child (and its own descendants, e.g. mcp-remote's spawned node
process) is orphaned and keeps running. Repeated ungraceful restarts pile up
N orphaned processes racing to hold the same upstream SSE session, producing
errors like 'Invalid request parameters' on legitimate reconnects.
macOS/Linux have no portable equivalent of prctl(PR_SET_PDEATHSIG) at the
Python subprocess level, so this adds a thin supervisor
(tools/mcp_stdio_watchdog.py) that:
- execs the real command as its own child in its own process group
- passes stdin/stdout/stderr through untouched (MCP stdio protocol
talks directly over those streams)
- polls the original spawning PID with the same orphan-detection
algorithm already proven in tui_gateway/slash_worker.py (ppid
comparison + psutil creation-time guard against PID reuse)
- SIGTERM-then-SIGKILL's the child's process group the moment the
original parent is gone
Wired into _run_stdio via a new _wrap_command_with_watchdog() helper,
POSIX-only (matches the existing killpg-based cleanup's platform scope),
fails open (any error resolving pid/create-time falls back to the
unwrapped command) so this can never be the reason a working MCP server
stops starting.
Verified: reproduced the exact orphan scenario standalone (fake parent
process spawns watchdog + fake long-running MCP child, kill -9 the fake
parent, confirm the watchdog reaps the child within its poll window with
zero leaked processes). Updated test_mcp_tool_issue_948.py's resolved-path
assertion to check the watchdog-wrapped command instead of the raw
resolved binary. Full test_mcp_tool.py + test_mcp_stability.py +
test_mcp_tool_issue_948.py suite: 232 passed. Full -k mcp sweep across the
whole test tree: 1003 passed, 2 skipped, 0 failed.
* fix(mcp): watchdog wrap after OSV preflight + forward SIGTERM to child group
Two fixes on top of the salvaged parent-death watchdog:
- Apply the watchdog wrap AFTER the OSV malware preflight so the check
inspects the real npx/uvx package instead of the python wrapper
(the wrap previously made the preflight a silent no-op for every
stdio server).
- The real server runs in its own process group under the watchdog, so
the graceful-shutdown killpg no longer reached it; the watchdog now
forwards SIGTERM/SIGINT to the child's group, keeping wedged servers
killable on clean shutdown.
* Recycle idle MCP stdio servers
* Handle minimal MCP server fakes
* chore(release): map rainbowgore + thestudionorth in AUTHOR_MAP for MCP leak salvages
* docs(mcp): document idle_timeout_seconds / max_lifetime_seconds recycle keys + handshake-bound note
* test(mcp): unblock recycle-reconnect test from the parked self-probe wait
The salvaged test predates the parked-server self-probe
(_PARKED_RETRY_INTERVAL, landed on main after the PR branched): after the
final failed retry, run() parks in a real asyncio.wait that the patched
asyncio.sleep doesn't cover, stalling the test 300s. Signal shutdown once
the retry budget is exhausted so the park exits immediately.
* fix(mcp): guard POSIX-only kill primitives in stdio watchdog for the Windows footgun linter
signal.SIGKILL / os.killpg don't exist on Windows. The watchdog is only
spawned on POSIX (wrap site gates on os.name), but guard via getattr with
a plain terminate/kill fallback so an accidental Windows import can't
AttributeError.
* feat(dashboard): report profile + gateway topology in /api/status (#60537)
/api/status (loopback/insecure binds only) now includes:
- profiles: every profile on the host (default + named)
- gateway_mode: none | single | multiple | multiplex
- gateways: one entry per live gateway with the host ports its
port-binding platforms listen on, plus served_profiles when the
default gateway is multiplexing
Ports resolve from each profile's config.yaml (top-level platforms:
wins over gateway.platforms:, matching load_gateway_config precedence)
with adapter defaults as fallback. Topology enumeration runs in an
executor so the profile scan + process-table probes stay off the event
loop, and the whole block is gated behind the same loopback-only split
as hermes_home/gateway_pid so gated binds leak nothing new.
* fix(tools): enable platform-native toolsets when their composite is explicitly configured (#35527)
When a user explicitly configures a platform with its native composite
(e.g. platform_toolsets.discord: [hermes-discord]), the discord and
discord_admin toolsets were silently stripped by _DEFAULT_OFF_TOOLSETS
even though the composite contains those tools. The strip could not tell
an explicit composite opt-in apart from the unconfigured default.
Track whether the platform was explicitly configured and, when it was,
exempt toolsets that are both default-off and platform-restricted to the
current platform from the strip. Only discord/discord_admin are affected
(the sole entries in both _DEFAULT_OFF_TOOLSETS and
_TOOLSET_PLATFORM_RESTRICTIONS). Unconfigured and empty-list platforms
keep the security default-off behaviour.
* docs(sessions): unify export docs under one overview section (#60554)
Restructures the five parallel export sections into a single 'Export
Sessions' section: a format table (jsonl/md/qmd/html/trace + --only
user-prompts), one shared-filters paragraph covering all formats, and
per-format subsections nested beneath. EN + zh-Hans.
* fix(discord): honor pairing grants for message auth
* fix(discord): explain fail-closed allowlist default
Log a one-shot structured warning when Discord denies traffic because
no allowlist/policy is configured, and correct the setup wizard's
inverted warning text. The fail-closed default itself is unchanged.
Fixes #58682.
* docs(discord): troubleshoot silent fail-closed denials
Docs portion of PR #57067: 'bot connects but never replies' section
pointing at the gateway.log warning and the allowlist/policy knobs.
Co-authored-by: ooovenenoso <120500656+ooovenenoso@users.noreply.github.com>
* fix(gateway): only session-discover channel targets for connected platforms (#60574)
Session-based channel discovery resurrected historical origins for
platforms with no connected adapter, exposing stale send_message
targets that can no longer deliver. Gate both the enum loop and the
plugin-registry loop on the live adapter set.
Surgical reapply of the channel-directory portion of PR #25959 (branch
was 6.5k commits stale; the text-batching delay changes bundled there
were dropped - separate concern, defaults have since been retuned on
main).
Co-authored-by: Marco-Olivier Lavoie <marcolivier@gmail.com>
* Fix delegation config precedence
* Use read-only config loader and honor HERMES_IGNORE_USER_CONFIG in delegation config
* fix(dashboard): advertise truecolor to the embedded chat TUI (#60576)
Headless/hosted deploys run the dashboard server without COLORTERM in
the process environment, so chalk inside the PTY-spawned TUI child
downgraded every skin hex color to the xterm 256 palette — the default
skin's bronze banner border (#CD7F32) snapped to palette 173 (#D7875F,
salmon red) and the gold caduceus rendered red/yellow on fresh cloud
instances. Local launches never reproduced it because the operator's
interactive terminal leaks COLORTERM=truecolor into the server env.
xterm.js always renders 24-bit RGB, so the dashboard PTY child should
always advertise truecolor: backfill COLORTERM=truecolor in
_resolve_chat_argv via setdefault (an explicit operator value wins).
Verified with a clean-env PTY probe of the real TUI binary:
no COLORTERM -> 0 truecolor SGRs / 165 palette-256 (salmon 38;5;173);
with the backfill -> 166 truecolor SGRs, exact bronze 38;2;205;127;50.
* feat(dashboard): expose profile names + gateway_mode on gated /api/status (#60585)
The profile+gateway topology added in #60537 sits entirely behind the
loopback/--insecure auth gate. But a hosted agent (Hermes Cloud) binds
non-loopback with OAuth, so should_require_auth is True, and NAS reads
/api/status over the network (fly-provider.ts getInstanceRuntimeStatus)
with no session token. On that gated path the whole topology block was
omitted, so the Portal could never render the profile list.
Split the topology readout by sensitivity:
- profile NAMES (profiles) + gateway_mode are low-sensitivity product
surface and now ride the always-public status body, surviving the auth
gate so NAS/the Portal can enumerate profiles.
- the per-gateway detail (gateways[], carrying host ports) is deployment
recon and stays gated alongside hermes_home / config_path / env_path /
gateway_pid / gateway_health_url.
The collector now runs unconditionally (still in the executor, off the
event loop). No new fields; only the gate placement changes.
* feat(relay): carry routed profile from the connector wire source (#60586)
The multiplex machinery already routes an inbound message to a profile via
SessionSource.profile (build_session_key namespacing + the per-turn
config/credential scope in SessionStore._resolve_profile_for_key). But the
relay path never populated it: _event_from_wire rebuilt the SessionSource
field-by-field and dropped any 'profile' the connector sent, so a
Team-Gateway (connector + relay) message could not be routed to a specific
profile the way the /p/<profile>/ HTTP prefix and per-credential polling
adapters already can.
Stamp source.profile from the wire payload in _event_from_wire. This is the
last missing link for NAS-driven per-profile routing over the relay in
multiplex mode; the connector populating the field ships separately
(gateway-gateway contract adds the optional wire field).
Back-compat: absent 'profile' → None → legacy agent:main namespace,
byte-identical to today for every single-profile gateway.
* Add dashboard memory provider switching
* fix: validate memory provider names before filesystem lookup and setup commands
Strict charset allowlist (alnum + - _, max 64) on the {name} path param of
the memory-provider config/setup endpoints. Prevents traversal-shaped names
from reaching find_provider_dir(), and setup now 404s when neither a
loadable provider nor a plugin manifest exists, so the command-running path
is only reachable for discoverable plugins. Adds regression tests.
* feat(gateway): GATEWAY_MULTIPLEX_PROFILES env override for multiplex flag (#60589)
The connector now depends on the single multiplexed gateway for per-profile
relay routing, so hosted deployments need to FORCE multiplexing on regardless
of the image's config.yaml. gateway.multiplex_profiles was config.yaml-only,
which a user could leave unset or flip off.
Add GATEWAY_MULTIPLEX_PROFILES as a standard operator override on top of the
existing config key — the same 'config.yaml is canonical, env is the operator
override' pattern the Telegram/Signal require_mention bridges use:
env (recognized token) > config.yaml (top-level or nested gateway.*) > False
- gateway/config.py: _env_multiplex_profiles_override() resolves the env var
tri-state — recognized truthy/falsy token → bool; unset/blank/unrecognized
→ None (fall through to config). Blank is deliberately None, not False, so a
provisioned-but-unpopulated Fly secret ('') can't shadow a config.yaml opt-in
(the empty-secret trap). Wired into GatewayConfig.from_dict so every consumer
(run.py, session.py via self.config) sees the resolved value.
- hermes_cli/gateway.py: the named-profile-start guard
(_guard_named_profile_under_multiplexer) reads config.yaml directly, so it
gets the SAME env precedence — otherwise env-forced multiplex would leave the
guard blind and someone could start a conflicting per-profile gateway that
double-binds a bot token. Env-forced-on trips the guard even with no
config.yaml key; env-forced-off disables it over a config opt-in.
Tests: full 3-tier precedence in test_config.py (incl. the discriminating
env-overrides-config cases + the empty/whitespace/unrecognized fall-through
trap + resolver tri-state), mutation-verified (flipping precedence fails
exactly the two env-wins tests); guard env cases in test_multiplex_lifecycle.py.
Force-on is safe on a single-profile instance: session keys stay byte-identical
(agent:main) and the _run_agent wrapper installs the per-turn secret scope, so
the fail-closed get_secret() path is satisfied.
* Fix dashboard chat model profile scoping
* fix: pass profile-scoped SessionDB to _session_latest_descendant in dashboard chat PTY resume
The chat PTY launch path landed on main after PR #50558 and still called
_session_latest_descendant() with the old one-arg signature. Open the
requested profile's state DB (matching the REST endpoint) so profile-scoped
resume resolves descendants in the right database.
* Add WhatsApp dashboard pairing flow
* chore: release v0.18.1 (2026.7.7) (#60595)
* fix(whatsapp): unpin Baileys from git commit, use published 7.0.0-rc13 (#60643)
The April 2026 pin to WhiskeySockets/Baileys#01047deb existed only to
pick up the abprops bad-request fix (Baileys PR #2473) before it was
released. That fix shipped in v7.0.0-rc11 (May 2026); our pinned commit
is now 48 commits behind rc13.
The git pin forced npm to clone the repo and compile Baileys from
TypeScript source on every fresh install (~3 min), which blew past the
dashboard pairing flow's timeout. Registry install takes ~3s.
Validation: all 9 bridge.js imports present in rc13, bridge.native.test.mjs
passes (13/13), live bridge boot renders pairing QR against real WA servers.
* chore: release v0.18.2 (2026.7.7.2) (#60651)
* feat(install): warn pip/Homebrew installs are unsupported (CLI, TUI, desktop) (#57225)
* feat(install): warn pip/Homebrew installs are unsupported (CLI, TUI, desktop)
pip and Homebrew are now Unsupported install methods per
website/docs/getting-started/platform-support.md. Surface a
warn-don't-block deprecation notice everywhere the install method is
already shown, pointing at the platform-support docs and noting these
installs will not receive further updates. NixOS (Tier 2) is untouched.
- hermes_cli/config.py: shared is_unsupported_install_method() /
format_unsupported_install_warning() helpers so the wording and docs
link stay consistent across every surface.
- hermes_cli/banner.py: generalize the existing pip-only banner
warning to also cover Homebrew.
- hermes_cli/main.py: hermes update and hermes update --check print
the warning before proceeding (still update; warn, don't block).
- tui_gateway/server.py: session.info gains install_warning.
- ui-tui: SessionPanel renders install_warning alongside the existing
'N commits behind' notice.
- apps/desktop: SessionRuntimeInfo/GatewayEventPayload gain
install_warning; applyRuntimeInfo + the live session.info event fire
a snoozable warning toast via a new reportInstallMethodWarning(),
mirroring the existing backend-contract-skew toast pattern. i18n
strings added for en/zh/zh-hant/ja.
- Tests: updated pip banner assertions for the new wording, added a
Homebrew banner test, and two tui_gateway session_info tests
(install_warning present for pip, absent for git).
* fix(nix): make `hermes` in developement environment actually work
install modules as editable overlay with uv
* feat: print install method when running --version
* fix: correct detect install method when running from a subtree
* Fail closed on invalid JSON/YAML/TOML writes instead of writing then reporting
write_file() previously called _atomic_write() first and only ran the
JSON/YAML/TOML/Python syntax check afterward as an informational lint
delta -- a parse failure never set the top-level `error` key, so a
corrupt structured-data write still landed on disk (and file_tools.py's
files_modified gating, which keys off `error`, silently reported it as
a successful modification).
Move the in-process syntax check for JSON/YAML/TOML ahead of
_atomic_write() and refuse the write outright on a parse failure: no
temp file, no rename, nothing touches disk, and the result carries a
top-level `error` so callers correctly see it as unmodified.
Deliberately scoped to _FAIL_CLOSED_INPROC_EXTS (JSON/YAML/TOML), not
all of LINTERS_INPROC -- .py is excluded because this codebase's own
test fixtures (TestPatchReplacePostWriteVerification et al.) write
arbitrary non-Python text through *.py paths purely to exercise
write-mechanics; a hard block there broke 3 previously-passing tests
during development. Python keeps its pre-existing non-blocking
lint-delta report.
Adds tests/tools/test_write_file_syntax_gate.py: invalid JSON/YAML/YML/
TOML refused with nothing written (new file) and nothing modified
(existing file); valid JSON/YAML still written byte-for-byte; a
non-linted extension with garbage content is unaffected; invalid Python
is confirmed NOT hard-refused (still just reported).
* fix(tools): make the YAML write gate syntax-only so multi-doc/tagged YAML isn't refused
safe_load() raises ComposerError on multi-document streams (k8s manifests)
and ConstructorError on application-defined tags (CloudFormation !Sub,
Ansible !vault) — both valid YAML syntax. Now that the linter's verdict is
a fail-closed write gate, those false positives would refuse legitimate
writes outright. Switch to yaml.parse() (scanner+parser only), which still
catches real syntax failures.
* chore: add AUTHOR_MAP entry for neoguyverx (PR #60526 salvage)
* fix(gateway): drain in-flight cron jobs before shutdown tool kill
/update and other shutdown paths only waited on gateway session agents,
so active cron tool work was killed immediately in final-cleanup while
the scheduler could still mark the job successful (#60432).
* test(gateway): cover cron drain during gateway shutdown (#60432)
* fix(gateway,cron): make shutdown drain visible to in-flight cron work
Cron jobs run through cron/scheduler.py's own ThreadPoolExecutor via a
standalone AIAgent (run_job/run_one_job), entirely outside
GatewayRunner._running_agents -- the dict _drain_active_agents() and
every other active-work check on that class reads. A gateway shutdown
(/update, /restart, and SIGUSR1 all funnel through the same stop())
could log active_at_start=0 and immediately kill tool subprocesses
while a cron job's terminal command was still running, with no wait
and no indication anything was interrupted.
Real-world impact (from the issue): a scheduled daily briefing cron
job was in flight during /update, its tool subprocess got killed
by the unconditional shutdown cleanup, and the job was never marked
failed -- it simply never completed or delivered, with no error
surfaced anywhere. A repro with a 30-minute `sleep` cron job in flight
during /update reproduced the same pattern: subprocess killed at
+0.22s of drain (active_at_start=0), the job's agent thread continued
in-process and produced a plausible-looking final response from the
truncated tool output, and the scheduler marked the run successful.
Root cause is layered, not a single line:
1. GatewayRunner._drain_active_agents() only waits on _running_agents.
Cron work was invisible to it, so drain returned instantly whenever
the only active work was a cron job.
2. Even with visibility, the shutdown's final tool-subprocess kill
(process_registry.kill_all()) is a global, unconditional sweep with
no per-job targeting -- a long-running cron job that outlives the
drain timeout still gets its subprocess killed.
3. cron/scheduler.py had no way to detect that a job's tool subprocess
was killed out from under it mid-run; the agent thread kept going
and its eventual (often degraded but plausible-looking) response
got reported as a normal successful completion.
Fix, three parts:
- cron/scheduler.py: expose get_running_job_ids() (thread-safe
snapshot of the existing _running_job_ids set, already used to
prevent double-dispatch) so the gateway can read cron's in-flight
state without reaching into private module internals.
- gateway/run.py: GatewayRunner._active_cron_job_count() reads that
snapshot. _drain_active_agents() now waits on
(_running_agents OR active cron jobs), so a cron-only workload gets
the same bounded wait chat sessions already get instead of an
instant active_at_start=0. Shutdown drain logging gains
cron_active_at_start/cron_active_now fields alongside the existing
ones (unchanged, for compat).
- cron/scheduler.py: mark_running_jobs_interrupted(reason), called by
gateway/run.py's _kill_tool_subprocesses() right after
process_registry.kill_all(), marks every job still in
_running_job_ids at that instant as failed/interrupted via the
existing mark_job_run() -- and records the job IDs in
_interrupted_job_ids BEFORE writing, so run_one_job()'s own
eventual completion for the same run (racing in its own thread)
checks that flag and skips its normal write instead of clobbering
the interrupted status with a false "ok" produced from the
now-truncated tool output. This does not attempt to correlate a
killed PID to a specific job ID (process_registry tracks PIDs, not
job IDs) -- any job still dispatched at the moment of a forced kill
is treated as interrupted, matching the existing coarser precedent
set by _interrupt_running_agents(), which interrupts every entry in
_running_agents on a drain timeout without per-agent correlation
either.
Deliberately out of scope (flagged in the issue as a separate,
lower-priority concern): startup-time reconciliation of cron runs that
started but never reached a terminal status.
Testing:
- tests/cron/test_shutdown_interrupt.py (12 tests): get_running_job_ids
snapshot semantics, mark_running_jobs_interrupted marking/no-op/
partial-failure behavior, and -- the core race guard -- run_one_job
skipping its own last_status write (both the success path and the
exception path) when the shutdown path already marked the run
interrupted, with a control test proving ordinary un-interrupted
completions are unaffected.
- tests/gateway/test_cron_active_work_drain.py (9 tests):
_active_cron_job_count reading cron state and failing closed (0) if
the cron module is unavailable; _drain_active_agents waiting for an
in-flight cron job the same way it waits for chat sessions, timing
out if the job outruns the window, and leaving existing chat-session
drain behavior unchanged; a full runner.stop() integration test
(drain-timeout path) proving mark_running_jobs_interrupted actually
fires with the right job ID when a tool subprocess is force-killed,
plus a no-op control when nothing cron-related is in flight.
- tests/gateway/test_shutdown_cache_cleanup.py: added
_active_cron_job_count() to that file's hand-rolled _FakeGateway test
double, which stop() now calls -- without it those 8 pre-existing
tests AttributeError (caught by fail-then-pass below, not a
production bug).
Fail-then-pass: reverted gateway/run.py + cron/scheduler.py, all 21
new tests fail (fixture/attribute errors -- the feature doesn't exist
yet); restored, all 21 pass.
Regression check: ran the full plausibly-affected surface --
tests/gateway/{test_gateway_shutdown,test_restart_drain,
test_restart_notification,test_restart_redelivery_dedup,
test_restart_resume_pending,test_restart_service_detection,
test_shutdown_cache_cleanup,test_stuck_loop,test_clean_shutdown_marker,
test_external_drain_control,test_session_state_cleanup,
test_update_command,test_update_streaming}.py plus tests/cron/ (944
tests) -- against a clean upstream/main checkout and against this
branch. Diffed the two FAILED lists: identical, 20 pre-existing
failures on both sides (Windows-locale/cp1252 file-encoding issues and
Unix-permission-bit assertions that don't apply on this Windows dev
box), zero new failures, zero fixed-by-accident. The 8
test_shutdown_cache_cleanup.py failures found mid-development were
from the _FakeGateway gap above, fixed in the same commit and
confirmed clean on the final rerun (diff against baseline: exit 0).
Fixes #60432
* fix(cron): stop interrupted jobs from delivering their pre-kill output
Follow-up to the previous commit on #60432. The status-write guard
(_consume_interrupted_flag, checked right before mark_job_run) closes
the false-success bookkeeping gap, but run_one_job delivers its result
BEFORE that check: delivery happens right after run_job() returns,
mark_job_run happens at the very end. A job whose tool subprocess was
killed mid-flight can still produce a plausible-looking final_response
from the truncated output, and that response would reach the user via
_deliver_result before the interrupted flag was ever consulted --
correct status in jobs.json, wrong message already sent.
Adds _is_interrupted(), a non-destructive peek at the same
_interrupted_job_ids set (_consume_interrupted_flag stays as the
consuming, authoritative check right before the status write -- this
needed a peek instead since the flag has to still be visible there).
Checked right after save_job_output, before the deliver_content
decision: if the run looked successful but was flagged interrupted,
force success=False with an explicit interruption message. This
routes delivery through the existing _summarize_cron_failure_for_delivery
path (the same one a real failure already uses) instead of the raw
final_response, so the user gets an honest "this run was interrupted"
instead of a truncated/misleading result.
Testing: 4 new tests in tests/cron/test_shutdown_interrupt.py --
_is_interrupted peek semantics (false/true/does-not-clear, as opposed
to the consuming _consume_interrupted_flag), and the delivery-gate
test itself, which mocks run_job to return a normal-looking success
with a "plausible final response" while the job is pre-marked
interrupted, and asserts _deliver_result receives the failure summary
("This run was interrupted.") instead, with the summarizer's error
argument confirmed to mention the interruption.
Fail-then-pass: reverted cron/scheduler.py only, the 4 new tests fail
(3 on the missing _is_interrupted attribute, 1 -- the delivery-gate
test -- on _summarize_cron_failure_for_delivery never being called,
i.e. the raw response would have gone out); restored, all 16 tests in
the file pass.
Regression: tests/cron/ (683 tests) + test_cron_active_work_drain.py +
test_gateway_shutdown.py + test_shutdown_cache_cleanup.py -- 11
pre-existing failures (Unix file-permission-bit and path-tilde
assertions that don't apply on this Windows dev box), matching the
same set already established as pre-existing in the prior commit's
regression check. Zero new failures.
Continues #60432
* fix(gateway,cron): reconcile #60612 + #60631 onto one drain surface
Keep #60631's get_running_job_ids() snapshot + _active_cron_job_count()
(import-guarded for minimal test doubles) as the single read path, and
retarget #60612's drain tests at it. Drops the redundant
cron_jobs_in_flight() helper so there is one surface, not two.
* fix(tui): prevent ws_orphan_reap from ending gateway-originated sessions
Guard _finalize_session's db.end_session() call against gateway-owned
sessions (telegram, bluebubbles, discord, etc.). The TUI is a viewer
for these sessions, not the lifecycle owner. Unconditionally ending
them in state.db creates a Groundhog Day routing loop: the gateway's
#54878 self-heal detects the stale entry, recovers to the parent
session, context compression splits back to the reaped child, and the
cycle repeats on every inbound message — causing complete conversational
context amnesia.
Fixes #60609
* fix(tui): derive gateway-owned sources from the Platform enum, not a hardcoded list
The salvaged guard used a hand-maintained frozenset of 14 platform names —
several of which (line, wechat, facebook, imessage, googlechat) aren't
actual Hermes Platform values, while real ones (whatsapp_cloud, feishu,
wecom, dingtalk, qqbot, yuanbao, plugin platforms like irc) were missing.
Resolve the source through gateway.config.Platform instead (built-ins +
registered plugin platforms via _missing_), with an explicit exclusion set
for self-owned/local sources. Adds tests for the guard and both reap paths.
* feat(gateway): generic OIDC client-credentials relay provisioning (NAS-free) (#60730)
For air-gapped / self-hosted-IdP deploys with NO Nous Portal, let the gateway
obtain its caller-identity bearer from a generic OAuth2 client_credentials grant
against the operator's own IdP (e.g. Microsoft Entra ID) instead of only
resolve_nous_access_token(). The connector's OIDC tenant resolver reads a claim
(default tid) off that token as the tenant.
- gateway/relay: new canonical _resolve_relay_identity_token() — client_credentials
when gateway.idp.token_url (or GATEWAY_RELAY_IDP_* env) is set, else Nous Portal
(unchanged default). Wired into self_provision_relay().
- hermes_cli/gateway_enroll: _resolve_identity_token() delegates to the canonical
resolver so the enroll CLI and the runtime self-provision path share ONE impl.
Config via gateway.idp.{token_url,client_id,client_secret,scope} in config.yaml
(env override GATEWAY_RELAY_IDP_*). No behaviour change when unset.
Tests: tests/gateway/relay/test_identity_token_resolver.py (6 — mode selection,
request shape, config/env precedence, fail-closed). Relay suite 162 pass.
Validated via the cross-repo gateway<->connector live E2E (provision, managed
self-provision, inbound round-trip, /link) against a connector running the OIDC
tenant resolver with zero NAS config.
* Fix slow Z.AI startup by caching auto-detected endpoint to disk
(cherry picked from commit 6ed884933a178d5540f02d80e3fe9e678ca844eb)
* chore: add veradim to AUTHOR_MAP for PR #41201 salvage
* fix: don't flip active_provider when caching Z.AI probe result
_save_provider_state() sets auth_store['active_provider'] as a side effect.
The Z.AI endpoint probe runs from credential-pool env seeding for any user
with a Z.AI key in env — persisting the probe cache must not silently make
zai the active provider. Use _store_provider_state(set_active=False).
Follow-up to PR #41201 salvage.
* fix: Z.AI endpoint persist failure must not break URL resolution
Review findings (hermes-pr-review Phase 2, 3-angle):
- _save_auth_store() does real filesystem I/O (mkdir, O_EXCL create, fsync,
atomic replace) and can raise on disk-full/permissions/lock-timeout. The
persist ran bare in the success path, so a persist failure aborted
_resolve_zai_base_url() after detection had already succeeded. Wrap the
persist in try/except: log a warning and still return the detected URL
(worst case: next start re-probes).
- Readability: stage the payload in a local detected_endpoint instead of
writing through the stale pre-lock 'state' dict, which is no longer what
gets persisted.
* fix(cron): stop the ticker from stalling forever on a wedged jobs lock (#60703) (#60855)
Three fixes for the silent post-restart ticker stall:
1. _jobs_lock() bounds its cross-process flock: LOCK_NB polled against a
30s deadline instead of an unbounded LOCK_EX taken while holding the
process-wide RLock. On timeout it logs at ERROR and degrades to
in-process-only locking (the existing fallback path), so a sibling
process wedged while holding .jobs.lock can no longer freeze every
cron function - including the ticker's get_due_jobs() and thus the
heartbeat - forever with zero logging.
2. fire_claim/run_claim freshness checks are bounded on both sides
(0 <= age < ttl): a claim stamped in the future (clock/TZ skew across
a restart) was previously fresh forever, making the job permanently
unfireable and every manual run report 'already being fired'.
3. _execute_job_now distinguishes paused/disabled/missing jobs from a
genuinely held claim instead of mislabeling them all as 'already
being fired'.
* fix(tui_gateway): back off notification poller when session is busy
The busy-session branch of _notification_poller_loop re-queued the
completion event and immediately re-polled it with no sleep, spinning
at full speed (100% CPU, ~1100 futex/s of GIL churn) for as long as
the session stayed running. This starved the dashboard asyncio loop:
/api/status went from 0.14s to 3-6s with 10s timeouts.
Sleep 0.25s outside history_lock before re-polling, mirroring the
0.1s back-off already used for foreign-session events.
* chore: add SiteupAgencia to AUTHOR_MAP for #57435 salvage
* test(tools): add unit tests for skill_gist
* fix(agent): tag desktop chat sessions as desktop
The desktop app's chat panel reuses tui_gateway as its backend, so every chat session was stamped platform="tui". That made the agent read terminal-specific platform guidance while running in the graphical desktop chat surface.
Resolve the misclassification at its source: tui_gateway now picks platform="desktop" when HERMES_DESKTOP=1 and HERMES_DESKTOP_TERMINAL is unset, and keeps platform="tui" for the embedded terminal pane and standalone TUI. Add a PLATFORM_HINTS["desktop"] entry describing the actual chat surface (full GFM markdown, MEDIA: intercept, inline images). Move the embedded-pane clarifier to the platform-hint resolution site so it appends only to the tui hint under HERMES_DESKTOP_TERMINAL=1. Delete the now-dead desktop-hint block from build_environment_hints() that competed with the platform hint.
Standalone TUI sessions produce byte-identical prompts as before; the new desktop hint and clarifier are assembled once per session in the stable tier, so prompt caching is preserved.
* fix(delegation): route async results to origin session
Carry the live TUI session id with async delegation completion events and prefer the commissioning UI session when desktop pollers share the completion queue. Resolve compressed session keys to their continuation before treating events as orphaned, and capture the live parent agent session id for TUI/ACP dispatch.
* fix(delegation): fail-closed orphan handling + session-scoped delegation lifecycle
Two invariants layered on the origin-routing commit (#55578):
1. Fail closed on orphaned async-delegation payloads. The poller's
belongs-elsewhere check handles events owned by another LIVE session,
but an event whose owner is gone previously fell through and was
adopted by whichever poller saw it - injecting one chat's delegation
output into another chat. Delegation completions are now injected
only into a session that PROVABLY owns them (origin UI id, or
session-key/lineage match via the compression chain); unowned
payloads are dropped from injection with a WARNING (the subagent's
output is already persisted in the delegation records, so nothing is
lost). The shutdown drain applies the same rule. Non-delegation
events keep the historical adopt-orphans behavior.
2. A session's in-flight async delegations end with the session.
_finalize_session now calls interrupt_for_session(): delegations
commissioned by the closing UI session are interrupted always;
key-matched delegations only when the TUI owns the session lifecycle,
so closing a viewer tab on a live gateway session never kills the
gateway's own background work.
* feat(models): swap curated Tencent Hy3 Preview for GA tencent/hy3, drop owl-alpha (#60943)
- OPENROUTER_MODELS: remove openrouter/owl-alpha (free) and
tencent/hy3-preview{,:free}; add tencent/hy3 and tencent/hy3:free
- _PROVIDER_MODELS[nous]: tencent/hy3-preview -> tencent/hy3
- run_agent.py reasoning-prefix list: tencent/hy3-preview -> tencent/hy3
(prefix match still covers -preview if pinned)
- model_metadata: register hy3 context length (262144) alongside hy3-preview
- regenerate website/static/api/model-catalog.json
- update tokenhub curated-list tests to the new IDs
The tencent-tokenhub direct provider still serves hy3-preview and is
intentionally unchanged.
* docs(i18n): align translated CONTRIBUTING files with pyproject Python range (3.11-3.13)
* docs(i18n): align translated CONTRIBUTING files with pyproject Python range (3.11-3.13)
* fix(delegation): route async delegate_task results back to originating session
The completion event already carries the dispatching session's session_key
(captured at dispatch time in delegate_tool.py:2798), but the delivery
router ignored it — results landed in whatever session was active at
completion time instead of the session that dispatched the subagent.
Changes:
- drain_notifications() in process_registry.py: optional session_key
filter. Non-matching async_delegation events are re-queued instead of
consumed, so they remain available for the correct session's drain.
- cli.py process_loop: passes active session_key to drain_notifications()
- tui_gateway/server.py post-turn drain: passes session_key from the
TUI session dict
- gateway/run.py _build_process_event_source: logs warning when routing
metadata is unresolvable (previously silent drop)
- Regression tests verifying session-scoped drain filtering
Fixes #58684
* fix(delegation): positive-proof ownership for the post-turn drain
Extends the salvaged session_key filter with the same fail-closed,
compression-chain-aware ownership gate the poller uses (#55578):
- drain_notifications() accepts an owns_event callback; when provided,
an async-delegation event is consumed ONLY on positive proof of
ownership, and a broken callback re-queues (never leaks). Bare key
equality remains for single-session callers (CLI); no filter remains
legacy behavior.
- The TUI post-turn drain passes _session_owns_notification_event, so
it can't adopt another session's (or an orphan's) delegation payload,
while a post-compression session still claims its own pre-compression
dispatches - the gap bare key equality left open.
* fix(desktop): register /compress command in TUI gateway dispatch so Desktop can invoke it
* fix(tui): route /compress and /compact past the slash worker to command.dispatch
Ported from #60834 (same author) — pending-input routing so clients that
fail the slash.exec->dispatch fallback still reach the new compress handler.
* fix(whatsapp): use windows_detach_popen_kwargs to prevent console window flash on Windows
* fix(cli): preserve chat -q answer by gating exit-summary screen clear (#53009)
In single-query (-q) mode, the assistant's final answer was printed and
then immediately erased by _print_exit_summary() — which unconditionally
called _clear_terminal_on_exit() (ESC[3J ESC[2J ESC[H]). The answer was
present in the session store but invisible in the terminal.
The clear is only needed for interactive TUI teardown (#38928) where
prompt_toolkit chrome must be cleaned up. Add a clear_screen parameter
to _print_exit_summary() (default True, preserving interactive behavior)
and pass False from the single-query call site so the answer stays
visible above the exit summary.
Regression tests cover:
- clear_screen=True (default) calls _clear_terminal_on_exit()
- clear_screen=False skips the clear
- Single-query -q path passes False end-to-end
- Interactive path still clears (preserving #38928)
* test(cli): update FakeCLI._print_exit_summary for new clear_screen kwarg
* perf(yuanbao): bounded-concurrency inbound media resolve
* feat(Yuanbao) optimizes media resource processing speed: parallel download
* fix(delegate): pin async completion to spawning parent session (#57498)
Background delegate_task completions only carried session_key. When multiple
active sessions shared a routing peer, get_or_create_session could recover the
latest ended_at IS NULL row and inject the subagent result into the wrong
session.
Capture parent_agent.session_id at dispatch time, include it on async-delegation
completion events, and pin gateway routing via switch_session when the
synthetic completion message is handled.
Fixes #57498
* fix(gateway): never resurrect ended sessions for delegation completions; /new severs in-flight delegations
Completes the session-binding class on the gateway surface (#55578),
matching the TUI rules:
1. Fail-closed pinning: switch_session() re-opens ended sessions, so
pinning a completion to a spawning session that has since ENDED
(user /new, closed rotation) would resurrect a conversation the user
explicitly ended and inject into it. The injection path now checks
the pinned row's ended_at first and drops the injection with a
WARNING when the spawning session is dead or unknown - the result
stays in the delegation records.
2. /new ends the old conversation's delegations: _handle_reset_command
calls interrupt_for_session() with the expiring durable session id
(matching the parent_session_id pin stamped at dispatch) plus the
routing key as fallback, so a reset can't leave dangling subagents
whose completions have no live owner.
interrupt_for_session() gains the parent_session_id selector because a
gateway chat's session_key (the platform conversation key) survives a
reset while the session id rotates - key-based matching alone could
never sever a gateway conversation's delegations.
* feat(gateway): add webhook payload filters
* fix(gateway): run webhook route scripts off the event loop + AUTHOR_MAP entry
- run_route_script shells out with subprocess.run (up to 30s timeout); wrap
the call in asyncio.to_thread so a slow script can't stall every other
webhook and gateway task on the loop.
- scripts/release.py: map grace@weeb.onl -> evelynburger for the salvaged
contributor commit.
* fix(desktop): continue the selected stored session instead of minting a new one (#55578) (#60874)
Two client-side halves of the #55578 session split:
1. Submit with a null activeSessionId but a SELECTED stored session now
resumes that stored session instead of falling straight through to
createBackendSessionForSend - which silently forked the user's
conversation into a brand-new session that then got orphan-reaped.
New-chat drafts (no stored selection) still create sessions as before.
2. prompt.submit recovery now also fires on gateway request timeouts,
not only 'session not found'. A starved backend loop (the async-
delegation poller spin) rejects the submit with 'request timed out'
even though the stored session is fine; previously that surfaced an
error, left the binding cleared, and set up the split on the next
send.
Fail-then-pass: 2 new tests fail with production code reverted.
* fix(compression): stop compaction thrash — 75% trigger floor under 512K, no summary output cap, reasoning-trace exclusion (#60989)
Sessions on sub-512K-context models were spending most of their wall-clock
re-summarizing: the 50% trigger left too little post-compaction headroom
(the incompressible floor — system prompt, tool schemas, protected tail,
rolling summary — ate most of the reclaimed space), so compaction re-fired
every 1-2 turns. Three compounding defects fixed:
- Threshold floor: models with context windows below 512K now trigger at
>=75% of the window (raise-only — a higher configured value or per-model
autoraise like Codex gpt-5.5's 85% always wins). Re-derived on
update_model() in both directions.
- No max_tokens on the summary call: the summary budget is prompt guidance
only ("Target ~N tokens"). The wire cap truncated summaries mid-section
on the Anthropic Messages / NVIDIA NIM paths (thinking models burn the
cap on reasoning first), yielding truncated or thinking-only summaries
and compaction loops. Summary token ceiling lowered 12K -> 10K to keep
the guidance within the intended 1K-10K envelope.
- Reasoning traces excluded end-to-end: inline <think>/<reasoning> blocks
are now stripped from assistant content before serialization to the
summarizer, and from the summarizer's own output before the summary is
stored (previously a thinking summarizer model's trace was persisted in
_previous_summary and re-fed into every iterative update, compounding
bloat). Native reasoning fields were already excluded.
Verified E2E with real imports against a temp HERMES_HOME: threshold table
across 64K-1M windows, override interactions (user 0.85 wins, spark 0.70
raised, gpt-5.5 0.85 kept), full compress() round-trip with a thinking
summarizer, and wire-kwargs capture proving no max_tokens is sent.
* docs(webhook): complete filters + route-scripts coverage across doc surfaces (#60983)
Follow-up to #60944 (webhook payload filters and route scripts):
- reference/cli-commands.md (en+zh): document the new --script option on
'hermes webhook subscribe'
- zh-Hans user-guide webhooks.md: mirror the Payload Filters and Script
Filters/Transforms sections plus the filters/script route properties
(the salvage shipped English-only docs)
- hermes-agent skill webhooks reference: teach the agent the filters/
script surface so agent-driven subscriptions can use them
* feat(xai): add grok-4.5 (GA) to model catalog, context lengths, and reasoning-effort allowlist (#60887)
* feat(xai): add grok-4.5 (early access) to catalog, context lengths, and reasoning-effort allowlist
- hermes_cli/models.py: grok-4.5 in _XAI_CURATED_EXTRAS (callable but absent
from models.dev) and _XAI_STATIC_FALLBACK, so the /model picker and
validation surface it on both xai and xai-oauth.
- agent/model_metadata.py: context lengths grok-4.5 -> 500K (per model card)
and grok-build-latest -> 500K (alias); grok-4.5 added to
_GROK_EFFORT_CAPABLE_PREFIXES.
Verified live against api.x.ai /v1/responses (2026-07-08): effort
low/medium/high accepted (server default: high), "none" rejected,
function calling works, full agent turn with terminal tool succeeded.
* feat(xai): grok-4.5 GA — add aggregator catalog entries, refresh comments
grok-4.5 is now GA: models.dev list…
1 task
santhreal
pushed a commit
to santhreal/hermes-agent
that referenced
this pull request
Jul 13, 2026
…urfaces (NousResearch#60983) Follow-up to NousResearch#60944 (webhook payload filters and route scripts): - reference/cli-commands.md (en+zh): document the new --script option on 'hermes webhook subscribe' - zh-Hans user-guide webhooks.md: mirror the Payload Filters and Script Filters/Transforms sections plus the filters/script route properties (the salvage shipped English-only docs) - hermes-agent skill webhooks reference: teach the agent the filters/ script surface so agent-driven subscriptions can use them
justemu
pushed a commit
to justemu/hermes-agent
that referenced
this pull request
Jul 18, 2026
…urfaces (NousResearch#60983) Follow-up to NousResearch#60944 (webhook payload filters and route scripts): - reference/cli-commands.md (en+zh): document the new --script option on 'hermes webhook subscribe' - zh-Hans user-guide webhooks.md: mirror the Payload Filters and Script Filters/Transforms sections plus the filters/script route properties (the salvage shipped English-only docs) - hermes-agent skill webhooks reference: teach the agent the filters/ script surface so agent-driven subscriptions can use them
1 task
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
…urfaces (NousResearch#60983) Follow-up to NousResearch#60944 (webhook payload filters and route scripts): - reference/cli-commands.md (en+zh): document the new --script option on 'hermes webhook subscribe' - zh-Hans user-guide webhooks.md: mirror the Payload Filters and Script Filters/Transforms sections plus the filters/script route properties (the salvage shipped English-only docs) - hermes-agent skill webhooks reference: teach the agent the filters/ script surface so agent-driven subscriptions can use them
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
…urfaces (NousResearch#60983) Follow-up to NousResearch#60944 (webhook payload filters and route scripts): - reference/cli-commands.md (en+zh): document the new --script option on 'hermes webhook subscribe' - zh-Hans user-guide webhooks.md: mirror the Payload Filters and Script Filters/Transforms sections plus the filters/script route properties (the salvage shipped English-only docs) - hermes-agent skill webhooks reference: teach the agent the filters/ script surface so agent-driven subscriptions can use them
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
…urfaces (NousResearch#60983) Follow-up to NousResearch#60944 (webhook payload filters and route scripts): - reference/cli-commands.md (en+zh): document the new --script option on 'hermes webhook subscribe' - zh-Hans user-guide webhooks.md: mirror the Payload Filters and Script Filters/Transforms sections plus the filters/script route properties (the salvage shipped English-only docs) - hermes-agent skill webhooks reference: teach the agent the filters/ script surface so agent-driven subscriptions can use them
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
Salvage of #57544 by @evelynburger — webhook routes gain payload-level filtering and optional route scripts, so broad webhook streams (Todoist, GitHub, ...) can be ignored, transformed, or narrowed before Hermes wakes the agent or performs deliver_only delivery. Previously the adapter only filtered on header-derived event type.
Changes
gateway/platforms/webhook_filters.py(new): filter matching (equals/not_equals/contains/exists/... on payload fields, event type, headers) +run_route_script— scripts confined toHERMES_HOME/scriptsviaresolve()+relative_to()(symlink-escape safe), sanitized subprocess env, timeout, output redaction, profile-awaregateway/platforms/webhook.py: filter + script hooks in_handle_webhookhermes_cli/webhook.py+hermes_cli/subcommands/webhook.py:--filter/--scriptonhermes webhook subscribe; config.yaml-driven, no new env varshermes_cli/web_server.py: dashboard API references pre-existing script files only (can't write them)run_route_scriptcall wrapped inasyncio.to_thread— it shells out withsubprocess.run(up to 30s), which would otherwise block the gateway event loop for every other webhook and task; plus AUTHOR_MAP entry for the contributor's unlinked commit identity.Validation
tests/gateway/test_webhook_adapter.py,test_webhook_cli.py,test_dashboard_admin_endpoints.pySupersedes narrower open PRs #21744, #18042, #52824, #59676 (single-purpose filter variants — this generalizes them). Contributor commit cherry-picked with authorship preserved (rebase-merge).
Infographic