feat(security): expose deterministic tool output risk - #61793
Merged
Conversation
teknium1
force-pushed
the
feat/tool-output-risk-metadata
branch
from
July 10, 2026 14:48
15810e9 to
5110eec
Compare
santhreal
pushed a commit
to santhreal/hermes-agent
that referenced
this pull request
Jul 13, 2026
…1793) * feat(security): expose deterministic tool output risk * fix(security): emit output-risk events only for findings
14 tasks
vashkartik
added a commit
to vashkartik/hermes-agent
that referenced
this pull request
Jul 13, 2026
* fix(desktop): preserve project cwd for new sessions
* fix(desktop): dismiss stale prompt overlays
* feat(desktop): Hermes Cloud connection mode — one sign-in, agent discovery, silent connect
Adds a third "Hermes Cloud" gateway mode to the desktop app: one portal
sign-in auto-discovers the agents on your account and connects to any of
them with no second interactive prompt.
- Electron: widen connection mode to 'local' | 'remote' | 'cloud', routed
through a centralized modeIsRemoteLike() so every resolution site treats
cloud exactly like remote; portal discovery (GET /api/agents over the
OAuth partition), Privy-cookie liveness, multi-org picker (NAS 409), and a
silent per-agent /oauth cascade (load protected root, not /login).
- Persist a cloudOrg on the cloud block; unselect cloud on mode switch.
- Renderer: Hermes Cloud ModeCard + agent picker (signed-out/loading/empty/
list), org picker, Change-org, connected-highlight + Connected pill.
- i18n (en + zh full; ja/zh-hant inherit via defineLocale), Cloud icon.
- IPC: hermes:cloud:{status,login,logout,discover,agent-sign-in}.
Salvage of #55402 onto current main: the original branch predates the
desktop electron .cjs -> .ts migration (39d09453f), so the electron half
was re-authored against the .ts files. Authorship preserved.
cloud-auto-discovery Phases 3 + 4.
* polish(desktop): normalize cloud-URL highlight match + correct signedIn doc
Cleanups on top of @ben's Hermes Cloud salvage:
- isConnectedAgent normalized both sides of the cloud-URL comparison (trim +
drop trailing slash + lowercase). The saved URL is host-lowercased by
normalizeRemoteBaseUrl but the discovered dashboardUrl is raw from NAS, so
a host-casing difference could silently break the connected-highlight.
- DesktopCloudStatus.signedIn doc said "AT-or-RT"; it actually reflects the
Nous portal Privy session (privy-token), not the gateway cookies.
* refactor(desktop): DRY the cloud helpers
Tighten the salvaged Hermes Cloud code with no behavior change:
- main: one `trimCloudOrg` projection reused by the success-echo and the 409
org list (drop the duplicated map), and a `cloudLoginError()` factory for the
three needsCloudLogin throw sites.
- renderer: a `cloudLoginLapsed()` predicate for the duplicated
needsCloudLogin→signed-out check.
* feat(desktop): point the no-agents link at the Hermes Cloud instance-setup page
Per review: the empty-state "create an agent" link went to the generic portal
agents list; point it at the Hermes Cloud create-instance flow
({portal}/cloud?setup=instance) instead. Derive the host from the portalBaseUrl
that cloud.status() already echoes so it honors HERMES_PORTAL_BASE_URL rather
than hardcoding a second copy of the portal host. Link text/copy → "Hermes
Cloud" (en + zh).
* simplify(desktop): hardcode the Hermes Cloud setup link
Drop the portalBaseUrl→IPC→useState plumbing I added for the "create an agent"
link. HERMES_PORTAL_BASE_URL is a dev/staging-only override; threading it
through cloud.status() into React state just to build one link isn't worth it —
in prod it's always portal.nousresearch.com. Module-level constant instead.
* simplify(desktop): inline the cloud setup link like the rest of the app
Match the sibling pattern (pet-generate/generate-unavailable.tsx): inline the
portal URL literal in the ExternalLink href instead of a one-off named const.
* feat(desktop): soft gateway switch + gateway-settings polish
Switching connection mode (local / cloud agent / remote) no longer
full-window-reloads into the cold-boot CONNECTING screen. The primary
backend is torn down in place (no renderer reload); the shell + Settings
stay up while session lists are wiped so sidebar skeletons retrigger, then
the socket re-dials and config/sessions refresh. Cold-boot CONNECTING
latches off after the first successful boot; the intentional teardown
suppresses the backend-exit toast. Dev affordance: a "Preview soft switch"
button under Gateway diagnostics (Electron has no ?query= entry).
Gateway settings UI brought in line with the rest of Settings:
- Mode cards use the shared selectableCardClass on an equal-height
auto-rows-fr grid, stacking 1→3 (never an orphaned 2+1); titles wrap
instead of truncating.
- Remote gateway's auth detail moves into a ? tooltip in the title; drop
the redundant "connects to the one you choose" from the cloud card.
- textStrong buttons force px-0 so the underline sits flush with the label.
- Tooltip chip uses box-decoration-break: clone so the background hugs each
wrapped line (bg only on the text), capped at max-w-64.
Fully i18n'd (en + zh; ja/zh-hant inherit via defineLocale).
* fix(desktop): stop Tip from sticking open and blocking clicks
Radix's hoverable-content grace area can leave tips stuck over Electron drag regions; disable it and make tip content pointer-events-none so open state tracks the trigger only.
* feat(agent): core affection reaction detector + reaction_callback
Add a token-free, curated affection matcher (agent/reactions.py) — the single
source of truth for detecting user "vibes" (ily / <3 / good bot / heart emoji).
No model call, no tokens. Generalized to return a reaction *kind* so future
reactions can ride the same signal.
Wire an opt-in AIAgent.reaction_callback that fires from build_turn_context on
the incoming user message. It never touches the conversation (cache-safe) and
never fatal — a purely cosmetic side-beat each host can consume.
* feat(gateway,cli): emit + consume the reaction signal
tui_gateway forwards reaction_callback as a `reaction` event (shared by the TUI
and the desktop app). The interactive CLI wires reaction_callback to flash the
pet's celebrate ("jump") pose — the CLI's analogue of hearts.
* refactor(tui): drive the vibe heart from the core reaction event
Replace the client-side GOOD_VIBES_RE detection with the backend `reaction`
event: on it, flash the status-bar heart and the pet's celebrate pose. Detection
now lives once in the core, so the TUI, CLI, and desktop stay in sync.
* feat(desktop): TikTok-style vibe hearts on a reusable particle system
Add a glyph-agnostic ParticleField (float-up + organic sway/bank + springy
pop-in), skinned as pink pixel hearts. Hearts play on the pet when one is out
(in-window or popped out) and celebrate alongside; otherwise they rise from the
composer. A generic $petReaction bus mirrors the burst to the pop-out overlay
window so it reacts even while the app is minimized.
Consume the core `reaction` event to fire hearts on affectionate messages. DEV
Shift+H previews a burst.
* fix(auth): recover runtime Nous token from shared store
* fix(auth): recompute Nous routing after shared recovery
* fix(auth): validate and persist shared Nous routing
* test(auth): pin runtime routing persistence on failure
* fix(models): remove unavailable OpenCode Zen free models (#61163)
* fix(tui): discover MCP tools in slash workers
* test(tui): cover profile-local MCP discovery
* fix(curator): forward credential pool from runtime resolution
Curator review forks now pass credential_pool and request_overrides from resolve_runtime_provider into AIAgent so pool-backed custom providers can rotate credentials on 401 like main chat.
* test(curator): assert review fork forwards pool and overrides
Regression test that _run_llm_review passes credential_pool and request_overrides from resolve_runtime_provider into the curator AIAgent fork.
* fix(runtime): preserve resolved fork metadata
* fix(runtime): preserve resolved fork metadata
* fix(tools): handle dict URLs in web_extract display and tool processing
When web_search results are passed directly to web_extract, the URLs
field contains dict objects (e.g., {"url": "...", "title": "..."})
rather than plain URL strings. Two code paths assumed URLs were always
strings and crashed:
- agent/display.py get_cute_tool_message for web_extract: tried to call
url.replace() on a dict, causing AttributeError
- tools/web_tools.py web_extract_tool loop: tried regex search on a dict,
causing TypeError
Both now extract the URL string from dict objects (url or href field) or
fall back to empty string, preserving the cosmetic display and allowing
the tool to process the URLs correctly.
Fixes #61693
* fix(web): harden extract input and display boundaries
* test(web): cover model-facing dict URL dispatch
* fix(web): preserve extract result input order
* fix(web): handle short extract provider results
* fix(display): harden fallback label formatting
* fix(acp): unwrap web extract object titles
* fix(telegram): classify PTB heartbeat transport errors
* test(telegram): mirror PTB errors in heartbeat recovery
* fix(cron): never stale-remove a one-shot whose run is still alive
get_due_jobs()'s one-shot stale-entry recovery (#38758) treated an
expired run_claim (#59229) as proof the claiming tick died, but a run
stalled on network I/O — or a laptop asleep mid-run — legitimately
outlives the TTL while very much alive. The recovery then deleted the
job record mid-flight: list showed the job gone, and when the run
finished mark_job_run() found nothing to update, so last_run_at /
last_status / last_delivery_error were never recorded.
Two guards, per the liveness signals available:
- Same process (the common single-gateway case): before removing a
dispatch-limit-reached one-shot, consult the scheduler's running set
via a lazy import; if the job is still running here it is slow, not
stale — keep the entry.
- Cross process: run_job's monitor loop now refreshes run_claim.at
every 60s while the run is alive (including under
HERMES_CRON_TIMEOUT=0, which previously blocked without polling), so
an expired claim really does mean the owner died and the TTL stays a
dead-owner detector.
Fixes #62002
* fix(agent): release pool FDs on owning-thread client close (#61979)
force_close_tcp_sockets stayed shutdown-only after #29507 to avoid
cross-thread FD recycle. That left CLOSED sockets unreclaimed when
httpx.close() skipped already-shutdown sockets under long-lived
gateways (~1 CLOSED fd / 6 min via proxy).
Add release_fds= for the owning-thread dispose path only; abort still
defaults to shutdown-only.
* fix tui finalize persist drop conversation_history so disconnect saves chat
finalize passed conversation_history=history aliasing the snapshot so flush
skipped every message and wrote nothing. now flush _session_messages via
marker dedup like gateway shutdown. add real db e2e tests.
* chore: map WilsonKinyua release attribution
* feat(security): expose deterministic tool output risk (#61793)
* feat(security): expose deterministic tool output risk
* fix(security): emit output-risk events only for findings
* fix(desktop): don't emit js files when we build desktop
* fix(desktop): type-check electron/ in CI typecheck
removing tsc -b from the build script (previous commit) also removed
the only step that type-checked the electron/ directory — the CI
typecheck job runs tsc -p . --noEmit, which uses tsconfig.json whose
include is only ["src", "../shared/src"], so electron/ was silently
uncovered. extend the typecheck script to also run against
tsconfig.electron.json so electron/ stays type-checked in CI.
* fix(tui): dismiss expired sensitive prompts
* fix(desktop): only show slash popover when / is first char
The SLASH_TRIGGER_RE regex used (?:^|[\s]) as its left anchor, so typing
a / anywhere in the message (e.g. "hello /") opened the slash command
popover — even though slash commands only execute at the beginning of a
message. Anchor the regex strictly at position 0 (^) so the popover only
appears when / is the first character, matching the actual execution
semantics. The @-mention trigger is left untouched since those work
anywhere in the text.
* fix(desktop): remove old .js files
we built .ts into .js for a minute there and dumped em in src, and those
old .js files are getting resolved over top of the ts updated ones so
desktop clients don't update.
just --clean the old files so there's never a conflict :3
* chore(desktop): remove the DEV Shift+H heart preview
The real trigger (core `reaction` event on affectionate messages) is live, so
drop the dev-only hotkey and its always-mounted listener.
* feat(dev): add isolated sandbox script for local dev
scripts/desktop-sandbox.sh runs a Hermes desktop instance in an isolated
sandbox — separate HERMES_HOME, separate Electron userData, and a
distinct
app name (HERMES_DESKTOP_APP_NAME) so it doesn't compete with the main
desktop instance's single-instance lock.
Two modes:
- Ephemeral (default): temp dir, cleaned up on exit
- --persistent: stored under .hermes-sandbox/ in the worktree git root,
survives restarts for repeat testing
In the Nix devShell the script is available as 'sandbox'.
Also makes APP_NAME overridable via HERMES_DESKTOP_APP_NAME in main.ts —
app.setName() runs before requestSingleInstanceLock(), so the overridden
name changes the lock key. collectRelaunchEnv already preserves
HERMES_DESKTOP_* vars through self-update relaunches; test updated to
cover the new env var.
* Revert "fix(agent): release pool FDs on owning-thread client close (#61979)" (#62141)
This reverts commit cd7a8dfde08b3f637f0383136497a711b856db66.
* fix(cron): bind claim heartbeats to dispatch owner (#62155)
* fix(codex): drop oversized message ids on Responses input replay
Codex assigns assistant message items server-side ids that can run
400+ chars (base64 encrypted blobs), but the Responses API caps
input[].id at 64 chars and rejects the whole request with a
non-retryable HTTP 400. Once a session captures one of these long
ids, every subsequent turn replays it and 400s forever, since the
history persists it in codex_message_items.
Add a 64-char length guard at both replay sites — the history-to-
input converter and the final preflight gate — so oversized ids are
dropped while short ids (msg_...) are kept for prefix-cache hits.
Mirrors the existing pattern for reasoning items, which already
strip their id before replay because store=False means the API
can't resolve ids server-side anyway.
Fixes #27038
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(desktop): pin composer draft scope to the swap-effect owner, not the render ref
Fixes #54527 — a message typed into one TUI session could be silently
misrouted into (or overwritten by) another concurrently-open session.
Root cause: activeQueueSessionKeyRef is written on every render, but the
debounced draft-persist timer, the pagehide flush, and dispatchSubmit's
reject-restore path all read it lazily at async-resolve time instead of
capturing the scope that was active when the operation started. A session
switch landing between capture and resolve relabels one session's text
under the other session's key. A large paste widens the window (slower
synchronous render), which matches the original report.
Fix: introduce draftScopeRef, written only by the draft-swap effect (so it
always reflects the session whose text is actually loaded in the editor)
and read it instead of the render-time ref at both async write sites.
dispatchSubmit's restore() now uses the submittedScope already captured at
dispatch instead of re-reading the live ref.
Also adds isPendingDraftPersistCurrent as defense-in-depth: before the
debounce timer commits a write, it verifies its captured {scope, text}
pair is still the one on file. This is a no-op under the fix above (a
session swap or a newer keystroke already clears/replaces the pending
entry via clearTimeout), but turns any future regression that reintroduces
a stale/live-ref read at this call site into a dropped write instead of a
silent cross-session misroute.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(desktop): pin session context during async prompt submit (#54527)
Snapshot the selected stored session and route token for the full async submit
pipeline so a mid-flight session switch cannot resume the wrong chat or
misroute the user's text. Includes regression tests.
* fix(cron): run gateway cron LLM calls synchronously (#62151)
Cron jobs in the gateway process wedged before HTTP on later non-streaming
API calls because interruptible_api_call spawned a daemon worker inside
nested cron thread pools. Route cron platform turns through direct_api_call
on the conversation thread instead.
* test(cron): guard direct API path for gateway deadlock (#62151)
* fix(agent): run cron LLM calls inline to avoid gateway deadlock (#62151)
* fix(cron): keep inline dispatch behind the agent call seam
* fix(cron): abort inline requests on timeout
* fix(cron): scope inline calls to reported transport
* fix(telegram): recover final delivery after stream flood
* fix(telegram): harden flood fallback recovery
Keep empty-tail recovery scoped to the current stream segment and bound fallback flood retries. Preserve Telegram's server retry hint without blocking final delivery through a long cooldown.
* fix(codex): never replay message-item id on Copilot Responses connections
Copilot (api.githubcopilot.com/responses) binds replayed assistant
codex_message_items ids to a specific backend "connection". Credential-
pool rotation, a gateway restart, or routine load-balancer churn between
turns all invalidate that binding, and Copilot rejects the stale id with
HTTP 401 "input item ID does not belong to this connection" — even for
short ids well under the #27038 64-char length cap, since this is a
connection-scope problem, not a length problem. Once a session captures
one of these ids it is persisted and replayed forever, permanently
bricking the session.
Thread an is_github_responses flag from build_kwargs/convert_messages
into _chat_messages_to_responses_input and drop the id unconditionally
on that path, mirroring how reasoning items already strip id on replay.
phase/status/content are still replayed so cache-relevant signal isn't
lost — only the connection-scoped id is unsafe to reuse.
Written to apply independently of the #27038 length-cap fix so the two
PRs don't block each other; they touch adjacent conditions in the same
block and merge cleanly in either order.
Fixes #32716
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(codex): also guard the auxiliary Copilot Responses adapter
_CodexCompletionsAdapter (agent/auxiliary_client.py) is a second,
independent producer of Codex Responses input — used by auxiliary
calls (context compression, flush_memories, MoA aggregation,
session_search) that route through CodexAuxiliaryClient instead of
the main agent's ResponsesApiTransport.build_kwargs. It calls
_chat_messages_to_responses_input() directly without is_github_responses,
so the previous commit's fix didn't cover it: an auxiliary call made
against a Copilot-backed session could still replay a connection-scoped
codex_message_items id and hit the same HTTP 401.
Detect the Copilot host from the adapter's own client.base_url (same
check the adapter already does further down for prompt_cache_key
opt-out) and pass is_github_responses through, closing the gap.
Still #32716.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(codex): harden Copilot replay classification
Require literal booleans for backend-specific replay policy and pin
non-default status and content preservation through both response paths.
* fix(codex): enforce Copilot replay policy at dispatch
Reapply the endpoint-aware preflight after request and execution
middleware so no override can reintroduce a connection-scoped ID.
* test(codex): pin final replay preflight boundary
Exercise request and execution middleware replacements through the real
conversation loop and assert the provider payload is sanitized.
* security(providers): strip credential headers on cross-host redirects in fetch_models
fetch_models() sends Authorization: Bearer <api_key> plus any
default_headers (x-api-key etc.) via urllib.request.urlopen, and
urllib's redirect handler forwards every header when following a
3xx — including to a different host. A catalog endpoint (or a
compromised/misconfigured proxy in front of it) answering with a
redirect to another origin therefore received the provider API key.
Install an HTTPRedirectHandler that drops authorization, x-api-key,
api-key, x-goog-api-key and cookie when the redirect target hostname
differs from the original request, mirroring the pattern already used
in skills/creative/comfyui/scripts/_common.py. Same-host redirects
keep credentials so legitimate path-level redirects still work.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* compare full origin (scheme, host, port), not hostname, before keeping credentials
Review feedback: a same-host redirect to a different port can land on a
different service, which must not inherit the provider API key. Compare
(scheme, hostname, effective port) — with 80/443 defaults — instead of
hostname alone, and add a two-server regression test for the
same-host/different-port case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(models): strip credentials on catalog redirects
* test(models): preserve catalog urlopen monkeypatches
* fix(security): enforce one redirect credential policy
* test(models): patch the secured request seam
* fix(security): preserve installed urllib policies
* fix(security): cover remaining catalog credential paths
* fix(security): preserve opener-level header policy
* fix(security): sanitize after installed request hooks
* fix(security): order sanitizer after installed hooks
* fix(security): secure Azure catalog probes
* test(models): patch secured Novita pricing seam
* fix(agent): restore primary credential pool after fallback (#62417)
* fix(xai): recover legacy encrypted replay failures (#62420)
* fix(cli): preserve -t/-m/--provider/--tui/--dev before chat subcommand
`hermes -t web chat` silently dropped the toolset filter (and the same
hold true for `-m`, `--provider`, `--tui`, `--dev` placed before
`chat`). Reported in #28780 for `-t/--toolsets`; the others are sibling
failures with the same root cause.
Root cause: the chat subparser re-declared these flags with `default=None`
(or `default=False` for store_true) on top of the matching top-level
parser flags. When argparse dispatches into the subparser it shares the
namespace via `dest`, so the subparser's default overwrites whatever the
top-level parser parsed before the subcommand. `-s/--skills`, `-r/-c/-w`,
`--yolo`, and `--pass-session-id` already use `default=argparse.SUPPRESS`
for exactly this reason — the chat-subparser action becomes a no-op
unless the user explicitly passes the flag after `chat`, and the parent
value survives.
Reproduction (origin/main, before fix):
>>> parser.parse_known_args(["-t", "web", "chat"]).toolsets
None
>>> parser.parse_known_args(["chat", "-t", "web"]).toolsets
'web'
After fix:
>>> parser.parse_known_args(["-t", "web", "chat"]).toolsets
'web'
>>> parser.parse_known_args(["chat", "-t", "web"]).toolsets
'web'
Sibling flags fixed in the same commit because they share the exact same
argparse pattern bug — verified via a new contract test that scans every
chat-subparser action whose `dest` is also on the top-level parser and
asserts `default is argparse.SUPPRESS`. The test fails on origin/main
listing all five offenders and passes after this fix.
Test additions in tests/hermes_cli/test_argparse_flag_propagation.py:
- TestChatSubparserInheritedValueFlags exercising real `_parser` build
(not the hand-rolled replica) so it catches future drift.
- Parametrized before-chat / after-chat cases for `-t`, `--toolsets`,
`-m`, `--model`, `--provider`.
- Negative case: passing none of the flags leaves attrs at the top-level
parser's `None` default (SUPPRESS does not remove existing attrs).
- Combined case: all three value flags before `chat` simultaneously.
- store_true cases for `--tui` / `--dev`.
- Contract test asserting every shared-`dest` flag on chat uses SUPPRESS.
Fixes #28780.
* fix(agent): preserve none vs unknown tool effects (#61783)
* fix(agent): persist truthful tool effect dispositions
* fix(agent): preserve successful siblings during orphan recovery
* fix(agent): narrow effect dispositions to none and unknown
* fix(cli): keep current provider visible in model pickers
* fix(model): keep configured provider authoritative
* feat(providers): add Fireworks AI as preferred provider
Bundle Fireworks AI as a first-class BYOK provider across the CLI, web/TUI,
and desktop onboarding.
- New model-provider plugin with attribution headers (HTTP-Referer / X-Title)
so Fireworks can attribute Hermes traffic; PAYG-safe default aux + fallback
models (accounts/fireworks/models/...), IDs tracking fw-ai/fireconnect.
- Registered in CANONICAL_PROVIDERS so it appears in the CLI/web/TUI pickers.
- Alias wiring (fireworks-ai, fw) into both CLI resolvers.
- First-class wiring: OPTIONAL_ENV_VARS, HERMES_OVERLAYS (FIREWORKS_BASE_URL
override), doctor env hints. Live catalog + model_metadata are auto-derived.
- doctor: treat Fireworks' native slash-form IDs (accounts/fireworks/...) as
valid, not aggregator vendor prefixes, so it no longer tells Fireworks users
to switch to openrouter or drop the prefix.
- picker: plugin providers with no static curated list now lead with their
profile fallback_models, so the default is an agentic chat model instead of
whatever the live catalog returns first (Fireworks listed an image model,
flux-*, ahead of its chat models).
- Desktop onboarding: Fireworks as a RECOMMENDED hero card with the official
Fireworks logomark and a brand-purple badge, routing to the BYOK key form;
i18n in en/ja/zh/zh-hant.
- Tests: profile contract, first-class wiring (both resolvers, overlay, config,
doctor incl. the slash-form regression, aux headers, credentials), discovery
spot-check, and a live smoke test driven through the Hermes runtime.
Fire Pass (fpk_) support is coming soon; the future wiring is kept as a
commented-out scaffold in the plugin.
* fix(providers): align Fireworks integration with project policy
* docs: add Fireworks provider infographic
* feat(agent): track per-model token usage for mid-session model switches
The `sessions` table records only the initial (model, billing_provider)
for a session, so when a user switches models mid-session (via `/model`
or programmatically) every token — including the switched model's — is
attributed to the first model. Insights/billing reports then hide the
cost of the new model entirely (e.g. a session that started on deepseek
and switched to opus shows $0 for opus).
Add a `session_model_usage` table keyed (session_id, model,
billing_provider) that accumulates each per-API-call delta under the
model active at the time of the call. `update_token_counts()` is the
single chokepoint every per-call delta flows through (CLI, gateway,
cron, delegated, codex), so recording there captures accurate
attribution on every platform. Only the incremental path records — the
gateway's `absolute=True` summary overwrite is skipped to avoid
double-counting cumulative totals that can't be split per model. When a
call omits the model, it falls back to the session's recorded model,
matching the existing COALESCE-from-session summary behaviour.
Insights `_compute_model_breakdown` now aggregates tokens and cost from
`session_model_usage`, so a switched session splits correctly across
models, with a defensive fallback to the per-session aggregate for any
session lacking usage rows. A v17 migration backfills one usage row per
existing token-bearing session from its aggregate totals (idempotent via
INSERT OR IGNORE), validated lossless against a 1.3 GB production DB.
Tests: per-model recording, mid-session split, model fallback, absolute
no-double-count, v17 backfill, and an insights-level switch breakdown.
Fixes #51607.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(telemetry): persist first accounted fallback route
* fix(insights): harden per-route usage attribution
Preserve deletability, route identity, stored costs, aggregate reconciliation,
and zero-usage Codex route accounting on top of the salvaged per-model usage
work.
* chore: map usage attribution contributors
* fix(desktop): keep model picker switches session-scoped
Desktop active-session picker calls already pass a session_id, but the gateway's model switch persistence is controlled by parsed model flags. Add --session so the shared parser keeps live-session selections, including MoA virtual provider presets, out of profile config.yaml.
Constraint: config.set model values are parsed by hermes_cli.model_switch before persistence is decided.
Rejected: backend special-case for desktop session_id | it would duplicate existing --session semantics and widen the gateway surface.
Confidence: high
Scope-risk: narrow
Directive: Keep desktop model picker active-session switches explicit with --session; do not rely on session_id alone for persistence.
Tested: npm run test:ui -- src/app/session/hooks/use-model-controls.test.tsx src/app/shell/model-menu-panel.test.tsx
Tested: npm run typecheck
Tested: git diff --check
Not-tested: full pytest suite; change is desktop TypeScript/UI routing only.
* fix(tui): preserve picker session scope across all paths
Fold in the TUI direction from #61192 and cover the remaining new-live-session picker path with one shared session-argument normalizer.
Co-authored-by: DatTheMaster <hermesagent424@gmail.com>
* feat(gateway): add authenticated runtime readiness checks
* fix(gateway): ground readiness in live runtime state
* fix(compaction): anti-thrashing guard never fired; score against the threshold
`should_compress()` documents anti-thrashing protection ("if the last two
compressions each saved less than 10%, skip compression to avoid infinite
loops"). In practice `_ineffective_compression_count` reset on every pass,
so the guard was dead code and a mis-sized context window presented as a
hung CLI instead of a warning.
Two defects:
1. Mixed measurement bases. Effectiveness was
`(current_tokens - estimate(compressed)) / current_tokens`, where
`current_tokens` is the provider's FULL prompt (system prompt + tool
schemas + messages) but `estimate(compressed)` covers messages only.
Every compaction therefore reported ~96% savings and reset the counter.
Savings is now scored messages-vs-messages.
2. Message shrinkage is the wrong yardstick. `should_compress()` trips on
the full prompt, but compaction can only shrink messages -- the system
prompt and tool schemas are an incompressible floor. When that floor
alone meets the threshold, each pass shrinks messages by a healthy
margin, legitimately resets the counter, and still leaves the prompt
over the line; the next turn compacts again, forever. Observed in the
wild: 45+ consecutive compactions, one auxiliary-LLM call each, zero
progress. Effectiveness is now scored against the goal -- did the
projected prompt get under the threshold? -- and a futile pass warns
with the numbers that prove it.
Also record an ineffective pass on the "only N messages (need > M)" early
return, which previously returned the transcript unchanged without moving
any anti-thrash state -- the same class of bug the neighbouring
"no compressable window" branch was already fixed for.
Tests: 4 of the 5 new cases fail on main and pass here; the fifth pins
that effective compaction still resets the counter (121 -> 15 messages,
88.9% savings).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(compaction): check the threshold against real tokens, not an estimated floor
Follow-up to the previous commit, whose futility check was unsound:
incompressible_floor = max(0, display_tokens - pre_estimate)
`display_tokens` is the provider's real prompt count; `pre_estimate` is
`estimate_messages_tokens_rough(messages)`. Subtracting an estimate from a real
count folds the tokenizer skew into "floor" and misreads it as incompressible
overhead. With a 1.6x skew on a 200K window (threshold 150K, true floor 30K):
rough_msgs=253,804 real_prompt=436,086
computed floor = 182,282 <-- mostly skew; exceeds the threshold
after compaction: 401 -> 77 msgs, real prompt = 106,361 (CLEARS 150,000)
verdict: ineffective_count = 1 <-- false positive
Two such passes would permanently disable compaction on a healthy session --
worse than the loop this PR set out to fix.
Move the check into should_compress(), where both sides of the comparison are
the caller's own token count:
* prompt under the threshold -> not thrashing; reset the counter
* a compaction just ran and we are STILL over -> one strike
Real-vs-real, so tokenizer skew can never be mistaken for a floor, and nothing
subtracts an estimate from a real count. compress() now only ever increments the
counter; the reset lives with the one measure the trigger uses.
Adds `test_no_false_positive_under_tokenizer_skew` (the case above) and
`test_counter_resets_once_the_prompt_fits_again` (one failed pass must not
disable compaction forever). Against upstream, 5 of the 7 cases fail; the 2 that
pass are the regression guards, which is the intended shape.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(compaction): judge the anti-thrash verdict on real usage, not in should_compress
Third correction, and the load-bearing one. The previous commit put the
"did compaction clear the threshold?" verdict inside should_compress(). But
conversation_loop calls should_compress() TWICE per turn with two different
measures (turn_context.py / conversation_loop.py:1033 and :4789):
* pre-API : request_pressure_tokens -- a rough estimate that can dip BELOW
the threshold
* post-API: real prompt tokens -- which stay above it
So the rough reading reset the strike every turn and the loop never stopped.
Reproduced: 8 compactions in 8 turns under the real two-call pattern, even
with the previous fix applied. (My earlier repro only called should_compress()
once per turn, which is why it looked contained.)
Move the verdict to update_from_response(), the one place that sees the
provider's real prompt_tokens for the just-compacted conversation, guarded by
the existing awaiting_real_usage_after_compression flag so it fires exactly
once per compaction. Real-vs-real: it cannot be fooled by a rough sub-threshold
reading, and (from the previous commit's lesson) never subtracts an estimate
from a real count. should_compress() goes back to a plain threshold test plus
the pre-existing cooldown and anti-thrash guards.
New test test_rough_preflight_reading_does_not_reopen_the_loop drives the real
two-call-per-turn pattern and fails on the prior should_compress()-based commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(compaction): initialize anti-thrash fixture state
* chore: map PR #62125 contributor
* fix(compaction): arm verdict after successful boundary
* fix(compaction): clear stale anti-thrash verdicts
* test(codex): cover usage-less compaction response
* fix(codex): evaluate native compaction usage
* fix(codex): consume manual compaction usage gaps
* fix(codex): reject interrupted manual compaction
* fix(gateway): @ context reference expansion never ran (AttributeError)
GatewayRunner._prepare_inbound_message_text's "@" context-reference
block read self._model / self._base_url to resolve the model for
get_model_context_length_async. GatewayRunner never sets either
attribute (copy-pasted from HermesCLI in da44c196b, which does carry
self.model/self.base_url). Every message containing "@" raised
AttributeError inside the try block, silently swallowed by the
surrounding except Exception at debug level, so
preprocess_context_references_async never ran and @file:/@folder:/@diff/
etc. references passed through to the model unexpanded.
Fix: resolve model/provider/base_url via
self._resolve_session_agent_runtime(source=, session_key=,
user_config=), the same session-aware resolution the hygiene
compression block already uses a few hundred lines later in this file.
Also raise the swallow log from debug to warning (with exc_info at
debug) so a future regression here is visible instead of silent.
* fix(gateway): scope context refs to runtime profile
* fix(gateway): scope queued context references
* fix(gateway): honor runtime context budgets
* test(gateway): cover effective context budget
* Merge pull request #62600 from HexLab98/fix/desktop-cron-no-agent-editor
fix(desktop): allow editing script-only (no_agent) cron jobs without a prompt
* fix(tui): dispatch custom skill bundles as agent turns (#62859)
* fix(desktop-terminal): stop idle prompt accumulation across relaunches
An idle terminal tab (no command ever typed) grew one extra copy of the
shell's boot prompt on every close/reopen: persistSnapshot re-serialized a
buffer that was just the replayed old prompt plus the fresh shell's new
prompt, and cleanReviveSnapshot's blank-line trim can't strip prompts on
shells like default PowerShell that print no separator line.
Track real user input (keystrokes/paste, drag-and-drop paths, injected
commands) and, when a session had none, skip re-serializing. If the buffer
we loaded carried no real scrollback (empty or only a repeated prompt),
clear it so the next launch shows a single fresh prompt and any existing
accumulation heals; otherwise leave the prior snapshot untouched so real
history from an earlier active session survives an idle reopen.
Salvages #61584 (activity tracking) and #61577 (clearing content-free idle
buffers) into one path: it also heals already-polluted buffers, counts
drag-and-drop and injected input as activity, and never discards genuine
short command history (only empty/all-identical buffers are cleared).
Co-authored-by: alelpoan <alelpoan@proton.me>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
* fix(desktop-terminal): trim trailing idle prompt on no-separator shells
cleanReviveSnapshot only dropped the trailing prompt when a blank separator
sat above it (starship add_newline), so shells that print the prompt with no
preceding blank line — default PowerShell (PS C:\..>), bash user@host:~$ —
kept the idle prompt in the saved buffer and showed a duplicate under the
fresh boot prompt on every relaunch of an *active* session.
An interactive shell always reprints its prompt after a command, so the tail
of an idle buffer is the prompt, never history. Drop the short block after a
blank separator when present, otherwise drop the trailing single-line prompt.
Command output is preserved; the fresh shell reprints the live prompt on boot.
* fix(desktop-terminal): reopen terminal tabs in the last-used directory
A reopened tab restarted the shell in its original launch dir, so the fresh
prompt showed the wrong folder after a prior `cd` (the issue's "separate
thing" note). Track the shell's working directory and restart the PTY there.
Two independent signals feed a persisted per-tab restoreCwd:
- a main-side PTY cwd probe (shell-agnostic; /proc on Linux, lsof on macOS;
Windows has no cheap per-process query so it falls back to the launch dir)
- cwd-reporting OSC sequences parsed in the renderer (OSC 7 file URIs, OSC 9;9
ConEmu/Windows-Terminal paths) for shells configured to emit them
On relaunch the fresh shell boots in restoreCwd, falling back to the launch
cwd (then home) when it no longer exists.
* fix(desktop): stop empty mispositioned tooltip on terminal rail hover
A block-level label child (e.g. `flex`) collapses TooltipContent's inline
`box-decoration-clone` wrapper, so Radix measures a zero-size chip and parks
an empty black rectangle in the panel corner instead of by the trigger
(#62022). The terminal rail's hotkey labels and the preview row's two-line
label both hit this.
Harden the shared wrapper (`[&>*]:!inline-flex`) so any call site's direct
child renders inline-flex, add a reusable `TipHintLabel` for the common
text+hotkey label, and keep the preview row's label explicitly inline-flex.
Salvages #62139 (shared-component hardening + TipHintLabel) and #62073
(inline-flex call-site fixes + rail/preview coverage).
Co-authored-by: alelpoan <alelpoan@proton.me>
Co-authored-by: zapabob <1920071390@campus.ouj.ac.jp>
* feat: make smart approvals the default (#62661)
* feat(reasoning): add max and ultra effort levels (#62650)
* chore(desktop): drop the dev-only "soft switch" preview from gateway settings
Remove the DEV-gated "Dev · soft switch" ListRow and its previewGatewaySwitch
helper. It was a temporary review affordance for exercising the soft-switch
reconnect; dead-stripped from production, but it doesn't belong in the tree.
wipeSessionListsForGatewaySwitch (the real path) and $gatewaySwitching stay.
* refactor(desktop): extract isRemoteConfig from the reauth predicate
Factor the "remote/cloud with a URL" check out of isRemoteReauthFailure into a
shared isRemoteConfig helper so the boot-failure overlay can tell any remote
failure apart from a local one.
* feat(desktop): embeddable Gateway settings panel
Add an `embedded` flag to GatewaySettings (and a `bare` variant to
SettingsContent) that drops the page title/intro, Diagnostics row,
"Save for next restart", and the page gutters — so the same panel can be reused
inside a tighter surface without a second connection form to maintain. No change
to the standalone Settings → Gateway page (defaults off).
* fix(desktop): recover a failed gateway from the boot-failure screen
A remote/VPS backend that failed to boot trapped the user on the recovery
screen — Retry/Repair/Use-local only target the local backend, so the only fix
was hand-editing connection.json. Add an in-place "Gateway settings" view (the
real GatewaySettings panel embedded via `embedded`, lazy-loaded) reached from
the recovery card, and shape the recovery actions by failure kind: Sign in for a
lapsed remote session, Gateway settings for any other remote failure (Retry
drops to secondary; Repair is dropped — it can't revive a remote), Retry for a
local backend. Use-local is scoped to remote failures.
* fix(desktop): treat connected-but-expired remote sessions as reauth
Add isRemoteReauthError so an auth-shaped boot error counts as a remote-reauth
failure even when the session indicator still reads connected (a stale refresh
cookie / failed ws-ticket mint). Wire the boot error into the overlay's reauth
check so those sessions route to Sign in instead of the local-only recovery
buttons.
Co-authored-by: Tony Antunez <57689194+smtony@users.noreply.github.com>
* fix(desktop): clear the OAuth partition before remote sign-in
Sign out of the dedicated OAuth partition before opening the login window so a
stale gateway/identity-provider cookie can't silently bounce an expired session
straight back into failure. Relabel the action "Sign out & sign in" and spell
out the sign-out step in the hint.
Co-authored-by: Tony Antunez <57689194+smtony@users.noreply.github.com>
* fix(desktop): preserve legacy non-git workspace groups
* fix(desktop): mirror Windows path identity in live overlay + WSL spelling
Addresses @teknium1's review of #61950:
- The desktop live overlay (workspace-groups.ts) matched cwd membership
case-sensitively, so a fresh mixed-case/separator Windows session missed
its explicit/auto project until the next backend tree refresh. Mirror the
backend identity (isWindowsPath/comparisonSegments/pathKey) in isPathUnder,
liveSessionProjectId, and overlayRepoLanes lane matching. Comparison-only —
emitted ids/labels keep their spelling. POSIX stays case-sensitive.
- Backend _is_windows_path missed root-relative `\wsl.localhost\...` (single
leading backslash), leaving that historical spelling case-sensitive. Classify
any backslash-rooted path as Windows.
Tests: WSL-spelling collapse + explicit-project precedence (project_tree),
Windows/WSL live-overlay membership + POSIX case-sensitivity (workspace-groups).
* fix(desktop): preserve sidebar workspace targets across new drafts
Squashed salvage of #45744 (@harjothkhara), rebased onto current main and
resolved against #58241 (which swapped the new-session cwd fallback to the
project-aware resolveNewSessionCwd).
An explicitly clicked sidebar workspace stays authoritative until session.create:
a one-shot $newChatWorkspaceTarget (null → detached, string → that folder) plus a
generation counter so a stale async `config.get project` normalization can't
overwrite a newer draft target. The start-workspace-session action is extracted
out of desktop-controller.tsx into a testable workspace-session-target module.
Integrated with #58241: the no-explicit-target branch now falls through to the
project-aware resolveNewSessionCwd() instead of the old workspaceCwdForNewSession.
Co-authored-by: harjoth <harjoth.khara@gmail.com>
* chore(attribution): map esthon@gmail.com → esthonjr (#61950 salvage)
* feat(desktop): add workspace path status action
* refactor(desktop): text-only workspace status menu + attribution
Align the workspace status-bar dropdown with the rest of the status bar: drop
the per-item icons (they mixed lucide size-4 with a Codicon 1rem glyph and were
the only status-bar menu carrying item icons), leaving text-only items on the
shared DropdownMenuItem primitive with default typography. The status-bar
trigger keeps its FolderOpen glyph, consistent with sibling items.
Also map true@supersynergy.de → Supersynergy in AUTHOR_MAP.
* feat(sessions): workspace_key grouping helper + tests
A session's coarse workspace identity: its git repo root when known, else its
cwd (branch excluded, so switching branches doesn't fragment history). Pure
helper over fields sessions already record — no new columns, no git shelling.
Co-authored-by: Cary Palmer <palmer@dugoutfantasy.com>
* feat(cli): sessions list --workspace filter + Workspace column
`hermes sessions list --workspace <needle>` filters to one workspace (git repo
root or project dir, matched by path substring or basename) and adds a
Workspace column. The column only appears once at least one listed session
carries a workspace, so all-unbound listings render exactly as before.
Co-authored-by: Cary Palmer <palmer@dugoutfantasy.com>
* feat(cli): restore cwd on resume (--no-restore-cwd)
Resuming a session cd's back into its recorded working directory, so it resumes
in the repo it belonged to. `--no-restore-cwd` opts out; skipped under
--worktree (that path owns its dir); best-effort — a missing dir warns and stays
put rather than failing the resume.
Co-authored-by: Cary Palmer <palmer@dugoutfantasy.com>
* chore(attribution): map palmer@dugoutfantasy.com -> professorpalmer (#48591 salvage)
* fix(desktop): keep answered clarify Q&A visible in the transcript
Answered clarifies were collapsing into a generic tool row, hiding the
choice. Settle into a Q&A panel instead, and route freeform input through
the shared Textarea chrome.
* test(desktop): cover settled clarify answer rendering
* docs(desktop): add judgment-first AGENTS guide and align DESIGN/README
Capture durable Desktop engineering principles from recent sessions —
state by authority, workspace-switch shapes, resolver ladders, optimistic
UI — and point root AGENTS.md at the scoped guide with current filenames.
* feat(desktop): bridge WSL paths for a Windows host + WSL backend
When the desktop UI runs on Windows and the gateway runs in WSL, a WSL/POSIX
cwd isn't openable/readable from the Windows host. Add wsl-path-bridge.ts to
translate the Windows-side direction only:
- native folder dialog defaultPath: `/home/...` → `\\wsl.localhost\<distro>\...`
- fs read path: WSL cwd → its UNC / `C:\` drive form
Distro detection reads `wsl.exe -l -q` with `WSL_UTF8=1` and strips stray NUL
bytes, since older wsl.exe emits UTF-16LE (microsoft/WSL#4607) — the original
utf8 read returned a garbled distro name. UNC uses `\\wsl.localhost\` with a
`\\wsl$\` fallback for older Windows. The reverse (any path → POSIX) is handled
once gateway-side, so the picker result needs no desktop translation.
Co-authored-by: Rage Lopez <VrtxOmega@pm.me>
* feat(gateway,acp): translate cross-boundary cwd when running in WSL
Add shared translators in hermes_constants (Windows drive → /mnt, `\\wsl(.localhost|$)\`
UNC → POSIX, gated on is_wsl) and apply them at the gateway session-cwd boundary
so a Windows-host UI can hand the WSL backend a path it can actually chdir into.
De-dups the ACP adapter's private `_win_path_to_wsl` onto the shared helper and
extends it to the UNC spelling.
Co-authored-by: Rage Lopez <VrtxOmega@pm.me>
* chore(attribution): map VrtxOmega@pm.me -> VrtxOmega (#43809 salvage)
* test(desktop): satisfy ToolCallMessagePartProps in clarify tests
CI typecheck requires argsText, status, addResult, and resume on rendered
tool parts.
* fix(windows): rewrite native drive paths to /c/ form for bash file ops
ShellFileOperations builds bash commands (wc/head/sed/cat/tee ...) with the
target path as an argument. On a Windows/Git-Bash host a native `C:\...` path
has its backslashes eaten by bash (and mangled by the msys runtime even when
single-quoted) — the "Directory \drivers\etc does not exist; exiting — update
your msys package" class of failures. Rewrite a native drive path to forward
slashes in `_escape_shell_arg`, reusing the env layer's `_windows_to_msys_path`.
Both `C:/...` and `/c/...` fix the backslash bug (the MSYS coreutils resolve
either via the POSIX API). We emit `/c/...` purely for consistency: it's the
same form `_windows_to_msys_path` already produces for the terminal `cd`
(LocalEnvironment._quote_cwd_for_cd), so shell file ops and `cd` share one
helper and one path form.
Scoped from #55481, which also patched BaseEnvironment._quote_cwd_for_cd — but
LocalEnvironment already overrides that through `_windows_to_msys_path`, so on a
real Windows host the base branch never ran (the cwd is already `/c/...`).
Co-authored-by: konsisumer <der@konsi.org>
* fix(model-switch): don't treat an exhausted credential pool as authenticated
An aggregator whose pooled credentials are all exhausted/dead still counted as
an authenticated provider during no-provider /model resolution. It then won the
model-name match, was set as the sticky session provider, and poisoned every
later switch with "empty API key" errors while still routing through the dead
aggregator.
list_authenticated_providers now requires a pool to have at least one available
entry (has_available, not has_credentials / bare key presence) at all three
credential-pool gates. Simple token-style entries that don't parse into
exhaustion-tracked entries keep the prior behaviour, so providers whose creds
live only in the auth-store credential_pool still appear.
Fixes #45759
* refactor(model): centralize picker credential availability
* fix(skills): install referenced bundle files with scan provenance
* fix(skills): bind bundles to exact files and origins
* fix(auth): enforce credential pool provider boundaries (#63048)
Retain the provider-boundary core of #52799 while reusing the pool reload and handoff paths already landed in #53591 and #62417.
Co-authored-by: Flownium <157689911+itsflownium@users.noreply.github.com>
* fix(desktop): autosave Mixture-of-Agents preset edits
MoA was internally inconsistent: preset-level ops (set default / add /
delete) persisted on click, but reference-model and aggregator slot edits
sat behind a manual Save button. Debounce-persist slot/aggregator edits
like the rest of settings and drop the redundant button, so MoA is
uniformly autosave.
* fix(model_switch): filter /model picker for unregistered providers (#57503)
list_authenticated_providers() emits picker rows for every slug in
PROVIDER_TO_MODELS_DEV that has any credential env-var set. Several of
those slugs (notably 'mistral') have no PROVIDER_REGISTRY entry, so
resolve_provider() rejects them as 'Unknown provider' once the user
selects a model — leaving the picker showing rows that cannot actually
be selected.
Add a resolve-gate in section 1: if PROVIDER_REGISTRY.get(hermes_id)
is None, skip the slug. The picker now only lists providers that can
actually be switched to at runtime.
This automatically resolves the duplicate-Mistral dedup symptom too:
once the broken-from-models.dev row is filtered, the conflict between
PROVIDER_TO_MODELS_DEV['mistral'] and a custom_providers 'Mistral' row
is moot.
Composes with #50289 (which promotes mistral to first-class via the
provider-plugin path): when that lands, PROVIDER_REGISTRY gains a
'mistral' entry and the gate becomes a no-op for it. No conflict.
Tests (regression suite):
- tests/hermes_cli/test_model_switch_filter_unresolved.py (new, 4 tests):
Picker excludes 'mistral' when MISTRAL_API_KEY is set; 'deepseek' and
'xai' (PROVIDER_REGISTRY-backed) still appear; 'mistral' stays
excluded when no key is set. Confirmed by reverting the fix and
seeing the test fail with 'mistral leaked into /model picker'.
Cross-checked against the existing 51 test_model_switch_* and
test_custom_provider_* cases — 55/55 PASS, no regressions.
* test(model): simplify routable picker invariants
* refactor(model): gate picker rows by runtime capability
* fix(model): merge configured models into picker rows (#63055)
Preserve the root cause and precedence direction from #43538 while applying the merge before truncation and covering all declared model shapes.
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
* fix(web): preserve declared providers in model writes (#63058)
Unify the named-provider fixes from #52506, #57185, #60337, and #60901 at the main-model normalization chokepoint.
Co-authored-by: izumi0uu <izumi0uu@gmail.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: Paulo Henrique <paulohenrique_789@hotmail.com>
* fix(api): keep live runs tracked past stream ttl
* fix(api): separate run control from stream lifetime
* fix(api): stop producers after run transport expires
* fix(kanban): make scratch cleanup explicit in dashboard (#63123)
* fix(approval): allow verifier temp cleanup
* chore(release): map @ansel-f in AUTHOR_MAP
* fix(desktop): structured Fallback Models editor
Settings → Model rendered `fallback_providers` (a list of `{provider,
model}` objects) through the generic `list` config field, which does
`value.join(', ')` and stringified each entry to `[object Object],
[object Object]`.
Add a dedicated provider+model row editor (add/remove), sourced from the
same `getGlobalModelOptions()` the composer picker uses, that reads and
writes the `{provider, model}` chain. Half-filled rows are kept in local
state so the config autosave never persists a partial entry, and an
out-of-catalog model stays selectable so existing custom entries render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(desktop): cover the Fallback Models editor
Asserts each {provider, model} entry renders as its own row (the bug
produced "[object Object]"), that removing a row emits the remaining
entries, that adding a blank row never persists a partial pair, and the
empty-state hint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(desktop): resync fallback editor after config reload
* fix(dashboard): correct approvals.mode select options
The web UI CONFIG_SCHEMA showed ['ask', 'yolo', 'deny'] for the
approvals.mode select field. These don't match any real config values
and 'smart' mode was entirely unreachable from the dashboard.
Correct the options to ['manual', 'smart', 'off'] which match the
values defined and documented in hermes_cli/config.py.
Adds a regression test to TestBuildSchemaFromConfig to pin the correct
option names and guard against future drift.
Fixes #31925
* docs(dashboard): align approval mode guidance
* Fix mobile channel setup modal
* feat(kanban): collect project directory when creating boards (#63249)
* fix(approval): honor canonical gateway timeout
* chore(release): map @jakelongvu-bot in AUTHOR_MAP
* fix(desktop): stop the submit drift guard from aborting every new chat
The #54527 context pin (7acaff5ef) snapshots the selected stored session
and route token at submit entry and aborts when either changes mid-flight.
But a NEW chat's create pipeline legitimately moves both: on success,
createBackendSessionForSend re-homes selection and navigates to the chat
it just minted. Judged against the pre-create draft baseline that read as
a user switch, so every first send of a new chat aborted before
prompt.submit — message dropped, no DB row persisted (row creation is
lazy, server-side in prompt.submit), and the window stranded on a route
whose REST reads 404 "Session not found" forever.
Fix: after a successful create, verify no one re-homed during create's
post-commit await via the active-session ref (a non-null return
guarantees create set it; every switch path retargets it synchronously),
then re-pin the drift baseline to the created chat. A mid-create switch
still aborts through create's own null return, or through the active-ref
check for the post-commit window. Re-pinning also restores the correct
stored-id association for the optimistic-message state updates, which the
pinned pre-create null had degraded.
Tests: red-first regression for the new-chat send, an abort case for a
switch landing in create's post-commit window, and the sleep/wake
new-chat stub made faithful to the real create (it sets the active ref
before returning — the inert stub is what let this ship green).
* fix(windows): bash-safe snapshot paths after #63113
#63113 rewrote native drive paths in ShellFileOperations, but init_session
/_wrap_command still embedded C:/... hermes-snap paths from get_temp_dir.
MSYS arg-converts those during bash -l and surfaces Directory \drivers\etc
— including for relative write_file targets, since the wrapper is the fault.
Add _bash_safe_path, override BaseEnvironment._quote_shell_path on
LocalEnvironment (no base→local import), and normalize mixed /c/Users\...
paths in file ops.
Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
* fix: reject empty credential pool leases (#63620)
* fix(kanban): preserve scratch completion artifacts
* fix(kanban): harden durable artifact handoff
* chore(release): map @yinkev in AUTHOR_MAP
* fix(kanban): spawn goal_mode workers with -Q so the goal loop actually runs
_default_spawn sets HERMES_KANBAN_GOAL_MODE=1 but launched 'chat -q' without
-Q; _run_kanban_goal_loop_q only executes in the quiet single-query branch,
so goal-mode never ran for dispatcher-spawned workers — they got one turn,
printed text, exited rc=0, and tripped the protocol-violation circuit
breaker (2026-06-09, cards t_d9cbe312 et al). Root-cause report + upstream
issue draft in kanban workspace t_720c5c60.
* fix(agent): honor custom-provider extra_body for multi-model catalogs
_custom_provider_model_matches() only compared the session model
against the entry's single 'model' field. A custom provider declaring
a multi-model catalog (providers.<name>.models mapping / models list)
whose default model differed from the session model silently failed to
match — dropping the entry's extra_body entirely. Real impact: an
OpenAI custom provider pinning service_tier=flex via extra_body ran
every request at STANDARD tier (~2.3x billing) with zero signal.
- Model matching now accepts the session model when it appears in the
entry's models catalog (dict keys or list), case-insensitive;
single-model 'model' field behavior unchanged; entries with neither
still match everything.
- Usage report ('hermes -z --usage-file') now carries service_tier
(the tier requested via request_overrides.extra_body) so batch
pipelines can audit the billed tier per run.
Validation: 8 new tests; live E2E via real 'hermes -p sweeper -z'
with httpx-level wire capture — service_tier=flex present in the
outgoing /v1/responses body and in the usage report.
* fix(image-gen): classify unsupported Codex image accounts
* test(image-gen): cover Codex capability HTTP boundary
* fix(config): preserve string-typed config values
* feat(kanban): surface final_result for Done cards; show run summary when task.result is empty
* fix(kanban): make Done-card results actionable
* test(kanban): remove duplicate final-results footer
* fix(approval): emit observer hooks for smart verdicts
* fix(patch): ignore inert context-only hunks (#63678)
* feat(desktop): add profile-aware approval mode control
* chore(release): map @Tortugasaur desktop commits
* feat(dashboard): add session import flow
* fix(sessions): validate imported session payloads
Reject metadata that would make session queries fail, bound import work, and detach cyclic lineage links. Guard lineage traversal against pre-existing corrupt cycles.
* fix(whatsapp_cloud): gate interactive taps on DM allowlist
* fix(approval): scope smart deny owner overrides to one operation
Co-authored-by: Sergei Ivanov <kavi@local.hermes>
* chore(release): map @kavioavio in AUTHOR_MAP
* test(approval): isolate smart observer redaction failure
* fix(gateway): deduplicate completion delivery
* feat(delegation): persist background completions
* fix(delegation): harden durable completion delivery
* chore(release): map delegation contributor
* docs(delegation): clarify background lifetime
* fix(gateway): replay pending clarify requests
* fix(desktop): recover clarify prompts after reconnect
* test(desktop): guard persistent clarify recovery
* chore(release): map upstream sync contributors
* test(gateway): isolate manual approval flow
---------
Co-authored-by: yoma <yingwaizhiying@gmail.com>
Co-authored-by: Jakub Wolniewicz <jakub.wolniewicz@gmail.com>
Co-authored-by: Ben <ben@nousresearch.com>
Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
Co-authored-by: Sami Rusani <sr@samirusani>
Co-authored-by: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com>
Co-authored-by: gigakun3030 <gigakun@agentmail.to>
Co-authored-by: HexLab98 <liruixinch@outlook.com>
Co-authored-by: infinitycrew39 <infinitycrew39@gmail.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: PRATHAMESH75 <prathamesh290504@gmail.com>
Co-authored-by: giggling-ginger <110955495+giggling-ginger@users.noreply.github.com>
Co-authored-by: Wilson Kinyua <wilsonkinyuam@gmail.com>
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
Co-authored-by: ethernet <arilotter@gmail.com>
Co-authored-by: embwl0x <embwl0x@users.noreply.github.com>
Co-authored-by: joaomarcos <joaomarcosdias444@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>
Co-authored-by: solyanviktor-star <233359899+solyanviktor-star@users.noreply.github.com>
Co-authored-by: dsad <sswdarius@gmail.com>
Co-authored-by: briandevans <252620095+briandevans@users.noreply.github.com>
Co-authored-by: konsisumer <der@konsi.org>
Co-authored-by: Alex Jestin Taylor <alex@fireworks.ai>
Co-authored-by: Thomas Connally <51974392+tcconnally@users.noreply.github.com>
Co-authored-by: Jan Hermes Integration <jan@mg5.org>
Co-authored-by: izumi0uu <izumi0uu@gmail.com>
Co-authored-by: DatTheMaster <hermesagent424@gmail.com>
Co-authored-by: Igor Ganapolsky <iganapolsky@gmail.com>
Co-authored-by: TheTom <tturney1@gmail.com>
Co-authored-by: HexLab <8422520+HexLab98@users.noreply.github.com>
Co-authored-by: Adolanium <94890352+Adolanium@users.noreply.github.com>
Co-authored-by: alelpoan <alelpoan@proton.me>
Co-authored-by: zapabob <1920071390@campus.ouj.ac.jp>
Co-authored-by: Tony Antunez <57689194+smtony@users.noreply.github.com>
Co-authored-by: Esthon Jr <esthon@gmail.com>
Co-authored-by: harjoth <harjoth.khara@gmail.com>
Co-authored-by: Maxim M <true@supersynergy.de>
Co-authored-by: Cary Palmer <palmer@dugoutfantasy.com>
Co-authored-by: Rage Lopez <VrtxOmega@pm.me>
Co-authored-by: AIalliAI <285906080+AIalliAI@users.noreply.github.com>
Co-authored-by: Flownium <157689911+itsflownium@users.noreply.github.com>
Co-authored-by: Ahmett101 <297889955+Ahmett101@users.noreply.github.com>
Co-authored-by: Paulo Henrique <paulohenrique_789@hotmail.com>
Co-authored-by: ansel-f <135129512+ansel-f@users.noreply.github.com>
Co-authored-by: Mark Vlcek <markvlcek@gmail.com>
Co-authored-by: Robert Blaas <robert@modern-minds.ai>
Co-authored-by: Shannon Sands <shannon.sands.1979@gmail.com>
Co-authored-by: Jeffrey Quesnelle <emozilla@nousresearch.com>
Co-authored-by: Jake Long Vu <jake.long.vu@vucar.net>
Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
Co-authored-by: Kevin Yin <182213728+yinkev@users.noreply.github.com>
Co-authored-by: agent <agent@agents-Mac-mini.local>
Co-authored-by: iborazzi <ibrahimozsarac@gmail.com>
Co-authored-by: hellno <hellno@users.noreply.github.com>
Co-authored-by: Luigi Razon <luigi@users.noreply.github.com>
Co-authored-by: SahilRakhaiya05 <144577420+SahilRakhaiya05@users.noreply.github.com>
Co-authored-by: Sergei Ivanov <kavi@local.hermes>
Co-authored-by: John Lussier <johnmlussier@gmail.com>
Co-authored-by: VECTOR <vector.hq@outlook.com>
vashkartik
added a commit
to vashkartik/hermes-agent
that referenced
this pull request
Jul 13, 2026
* fix(desktop): preserve project cwd for new sessions
* fix(desktop): dismiss stale prompt overlays
* feat(desktop): Hermes Cloud connection mode — one sign-in, agent discovery, silent connect
Adds a third "Hermes Cloud" gateway mode to the desktop app: one portal
sign-in auto-discovers the agents on your account and connects to any of
them with no second interactive prompt.
- Electron: widen connection mode to 'local' | 'remote' | 'cloud', routed
through a centralized modeIsRemoteLike() so every resolution site treats
cloud exactly like remote; portal discovery (GET /api/agents over the
OAuth partition), Privy-cookie liveness, multi-org picker (NAS 409), and a
silent per-agent /oauth cascade (load protected root, not /login).
- Persist a cloudOrg on the cloud block; unselect cloud on mode switch.
- Renderer: Hermes Cloud ModeCard + agent picker (signed-out/loading/empty/
list), org picker, Change-org, connected-highlight + Connected pill.
- i18n (en + zh full; ja/zh-hant inherit via defineLocale), Cloud icon.
- IPC: hermes:cloud:{status,login,logout,discover,agent-sign-in}.
Salvage of #55402 onto current main: the original branch predates the
desktop electron .cjs -> .ts migration (39d09453f), so the electron half
was re-authored against the .ts files. Authorship preserved.
cloud-auto-discovery Phases 3 + 4.
* polish(desktop): normalize cloud-URL highlight match + correct signedIn doc
Cleanups on top of @ben's Hermes Cloud salvage:
- isConnectedAgent normalized both sides of the cloud-URL comparison (trim +
drop trailing slash + lowercase). The saved URL is host-lowercased by
normalizeRemoteBaseUrl but the discovered dashboardUrl is raw from NAS, so
a host-casing difference could silently break the connected-highlight.
- DesktopCloudStatus.signedIn doc said "AT-or-RT"; it actually reflects the
Nous portal Privy session (privy-token), not the gateway cookies.
* refactor(desktop): DRY the cloud helpers
Tighten the salvaged Hermes Cloud code with no behavior change:
- main: one `trimCloudOrg` projection reused by the success-echo and the 409
org list (drop the duplicated map), and a `cloudLoginError()` factory for the
three needsCloudLogin throw sites.
- renderer: a `cloudLoginLapsed()` predicate for the duplicated
needsCloudLogin→signed-out check.
* feat(desktop): point the no-agents link at the Hermes Cloud instance-setup page
Per review: the empty-state "create an agent" link went to the generic portal
agents list; point it at the Hermes Cloud create-instance flow
({portal}/cloud?setup=instance) instead. Derive the host from the portalBaseUrl
that cloud.status() already echoes so it honors HERMES_PORTAL_BASE_URL rather
than hardcoding a second copy of the portal host. Link text/copy → "Hermes
Cloud" (en + zh).
* simplify(desktop): hardcode the Hermes Cloud setup link
Drop the portalBaseUrl→IPC→useState plumbing I added for the "create an agent"
link. HERMES_PORTAL_BASE_URL is a dev/staging-only override; threading it
through cloud.status() into React state just to build one link isn't worth it —
in prod it's always portal.nousresearch.com. Module-level constant instead.
* simplify(desktop): inline the cloud setup link like the rest of the app
Match the sibling pattern (pet-generate/generate-unavailable.tsx): inline the
portal URL literal in the ExternalLink href instead of a one-off named const.
* feat(desktop): soft gateway switch + gateway-settings polish
Switching connection mode (local / cloud agent / remote) no longer
full-window-reloads into the cold-boot CONNECTING screen. The primary
backend is torn down in place (no renderer reload); the shell + Settings
stay up while session lists are wiped so sidebar skeletons retrigger, then
the socket re-dials and config/sessions refresh. Cold-boot CONNECTING
latches off after the first successful boot; the intentional teardown
suppresses the backend-exit toast. Dev affordance: a "Preview soft switch"
button under Gateway diagnostics (Electron has no ?query= entry).
Gateway settings UI brought in line with the rest of Settings:
- Mode cards use the shared selectableCardClass on an equal-height
auto-rows-fr grid, stacking 1→3 (never an orphaned 2+1); titles wrap
instead of truncating.
- Remote gateway's auth detail moves into a ? tooltip in the title; drop
the redundant "connects to the one you choose" from the cloud card.
- textStrong buttons force px-0 so the underline sits flush with the label.
- Tooltip chip uses box-decoration-break: clone so the background hugs each
wrapped line (bg only on the text), capped at max-w-64.
Fully i18n'd (en + zh; ja/zh-hant inherit via defineLocale).
* fix(desktop): stop Tip from sticking open and blocking clicks
Radix's hoverable-content grace area can leave tips stuck over Electron drag regions; disable it and make tip content pointer-events-none so open state tracks the trigger only.
* feat(agent): core affection reaction detector + reaction_callback
Add a token-free, curated affection matcher (agent/reactions.py) — the single
source of truth for detecting user "vibes" (ily / <3 / good bot / heart emoji).
No model call, no tokens. Generalized to return a reaction *kind* so future
reactions can ride the same signal.
Wire an opt-in AIAgent.reaction_callback that fires from build_turn_context on
the incoming user message. It never touches the conversation (cache-safe) and
never fatal — a purely cosmetic side-beat each host can consume.
* feat(gateway,cli): emit + consume the reaction signal
tui_gateway forwards reaction_callback as a `reaction` event (shared by the TUI
and the desktop app). The interactive CLI wires reaction_callback to flash the
pet's celebrate ("jump") pose — the CLI's analogue of hearts.
* refactor(tui): drive the vibe heart from the core reaction event
Replace the client-side GOOD_VIBES_RE detection with the backend `reaction`
event: on it, flash the status-bar heart and the pet's celebrate pose. Detection
now lives once in the core, so the TUI, CLI, and desktop stay in sync.
* feat(desktop): TikTok-style vibe hearts on a reusable particle system
Add a glyph-agnostic ParticleField (float-up + organic sway/bank + springy
pop-in), skinned as pink pixel hearts. Hearts play on the pet when one is out
(in-window or popped out) and celebrate alongside; otherwise they rise from the
composer. A generic $petReaction bus mirrors the burst to the pop-out overlay
window so it reacts even while the app is minimized.
Consume the core `reaction` event to fire hearts on affectionate messages. DEV
Shift+H previews a burst.
* fix(auth): recover runtime Nous token from shared store
* fix(auth): recompute Nous routing after shared recovery
* fix(auth): validate and persist shared Nous routing
* test(auth): pin runtime routing persistence on failure
* fix(models): remove unavailable OpenCode Zen free models (#61163)
* fix(tui): discover MCP tools in slash workers
* test(tui): cover profile-local MCP discovery
* fix(curator): forward credential pool from runtime resolution
Curator review forks now pass credential_pool and request_overrides from resolve_runtime_provider into AIAgent so pool-backed custom providers can rotate credentials on 401 like main chat.
* test(curator): assert review fork forwards pool and overrides
Regression test that _run_llm_review passes credential_pool and request_overrides from resolve_runtime_provider into the curator AIAgent fork.
* fix(runtime): preserve resolved fork metadata
* fix(runtime): preserve resolved fork metadata
* fix(tools): handle dict URLs in web_extract display and tool processing
When web_search results are passed directly to web_extract, the URLs
field contains dict objects (e.g., {"url": "...", "title": "..."})
rather than plain URL strings. Two code paths assumed URLs were always
strings and crashed:
- agent/display.py get_cute_tool_message for web_extract: tried to call
url.replace() on a dict, causing AttributeError
- tools/web_tools.py web_extract_tool loop: tried regex search on a dict,
causing TypeError
Both now extract the URL string from dict objects (url or href field) or
fall back to empty string, preserving the cosmetic display and allowing
the tool to process the URLs correctly.
Fixes #61693
* fix(web): harden extract input and display boundaries
* test(web): cover model-facing dict URL dispatch
* fix(web): preserve extract result input order
* fix(web): handle short extract provider results
* fix(display): harden fallback label formatting
* fix(acp): unwrap web extract object titles
* fix(telegram): classify PTB heartbeat transport errors
* test(telegram): mirror PTB errors in heartbeat recovery
* fix(cron): never stale-remove a one-shot whose run is still alive
get_due_jobs()'s one-shot stale-entry recovery (#38758) treated an
expired run_claim (#59229) as proof the claiming tick died, but a run
stalled on network I/O — or a laptop asleep mid-run — legitimately
outlives the TTL while very much alive. The recovery then deleted the
job record mid-flight: list showed the job gone, and when the run
finished mark_job_run() found nothing to update, so last_run_at /
last_status / last_delivery_error were never recorded.
Two guards, per the liveness signals available:
- Same process (the common single-gateway case): before removing a
dispatch-limit-reached one-shot, consult the scheduler's running set
via a lazy import; if the job is still running here it is slow, not
stale — keep the entry.
- Cross process: run_job's monitor loop now refreshes run_claim.at
every 60s while the run is alive (including under
HERMES_CRON_TIMEOUT=0, which previously blocked without polling), so
an expired claim really does mean the owner died and the TTL stays a
dead-owner detector.
Fixes #62002
* fix(agent): release pool FDs on owning-thread client close (#61979)
force_close_tcp_sockets stayed shutdown-only after #29507 to avoid
cross-thread FD recycle. That left CLOSED sockets unreclaimed when
httpx.close() skipped already-shutdown sockets under long-lived
gateways (~1 CLOSED fd / 6 min via proxy).
Add release_fds= for the owning-thread dispose path only; abort still
defaults to shutdown-only.
* fix tui finalize persist drop conversation_history so disconnect saves chat
finalize passed conversation_history=history aliasing the snapshot so flush
skipped every message and wrote nothing. now flush _session_messages via
marker dedup like gateway shutdown. add real db e2e tests.
* chore: map WilsonKinyua release attribution
* feat(security): expose deterministic tool output risk (#61793)
* feat(security): expose deterministic tool output risk
* fix(security): emit output-risk events only for findings
* fix(desktop): don't emit js files when we build desktop
* fix(desktop): type-check electron/ in CI typecheck
removing tsc -b from the build script (previous commit) also removed
the only step that type-checked the electron/ directory — the CI
typecheck job runs tsc -p . --noEmit, which uses tsconfig.json whose
include is only ["src", "../shared/src"], so electron/ was silently
uncovered. extend the typecheck script to also run against
tsconfig.electron.json so electron/ stays type-checked in CI.
* fix(tui): dismiss expired sensitive prompts
* fix(desktop): only show slash popover when / is first char
The SLASH_TRIGGER_RE regex used (?:^|[\s]) as its left anchor, so typing
a / anywhere in the message (e.g. "hello /") opened the slash command
popover — even though slash commands only execute at the beginning of a
message. Anchor the regex strictly at position 0 (^) so the popover only
appears when / is the first character, matching the actual execution
semantics. The @-mention trigger is left untouched since those work
anywhere in the text.
* fix(desktop): remove old .js files
we built .ts into .js for a minute there and dumped em in src, and those
old .js files are getting resolved over top of the ts updated ones so
desktop clients don't update.
just --clean the old files so there's never a conflict :3
* chore(desktop): remove the DEV Shift+H heart preview
The real trigger (core `reaction` event on affectionate messages) is live, so
drop the dev-only hotkey and its always-mounted listener.
* feat(dev): add isolated sandbox script for local dev
scripts/desktop-sandbox.sh runs a Hermes desktop instance in an isolated
sandbox — separate HERMES_HOME, separate Electron userData, and a
distinct
app name (HERMES_DESKTOP_APP_NAME) so it doesn't compete with the main
desktop instance's single-instance lock.
Two modes:
- Ephemeral (default): temp dir, cleaned up on exit
- --persistent: stored under .hermes-sandbox/ in the worktree git root,
survives restarts for repeat testing
In the Nix devShell the script is available as 'sandbox'.
Also makes APP_NAME overridable via HERMES_DESKTOP_APP_NAME in main.ts —
app.setName() runs before requestSingleInstanceLock(), so the overridden
name changes the lock key. collectRelaunchEnv already preserves
HERMES_DESKTOP_* vars through self-update relaunches; test updated to
cover the new env var.
* Revert "fix(agent): release pool FDs on owning-thread client close (#61979)" (#62141)
This reverts commit cd7a8dfde08b3f637f0383136497a711b856db66.
* fix(cron): bind claim heartbeats to dispatch owner (#62155)
* fix(codex): drop oversized message ids on Responses input replay
Codex assigns assistant message items server-side ids that can run
400+ chars (base64 encrypted blobs), but the Responses API caps
input[].id at 64 chars and rejects the whole request with a
non-retryable HTTP 400. Once a session captures one of these long
ids, every subsequent turn replays it and 400s forever, since the
history persists it in codex_message_items.
Add a 64-char length guard at both replay sites — the history-to-
input converter and the final preflight gate — so oversized ids are
dropped while short ids (msg_...) are kept for prefix-cache hits.
Mirrors the existing pattern for reasoning items, which already
strip their id before replay because store=False means the API
can't resolve ids server-side anyway.
Fixes #27038
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(desktop): pin composer draft scope to the swap-effect owner, not the render ref
Fixes #54527 — a message typed into one TUI session could be silently
misrouted into (or overwritten by) another concurrently-open session.
Root cause: activeQueueSessionKeyRef is written on every render, but the
debounced draft-persist timer, the pagehide flush, and dispatchSubmit's
reject-restore path all read it lazily at async-resolve time instead of
capturing the scope that was active when the operation started. A session
switch landing between capture and resolve relabels one session's text
under the other session's key. A large paste widens the window (slower
synchronous render), which matches the original report.
Fix: introduce draftScopeRef, written only by the draft-swap effect (so it
always reflects the session whose text is actually loaded in the editor)
and read it instead of the render-time ref at both async write sites.
dispatchSubmit's restore() now uses the submittedScope already captured at
dispatch instead of re-reading the live ref.
Also adds isPendingDraftPersistCurrent as defense-in-depth: before the
debounce timer commits a write, it verifies its captured {scope, text}
pair is still the one on file. This is a no-op under the fix above (a
session swap or a newer keystroke already clears/replaces the pending
entry via clearTimeout), but turns any future regression that reintroduces
a stale/live-ref read at this call site into a dropped write instead of a
silent cross-session misroute.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(desktop): pin session context during async prompt submit (#54527)
Snapshot the selected stored session and route token for the full async submit
pipeline so a mid-flight session switch cannot resume the wrong chat or
misroute the user's text. Includes regression tests.
* fix(cron): run gateway cron LLM calls synchronously (#62151)
Cron jobs in the gateway process wedged before HTTP on later non-streaming
API calls because interruptible_api_call spawned a daemon worker inside
nested cron thread pools. Route cron platform turns through direct_api_call
on the conversation thread instead.
* test(cron): guard direct API path for gateway deadlock (#62151)
* fix(agent): run cron LLM calls inline to avoid gateway deadlock (#62151)
* fix(cron): keep inline dispatch behind the agent call seam
* fix(cron): abort inline requests on timeout
* fix(cron): scope inline calls to reported transport
* fix(telegram): recover final delivery after stream flood
* fix(telegram): harden flood fallback recovery
Keep empty-tail recovery scoped to the current stream segment and bound fallback flood retries. Preserve Telegram's server retry hint without blocking final delivery through a long cooldown.
* fix(codex): never replay message-item id on Copilot Responses connections
Copilot (api.githubcopilot.com/responses) binds replayed assistant
codex_message_items ids to a specific backend "connection". Credential-
pool rotation, a gateway restart, or routine load-balancer churn between
turns all invalidate that binding, and Copilot rejects the stale id with
HTTP 401 "input item ID does not belong to this connection" — even for
short ids well under the #27038 64-char length cap, since this is a
connection-scope problem, not a length problem. Once a session captures
one of these ids it is persisted and replayed forever, permanently
bricking the session.
Thread an is_github_responses flag from build_kwargs/convert_messages
into _chat_messages_to_responses_input and drop the id unconditionally
on that path, mirroring how reasoning items already strip id on replay.
phase/status/content are still replayed so cache-relevant signal isn't
lost — only the connection-scoped id is unsafe to reuse.
Written to apply independently of the #27038 length-cap fix so the two
PRs don't block each other; they touch adjacent conditions in the same
block and merge cleanly in either order.
Fixes #32716
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(codex): also guard the auxiliary Copilot Responses adapter
_CodexCompletionsAdapter (agent/auxiliary_client.py) is a second,
independent producer of Codex Responses input — used by auxiliary
calls (context compression, flush_memories, MoA aggregation,
session_search) that route through CodexAuxiliaryClient instead of
the main agent's ResponsesApiTransport.build_kwargs. It calls
_chat_messages_to_responses_input() directly without is_github_responses,
so the previous commit's fix didn't cover it: an auxiliary call made
against a Copilot-backed session could still replay a connection-scoped
codex_message_items id and hit the same HTTP 401.
Detect the Copilot host from the adapter's own client.base_url (same
check the adapter already does further down for prompt_cache_key
opt-out) and pass is_github_responses through, closing the gap.
Still #32716.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(codex): harden Copilot replay classification
Require literal booleans for backend-specific replay policy and pin
non-default status and content preservation through both response paths.
* fix(codex): enforce Copilot replay policy at dispatch
Reapply the endpoint-aware preflight after request and execution
middleware so no override can reintroduce a connection-scoped ID.
* test(codex): pin final replay preflight boundary
Exercise request and execution middleware replacements through the real
conversation loop and assert the provider payload is sanitized.
* security(providers): strip credential headers on cross-host redirects in fetch_models
fetch_models() sends Authorization: Bearer <api_key> plus any
default_headers (x-api-key etc.) via urllib.request.urlopen, and
urllib's redirect handler forwards every header when following a
3xx — including to a different host. A catalog endpoint (or a
compromised/misconfigured proxy in front of it) answering with a
redirect to another origin therefore received the provider API key.
Install an HTTPRedirectHandler that drops authorization, x-api-key,
api-key, x-goog-api-key and cookie when the redirect target hostname
differs from the original request, mirroring the pattern already used
in skills/creative/comfyui/scripts/_common.py. Same-host redirects
keep credentials so legitimate path-level redirects still work.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* compare full origin (scheme, host, port), not hostname, before keeping credentials
Review feedback: a same-host redirect to a different port can land on a
different service, which must not inherit the provider API key. Compare
(scheme, hostname, effective port) — with 80/443 defaults — instead of
hostname alone, and add a two-server regression test for the
same-host/different-port case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(models): strip credentials on catalog redirects
* test(models): preserve catalog urlopen monkeypatches
* fix(security): enforce one redirect credential policy
* test(models): patch the secured request seam
* fix(security): preserve installed urllib policies
* fix(security): cover remaining catalog credential paths
* fix(security): preserve opener-level header policy
* fix(security): sanitize after installed request hooks
* fix(security): order sanitizer after installed hooks
* fix(security): secure Azure catalog probes
* test(models): patch secured Novita pricing seam
* fix(agent): restore primary credential pool after fallback (#62417)
* fix(xai): recover legacy encrypted replay failures (#62420)
* fix(cli): preserve -t/-m/--provider/--tui/--dev before chat subcommand
`hermes -t web chat` silently dropped the toolset filter (and the same
hold true for `-m`, `--provider`, `--tui`, `--dev` placed before
`chat`). Reported in #28780 for `-t/--toolsets`; the others are sibling
failures with the same root cause.
Root cause: the chat subparser re-declared these flags with `default=None`
(or `default=False` for store_true) on top of the matching top-level
parser flags. When argparse dispatches into the subparser it shares the
namespace via `dest`, so the subparser's default overwrites whatever the
top-level parser parsed before the subcommand. `-s/--skills`, `-r/-c/-w`,
`--yolo`, and `--pass-session-id` already use `default=argparse.SUPPRESS`
for exactly this reason — the chat-subparser action becomes a no-op
unless the user explicitly passes the flag after `chat`, and the parent
value survives.
Reproduction (origin/main, before fix):
>>> parser.parse_known_args(["-t", "web", "chat"]).toolsets
None
>>> parser.parse_known_args(["chat", "-t", "web"]).toolsets
'web'
After fix:
>>> parser.parse_known_args(["-t", "web", "chat"]).toolsets
'web'
>>> parser.parse_known_args(["chat", "-t", "web"]).toolsets
'web'
Sibling flags fixed in the same commit because they share the exact same
argparse pattern bug — verified via a new contract test that scans every
chat-subparser action whose `dest` is also on the top-level parser and
asserts `default is argparse.SUPPRESS`. The test fails on origin/main
listing all five offenders and passes after this fix.
Test additions in tests/hermes_cli/test_argparse_flag_propagation.py:
- TestChatSubparserInheritedValueFlags exercising real `_parser` build
(not the hand-rolled replica) so it catches future drift.
- Parametrized before-chat / after-chat cases for `-t`, `--toolsets`,
`-m`, `--model`, `--provider`.
- Negative case: passing none of the flags leaves attrs at the top-level
parser's `None` default (SUPPRESS does not remove existing attrs).
- Combined case: all three value flags before `chat` simultaneously.
- store_true cases for `--tui` / `--dev`.
- Contract test asserting every shared-`dest` flag on chat uses SUPPRESS.
Fixes #28780.
* fix(agent): preserve none vs unknown tool effects (#61783)
* fix(agent): persist truthful tool effect dispositions
* fix(agent): preserve successful siblings during orphan recovery
* fix(agent): narrow effect dispositions to none and unknown
* fix(cli): keep current provider visible in model pickers
* fix(model): keep configured provider authoritative
* feat(providers): add Fireworks AI as preferred provider
Bundle Fireworks AI as a first-class BYOK provider across the CLI, web/TUI,
and desktop onboarding.
- New model-provider plugin with attribution headers (HTTP-Referer / X-Title)
so Fireworks can attribute Hermes traffic; PAYG-safe default aux + fallback
models (accounts/fireworks/models/...), IDs tracking fw-ai/fireconnect.
- Registered in CANONICAL_PROVIDERS so it appears in the CLI/web/TUI pickers.
- Alias wiring (fireworks-ai, fw) into both CLI resolvers.
- First-class wiring: OPTIONAL_ENV_VARS, HERMES_OVERLAYS (FIREWORKS_BASE_URL
override), doctor env hints. Live catalog + model_metadata are auto-derived.
- doctor: treat Fireworks' native slash-form IDs (accounts/fireworks/...) as
valid, not aggregator vendor prefixes, so it no longer tells Fireworks users
to switch to openrouter or drop the prefix.
- picker: plugin providers with no static curated list now lead with their
profile fallback_models, so the default is an agentic chat model instead of
whatever the live catalog returns first (Fireworks listed an image model,
flux-*, ahead of its chat models).
- Desktop onboarding: Fireworks as a RECOMMENDED hero card with the official
Fireworks logomark and a brand-purple badge, routing to the BYOK key form;
i18n in en/ja/zh/zh-hant.
- Tests: profile contract, first-class wiring (both resolvers, overlay, config,
doctor incl. the slash-form regression, aux headers, credentials), discovery
spot-check, and a live smoke test driven through the Hermes runtime.
Fire Pass (fpk_) support is coming soon; the future wiring is kept as a
commented-out scaffold in the plugin.
* fix(providers): align Fireworks integration with project policy
* docs: add Fireworks provider infographic
* feat(agent): track per-model token usage for mid-session model switches
The `sessions` table records only the initial (model, billing_provider)
for a session, so when a user switches models mid-session (via `/model`
or programmatically) every token — including the switched model's — is
attributed to the first model. Insights/billing reports then hide the
cost of the new model entirely (e.g. a session that started on deepseek
and switched to opus shows $0 for opus).
Add a `session_model_usage` table keyed (session_id, model,
billing_provider) that accumulates each per-API-call delta under the
model active at the time of the call. `update_token_counts()` is the
single chokepoint every per-call delta flows through (CLI, gateway,
cron, delegated, codex), so recording there captures accurate
attribution on every platform. Only the incremental path records — the
gateway's `absolute=True` summary overwrite is skipped to avoid
double-counting cumulative totals that can't be split per model. When a
call omits the model, it falls back to the session's recorded model,
matching the existing COALESCE-from-session summary behaviour.
Insights `_compute_model_breakdown` now aggregates tokens and cost from
`session_model_usage`, so a switched session splits correctly across
models, with a defensive fallback to the per-session aggregate for any
session lacking usage rows. A v17 migration backfills one usage row per
existing token-bearing session from its aggregate totals (idempotent via
INSERT OR IGNORE), validated lossless against a 1.3 GB production DB.
Tests: per-model recording, mid-session split, model fallback, absolute
no-double-count, v17 backfill, and an insights-level switch breakdown.
Fixes #51607.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(telemetry): persist first accounted fallback route
* fix(insights): harden per-route usage attribution
Preserve deletability, route identity, stored costs, aggregate reconciliation,
and zero-usage Codex route accounting on top of the salvaged per-model usage
work.
* chore: map usage attribution contributors
* fix(desktop): keep model picker switches session-scoped
Desktop active-session picker calls already pass a session_id, but the gateway's model switch persistence is controlled by parsed model flags. Add --session so the shared parser keeps live-session selections, including MoA virtual provider presets, out of profile config.yaml.
Constraint: config.set model values are parsed by hermes_cli.model_switch before persistence is decided.
Rejected: backend special-case for desktop session_id | it would duplicate existing --session semantics and widen the gateway surface.
Confidence: high
Scope-risk: narrow
Directive: Keep desktop model picker active-session switches explicit with --session; do not rely on session_id alone for persistence.
Tested: npm run test:ui -- src/app/session/hooks/use-model-controls.test.tsx src/app/shell/model-menu-panel.test.tsx
Tested: npm run typecheck
Tested: git diff --check
Not-tested: full pytest suite; change is desktop TypeScript/UI routing only.
* fix(tui): preserve picker session scope across all paths
Fold in the TUI direction from #61192 and cover the remaining new-live-session picker path with one shared session-argument normalizer.
Co-authored-by: DatTheMaster <hermesagent424@gmail.com>
* feat(gateway): add authenticated runtime readiness checks
* fix(gateway): ground readiness in live runtime state
* fix(compaction): anti-thrashing guard never fired; score against the threshold
`should_compress()` documents anti-thrashing protection ("if the last two
compressions each saved less than 10%, skip compression to avoid infinite
loops"). In practice `_ineffective_compression_count` reset on every pass,
so the guard was dead code and a mis-sized context window presented as a
hung CLI instead of a warning.
Two defects:
1. Mixed measurement bases. Effectiveness was
`(current_tokens - estimate(compressed)) / current_tokens`, where
`current_tokens` is the provider's FULL prompt (system prompt + tool
schemas + messages) but `estimate(compressed)` covers messages only.
Every compaction therefore reported ~96% savings and reset the counter.
Savings is now scored messages-vs-messages.
2. Message shrinkage is the wrong yardstick. `should_compress()` trips on
the full prompt, but compaction can only shrink messages -- the system
prompt and tool schemas are an incompressible floor. When that floor
alone meets the threshold, each pass shrinks messages by a healthy
margin, legitimately resets the counter, and still leaves the prompt
over the line; the next turn compacts again, forever. Observed in the
wild: 45+ consecutive compactions, one auxiliary-LLM call each, zero
progress. Effectiveness is now scored against the goal -- did the
projected prompt get under the threshold? -- and a futile pass warns
with the numbers that prove it.
Also record an ineffective pass on the "only N messages (need > M)" early
return, which previously returned the transcript unchanged without moving
any anti-thrash state -- the same class of bug the neighbouring
"no compressable window" branch was already fixed for.
Tests: 4 of the 5 new cases fail on main and pass here; the fifth pins
that effective compaction still resets the counter (121 -> 15 messages,
88.9% savings).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(compaction): check the threshold against real tokens, not an estimated floor
Follow-up to the previous commit, whose futility check was unsound:
incompressible_floor = max(0, display_tokens - pre_estimate)
`display_tokens` is the provider's real prompt count; `pre_estimate` is
`estimate_messages_tokens_rough(messages)`. Subtracting an estimate from a real
count folds the tokenizer skew into "floor" and misreads it as incompressible
overhead. With a 1.6x skew on a 200K window (threshold 150K, true floor 30K):
rough_msgs=253,804 real_prompt=436,086
computed floor = 182,282 <-- mostly skew; exceeds the threshold
after compaction: 401 -> 77 msgs, real prompt = 106,361 (CLEARS 150,000)
verdict: ineffective_count = 1 <-- false positive
Two such passes would permanently disable compaction on a healthy session --
worse than the loop this PR set out to fix.
Move the check into should_compress(), where both sides of the comparison are
the caller's own token count:
* prompt under the threshold -> not thrashing; reset the counter
* a compaction just ran and we are STILL over -> one strike
Real-vs-real, so tokenizer skew can never be mistaken for a floor, and nothing
subtracts an estimate from a real count. compress() now only ever increments the
counter; the reset lives with the one measure the trigger uses.
Adds `test_no_false_positive_under_tokenizer_skew` (the case above) and
`test_counter_resets_once_the_prompt_fits_again` (one failed pass must not
disable compaction forever). Against upstream, 5 of the 7 cases fail; the 2 that
pass are the regression guards, which is the intended shape.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(compaction): judge the anti-thrash verdict on real usage, not in should_compress
Third correction, and the load-bearing one. The previous commit put the
"did compaction clear the threshold?" verdict inside should_compress(). But
conversation_loop calls should_compress() TWICE per turn with two different
measures (turn_context.py / conversation_loop.py:1033 and :4789):
* pre-API : request_pressure_tokens -- a rough estimate that can dip BELOW
the threshold
* post-API: real prompt tokens -- which stay above it
So the rough reading reset the strike every turn and the loop never stopped.
Reproduced: 8 compactions in 8 turns under the real two-call pattern, even
with the previous fix applied. (My earlier repro only called should_compress()
once per turn, which is why it looked contained.)
Move the verdict to update_from_response(), the one place that sees the
provider's real prompt_tokens for the just-compacted conversation, guarded by
the existing awaiting_real_usage_after_compression flag so it fires exactly
once per compaction. Real-vs-real: it cannot be fooled by a rough sub-threshold
reading, and (from the previous commit's lesson) never subtracts an estimate
from a real count. should_compress() goes back to a plain threshold test plus
the pre-existing cooldown and anti-thrash guards.
New test test_rough_preflight_reading_does_not_reopen_the_loop drives the real
two-call-per-turn pattern and fails on the prior should_compress()-based commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(compaction): initialize anti-thrash fixture state
* chore: map PR #62125 contributor
* fix(compaction): arm verdict after successful boundary
* fix(compaction): clear stale anti-thrash verdicts
* test(codex): cover usage-less compaction response
* fix(codex): evaluate native compaction usage
* fix(codex): consume manual compaction usage gaps
* fix(codex): reject interrupted manual compaction
* fix(gateway): @ context reference expansion never ran (AttributeError)
GatewayRunner._prepare_inbound_message_text's "@" context-reference
block read self._model / self._base_url to resolve the model for
get_model_context_length_async. GatewayRunner never sets either
attribute (copy-pasted from HermesCLI in da44c196b, which does carry
self.model/self.base_url). Every message containing "@" raised
AttributeError inside the try block, silently swallowed by the
surrounding except Exception at debug level, so
preprocess_context_references_async never ran and @file:/@folder:/@diff/
etc. references passed through to the model unexpanded.
Fix: resolve model/provider/base_url via
self._resolve_session_agent_runtime(source=, session_key=,
user_config=), the same session-aware resolution the hygiene
compression block already uses a few hundred lines later in this file.
Also raise the swallow log from debug to warning (with exc_info at
debug) so a future regression here is visible instead of silent.
* fix(gateway): scope context refs to runtime profile
* fix(gateway): scope queued context references
* fix(gateway): honor runtime context budgets
* test(gateway): cover effective context budget
* Merge pull request #62600 from HexLab98/fix/desktop-cron-no-agent-editor
fix(desktop): allow editing script-only (no_agent) cron jobs without a prompt
* fix(tui): dispatch custom skill bundles as agent turns (#62859)
* fix(desktop-terminal): stop idle prompt accumulation across relaunches
An idle terminal tab (no command ever typed) grew one extra copy of the
shell's boot prompt on every close/reopen: persistSnapshot re-serialized a
buffer that was just the replayed old prompt plus the fresh shell's new
prompt, and cleanReviveSnapshot's blank-line trim can't strip prompts on
shells like default PowerShell that print no separator line.
Track real user input (keystrokes/paste, drag-and-drop paths, injected
commands) and, when a session had none, skip re-serializing. If the buffer
we loaded carried no real scrollback (empty or only a repeated prompt),
clear it so the next launch shows a single fresh prompt and any existing
accumulation heals; otherwise leave the prior snapshot untouched so real
history from an earlier active session survives an idle reopen.
Salvages #61584 (activity tracking) and #61577 (clearing content-free idle
buffers) into one path: it also heals already-polluted buffers, counts
drag-and-drop and injected input as activity, and never discards genuine
short command history (only empty/all-identical buffers are cleared).
Co-authored-by: alelpoan <alelpoan@proton.me>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
* fix(desktop-terminal): trim trailing idle prompt on no-separator shells
cleanReviveSnapshot only dropped the trailing prompt when a blank separator
sat above it (starship add_newline), so shells that print the prompt with no
preceding blank line — default PowerShell (PS C:\..>), bash user@host:~$ —
kept the idle prompt in the saved buffer and showed a duplicate under the
fresh boot prompt on every relaunch of an *active* session.
An interactive shell always reprints its prompt after a command, so the tail
of an idle buffer is the prompt, never history. Drop the short block after a
blank separator when present, otherwise drop the trailing single-line prompt.
Command output is preserved; the fresh shell reprints the live prompt on boot.
* fix(desktop-terminal): reopen terminal tabs in the last-used directory
A reopened tab restarted the shell in its original launch dir, so the fresh
prompt showed the wrong folder after a prior `cd` (the issue's "separate
thing" note). Track the shell's working directory and restart the PTY there.
Two independent signals feed a persisted per-tab restoreCwd:
- a main-side PTY cwd probe (shell-agnostic; /proc on Linux, lsof on macOS;
Windows has no cheap per-process query so it falls back to the launch dir)
- cwd-reporting OSC sequences parsed in the renderer (OSC 7 file URIs, OSC 9;9
ConEmu/Windows-Terminal paths) for shells configured to emit them
On relaunch the fresh shell boots in restoreCwd, falling back to the launch
cwd (then home) when it no longer exists.
* fix(desktop): stop empty mispositioned tooltip on terminal rail hover
A block-level label child (e.g. `flex`) collapses TooltipContent's inline
`box-decoration-clone` wrapper, so Radix measures a zero-size chip and parks
an empty black rectangle in the panel corner instead of by the trigger
(#62022). The terminal rail's hotkey labels and the preview row's two-line
label both hit this.
Harden the shared wrapper (`[&>*]:!inline-flex`) so any call site's direct
child renders inline-flex, add a reusable `TipHintLabel` for the common
text+hotkey label, and keep the preview row's label explicitly inline-flex.
Salvages #62139 (shared-component hardening + TipHintLabel) and #62073
(inline-flex call-site fixes + rail/preview coverage).
Co-authored-by: alelpoan <alelpoan@proton.me>
Co-authored-by: zapabob <1920071390@campus.ouj.ac.jp>
* feat: make smart approvals the default (#62661)
* feat(reasoning): add max and ultra effort levels (#62650)
* chore(desktop): drop the dev-only "soft switch" preview from gateway settings
Remove the DEV-gated "Dev · soft switch" ListRow and its previewGatewaySwitch
helper. It was a temporary review affordance for exercising the soft-switch
reconnect; dead-stripped from production, but it doesn't belong in the tree.
wipeSessionListsForGatewaySwitch (the real path) and $gatewaySwitching stay.
* refactor(desktop): extract isRemoteConfig from the reauth predicate
Factor the "remote/cloud with a URL" check out of isRemoteReauthFailure into a
shared isRemoteConfig helper so the boot-failure overlay can tell any remote
failure apart from a local one.
* feat(desktop): embeddable Gateway settings panel
Add an `embedded` flag to GatewaySettings (and a `bare` variant to
SettingsContent) that drops the page title/intro, Diagnostics row,
"Save for next restart", and the page gutters — so the same panel can be reused
inside a tighter surface without a second connection form to maintain. No change
to the standalone Settings → Gateway page (defaults off).
* fix(desktop): recover a failed gateway from the boot-failure screen
A remote/VPS backend that failed to boot trapped the user on the recovery
screen — Retry/Repair/Use-local only target the local backend, so the only fix
was hand-editing connection.json. Add an in-place "Gateway settings" view (the
real GatewaySettings panel embedded via `embedded`, lazy-loaded) reached from
the recovery card, and shape the recovery actions by failure kind: Sign in for a
lapsed remote session, Gateway settings for any other remote failure (Retry
drops to secondary; Repair is dropped — it can't revive a remote), Retry for a
local backend. Use-local is scoped to remote failures.
* fix(desktop): treat connected-but-expired remote sessions as reauth
Add isRemoteReauthError so an auth-shaped boot error counts as a remote-reauth
failure even when the session indicator still reads connected (a stale refresh
cookie / failed ws-ticket mint). Wire the boot error into the overlay's reauth
check so those sessions route to Sign in instead of the local-only recovery
buttons.
Co-authored-by: Tony Antunez <57689194+smtony@users.noreply.github.com>
* fix(desktop): clear the OAuth partition before remote sign-in
Sign out of the dedicated OAuth partition before opening the login window so a
stale gateway/identity-provider cookie can't silently bounce an expired session
straight back into failure. Relabel the action "Sign out & sign in" and spell
out the sign-out step in the hint.
Co-authored-by: Tony Antunez <57689194+smtony@users.noreply.github.com>
* fix(desktop): preserve legacy non-git workspace groups
* fix(desktop): mirror Windows path identity in live overlay + WSL spelling
Addresses @teknium1's review of #61950:
- The desktop live overlay (workspace-groups.ts) matched cwd membership
case-sensitively, so a fresh mixed-case/separator Windows session missed
its explicit/auto project until the next backend tree refresh. Mirror the
backend identity (isWindowsPath/comparisonSegments/pathKey) in isPathUnder,
liveSessionProjectId, and overlayRepoLanes lane matching. Comparison-only —
emitted ids/labels keep their spelling. POSIX stays case-sensitive.
- Backend _is_windows_path missed root-relative `\wsl.localhost\...` (single
leading backslash), leaving that historical spelling case-sensitive. Classify
any backslash-rooted path as Windows.
Tests: WSL-spelling collapse + explicit-project precedence (project_tree),
Windows/WSL live-overlay membership + POSIX case-sensitivity (workspace-groups).
* fix(desktop): preserve sidebar workspace targets across new drafts
Squashed salvage of #45744 (@harjothkhara), rebased onto current main and
resolved against #58241 (which swapped the new-session cwd fallback to the
project-aware resolveNewSessionCwd).
An explicitly clicked sidebar workspace stays authoritative until session.create:
a one-shot $newChatWorkspaceTarget (null → detached, string → that folder) plus a
generation counter so a stale async `config.get project` normalization can't
overwrite a newer draft target. The start-workspace-session action is extracted
out of desktop-controller.tsx into a testable workspace-session-target module.
Integrated with #58241: the no-explicit-target branch now falls through to the
project-aware resolveNewSessionCwd() instead of the old workspaceCwdForNewSession.
Co-authored-by: harjoth <harjoth.khara@gmail.com>
* chore(attribution): map esthon@gmail.com → esthonjr (#61950 salvage)
* feat(desktop): add workspace path status action
* refactor(desktop): text-only workspace status menu + attribution
Align the workspace status-bar dropdown with the rest of the status bar: drop
the per-item icons (they mixed lucide size-4 with a Codicon 1rem glyph and were
the only status-bar menu carrying item icons), leaving text-only items on the
shared DropdownMenuItem primitive with default typography. The status-bar
trigger keeps its FolderOpen glyph, consistent with sibling items.
Also map true@supersynergy.de → Supersynergy in AUTHOR_MAP.
* feat(sessions): workspace_key grouping helper + tests
A session's coarse workspace identity: its git repo root when known, else its
cwd (branch excluded, so switching branches doesn't fragment history). Pure
helper over fields sessions already record — no new columns, no git shelling.
Co-authored-by: Cary Palmer <palmer@dugoutfantasy.com>
* feat(cli): sessions list --workspace filter + Workspace column
`hermes sessions list --workspace <needle>` filters to one workspace (git repo
root or project dir, matched by path substring or basename) and adds a
Workspace column. The column only appears once at least one listed session
carries a workspace, so all-unbound listings render exactly as before.
Co-authored-by: Cary Palmer <palmer@dugoutfantasy.com>
* feat(cli): restore cwd on resume (--no-restore-cwd)
Resuming a session cd's back into its recorded working directory, so it resumes
in the repo it belonged to. `--no-restore-cwd` opts out; skipped under
--worktree (that path owns its dir); best-effort — a missing dir warns and stays
put rather than failing the resume.
Co-authored-by: Cary Palmer <palmer@dugoutfantasy.com>
* chore(attribution): map palmer@dugoutfantasy.com -> professorpalmer (#48591 salvage)
* fix(desktop): keep answered clarify Q&A visible in the transcript
Answered clarifies were collapsing into a generic tool row, hiding the
choice. Settle into a Q&A panel instead, and route freeform input through
the shared Textarea chrome.
* test(desktop): cover settled clarify answer rendering
* docs(desktop): add judgment-first AGENTS guide and align DESIGN/README
Capture durable Desktop engineering principles from recent sessions —
state by authority, workspace-switch shapes, resolver ladders, optimistic
UI — and point root AGENTS.md at the scoped guide with current filenames.
* feat(desktop): bridge WSL paths for a Windows host + WSL backend
When the desktop UI runs on Windows and the gateway runs in WSL, a WSL/POSIX
cwd isn't openable/readable from the Windows host. Add wsl-path-bridge.ts to
translate the Windows-side direction only:
- native folder dialog defaultPath: `/home/...` → `\\wsl.localhost\<distro>\...`
- fs read path: WSL cwd → its UNC / `C:\` drive form
Distro detection reads `wsl.exe -l -q` with `WSL_UTF8=1` and strips stray NUL
bytes, since older wsl.exe emits UTF-16LE (microsoft/WSL#4607) — the original
utf8 read returned a garbled distro name. UNC uses `\\wsl.localhost\` with a
`\\wsl$\` fallback for older Windows. The reverse (any path → POSIX) is handled
once gateway-side, so the picker result needs no desktop translation.
Co-authored-by: Rage Lopez <VrtxOmega@pm.me>
* feat(gateway,acp): translate cross-boundary cwd when running in WSL
Add shared translators in hermes_constants (Windows drive → /mnt, `\\wsl(.localhost|$)\`
UNC → POSIX, gated on is_wsl) and apply them at the gateway session-cwd boundary
so a Windows-host UI can hand the WSL backend a path it can actually chdir into.
De-dups the ACP adapter's private `_win_path_to_wsl` onto the shared helper and
extends it to the UNC spelling.
Co-authored-by: Rage Lopez <VrtxOmega@pm.me>
* chore(attribution): map VrtxOmega@pm.me -> VrtxOmega (#43809 salvage)
* test(desktop): satisfy ToolCallMessagePartProps in clarify tests
CI typecheck requires argsText, status, addResult, and resume on rendered
tool parts.
* fix(windows): rewrite native drive paths to /c/ form for bash file ops
ShellFileOperations builds bash commands (wc/head/sed/cat/tee ...) with the
target path as an argument. On a Windows/Git-Bash host a native `C:\...` path
has its backslashes eaten by bash (and mangled by the msys runtime even when
single-quoted) — the "Directory \drivers\etc does not exist; exiting — update
your msys package" class of failures. Rewrite a native drive path to forward
slashes in `_escape_shell_arg`, reusing the env layer's `_windows_to_msys_path`.
Both `C:/...` and `/c/...` fix the backslash bug (the MSYS coreutils resolve
either via the POSIX API). We emit `/c/...` purely for consistency: it's the
same form `_windows_to_msys_path` already produces for the terminal `cd`
(LocalEnvironment._quote_cwd_for_cd), so shell file ops and `cd` share one
helper and one path form.
Scoped from #55481, which also patched BaseEnvironment._quote_cwd_for_cd — but
LocalEnvironment already overrides that through `_windows_to_msys_path`, so on a
real Windows host the base branch never ran (the cwd is already `/c/...`).
Co-authored-by: konsisumer <der@konsi.org>
* fix(model-switch): don't treat an exhausted credential pool as authenticated
An aggregator whose pooled credentials are all exhausted/dead still counted as
an authenticated provider during no-provider /model resolution. It then won the
model-name match, was set as the sticky session provider, and poisoned every
later switch with "empty API key" errors while still routing through the dead
aggregator.
list_authenticated_providers now requires a pool to have at least one available
entry (has_available, not has_credentials / bare key presence) at all three
credential-pool gates. Simple token-style entries that don't parse into
exhaustion-tracked entries keep the prior behaviour, so providers whose creds
live only in the auth-store credential_pool still appear.
Fixes #45759
* refactor(model): centralize picker credential availability
* fix(skills): install referenced bundle files with scan provenance
* fix(skills): bind bundles to exact files and origins
* fix(auth): enforce credential pool provider boundaries (#63048)
Retain the provider-boundary core of #52799 while reusing the pool reload and handoff paths already landed in #53591 and #62417.
Co-authored-by: Flownium <157689911+itsflownium@users.noreply.github.com>
* fix(desktop): autosave Mixture-of-Agents preset edits
MoA was internally inconsistent: preset-level ops (set default / add /
delete) persisted on click, but reference-model and aggregator slot edits
sat behind a manual Save button. Debounce-persist slot/aggregator edits
like the rest of settings and drop the redundant button, so MoA is
uniformly autosave.
* fix(model_switch): filter /model picker for unregistered providers (#57503)
list_authenticated_providers() emits picker rows for every slug in
PROVIDER_TO_MODELS_DEV that has any credential env-var set. Several of
those slugs (notably 'mistral') have no PROVIDER_REGISTRY entry, so
resolve_provider() rejects them as 'Unknown provider' once the user
selects a model — leaving the picker showing rows that cannot actually
be selected.
Add a resolve-gate in section 1: if PROVIDER_REGISTRY.get(hermes_id)
is None, skip the slug. The picker now only lists providers that can
actually be switched to at runtime.
This automatically resolves the duplicate-Mistral dedup symptom too:
once the broken-from-models.dev row is filtered, the conflict between
PROVIDER_TO_MODELS_DEV['mistral'] and a custom_providers 'Mistral' row
is moot.
Composes with #50289 (which promotes mistral to first-class via the
provider-plugin path): when that lands, PROVIDER_REGISTRY gains a
'mistral' entry and the gate becomes a no-op for it. No conflict.
Tests (regression suite):
- tests/hermes_cli/test_model_switch_filter_unresolved.py (new, 4 tests):
Picker excludes 'mistral' when MISTRAL_API_KEY is set; 'deepseek' and
'xai' (PROVIDER_REGISTRY-backed) still appear; 'mistral' stays
excluded when no key is set. Confirmed by reverting the fix and
seeing the test fail with 'mistral leaked into /model picker'.
Cross-checked against the existing 51 test_model_switch_* and
test_custom_provider_* cases — 55/55 PASS, no regressions.
* test(model): simplify routable picker invariants
* refactor(model): gate picker rows by runtime capability
* fix(model): merge configured models into picker rows (#63055)
Preserve the root cause and precedence direction from #43538 while applying the merge before truncation and covering all declared model shapes.
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
* fix(web): preserve declared providers in model writes (#63058)
Unify the named-provider fixes from #52506, #57185, #60337, and #60901 at the main-model normalization chokepoint.
Co-authored-by: izumi0uu <izumi0uu@gmail.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: Paulo Henrique <paulohenrique_789@hotmail.com>
* fix(api): keep live runs tracked past stream ttl
* fix(api): separate run control from stream lifetime
* fix(api): stop producers after run transport expires
* fix(kanban): make scratch cleanup explicit in dashboard (#63123)
* fix(approval): allow verifier temp cleanup
* chore(release): map @ansel-f in AUTHOR_MAP
* fix(desktop): structured Fallback Models editor
Settings → Model rendered `fallback_providers` (a list of `{provider,
model}` objects) through the generic `list` config field, which does
`value.join(', ')` and stringified each entry to `[object Object],
[object Object]`.
Add a dedicated provider+model row editor (add/remove), sourced from the
same `getGlobalModelOptions()` the composer picker uses, that reads and
writes the `{provider, model}` chain. Half-filled rows are kept in local
state so the config autosave never persists a partial entry, and an
out-of-catalog model stays selectable so existing custom entries render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(desktop): cover the Fallback Models editor
Asserts each {provider, model} entry renders as its own row (the bug
produced "[object Object]"), that removing a row emits the remaining
entries, that adding a blank row never persists a partial pair, and the
empty-state hint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(desktop): resync fallback editor after config reload
* fix(dashboard): correct approvals.mode select options
The web UI CONFIG_SCHEMA showed ['ask', 'yolo', 'deny'] for the
approvals.mode select field. These don't match any real config values
and 'smart' mode was entirely unreachable from the dashboard.
Correct the options to ['manual', 'smart', 'off'] which match the
values defined and documented in hermes_cli/config.py.
Adds a regression test to TestBuildSchemaFromConfig to pin the correct
option names and guard against future drift.
Fixes #31925
* docs(dashboard): align approval mode guidance
* Fix mobile channel setup modal
* feat(kanban): collect project directory when creating boards (#63249)
* fix(approval): honor canonical gateway timeout
* chore(release): map @jakelongvu-bot in AUTHOR_MAP
* fix(desktop): stop the submit drift guard from aborting every new chat
The #54527 context pin (7acaff5ef) snapshots the selected stored session
and route token at submit entry and aborts when either changes mid-flight.
But a NEW chat's create pipeline legitimately moves both: on success,
createBackendSessionForSend re-homes selection and navigates to the chat
it just minted. Judged against the pre-create draft baseline that read as
a user switch, so every first send of a new chat aborted before
prompt.submit — message dropped, no DB row persisted (row creation is
lazy, server-side in prompt.submit), and the window stranded on a route
whose REST reads 404 "Session not found" forever.
Fix: after a successful create, verify no one re-homed during create's
post-commit await via the active-session ref (a non-null return
guarantees create set it; every switch path retargets it synchronously),
then re-pin the drift baseline to the created chat. A mid-create switch
still aborts through create's own null return, or through the active-ref
check for the post-commit window. Re-pinning also restores the correct
stored-id association for the optimistic-message state updates, which the
pinned pre-create null had degraded.
Tests: red-first regression for the new-chat send, an abort case for a
switch landing in create's post-commit window, and the sleep/wake
new-chat stub made faithful to the real create (it sets the active ref
before returning — the inert stub is what let this ship green).
* fix(windows): bash-safe snapshot paths after #63113
#63113 rewrote native drive paths in ShellFileOperations, but init_session
/_wrap_command still embedded C:/... hermes-snap paths from get_temp_dir.
MSYS arg-converts those during bash -l and surfaces Directory \drivers\etc
— including for relative write_file targets, since the wrapper is the fault.
Add _bash_safe_path, override BaseEnvironment._quote_shell_path on
LocalEnvironment (no base→local import), and normalize mixed /c/Users\...
paths in file ops.
Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
* fix: reject empty credential pool leases (#63620)
* fix(kanban): preserve scratch completion artifacts
* fix(kanban): harden durable artifact handoff
* chore(release): map @yinkev in AUTHOR_MAP
* fix(kanban): spawn goal_mode workers with -Q so the goal loop actually runs
_default_spawn sets HERMES_KANBAN_GOAL_MODE=1 but launched 'chat -q' without
-Q; _run_kanban_goal_loop_q only executes in the quiet single-query branch,
so goal-mode never ran for dispatcher-spawned workers — they got one turn,
printed text, exited rc=0, and tripped the protocol-violation circuit
breaker (2026-06-09, cards t_d9cbe312 et al). Root-cause report + upstream
issue draft in kanban workspace t_720c5c60.
* fix(agent): honor custom-provider extra_body for multi-model catalogs
_custom_provider_model_matches() only compared the session model
against the entry's single 'model' field. A custom provider declaring
a multi-model catalog (providers.<name>.models mapping / models list)
whose default model differed from the session model silently failed to
match — dropping the entry's extra_body entirely. Real impact: an
OpenAI custom provider pinning service_tier=flex via extra_body ran
every request at STANDARD tier (~2.3x billing) with zero signal.
- Model matching now accepts the session model when it appears in the
entry's models catalog (dict keys or list), case-insensitive;
single-model 'model' field behavior unchanged; entries with neither
still match everything.
- Usage report ('hermes -z --usage-file') now carries service_tier
(the tier requested via request_overrides.extra_body) so batch
pipelines can audit the billed tier per run.
Validation: 8 new tests; live E2E via real 'hermes -p sweeper -z'
with httpx-level wire capture — service_tier=flex present in the
outgoing /v1/responses body and in the usage report.
* fix(image-gen): classify unsupported Codex image accounts
* test(image-gen): cover Codex capability HTTP boundary
* fix(config): preserve string-typed config values
* feat(kanban): surface final_result for Done cards; show run summary when task.result is empty
* fix(kanban): make Done-card results actionable
* test(kanban): remove duplicate final-results footer
* fix(approval): emit observer hooks for smart verdicts
* fix(patch): ignore inert context-only hunks (#63678)
* feat(desktop): add profile-aware approval mode control
* chore(release): map @Tortugasaur desktop commits
* feat(dashboard): add session import flow
* fix(sessions): validate imported session payloads
Reject metadata that would make session queries fail, bound import work, and detach cyclic lineage links. Guard lineage traversal against pre-existing corrupt cycles.
* fix(whatsapp_cloud): gate interactive taps on DM allowlist
* fix(approval): scope smart deny owner overrides to one operation
Co-authored-by: Sergei Ivanov <kavi@local.hermes>
* chore(release): map @kavioavio in AUTHOR_MAP
* test(approval): isolate smart observer redaction failure
* fix(gateway): deduplicate completion delivery
* feat(delegation): persist background completions
* fix(delegation): harden durable completion delivery
* chore(release): map delegation contributor
* docs(delegation): clarify background lifetime
* fix(desktop): keep draft fallback rows across autosave echo
Add fallback only updates local editor state; complete pairs are filtered
before onChange. The post-#7b5ba205 resync effect then saw the unchanged
persisted chain and wiped the draft — button looked dead.
Ignore value updates that match the last chain we emitted; still resync
on real external changes (profile/config reload).
Co-authored-by: HexLab98 <liruixinch@outlook.com>
* chore: record Nous upstream baseline 7f7a40381e86
---------
Co-authored-by: yoma <yingwaizhiying@gmail.com>
Co-authored-by: Jakub Wolniewicz <jakub.wolniewicz@gmail.com>
Co-authored-by: Ben <ben@nousresearch.com>
Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
Co-authored-by: Sami Rusani <sr@samirusani>
Co-authored-by: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com>
Co-authored-by: gigakun3030 <gigakun@agentmail.to>
Co-authored-by: HexLab98 <liruixinch@outlook.com>
Co-authored-by: infinitycrew39 <infinitycrew39@gmail.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: PRATHAMESH75 <prathamesh290504@gmail.com>
Co-authored-by: giggling-ginger <110955495+giggling-ginger@users.noreply.github.com>
Co-authored-by: Wilson Kinyua <wilsonkinyuam@gmail.com>
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
Co-authored-by: ethernet <arilotter@gmail.com>
Co-authored-by: embwl0x <embwl0x@users.noreply.github.com>
Co-authored-by: joaomarcos <joaomarcosdias444@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>
Co-authored-by: solyanviktor-star <233359899+solyanviktor-star@users.noreply.github.com>
Co-authored-by: dsad <sswdarius@gmail.com>
Co-authored-by: briandevans <252620095+briandevans@users.noreply.github.com>
Co-authored-by: konsisumer <der@konsi.org>
Co-authored-by: Alex Jestin Taylor <alex@fireworks.ai>
Co-authored-by: Thomas Connally <51974392+tcconnally@users.noreply.github.com>
Co-authored-by: Jan Hermes Integration <jan@mg5.org>
Co-authored-by: izumi0uu <izumi0uu@gmail.com>
Co-authored-by: DatTheMaster <hermesagent424@gmail.com>
Co-authored-by: Igor Ganapolsky <iganapolsky@gmail.com>
Co-authored-by: TheTom <tturney1@gmail.com>
Co-authored-by: HexLab <8422520+HexLab98@users.noreply.github.com>
Co-authored-by: Adolanium <94890352+Adolanium@users.noreply.github.com>
Co-authored-by: alelpoan <alelpoan@proton.me>
Co-authored-by: zapabob <1920071390@campus.ouj.ac.jp>
Co-authored-by: Tony Antunez <57689194+smtony@users.noreply.github.com>
Co-authored-by: Esthon Jr <esthon@gmail.com>
Co-authored-by: harjoth <harjoth.khara@gmail.com>
Co-authored-by: Maxim M <true@supersynergy.de>
Co-authored-by: Cary Palmer <palmer@dugoutfantasy.com>
Co-authored-by: Rage Lopez <VrtxOmega@pm.me>
Co-authored-by: AIalliAI <285906080+AIalliAI@users.noreply.github.com>
Co-authored-by: Flownium <157689911+itsflownium@users.noreply.github.com>
Co-authored-by: Ahmett101 <297889955+Ahmett101@users.noreply.github.com>
Co-authored-by: Paulo Henrique <paulohenrique_789@hotmail.com>
Co-authored-by: ansel-f <135129512+ansel-f@users.noreply.github.com>
Co-authored-by: Mark Vlcek <markvlcek@gmail.com>
Co-authored-by: Robert Blaas <robert@modern-minds.ai>
Co-authored-by: Shannon Sands <shannon.sands.1979@gmail.com>
Co-authored-by: Jeffrey Quesnelle <emozilla@nousresearch.com>
Co-authored-by: Jake Long Vu <jake.long.vu@vucar.net>
Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
Co-authored-by: Kevin Yin <182213728+yinkev@users.noreply.github.com>
Co-authored-by: agent <agent@agents-Mac-mini.local>
Co-authored-by: iborazzi <ibrahimozsarac@gmail.com>
Co-authored-by: hellno <hellno@users.noreply.github.com>
Co-authored-by: Luigi Razon <luigi@users.noreply.github.com>
Co-authored-by: SahilRakhaiya05 <144577420+SahilRakhaiya05@users.noreply.github.com>
Co-authored-…
vashkartik
added a commit
to vashkartik/hermes-agent
that referenced
this pull request
Jul 13, 2026
* fix(desktop): preserve project cwd for new sessions
* fix(desktop): dismiss stale prompt overlays
* feat(desktop): Hermes Cloud connection mode — one sign-in, agent discovery, silent connect
Adds a third "Hermes Cloud" gateway mode to the desktop app: one portal
sign-in auto-discovers the agents on your account and connects to any of
them with no second interactive prompt.
- Electron: widen connection mode to 'local' | 'remote' | 'cloud', routed
through a centralized modeIsRemoteLike() so every resolution site treats
cloud exactly like remote; portal discovery (GET /api/agents over the
OAuth partition), Privy-cookie liveness, multi-org picker (NAS 409), and a
silent per-agent /oauth cascade (load protected root, not /login).
- Persist a cloudOrg on the cloud block; unselect cloud on mode switch.
- Renderer: Hermes Cloud ModeCard + agent picker (signed-out/loading/empty/
list), org picker, Change-org, connected-highlight + Connected pill.
- i18n (en + zh full; ja/zh-hant inherit via defineLocale), Cloud icon.
- IPC: hermes:cloud:{status,login,logout,discover,agent-sign-in}.
Salvage of #55402 onto current main: the original branch predates the
desktop electron .cjs -> .ts migration (39d09453f), so the electron half
was re-authored against the .ts files. Authorship preserved.
cloud-auto-discovery Phases 3 + 4.
* polish(desktop): normalize cloud-URL highlight match + correct signedIn doc
Cleanups on top of @ben's Hermes Cloud salvage:
- isConnectedAgent normalized both sides of the cloud-URL comparison (trim +
drop trailing slash + lowercase). The saved URL is host-lowercased by
normalizeRemoteBaseUrl but the discovered dashboardUrl is raw from NAS, so
a host-casing difference could silently break the connected-highlight.
- DesktopCloudStatus.signedIn doc said "AT-or-RT"; it actually reflects the
Nous portal Privy session (privy-token), not the gateway cookies.
* refactor(desktop): DRY the cloud helpers
Tighten the salvaged Hermes Cloud code with no behavior change:
- main: one `trimCloudOrg` projection reused by the success-echo and the 409
org list (drop the duplicated map), and a `cloudLoginError()` factory for the
three needsCloudLogin throw sites.
- renderer: a `cloudLoginLapsed()` predicate for the duplicated
needsCloudLogin→signed-out check.
* feat(desktop): point the no-agents link at the Hermes Cloud instance-setup page
Per review: the empty-state "create an agent" link went to the generic portal
agents list; point it at the Hermes Cloud create-instance flow
({portal}/cloud?setup=instance) instead. Derive the host from the portalBaseUrl
that cloud.status() already echoes so it honors HERMES_PORTAL_BASE_URL rather
than hardcoding a second copy of the portal host. Link text/copy → "Hermes
Cloud" (en + zh).
* simplify(desktop): hardcode the Hermes Cloud setup link
Drop the portalBaseUrl→IPC→useState plumbing I added for the "create an agent"
link. HERMES_PORTAL_BASE_URL is a dev/staging-only override; threading it
through cloud.status() into React state just to build one link isn't worth it —
in prod it's always portal.nousresearch.com. Module-level constant instead.
* simplify(desktop): inline the cloud setup link like the rest of the app
Match the sibling pattern (pet-generate/generate-unavailable.tsx): inline the
portal URL literal in the ExternalLink href instead of a one-off named const.
* feat(desktop): soft gateway switch + gateway-settings polish
Switching connection mode (local / cloud agent / remote) no longer
full-window-reloads into the cold-boot CONNECTING screen. The primary
backend is torn down in place (no renderer reload); the shell + Settings
stay up while session lists are wiped so sidebar skeletons retrigger, then
the socket re-dials and config/sessions refresh. Cold-boot CONNECTING
latches off after the first successful boot; the intentional teardown
suppresses the backend-exit toast. Dev affordance: a "Preview soft switch"
button under Gateway diagnostics (Electron has no ?query= entry).
Gateway settings UI brought in line with the rest of Settings:
- Mode cards use the shared selectableCardClass on an equal-height
auto-rows-fr grid, stacking 1→3 (never an orphaned 2+1); titles wrap
instead of truncating.
- Remote gateway's auth detail moves into a ? tooltip in the title; drop
the redundant "connects to the one you choose" from the cloud card.
- textStrong buttons force px-0 so the underline sits flush with the label.
- Tooltip chip uses box-decoration-break: clone so the background hugs each
wrapped line (bg only on the text), capped at max-w-64.
Fully i18n'd (en + zh; ja/zh-hant inherit via defineLocale).
* fix(desktop): stop Tip from sticking open and blocking clicks
Radix's hoverable-content grace area can leave tips stuck over Electron drag regions; disable it and make tip content pointer-events-none so open state tracks the trigger only.
* feat(agent): core affection reaction detector + reaction_callback
Add a token-free, curated affection matcher (agent/reactions.py) — the single
source of truth for detecting user "vibes" (ily / <3 / good bot / heart emoji).
No model call, no tokens. Generalized to return a reaction *kind* so future
reactions can ride the same signal.
Wire an opt-in AIAgent.reaction_callback that fires from build_turn_context on
the incoming user message. It never touches the conversation (cache-safe) and
never fatal — a purely cosmetic side-beat each host can consume.
* feat(gateway,cli): emit + consume the reaction signal
tui_gateway forwards reaction_callback as a `reaction` event (shared by the TUI
and the desktop app). The interactive CLI wires reaction_callback to flash the
pet's celebrate ("jump") pose — the CLI's analogue of hearts.
* refactor(tui): drive the vibe heart from the core reaction event
Replace the client-side GOOD_VIBES_RE detection with the backend `reaction`
event: on it, flash the status-bar heart and the pet's celebrate pose. Detection
now lives once in the core, so the TUI, CLI, and desktop stay in sync.
* feat(desktop): TikTok-style vibe hearts on a reusable particle system
Add a glyph-agnostic ParticleField (float-up + organic sway/bank + springy
pop-in), skinned as pink pixel hearts. Hearts play on the pet when one is out
(in-window or popped out) and celebrate alongside; otherwise they rise from the
composer. A generic $petReaction bus mirrors the burst to the pop-out overlay
window so it reacts even while the app is minimized.
Consume the core `reaction` event to fire hearts on affectionate messages. DEV
Shift+H previews a burst.
* fix(auth): recover runtime Nous token from shared store
* fix(auth): recompute Nous routing after shared recovery
* fix(auth): validate and persist shared Nous routing
* test(auth): pin runtime routing persistence on failure
* fix(models): remove unavailable OpenCode Zen free models (#61163)
* fix(tui): discover MCP tools in slash workers
* test(tui): cover profile-local MCP discovery
* fix(curator): forward credential pool from runtime resolution
Curator review forks now pass credential_pool and request_overrides from resolve_runtime_provider into AIAgent so pool-backed custom providers can rotate credentials on 401 like main chat.
* test(curator): assert review fork forwards pool and overrides
Regression test that _run_llm_review passes credential_pool and request_overrides from resolve_runtime_provider into the curator AIAgent fork.
* fix(runtime): preserve resolved fork metadata
* fix(runtime): preserve resolved fork metadata
* fix(tools): handle dict URLs in web_extract display and tool processing
When web_search results are passed directly to web_extract, the URLs
field contains dict objects (e.g., {"url": "...", "title": "..."})
rather than plain URL strings. Two code paths assumed URLs were always
strings and crashed:
- agent/display.py get_cute_tool_message for web_extract: tried to call
url.replace() on a dict, causing AttributeError
- tools/web_tools.py web_extract_tool loop: tried regex search on a dict,
causing TypeError
Both now extract the URL string from dict objects (url or href field) or
fall back to empty string, preserving the cosmetic display and allowing
the tool to process the URLs correctly.
Fixes #61693
* fix(web): harden extract input and display boundaries
* test(web): cover model-facing dict URL dispatch
* fix(web): preserve extract result input order
* fix(web): handle short extract provider results
* fix(display): harden fallback label formatting
* fix(acp): unwrap web extract object titles
* fix(telegram): classify PTB heartbeat transport errors
* test(telegram): mirror PTB errors in heartbeat recovery
* fix(cron): never stale-remove a one-shot whose run is still alive
get_due_jobs()'s one-shot stale-entry recovery (#38758) treated an
expired run_claim (#59229) as proof the claiming tick died, but a run
stalled on network I/O — or a laptop asleep mid-run — legitimately
outlives the TTL while very much alive. The recovery then deleted the
job record mid-flight: list showed the job gone, and when the run
finished mark_job_run() found nothing to update, so last_run_at /
last_status / last_delivery_error were never recorded.
Two guards, per the liveness signals available:
- Same process (the common single-gateway case): before removing a
dispatch-limit-reached one-shot, consult the scheduler's running set
via a lazy import; if the job is still running here it is slow, not
stale — keep the entry.
- Cross process: run_job's monitor loop now refreshes run_claim.at
every 60s while the run is alive (including under
HERMES_CRON_TIMEOUT=0, which previously blocked without polling), so
an expired claim really does mean the owner died and the TTL stays a
dead-owner detector.
Fixes #62002
* fix(agent): release pool FDs on owning-thread client close (#61979)
force_close_tcp_sockets stayed shutdown-only after #29507 to avoid
cross-thread FD recycle. That left CLOSED sockets unreclaimed when
httpx.close() skipped already-shutdown sockets under long-lived
gateways (~1 CLOSED fd / 6 min via proxy).
Add release_fds= for the owning-thread dispose path only; abort still
defaults to shutdown-only.
* fix tui finalize persist drop conversation_history so disconnect saves chat
finalize passed conversation_history=history aliasing the snapshot so flush
skipped every message and wrote nothing. now flush _session_messages via
marker dedup like gateway shutdown. add real db e2e tests.
* chore: map WilsonKinyua release attribution
* feat(security): expose deterministic tool output risk (#61793)
* feat(security): expose deterministic tool output risk
* fix(security): emit output-risk events only for findings
* fix(desktop): don't emit js files when we build desktop
* fix(desktop): type-check electron/ in CI typecheck
removing tsc -b from the build script (previous commit) also removed
the only step that type-checked the electron/ directory — the CI
typecheck job runs tsc -p . --noEmit, which uses tsconfig.json whose
include is only ["src", "../shared/src"], so electron/ was silently
uncovered. extend the typecheck script to also run against
tsconfig.electron.json so electron/ stays type-checked in CI.
* fix(tui): dismiss expired sensitive prompts
* fix(desktop): only show slash popover when / is first char
The SLASH_TRIGGER_RE regex used (?:^|[\s]) as its left anchor, so typing
a / anywhere in the message (e.g. "hello /") opened the slash command
popover — even though slash commands only execute at the beginning of a
message. Anchor the regex strictly at position 0 (^) so the popover only
appears when / is the first character, matching the actual execution
semantics. The @-mention trigger is left untouched since those work
anywhere in the text.
* fix(desktop): remove old .js files
we built .ts into .js for a minute there and dumped em in src, and those
old .js files are getting resolved over top of the ts updated ones so
desktop clients don't update.
just --clean the old files so there's never a conflict :3
* chore(desktop): remove the DEV Shift+H heart preview
The real trigger (core `reaction` event on affectionate messages) is live, so
drop the dev-only hotkey and its always-mounted listener.
* feat(dev): add isolated sandbox script for local dev
scripts/desktop-sandbox.sh runs a Hermes desktop instance in an isolated
sandbox — separate HERMES_HOME, separate Electron userData, and a
distinct
app name (HERMES_DESKTOP_APP_NAME) so it doesn't compete with the main
desktop instance's single-instance lock.
Two modes:
- Ephemeral (default): temp dir, cleaned up on exit
- --persistent: stored under .hermes-sandbox/ in the worktree git root,
survives restarts for repeat testing
In the Nix devShell the script is available as 'sandbox'.
Also makes APP_NAME overridable via HERMES_DESKTOP_APP_NAME in main.ts —
app.setName() runs before requestSingleInstanceLock(), so the overridden
name changes the lock key. collectRelaunchEnv already preserves
HERMES_DESKTOP_* vars through self-update relaunches; test updated to
cover the new env var.
* Revert "fix(agent): release pool FDs on owning-thread client close (#61979)" (#62141)
This reverts commit cd7a8dfde08b3f637f0383136497a711b856db66.
* fix(cron): bind claim heartbeats to dispatch owner (#62155)
* fix(codex): drop oversized message ids on Responses input replay
Codex assigns assistant message items server-side ids that can run
400+ chars (base64 encrypted blobs), but the Responses API caps
input[].id at 64 chars and rejects the whole request with a
non-retryable HTTP 400. Once a session captures one of these long
ids, every subsequent turn replays it and 400s forever, since the
history persists it in codex_message_items.
Add a 64-char length guard at both replay sites — the history-to-
input converter and the final preflight gate — so oversized ids are
dropped while short ids (msg_...) are kept for prefix-cache hits.
Mirrors the existing pattern for reasoning items, which already
strip their id before replay because store=False means the API
can't resolve ids server-side anyway.
Fixes #27038
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(desktop): pin composer draft scope to the swap-effect owner, not the render ref
Fixes #54527 — a message typed into one TUI session could be silently
misrouted into (or overwritten by) another concurrently-open session.
Root cause: activeQueueSessionKeyRef is written on every render, but the
debounced draft-persist timer, the pagehide flush, and dispatchSubmit's
reject-restore path all read it lazily at async-resolve time instead of
capturing the scope that was active when the operation started. A session
switch landing between capture and resolve relabels one session's text
under the other session's key. A large paste widens the window (slower
synchronous render), which matches the original report.
Fix: introduce draftScopeRef, written only by the draft-swap effect (so it
always reflects the session whose text is actually loaded in the editor)
and read it instead of the render-time ref at both async write sites.
dispatchSubmit's restore() now uses the submittedScope already captured at
dispatch instead of re-reading the live ref.
Also adds isPendingDraftPersistCurrent as defense-in-depth: before the
debounce timer commits a write, it verifies its captured {scope, text}
pair is still the one on file. This is a no-op under the fix above (a
session swap or a newer keystroke already clears/replaces the pending
entry via clearTimeout), but turns any future regression that reintroduces
a stale/live-ref read at this call site into a dropped write instead of a
silent cross-session misroute.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(desktop): pin session context during async prompt submit (#54527)
Snapshot the selected stored session and route token for the full async submit
pipeline so a mid-flight session switch cannot resume the wrong chat or
misroute the user's text. Includes regression tests.
* fix(cron): run gateway cron LLM calls synchronously (#62151)
Cron jobs in the gateway process wedged before HTTP on later non-streaming
API calls because interruptible_api_call spawned a daemon worker inside
nested cron thread pools. Route cron platform turns through direct_api_call
on the conversation thread instead.
* test(cron): guard direct API path for gateway deadlock (#62151)
* fix(agent): run cron LLM calls inline to avoid gateway deadlock (#62151)
* fix(cron): keep inline dispatch behind the agent call seam
* fix(cron): abort inline requests on timeout
* fix(cron): scope inline calls to reported transport
* fix(telegram): recover final delivery after stream flood
* fix(telegram): harden flood fallback recovery
Keep empty-tail recovery scoped to the current stream segment and bound fallback flood retries. Preserve Telegram's server retry hint without blocking final delivery through a long cooldown.
* fix(codex): never replay message-item id on Copilot Responses connections
Copilot (api.githubcopilot.com/responses) binds replayed assistant
codex_message_items ids to a specific backend "connection". Credential-
pool rotation, a gateway restart, or routine load-balancer churn between
turns all invalidate that binding, and Copilot rejects the stale id with
HTTP 401 "input item ID does not belong to this connection" — even for
short ids well under the #27038 64-char length cap, since this is a
connection-scope problem, not a length problem. Once a session captures
one of these ids it is persisted and replayed forever, permanently
bricking the session.
Thread an is_github_responses flag from build_kwargs/convert_messages
into _chat_messages_to_responses_input and drop the id unconditionally
on that path, mirroring how reasoning items already strip id on replay.
phase/status/content are still replayed so cache-relevant signal isn't
lost — only the connection-scoped id is unsafe to reuse.
Written to apply independently of the #27038 length-cap fix so the two
PRs don't block each other; they touch adjacent conditions in the same
block and merge cleanly in either order.
Fixes #32716
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(codex): also guard the auxiliary Copilot Responses adapter
_CodexCompletionsAdapter (agent/auxiliary_client.py) is a second,
independent producer of Codex Responses input — used by auxiliary
calls (context compression, flush_memories, MoA aggregation,
session_search) that route through CodexAuxiliaryClient instead of
the main agent's ResponsesApiTransport.build_kwargs. It calls
_chat_messages_to_responses_input() directly without is_github_responses,
so the previous commit's fix didn't cover it: an auxiliary call made
against a Copilot-backed session could still replay a connection-scoped
codex_message_items id and hit the same HTTP 401.
Detect the Copilot host from the adapter's own client.base_url (same
check the adapter already does further down for prompt_cache_key
opt-out) and pass is_github_responses through, closing the gap.
Still #32716.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(codex): harden Copilot replay classification
Require literal booleans for backend-specific replay policy and pin
non-default status and content preservation through both response paths.
* fix(codex): enforce Copilot replay policy at dispatch
Reapply the endpoint-aware preflight after request and execution
middleware so no override can reintroduce a connection-scoped ID.
* test(codex): pin final replay preflight boundary
Exercise request and execution middleware replacements through the real
conversation loop and assert the provider payload is sanitized.
* security(providers): strip credential headers on cross-host redirects in fetch_models
fetch_models() sends Authorization: Bearer <api_key> plus any
default_headers (x-api-key etc.) via urllib.request.urlopen, and
urllib's redirect handler forwards every header when following a
3xx — including to a different host. A catalog endpoint (or a
compromised/misconfigured proxy in front of it) answering with a
redirect to another origin therefore received the provider API key.
Install an HTTPRedirectHandler that drops authorization, x-api-key,
api-key, x-goog-api-key and cookie when the redirect target hostname
differs from the original request, mirroring the pattern already used
in skills/creative/comfyui/scripts/_common.py. Same-host redirects
keep credentials so legitimate path-level redirects still work.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* compare full origin (scheme, host, port), not hostname, before keeping credentials
Review feedback: a same-host redirect to a different port can land on a
different service, which must not inherit the provider API key. Compare
(scheme, hostname, effective port) — with 80/443 defaults — instead of
hostname alone, and add a two-server regression test for the
same-host/different-port case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(models): strip credentials on catalog redirects
* test(models): preserve catalog urlopen monkeypatches
* fix(security): enforce one redirect credential policy
* test(models): patch the secured request seam
* fix(security): preserve installed urllib policies
* fix(security): cover remaining catalog credential paths
* fix(security): preserve opener-level header policy
* fix(security): sanitize after installed request hooks
* fix(security): order sanitizer after installed hooks
* fix(security): secure Azure catalog probes
* test(models): patch secured Novita pricing seam
* fix(agent): restore primary credential pool after fallback (#62417)
* fix(xai): recover legacy encrypted replay failures (#62420)
* fix(cli): preserve -t/-m/--provider/--tui/--dev before chat subcommand
`hermes -t web chat` silently dropped the toolset filter (and the same
hold true for `-m`, `--provider`, `--tui`, `--dev` placed before
`chat`). Reported in #28780 for `-t/--toolsets`; the others are sibling
failures with the same root cause.
Root cause: the chat subparser re-declared these flags with `default=None`
(or `default=False` for store_true) on top of the matching top-level
parser flags. When argparse dispatches into the subparser it shares the
namespace via `dest`, so the subparser's default overwrites whatever the
top-level parser parsed before the subcommand. `-s/--skills`, `-r/-c/-w`,
`--yolo`, and `--pass-session-id` already use `default=argparse.SUPPRESS`
for exactly this reason — the chat-subparser action becomes a no-op
unless the user explicitly passes the flag after `chat`, and the parent
value survives.
Reproduction (origin/main, before fix):
>>> parser.parse_known_args(["-t", "web", "chat"]).toolsets
None
>>> parser.parse_known_args(["chat", "-t", "web"]).toolsets
'web'
After fix:
>>> parser.parse_known_args(["-t", "web", "chat"]).toolsets
'web'
>>> parser.parse_known_args(["chat", "-t", "web"]).toolsets
'web'
Sibling flags fixed in the same commit because they share the exact same
argparse pattern bug — verified via a new contract test that scans every
chat-subparser action whose `dest` is also on the top-level parser and
asserts `default is argparse.SUPPRESS`. The test fails on origin/main
listing all five offenders and passes after this fix.
Test additions in tests/hermes_cli/test_argparse_flag_propagation.py:
- TestChatSubparserInheritedValueFlags exercising real `_parser` build
(not the hand-rolled replica) so it catches future drift.
- Parametrized before-chat / after-chat cases for `-t`, `--toolsets`,
`-m`, `--model`, `--provider`.
- Negative case: passing none of the flags leaves attrs at the top-level
parser's `None` default (SUPPRESS does not remove existing attrs).
- Combined case: all three value flags before `chat` simultaneously.
- store_true cases for `--tui` / `--dev`.
- Contract test asserting every shared-`dest` flag on chat uses SUPPRESS.
Fixes #28780.
* fix(agent): preserve none vs unknown tool effects (#61783)
* fix(agent): persist truthful tool effect dispositions
* fix(agent): preserve successful siblings during orphan recovery
* fix(agent): narrow effect dispositions to none and unknown
* fix(cli): keep current provider visible in model pickers
* fix(model): keep configured provider authoritative
* feat(providers): add Fireworks AI as preferred provider
Bundle Fireworks AI as a first-class BYOK provider across the CLI, web/TUI,
and desktop onboarding.
- New model-provider plugin with attribution headers (HTTP-Referer / X-Title)
so Fireworks can attribute Hermes traffic; PAYG-safe default aux + fallback
models (accounts/fireworks/models/...), IDs tracking fw-ai/fireconnect.
- Registered in CANONICAL_PROVIDERS so it appears in the CLI/web/TUI pickers.
- Alias wiring (fireworks-ai, fw) into both CLI resolvers.
- First-class wiring: OPTIONAL_ENV_VARS, HERMES_OVERLAYS (FIREWORKS_BASE_URL
override), doctor env hints. Live catalog + model_metadata are auto-derived.
- doctor: treat Fireworks' native slash-form IDs (accounts/fireworks/...) as
valid, not aggregator vendor prefixes, so it no longer tells Fireworks users
to switch to openrouter or drop the prefix.
- picker: plugin providers with no static curated list now lead with their
profile fallback_models, so the default is an agentic chat model instead of
whatever the live catalog returns first (Fireworks listed an image model,
flux-*, ahead of its chat models).
- Desktop onboarding: Fireworks as a RECOMMENDED hero card with the official
Fireworks logomark and a brand-purple badge, routing to the BYOK key form;
i18n in en/ja/zh/zh-hant.
- Tests: profile contract, first-class wiring (both resolvers, overlay, config,
doctor incl. the slash-form regression, aux headers, credentials), discovery
spot-check, and a live smoke test driven through the Hermes runtime.
Fire Pass (fpk_) support is coming soon; the future wiring is kept as a
commented-out scaffold in the plugin.
* fix(providers): align Fireworks integration with project policy
* docs: add Fireworks provider infographic
* feat(agent): track per-model token usage for mid-session model switches
The `sessions` table records only the initial (model, billing_provider)
for a session, so when a user switches models mid-session (via `/model`
or programmatically) every token — including the switched model's — is
attributed to the first model. Insights/billing reports then hide the
cost of the new model entirely (e.g. a session that started on deepseek
and switched to opus shows $0 for opus).
Add a `session_model_usage` table keyed (session_id, model,
billing_provider) that accumulates each per-API-call delta under the
model active at the time of the call. `update_token_counts()` is the
single chokepoint every per-call delta flows through (CLI, gateway,
cron, delegated, codex), so recording there captures accurate
attribution on every platform. Only the incremental path records — the
gateway's `absolute=True` summary overwrite is skipped to avoid
double-counting cumulative totals that can't be split per model. When a
call omits the model, it falls back to the session's recorded model,
matching the existing COALESCE-from-session summary behaviour.
Insights `_compute_model_breakdown` now aggregates tokens and cost from
`session_model_usage`, so a switched session splits correctly across
models, with a defensive fallback to the per-session aggregate for any
session lacking usage rows. A v17 migration backfills one usage row per
existing token-bearing session from its aggregate totals (idempotent via
INSERT OR IGNORE), validated lossless against a 1.3 GB production DB.
Tests: per-model recording, mid-session split, model fallback, absolute
no-double-count, v17 backfill, and an insights-level switch breakdown.
Fixes #51607.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(telemetry): persist first accounted fallback route
* fix(insights): harden per-route usage attribution
Preserve deletability, route identity, stored costs, aggregate reconciliation,
and zero-usage Codex route accounting on top of the salvaged per-model usage
work.
* chore: map usage attribution contributors
* fix(desktop): keep model picker switches session-scoped
Desktop active-session picker calls already pass a session_id, but the gateway's model switch persistence is controlled by parsed model flags. Add --session so the shared parser keeps live-session selections, including MoA virtual provider presets, out of profile config.yaml.
Constraint: config.set model values are parsed by hermes_cli.model_switch before persistence is decided.
Rejected: backend special-case for desktop session_id | it would duplicate existing --session semantics and widen the gateway surface.
Confidence: high
Scope-risk: narrow
Directive: Keep desktop model picker active-session switches explicit with --session; do not rely on session_id alone for persistence.
Tested: npm run test:ui -- src/app/session/hooks/use-model-controls.test.tsx src/app/shell/model-menu-panel.test.tsx
Tested: npm run typecheck
Tested: git diff --check
Not-tested: full pytest suite; change is desktop TypeScript/UI routing only.
* fix(tui): preserve picker session scope across all paths
Fold in the TUI direction from #61192 and cover the remaining new-live-session picker path with one shared session-argument normalizer.
Co-authored-by: DatTheMaster <hermesagent424@gmail.com>
* feat(gateway): add authenticated runtime readiness checks
* fix(gateway): ground readiness in live runtime state
* fix(compaction): anti-thrashing guard never fired; score against the threshold
`should_compress()` documents anti-thrashing protection ("if the last two
compressions each saved less than 10%, skip compression to avoid infinite
loops"). In practice `_ineffective_compression_count` reset on every pass,
so the guard was dead code and a mis-sized context window presented as a
hung CLI instead of a warning.
Two defects:
1. Mixed measurement bases. Effectiveness was
`(current_tokens - estimate(compressed)) / current_tokens`, where
`current_tokens` is the provider's FULL prompt (system prompt + tool
schemas + messages) but `estimate(compressed)` covers messages only.
Every compaction therefore reported ~96% savings and reset the counter.
Savings is now scored messages-vs-messages.
2. Message shrinkage is the wrong yardstick. `should_compress()` trips on
the full prompt, but compaction can only shrink messages -- the system
prompt and tool schemas are an incompressible floor. When that floor
alone meets the threshold, each pass shrinks messages by a healthy
margin, legitimately resets the counter, and still leaves the prompt
over the line; the next turn compacts again, forever. Observed in the
wild: 45+ consecutive compactions, one auxiliary-LLM call each, zero
progress. Effectiveness is now scored against the goal -- did the
projected prompt get under the threshold? -- and a futile pass warns
with the numbers that prove it.
Also record an ineffective pass on the "only N messages (need > M)" early
return, which previously returned the transcript unchanged without moving
any anti-thrash state -- the same class of bug the neighbouring
"no compressable window" branch was already fixed for.
Tests: 4 of the 5 new cases fail on main and pass here; the fifth pins
that effective compaction still resets the counter (121 -> 15 messages,
88.9% savings).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(compaction): check the threshold against real tokens, not an estimated floor
Follow-up to the previous commit, whose futility check was unsound:
incompressible_floor = max(0, display_tokens - pre_estimate)
`display_tokens` is the provider's real prompt count; `pre_estimate` is
`estimate_messages_tokens_rough(messages)`. Subtracting an estimate from a real
count folds the tokenizer skew into "floor" and misreads it as incompressible
overhead. With a 1.6x skew on a 200K window (threshold 150K, true floor 30K):
rough_msgs=253,804 real_prompt=436,086
computed floor = 182,282 <-- mostly skew; exceeds the threshold
after compaction: 401 -> 77 msgs, real prompt = 106,361 (CLEARS 150,000)
verdict: ineffective_count = 1 <-- false positive
Two such passes would permanently disable compaction on a healthy session --
worse than the loop this PR set out to fix.
Move the check into should_compress(), where both sides of the comparison are
the caller's own token count:
* prompt under the threshold -> not thrashing; reset the counter
* a compaction just ran and we are STILL over -> one strike
Real-vs-real, so tokenizer skew can never be mistaken for a floor, and nothing
subtracts an estimate from a real count. compress() now only ever increments the
counter; the reset lives with the one measure the trigger uses.
Adds `test_no_false_positive_under_tokenizer_skew` (the case above) and
`test_counter_resets_once_the_prompt_fits_again` (one failed pass must not
disable compaction forever). Against upstream, 5 of the 7 cases fail; the 2 that
pass are the regression guards, which is the intended shape.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(compaction): judge the anti-thrash verdict on real usage, not in should_compress
Third correction, and the load-bearing one. The previous commit put the
"did compaction clear the threshold?" verdict inside should_compress(). But
conversation_loop calls should_compress() TWICE per turn with two different
measures (turn_context.py / conversation_loop.py:1033 and :4789):
* pre-API : request_pressure_tokens -- a rough estimate that can dip BELOW
the threshold
* post-API: real prompt tokens -- which stay above it
So the rough reading reset the strike every turn and the loop never stopped.
Reproduced: 8 compactions in 8 turns under the real two-call pattern, even
with the previous fix applied. (My earlier repro only called should_compress()
once per turn, which is why it looked contained.)
Move the verdict to update_from_response(), the one place that sees the
provider's real prompt_tokens for the just-compacted conversation, guarded by
the existing awaiting_real_usage_after_compression flag so it fires exactly
once per compaction. Real-vs-real: it cannot be fooled by a rough sub-threshold
reading, and (from the previous commit's lesson) never subtracts an estimate
from a real count. should_compress() goes back to a plain threshold test plus
the pre-existing cooldown and anti-thrash guards.
New test test_rough_preflight_reading_does_not_reopen_the_loop drives the real
two-call-per-turn pattern and fails on the prior should_compress()-based commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(compaction): initialize anti-thrash fixture state
* chore: map PR #62125 contributor
* fix(compaction): arm verdict after successful boundary
* fix(compaction): clear stale anti-thrash verdicts
* test(codex): cover usage-less compaction response
* fix(codex): evaluate native compaction usage
* fix(codex): consume manual compaction usage gaps
* fix(codex): reject interrupted manual compaction
* fix(gateway): @ context reference expansion never ran (AttributeError)
GatewayRunner._prepare_inbound_message_text's "@" context-reference
block read self._model / self._base_url to resolve the model for
get_model_context_length_async. GatewayRunner never sets either
attribute (copy-pasted from HermesCLI in da44c196b, which does carry
self.model/self.base_url). Every message containing "@" raised
AttributeError inside the try block, silently swallowed by the
surrounding except Exception at debug level, so
preprocess_context_references_async never ran and @file:/@folder:/@diff/
etc. references passed through to the model unexpanded.
Fix: resolve model/provider/base_url via
self._resolve_session_agent_runtime(source=, session_key=,
user_config=), the same session-aware resolution the hygiene
compression block already uses a few hundred lines later in this file.
Also raise the swallow log from debug to warning (with exc_info at
debug) so a future regression here is visible instead of silent.
* fix(gateway): scope context refs to runtime profile
* fix(gateway): scope queued context references
* fix(gateway): honor runtime context budgets
* test(gateway): cover effective context budget
* Merge pull request #62600 from HexLab98/fix/desktop-cron-no-agent-editor
fix(desktop): allow editing script-only (no_agent) cron jobs without a prompt
* fix(tui): dispatch custom skill bundles as agent turns (#62859)
* fix(desktop-terminal): stop idle prompt accumulation across relaunches
An idle terminal tab (no command ever typed) grew one extra copy of the
shell's boot prompt on every close/reopen: persistSnapshot re-serialized a
buffer that was just the replayed old prompt plus the fresh shell's new
prompt, and cleanReviveSnapshot's blank-line trim can't strip prompts on
shells like default PowerShell that print no separator line.
Track real user input (keystrokes/paste, drag-and-drop paths, injected
commands) and, when a session had none, skip re-serializing. If the buffer
we loaded carried no real scrollback (empty or only a repeated prompt),
clear it so the next launch shows a single fresh prompt and any existing
accumulation heals; otherwise leave the prior snapshot untouched so real
history from an earlier active session survives an idle reopen.
Salvages #61584 (activity tracking) and #61577 (clearing content-free idle
buffers) into one path: it also heals already-polluted buffers, counts
drag-and-drop and injected input as activity, and never discards genuine
short command history (only empty/all-identical buffers are cleared).
Co-authored-by: alelpoan <alelpoan@proton.me>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
* fix(desktop-terminal): trim trailing idle prompt on no-separator shells
cleanReviveSnapshot only dropped the trailing prompt when a blank separator
sat above it (starship add_newline), so shells that print the prompt with no
preceding blank line — default PowerShell (PS C:\..>), bash user@host:~$ —
kept the idle prompt in the saved buffer and showed a duplicate under the
fresh boot prompt on every relaunch of an *active* session.
An interactive shell always reprints its prompt after a command, so the tail
of an idle buffer is the prompt, never history. Drop the short block after a
blank separator when present, otherwise drop the trailing single-line prompt.
Command output is preserved; the fresh shell reprints the live prompt on boot.
* fix(desktop-terminal): reopen terminal tabs in the last-used directory
A reopened tab restarted the shell in its original launch dir, so the fresh
prompt showed the wrong folder after a prior `cd` (the issue's "separate
thing" note). Track the shell's working directory and restart the PTY there.
Two independent signals feed a persisted per-tab restoreCwd:
- a main-side PTY cwd probe (shell-agnostic; /proc on Linux, lsof on macOS;
Windows has no cheap per-process query so it falls back to the launch dir)
- cwd-reporting OSC sequences parsed in the renderer (OSC 7 file URIs, OSC 9;9
ConEmu/Windows-Terminal paths) for shells configured to emit them
On relaunch the fresh shell boots in restoreCwd, falling back to the launch
cwd (then home) when it no longer exists.
* fix(desktop): stop empty mispositioned tooltip on terminal rail hover
A block-level label child (e.g. `flex`) collapses TooltipContent's inline
`box-decoration-clone` wrapper, so Radix measures a zero-size chip and parks
an empty black rectangle in the panel corner instead of by the trigger
(#62022). The terminal rail's hotkey labels and the preview row's two-line
label both hit this.
Harden the shared wrapper (`[&>*]:!inline-flex`) so any call site's direct
child renders inline-flex, add a reusable `TipHintLabel` for the common
text+hotkey label, and keep the preview row's label explicitly inline-flex.
Salvages #62139 (shared-component hardening + TipHintLabel) and #62073
(inline-flex call-site fixes + rail/preview coverage).
Co-authored-by: alelpoan <alelpoan@proton.me>
Co-authored-by: zapabob <1920071390@campus.ouj.ac.jp>
* feat: make smart approvals the default (#62661)
* feat(reasoning): add max and ultra effort levels (#62650)
* chore(desktop): drop the dev-only "soft switch" preview from gateway settings
Remove the DEV-gated "Dev · soft switch" ListRow and its previewGatewaySwitch
helper. It was a temporary review affordance for exercising the soft-switch
reconnect; dead-stripped from production, but it doesn't belong in the tree.
wipeSessionListsForGatewaySwitch (the real path) and $gatewaySwitching stay.
* refactor(desktop): extract isRemoteConfig from the reauth predicate
Factor the "remote/cloud with a URL" check out of isRemoteReauthFailure into a
shared isRemoteConfig helper so the boot-failure overlay can tell any remote
failure apart from a local one.
* feat(desktop): embeddable Gateway settings panel
Add an `embedded` flag to GatewaySettings (and a `bare` variant to
SettingsContent) that drops the page title/intro, Diagnostics row,
"Save for next restart", and the page gutters — so the same panel can be reused
inside a tighter surface without a second connection form to maintain. No change
to the standalone Settings → Gateway page (defaults off).
* fix(desktop): recover a failed gateway from the boot-failure screen
A remote/VPS backend that failed to boot trapped the user on the recovery
screen — Retry/Repair/Use-local only target the local backend, so the only fix
was hand-editing connection.json. Add an in-place "Gateway settings" view (the
real GatewaySettings panel embedded via `embedded`, lazy-loaded) reached from
the recovery card, and shape the recovery actions by failure kind: Sign in for a
lapsed remote session, Gateway settings for any other remote failure (Retry
drops to secondary; Repair is dropped — it can't revive a remote), Retry for a
local backend. Use-local is scoped to remote failures.
* fix(desktop): treat connected-but-expired remote sessions as reauth
Add isRemoteReauthError so an auth-shaped boot error counts as a remote-reauth
failure even when the session indicator still reads connected (a stale refresh
cookie / failed ws-ticket mint). Wire the boot error into the overlay's reauth
check so those sessions route to Sign in instead of the local-only recovery
buttons.
Co-authored-by: Tony Antunez <57689194+smtony@users.noreply.github.com>
* fix(desktop): clear the OAuth partition before remote sign-in
Sign out of the dedicated OAuth partition before opening the login window so a
stale gateway/identity-provider cookie can't silently bounce an expired session
straight back into failure. Relabel the action "Sign out & sign in" and spell
out the sign-out step in the hint.
Co-authored-by: Tony Antunez <57689194+smtony@users.noreply.github.com>
* fix(desktop): preserve legacy non-git workspace groups
* fix(desktop): mirror Windows path identity in live overlay + WSL spelling
Addresses @teknium1's review of #61950:
- The desktop live overlay (workspace-groups.ts) matched cwd membership
case-sensitively, so a fresh mixed-case/separator Windows session missed
its explicit/auto project until the next backend tree refresh. Mirror the
backend identity (isWindowsPath/comparisonSegments/pathKey) in isPathUnder,
liveSessionProjectId, and overlayRepoLanes lane matching. Comparison-only —
emitted ids/labels keep their spelling. POSIX stays case-sensitive.
- Backend _is_windows_path missed root-relative `\wsl.localhost\...` (single
leading backslash), leaving that historical spelling case-sensitive. Classify
any backslash-rooted path as Windows.
Tests: WSL-spelling collapse + explicit-project precedence (project_tree),
Windows/WSL live-overlay membership + POSIX case-sensitivity (workspace-groups).
* fix(desktop): preserve sidebar workspace targets across new drafts
Squashed salvage of #45744 (@harjothkhara), rebased onto current main and
resolved against #58241 (which swapped the new-session cwd fallback to the
project-aware resolveNewSessionCwd).
An explicitly clicked sidebar workspace stays authoritative until session.create:
a one-shot $newChatWorkspaceTarget (null → detached, string → that folder) plus a
generation counter so a stale async `config.get project` normalization can't
overwrite a newer draft target. The start-workspace-session action is extracted
out of desktop-controller.tsx into a testable workspace-session-target module.
Integrated with #58241: the no-explicit-target branch now falls through to the
project-aware resolveNewSessionCwd() instead of the old workspaceCwdForNewSession.
Co-authored-by: harjoth <harjoth.khara@gmail.com>
* chore(attribution): map esthon@gmail.com → esthonjr (#61950 salvage)
* feat(desktop): add workspace path status action
* refactor(desktop): text-only workspace status menu + attribution
Align the workspace status-bar dropdown with the rest of the status bar: drop
the per-item icons (they mixed lucide size-4 with a Codicon 1rem glyph and were
the only status-bar menu carrying item icons), leaving text-only items on the
shared DropdownMenuItem primitive with default typography. The status-bar
trigger keeps its FolderOpen glyph, consistent with sibling items.
Also map true@supersynergy.de → Supersynergy in AUTHOR_MAP.
* feat(sessions): workspace_key grouping helper + tests
A session's coarse workspace identity: its git repo root when known, else its
cwd (branch excluded, so switching branches doesn't fragment history). Pure
helper over fields sessions already record — no new columns, no git shelling.
Co-authored-by: Cary Palmer <palmer@dugoutfantasy.com>
* feat(cli): sessions list --workspace filter + Workspace column
`hermes sessions list --workspace <needle>` filters to one workspace (git repo
root or project dir, matched by path substring or basename) and adds a
Workspace column. The column only appears once at least one listed session
carries a workspace, so all-unbound listings render exactly as before.
Co-authored-by: Cary Palmer <palmer@dugoutfantasy.com>
* feat(cli): restore cwd on resume (--no-restore-cwd)
Resuming a session cd's back into its recorded working directory, so it resumes
in the repo it belonged to. `--no-restore-cwd` opts out; skipped under
--worktree (that path owns its dir); best-effort — a missing dir warns and stays
put rather than failing the resume.
Co-authored-by: Cary Palmer <palmer@dugoutfantasy.com>
* chore(attribution): map palmer@dugoutfantasy.com -> professorpalmer (#48591 salvage)
* fix(desktop): keep answered clarify Q&A visible in the transcript
Answered clarifies were collapsing into a generic tool row, hiding the
choice. Settle into a Q&A panel instead, and route freeform input through
the shared Textarea chrome.
* test(desktop): cover settled clarify answer rendering
* docs(desktop): add judgment-first AGENTS guide and align DESIGN/README
Capture durable Desktop engineering principles from recent sessions —
state by authority, workspace-switch shapes, resolver ladders, optimistic
UI — and point root AGENTS.md at the scoped guide with current filenames.
* feat(desktop): bridge WSL paths for a Windows host + WSL backend
When the desktop UI runs on Windows and the gateway runs in WSL, a WSL/POSIX
cwd isn't openable/readable from the Windows host. Add wsl-path-bridge.ts to
translate the Windows-side direction only:
- native folder dialog defaultPath: `/home/...` → `\\wsl.localhost\<distro>\...`
- fs read path: WSL cwd → its UNC / `C:\` drive form
Distro detection reads `wsl.exe -l -q` with `WSL_UTF8=1` and strips stray NUL
bytes, since older wsl.exe emits UTF-16LE (microsoft/WSL#4607) — the original
utf8 read returned a garbled distro name. UNC uses `\\wsl.localhost\` with a
`\\wsl$\` fallback for older Windows. The reverse (any path → POSIX) is handled
once gateway-side, so the picker result needs no desktop translation.
Co-authored-by: Rage Lopez <VrtxOmega@pm.me>
* feat(gateway,acp): translate cross-boundary cwd when running in WSL
Add shared translators in hermes_constants (Windows drive → /mnt, `\\wsl(.localhost|$)\`
UNC → POSIX, gated on is_wsl) and apply them at the gateway session-cwd boundary
so a Windows-host UI can hand the WSL backend a path it can actually chdir into.
De-dups the ACP adapter's private `_win_path_to_wsl` onto the shared helper and
extends it to the UNC spelling.
Co-authored-by: Rage Lopez <VrtxOmega@pm.me>
* chore(attribution): map VrtxOmega@pm.me -> VrtxOmega (#43809 salvage)
* test(desktop): satisfy ToolCallMessagePartProps in clarify tests
CI typecheck requires argsText, status, addResult, and resume on rendered
tool parts.
* fix(windows): rewrite native drive paths to /c/ form for bash file ops
ShellFileOperations builds bash commands (wc/head/sed/cat/tee ...) with the
target path as an argument. On a Windows/Git-Bash host a native `C:\...` path
has its backslashes eaten by bash (and mangled by the msys runtime even when
single-quoted) — the "Directory \drivers\etc does not exist; exiting — update
your msys package" class of failures. Rewrite a native drive path to forward
slashes in `_escape_shell_arg`, reusing the env layer's `_windows_to_msys_path`.
Both `C:/...` and `/c/...` fix the backslash bug (the MSYS coreutils resolve
either via the POSIX API). We emit `/c/...` purely for consistency: it's the
same form `_windows_to_msys_path` already produces for the terminal `cd`
(LocalEnvironment._quote_cwd_for_cd), so shell file ops and `cd` share one
helper and one path form.
Scoped from #55481, which also patched BaseEnvironment._quote_cwd_for_cd — but
LocalEnvironment already overrides that through `_windows_to_msys_path`, so on a
real Windows host the base branch never ran (the cwd is already `/c/...`).
Co-authored-by: konsisumer <der@konsi.org>
* fix(model-switch): don't treat an exhausted credential pool as authenticated
An aggregator whose pooled credentials are all exhausted/dead still counted as
an authenticated provider during no-provider /model resolution. It then won the
model-name match, was set as the sticky session provider, and poisoned every
later switch with "empty API key" errors while still routing through the dead
aggregator.
list_authenticated_providers now requires a pool to have at least one available
entry (has_available, not has_credentials / bare key presence) at all three
credential-pool gates. Simple token-style entries that don't parse into
exhaustion-tracked entries keep the prior behaviour, so providers whose creds
live only in the auth-store credential_pool still appear.
Fixes #45759
* refactor(model): centralize picker credential availability
* fix(skills): install referenced bundle files with scan provenance
* fix(skills): bind bundles to exact files and origins
* fix(auth): enforce credential pool provider boundaries (#63048)
Retain the provider-boundary core of #52799 while reusing the pool reload and handoff paths already landed in #53591 and #62417.
Co-authored-by: Flownium <157689911+itsflownium@users.noreply.github.com>
* fix(desktop): autosave Mixture-of-Agents preset edits
MoA was internally inconsistent: preset-level ops (set default / add /
delete) persisted on click, but reference-model and aggregator slot edits
sat behind a manual Save button. Debounce-persist slot/aggregator edits
like the rest of settings and drop the redundant button, so MoA is
uniformly autosave.
* fix(model_switch): filter /model picker for unregistered providers (#57503)
list_authenticated_providers() emits picker rows for every slug in
PROVIDER_TO_MODELS_DEV that has any credential env-var set. Several of
those slugs (notably 'mistral') have no PROVIDER_REGISTRY entry, so
resolve_provider() rejects them as 'Unknown provider' once the user
selects a model — leaving the picker showing rows that cannot actually
be selected.
Add a resolve-gate in section 1: if PROVIDER_REGISTRY.get(hermes_id)
is None, skip the slug. The picker now only lists providers that can
actually be switched to at runtime.
This automatically resolves the duplicate-Mistral dedup symptom too:
once the broken-from-models.dev row is filtered, the conflict between
PROVIDER_TO_MODELS_DEV['mistral'] and a custom_providers 'Mistral' row
is moot.
Composes with #50289 (which promotes mistral to first-class via the
provider-plugin path): when that lands, PROVIDER_REGISTRY gains a
'mistral' entry and the gate becomes a no-op for it. No conflict.
Tests (regression suite):
- tests/hermes_cli/test_model_switch_filter_unresolved.py (new, 4 tests):
Picker excludes 'mistral' when MISTRAL_API_KEY is set; 'deepseek' and
'xai' (PROVIDER_REGISTRY-backed) still appear; 'mistral' stays
excluded when no key is set. Confirmed by reverting the fix and
seeing the test fail with 'mistral leaked into /model picker'.
Cross-checked against the existing 51 test_model_switch_* and
test_custom_provider_* cases — 55/55 PASS, no regressions.
* test(model): simplify routable picker invariants
* refactor(model): gate picker rows by runtime capability
* fix(model): merge configured models into picker rows (#63055)
Preserve the root cause and precedence direction from #43538 while applying the merge before truncation and covering all declared model shapes.
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
* fix(web): preserve declared providers in model writes (#63058)
Unify the named-provider fixes from #52506, #57185, #60337, and #60901 at the main-model normalization chokepoint.
Co-authored-by: izumi0uu <izumi0uu@gmail.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: Paulo Henrique <paulohenrique_789@hotmail.com>
* fix(api): keep live runs tracked past stream ttl
* fix(api): separate run control from stream lifetime
* fix(api): stop producers after run transport expires
* fix(kanban): make scratch cleanup explicit in dashboard (#63123)
* fix(approval): allow verifier temp cleanup
* chore(release): map @ansel-f in AUTHOR_MAP
* fix(desktop): structured Fallback Models editor
Settings → Model rendered `fallback_providers` (a list of `{provider,
model}` objects) through the generic `list` config field, which does
`value.join(', ')` and stringified each entry to `[object Object],
[object Object]`.
Add a dedicated provider+model row editor (add/remove), sourced from the
same `getGlobalModelOptions()` the composer picker uses, that reads and
writes the `{provider, model}` chain. Half-filled rows are kept in local
state so the config autosave never persists a partial entry, and an
out-of-catalog model stays selectable so existing custom entries render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(desktop): cover the Fallback Models editor
Asserts each {provider, model} entry renders as its own row (the bug
produced "[object Object]"), that removing a row emits the remaining
entries, that adding a blank row never persists a partial pair, and the
empty-state hint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(desktop): resync fallback editor after config reload
* fix(dashboard): correct approvals.mode select options
The web UI CONFIG_SCHEMA showed ['ask', 'yolo', 'deny'] for the
approvals.mode select field. These don't match any real config values
and 'smart' mode was entirely unreachable from the dashboard.
Correct the options to ['manual', 'smart', 'off'] which match the
values defined and documented in hermes_cli/config.py.
Adds a regression test to TestBuildSchemaFromConfig to pin the correct
option names and guard against future drift.
Fixes #31925
* docs(dashboard): align approval mode guidance
* Fix mobile channel setup modal
* feat(kanban): collect project directory when creating boards (#63249)
* fix(approval): honor canonical gateway timeout
* chore(release): map @jakelongvu-bot in AUTHOR_MAP
* fix(desktop): stop the submit drift guard from aborting every new chat
The #54527 context pin (7acaff5ef) snapshots the selected stored session
and route token at submit entry and aborts when either changes mid-flight.
But a NEW chat's create pipeline legitimately moves both: on success,
createBackendSessionForSend re-homes selection and navigates to the chat
it just minted. Judged against the pre-create draft baseline that read as
a user switch, so every first send of a new chat aborted before
prompt.submit — message dropped, no DB row persisted (row creation is
lazy, server-side in prompt.submit), and the window stranded on a route
whose REST reads 404 "Session not found" forever.
Fix: after a successful create, verify no one re-homed during create's
post-commit await via the active-session ref (a non-null return
guarantees create set it; every switch path retargets it synchronously),
then re-pin the drift baseline to the created chat. A mid-create switch
still aborts through create's own null return, or through the active-ref
check for the post-commit window. Re-pinning also restores the correct
stored-id association for the optimistic-message state updates, which the
pinned pre-create null had degraded.
Tests: red-first regression for the new-chat send, an abort case for a
switch landing in create's post-commit window, and the sleep/wake
new-chat stub made faithful to the real create (it sets the active ref
before returning — the inert stub is what let this ship green).
* fix(windows): bash-safe snapshot paths after #63113
#63113 rewrote native drive paths in ShellFileOperations, but init_session
/_wrap_command still embedded C:/... hermes-snap paths from get_temp_dir.
MSYS arg-converts those during bash -l and surfaces Directory \drivers\etc
— including for relative write_file targets, since the wrapper is the fault.
Add _bash_safe_path, override BaseEnvironment._quote_shell_path on
LocalEnvironment (no base→local import), and normalize mixed /c/Users\...
paths in file ops.
Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
* fix: reject empty credential pool leases (#63620)
* fix(kanban): preserve scratch completion artifacts
* fix(kanban): harden durable artifact handoff
* chore(release): map @yinkev in AUTHOR_MAP
* fix(kanban): spawn goal_mode workers with -Q so the goal loop actually runs
_default_spawn sets HERMES_KANBAN_GOAL_MODE=1 but launched 'chat -q' without
-Q; _run_kanban_goal_loop_q only executes in the quiet single-query branch,
so goal-mode never ran for dispatcher-spawned workers — they got one turn,
printed text, exited rc=0, and tripped the protocol-violation circuit
breaker (2026-06-09, cards t_d9cbe312 et al). Root-cause report + upstream
issue draft in kanban workspace t_720c5c60.
* fix(agent): honor custom-provider extra_body for multi-model catalogs
_custom_provider_model_matches() only compared the session model
against the entry's single 'model' field. A custom provider declaring
a multi-model catalog (providers.<name>.models mapping / models list)
whose default model differed from the session model silently failed to
match — dropping the entry's extra_body entirely. Real impact: an
OpenAI custom provider pinning service_tier=flex via extra_body ran
every request at STANDARD tier (~2.3x billing) with zero signal.
- Model matching now accepts the session model when it appears in the
entry's models catalog (dict keys or list), case-insensitive;
single-model 'model' field behavior unchanged; entries with neither
still match everything.
- Usage report ('hermes -z --usage-file') now carries service_tier
(the tier requested via request_overrides.extra_body) so batch
pipelines can audit the billed tier per run.
Validation: 8 new tests; live E2E via real 'hermes -p sweeper -z'
with httpx-level wire capture — service_tier=flex present in the
outgoing /v1/responses body and in the usage report.
* fix(image-gen): classify unsupported Codex image accounts
* test(image-gen): cover Codex capability HTTP boundary
* fix(config): preserve string-typed config values
* feat(kanban): surface final_result for Done cards; show run summary when task.result is empty
* fix(kanban): make Done-card results actionable
* test(kanban): remove duplicate final-results footer
* fix(approval): emit observer hooks for smart verdicts
* fix(patch): ignore inert context-only hunks (#63678)
* feat(desktop): add profile-aware approval mode control
* chore(release): map @Tortugasaur desktop commits
* feat(dashboard): add session import flow
* fix(sessions): validate imported session payloads
Reject metadata that would make session queries fail, bound import work, and detach cyclic lineage links. Guard lineage traversal against pre-existing corrupt cycles.
* fix(whatsapp_cloud): gate interactive taps on DM allowlist
* fix(approval): scope smart deny owner overrides to one operation
Co-authored-by: Sergei Ivanov <kavi@local.hermes>
* chore(release): map @kavioavio in AUTHOR_MAP
* test(approval): isolate smart observer redaction failure
* fix(gateway): deduplicate completion delivery
* feat(delegation): persist background completions
* fix(delegation): harden durable completion delivery
* chore(release): map delegation contributor
* docs(delegation): clarify background lifetime
* fix(desktop): keep draft fallback rows across autosave echo
Add fallback only updates local editor state; complete pairs are filtered
before onChange. The post-#7b5ba205 resync effect then saw the unchanged
persisted chain and wiped the draft — button looked dead.
Ignore value updates that match the last chain we emitted; still resync
on real external changes (profile/config reload).
Co-authored-by: HexLab98 <liruixinch@outlook.com>
* fix(desktop): ensure node-pty spawn-helper is executable
resolves issue https://x.com/dineshgadge/status/2076024678452539691
* chore: record Nous upstream baseline 7fdae5d22acc
---------
Co-authored-by: yoma <yingwaizhiying@gmail.com>
Co-authored-by: Jakub Wolniewicz <jakub.wolniewicz@gmail.com>
Co-authored-by: Ben <ben@nousresearch.com>
Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
Co-authored-by: Sami Rusani <sr@samirusani>
Co-authored-by: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com>
Co-authored-by: gigakun3030 <gigakun@agentmail.to>
Co-authored-by: HexLab98 <liruixinch@outlook.com>
Co-authored-by: infinitycrew39 <infinitycrew39@gmail.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: PRATHAMESH75 <prathamesh290504@gmail.com>
Co-authored-by: giggling-ginger <110955495+giggling-ginger@users.noreply.github.com>
Co-authored-by: Wilson Kinyua <wilsonkinyuam@gmail.com>
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
Co-authored-by: ethernet <arilotter@gmail.com>
Co-authored-by: embwl0x <embwl0x@users.noreply.github.com>
Co-authored-by: joaomarcos <joaomarcosdias444@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>
Co-authored-by: solyanviktor-star <233359899+solyanviktor-star@users.noreply.github.com>
Co-authored-by: dsad <sswdarius@gmail.com>
Co-authored-by: briandevans <252620095+briandevans@users.noreply.github.com>
Co-authored-by: konsisumer <der@konsi.org>
Co-authored-by: Alex Jestin Taylor <alex@fireworks.ai>
Co-authored-by: Thomas Connally <51974392+tcconnally@users.noreply.github.com>
Co-authored-by: Jan Hermes Integration <jan@mg5.org>
Co-authored-by: izumi0uu <izumi0uu@gmail.com>
Co-authored-by: DatTheMaster <hermesagent424@gmail.com>
Co-authored-by: Igor Ganapolsky <iganapolsky@gmail.com>
Co-authored-by: TheTom <tturney1@gmail.com>
Co-authored-by: HexLab <8422520+HexLab98@users.noreply.github.com>
Co-authored-by: Adolanium <94890352+Adolanium@users.noreply.github.com>
Co-authored-by: alelpoan <alelpoan@proton.me>
Co-authored-by: zapabob <1920071390@campus.ouj.ac.jp>
Co-authored-by: Tony Antunez <57689194+smtony@users.noreply.github.com>
Co-authored-by: Esthon Jr <esthon@gmail.com>
Co-authored-by: harjoth <harjoth.khara@gmail.com>
Co-authored-by: Maxim M <true@supersynergy.de>
Co-authored-by: Cary Palmer <palmer@dugoutfantasy.com>
Co-authored-by: Rage Lopez <VrtxOmega@pm.me>
Co-authored-by: AIalliAI <285906080+AIalliAI@users.noreply.github.com>
Co-authored-by: Flownium <157689911+itsflownium@users.noreply.github.com>
Co-authored-by: Ahmett101 <297889955+Ahmett101@users.noreply.github.com>
Co-authored-by: Paulo Henrique <paulohenrique_789@hotmail.com>
Co-authored-by: ansel-f <135129512+ansel-f@users.noreply.github.com>
Co-authored-by: Mark Vlcek <markvlcek@gmail.com>
Co-authored-by: Robert Blaas <robert@modern-minds.ai>
Co-authored-by: Shannon Sands <shannon.sands.1979@gmail.com>
Co-authored-by: Jeffrey Quesnelle <emozilla@nousresearch.com>
Co-authored-by: Jake Long Vu <jake.long.vu@vucar.net>
Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
Co-authored-by: Kevin Yin <182213728+yinkev@users.noreply.github.com>
Co-authored-by: agent <agent@agents-Mac-mini.local>
Co-authored-by: iborazzi <ibrahimozsarac@gmail.com>
Co-authored-by: hellno <hellno@users.noreply.github.com>
Co-authored-by: Luigi Razon …
justemu
pushed a commit
to justemu/hermes-agent
that referenced
this pull request
Jul 18, 2026
…1793) * feat(security): expose deterministic tool output risk * fix(security): emit output-risk events only for findings
1 task
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
…1793) * feat(security): expose deterministic tool output risk * fix(security): emit output-risk events only for findings
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
…1793) * feat(security): expose deterministic tool output risk * fix(security): emit output-risk events only for findings
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
…1793) * feat(security): expose deterministic tool output risk * fix(security): emit output-risk events only for findings
melon-xf
added a commit
to melon-xf/hermes-agent
that referenced
this pull request
Sep 3, 2026
…1793) * feat(security): expose deterministic tool output risk * fix(security): emit output-risk events only for findings
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
Attacker-controlled tool output now carries deterministic risk metadata to operator surfaces while framing remains intact and internal metadata stays off provider wires.
Changes
Validation
123 targeted tests + TUI/failure regressions passed.
Infographic