Commit graph

488 commits

Author SHA1 Message Date
Rod Boev
9b9ff4053f fix(#5047): honor bounded refresh follower latency 2026-06-27 13:35:24 +00:00
Rod Boev
6649db7041 fix(#5047): prevent sync refresh follower hangs 2026-06-27 13:35:24 +00:00
Rod Boev
4f2bcada31 fix(#5047): keep sync refresh followers coalesced 2026-06-27 13:35:24 +00:00
Rod Boev
8da4c7a334 fix(#5047): require live rebuild publication for refresh reuse 2026-06-27 13:35:24 +00:00
Rod Boev
bcfec68c5c fix(#5047): cover bounded refresh followers 2026-06-27 13:35:24 +00:00
Rod Boev
53d44d3795 fix(#5047): coalesce session-visit refresh followers 2026-06-27 13:35:24 +00:00
Rod Boev
781223fa18 fix(agent): keep real checkouts ahead of pip-style lookalikes (#4700) 2026-06-26 17:15:03 +00:00
Rod Boev
b4dadc9939 fix(#4700): recognize nested pip-style agent roots 2026-06-26 17:15:03 +00:00
Rod Boev
b5d462812c fix(#4700): recognize pip-style agent roots for cron imports 2026-06-26 17:15:02 +00:00
nesquena-hermes
903dabd29b fix(providers): strip Anthropic env values so whitespace-only OAuth tokens don't false-positive (#4995 gate finding) 2026-06-26 15:44:15 +00:00
Rod Boev
06902d8038 fix(providers): detect Anthropic OAuth credentials in WebUI parity paths (#4770) 2026-06-26 15:34:59 +00:00
Rod Boev
61ea7360a0 fix(#4536): reject openai-codex provider before empty-model early return 2026-06-26 01:46:57 +00:00
Rod Boev
2f2d36d4eb fix(#4536): gate service tier on effective send model, not saved default 2026-06-26 01:46:57 +00:00
Rod Boev
4d64c18832 fix(#4536): block Codex models from priority service tier 2026-06-26 01:46:57 +00:00
Rod Boev
6f9d23eb48 fix(#4536): gate service tier to real OpenAI model ids 2026-06-26 01:46:57 +00:00
Rod Boev
a340c69b08 fix(#4536): clear stale service tier on model switch 2026-06-26 01:46:57 +00:00
Rod Boev
24cf7069a6 fix(#4536): persist OpenAI priority tier in main model settings 2026-06-26 01:46:57 +00:00
nesquena-hermes
57a78e9284 fix(#4728): clear stale custom:* base_url + default wins without loaded session (gate findings)
Two gate-found CORE/SILENT edges fixed on-branch:
- api/config.py: switching the main default to a different custom:* provider kept
  the OLD provider's base_url, routing New Chat to the wrong endpoint. The
  base_url-clear on provider-change now covers custom:* too (was exempted).
- static/sessions.js: newSession() skipped the configured default when S.session
  was null but the picker held stale state (e.g. after deleting the last session),
  inheriting the stale model. Default now wins over legacy picker regardless of
  loaded-session state (explicit empty-composer override still takes priority).
- Updated 2 stale source-grep precedence assertions + widened a brittle 6000-char
  newSession() slice window to 7000 (assignment block grew; intent unchanged).
- 2 new non-vacuous regression tests (custom base_url clear; precedence).

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-24 20:17:29 +00:00
Rod Boev
c388d15f2b fix(#4728): clear stale base URLs when default provider changes 2026-06-24 20:09:30 +00:00
Rod Boev
bede035a5e fix(#4728): persist provider-only default-model changes end to end 2026-06-24 20:09:30 +00:00
nesquena-hermes
b9f1491de7 Merge branch 'peek-4798-v2' into gate/4798-v2 2026-06-24 06:25:59 +00:00
Rod Boev
53f18556fb Keep model refresh builds authoritative until cache save completes 2026-06-23 17:53:36 -04:00
Rod Boev
fdedb2078a Keep session-visit model refresh from restamping stale catalogs 2026-06-23 17:40:09 -04:00
Charles Bine
dd66640fae
feat(#484): configurable default view for JSON/YAML code blocks
The JSON/YAML tree viewer (#484) hardcoded its default: valid json/yaml
fenced blocks opened in Tree view at 10+ lines (`const showTree=lineCount>=10;`).
Make that default user-configurable while preserving current behavior.

- New settings (persisted via /api/settings -> settings.json):
  - structured_code_default_view: auto | on | off  (default auto)
  - structured_code_auto_tree_lines: int 1..1000    (default 10)
- ui.js: replace the hardcode with a sanitized, unit-testable decision
  helper _structuredCodeShowTree(mode, threshold, lineCount); Tree/Raw
  toggle, Raw <pre><code>, parse-failure fallback, and YAML lazy-load
  are unchanged.
- Appearance settings: 'JSON/YAML code blocks' select + 'Auto threshold
  lines' input (disabled outside auto mode); wired through the existing
  appearance-autosave and explicit-save paths; live re-render on change.
- boot.js seeds window globals; i18n labels added to all 13 locales.
- Default (auto + 10) reproduces the original >=10 behavior exactly.
2026-06-23 13:51:45 -04:00
Rod Boev
e5aa548d67 fix(#4756): refresh stale model catalogs on session visit 2026-06-23 12:50:28 -04:00
nesquena-hermes
2f250a4198 review(#4741): address greptile P2s — drop redundant lazy import, deep-copy raw config cache
Two greptile findings on #4741:
- api/routes.py: removed the redundant 'from api.config import load_settings'
  inside _session_list_payload_to_response — load_settings is already imported at
  module scope, and this function only runs at request time (after module load).
- api/config.py: _load_yaml_config_file_raw now deep-copies the cached raw dict by
  default so a caller can never mutate the shared _yaml_file_cache entry. The
  internal _load_yaml_config_file passes _copy=False (it immediately runs
  _expand_env_vars, which returns a fresh structure and never mutates its input)
  so the /api/reasoning hot path keeps its cheap no-copy fast path; reload_config
  uses the safe default copy.
2026-06-23 05:43:17 +00:00
nesquena-hermes
001ee0a986 fix(#4662): stamp _cfg_mtime for empty-dict config (Opus gate finding)
The 'and loaded' guard skipped the _cfg_mtime stamp for a config that parses to
an empty {} dict, so get_config()'s current_mtime != _cfg_mtime stale check fired
on every call and re-entered reload_config() under _cfg_lock forever — a hot-path
spin for a freshly created/reset profile's {} config. Split the guard: the no-op
_cfg_cache.update is still skipped for {}, but _cfg_mtime is always stamped when
the file parsed to a dict, matching master's pre-#4662 behavior. Incidentally
fixes the pre-existing empty/None-config spin. Adds a regression test.
2026-06-23 05:43:17 +00:00
nesquena-hermes
8006db22fc perf(#4662): route reload_config parse through the mtime config cache
Phase 2 of #4662. reload_config() did a raw yaml.safe_load of config.yaml on
every call (~125ms+ on a large file), including on the profile-switch hot path
(~446ms load_settings stage in /api/sessions diagnostics). It now parses through
the mtime+size-keyed cache added in #4652, via a shared _load_yaml_config_file_raw
core (DRY: _load_yaml_config_file = raw + env-expand). reload_config still runs
its OWN env expansion pinned to the unscoped process-env view (block_process_env_fallback
reset + _thread_ctx.env={}) so the #798 TLS profile-isolation invariant is byte-for-byte
preserved; only the disk parse is now cached. An unchanged config.yaml is no longer
reparsed; an mtime change still busts the cache.
2026-06-23 05:42:32 +00:00
nesquena-hermes
d2ee837796 fix(#3961): resolve Opus ship-gate findings (startup NameError + reload cache poisoning)
Opus final gate reproduced two issues in the config-expansion hardening:

MUST-FIX — import-order forward reference. _expand_env_vars (config.py top) calls
_thread_local_env_value, but that fn + _thread_ctx were defined ~6900 lines later,
AFTER the import-time reload_config(). A ${VAR} in a startup config.yaml raised a
(swallowed) NameError, leaving import-time config consumers with empty config.
Fix: relocate _thread_ctx + _thread_local_env_value above the config-file section
(they depend only on os + threading). Verified: startup ${VAR} now expands, no
NameError.

SHOULD-FIX — reload_config baked block-scoped expansion into the process-global
_cfg_cache. If a reload fired while a readonly/worker scope was active (profile
alternation resolves _get_config_path to the named profile), under-expanded
literal ${VAR}s were written to the shared cache, starving concurrent readers of
the `cfg` alias. Fix: reload_config now expands the global cache with the
block flag forced off (process-env view), since the cache is process-global;
per-request scoped reads still go through _thread_local_env_value at their own
sites. Verified: global cache holds the process-expanded value, not the literal.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-22 20:18:01 +00:00
nesquena-hermes
51dd69b4f2 fix(#3961): route config ${VAR} expansion through the profile-aware env lookup
Codex re-gate found a config-template leak vector distinct from the env-name
scrub: _expand_env_vars() expanded ${VAR} references in config values via raw
os.environ, so a profile config.yaml of e.g. `api_key: ${ANTHROPIC_TOKEN}` let
_get_provider_api_key reconstruct the server-process token and pass it through as
the API key argument — even when the child-env scrub stripped the env var. This
bypassed the thread-local channel entirely.

_expand_env_vars now substitutes via _thread_local_env_value (the same
thread-local-first accessor with the block_process_env_fallback guard the PR
introduced): a ${VAR} resolves to the profile's own value, and refuses the
process-env fallback when a profile-scoped readonly/background scope set the block
flag (leaving the reference unexpanded). Cache integrity preserved — only the RAW
unexpanded dict is cached; expansion re-runs per read against the current thread's
scope. Normal (no-scope) behavior unchanged. + regression that fails without the fix.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-22 19:49:44 +00:00
nesquena-hermes
239529f9bd stage #4544 (rodboev): route provider/model credential reads through thread-local profile channel (fixes #3961) 2026-06-22 18:47:59 +00:00
nesquena-hermes
1f4e70a5d4
stage #4652 (nesquena-hermes): stop /api/reasoning storm + memoize YAML config parse (fixes #4650) (#4659)
Two-layer perf fix for the /api/reasoning storm dropping streaming to ~2 tok/s:
- ui.js: syncReasoningChip() serves the cached chip on routine topbar syncs, only
  re-fetching when the model/provider key (_lastReasoningFetchKey) changes — restores
  pre-a9ce2889 behavior while keeping its model-switch-refresh intent.
- config.py: memoize _load_yaml_config_file keyed on (path, st_mtime_ns, st_size) under
  a lock; cache the RAW parsed dict and re-run cheap _expand_env_vars per call (cache stays
  correct vs os.environ and read-modify-save callers get a fresh structure; on-disk edits
  picked up via mtime/size). Code byte-faithful from PR head f868f10a1ad7. 9 own tests pass.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-06-21 21:45:13 -07:00
nesquena-hermes
b575aaad1e
absorb #3177 (RobinAngele): global profile switcher in titlebar — gated behind opt-in setting (#4648)
Rebased RobinAngele's titlebar profile-switcher onto current master (resolved
index.html + panels.js conflicts from ~1300 commits of drift). Added a Settings ->
Appearance opt-in toggle 'show_titlebar_profile' (default OFF) per Nathan: the
titlebar button is hidden unless explicitly enabled, so default behavior is
unchanged (composer footer keeps its profile switcher). Wired config bool key +
boot.js apply (_applyTitlebarProfileVisibility) + panels.js load/save + index.html
checkbox + 2 i18n keys across all 13 locales (zh-Hant real translation).
Live-verified: default OFF = titlebar empty; opt-in ON = button appears + persists.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: RobinAngele <RobinAngele@users.noreply.github.com>
2026-06-21 18:56:31 -07:00
nesquena-hermes
b549c004cb
stage #4598 (Paladin173): composer footer control visibility toggles + rebase + contract test (#4630)
Per-control show/hide for composer footer controls (primary vs situational groups),
per-profile persist + live-apply. Visibility-only — footer layout unchanged.
nesquena-hermes deep-reviewed safe (Codex+Opus+suite 9930). Rebased onto master:
resolved api/config.py boolean-keys conflict (kept both auth_disabled_acknowledged +
the 15 hide_composer_* keys) and panels.js settings-load conflict (kept both
render_user_markdown wiring + composer-control chip render). Contributor closed the
en-only-i18n CI blocker (19 keys x 13 locales verified). Added the missing static-
contract test (sibling pattern test_sidebar_tab_visibility.py).

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: Paladin173 <Paladin173@users.noreply.github.com>
2026-06-21 12:42:42 -07:00
nesquena-hermes
8ea7ad582e rebase #3581 onto master (v0.51.546) for review+ship-gate — require current password before disabling auth
starship-s: sudo-mode re-auth on POST /api/settings before any change/clear/passwordless transition
of password auth, + cosmetic auth-disabled-acknowledged nav badge. Operator-hardening (rubric-named).
Security-boundary + visible UI -> Nathan visual sign-off. Co-authored-by: starship-s <starship-s@users.noreply.github.com>
2026-06-21 02:26:20 +00:00
nesquena-hermes
6b2ac55b8d stage #4566 + stamp v0.51.546 — fix flaky gateway_sync test (#4557)
rodboev: settings-write-version counter widens the session-list cache key + flush CLI-sessions
cache on visibility toggle, killing the mtime_ns-bucket collision flake. Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-21 02:04:09 +00:00
nesquena-hermes
2471e277cb rebase #3933 + stamp v0.51.544 — opt-in render markdown in user messages
rodboev. Default-off; OFF path byte-identical to master (cache key 'u' + _renderUserFencedBlocks
unchanged when window._renderUserMarkdown falsy). ON routes user text through the same renderMd
SAFE_TAGS/_isSafeUrl sanitizer as assistant msgs. Nathan override-approved (likes the idea, opt-in).
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-21 00:03:02 +00:00
Rod Boev
c237946ea3 fix(#3961): keep mirrored worker env updates and pool helpers compatible 2026-06-20 20:01:58 -04:00
Rod Boev
62ff54d5da fix(#3961): preserve live pool entry state outside readonly scopes 2026-06-20 19:58:04 -04:00
Rod Boev
44437f7a26 fix(#3961): fail closed for profile-scoped credential pool reads 2026-06-20 19:52:52 -04:00
nesquena-hermes
acd57d847d rebase #4413 re-pushed head (553e9584) for re-gate
Converged both bounce items: enrich-existing-only (continue on webui_list-is-None,
no new providers -> routing side-effect gone, routes.py no longer touched) + respects
per-provider ID prefix convention (@nous:). Co-authored-by: kaishi00 <kaishi00@users.noreply.github.com>
2026-06-20 21:22:53 +00:00
nesquena-hermes
85e3be6a42 fix(#4555): preserve /api/models aliases contract on disk-cache fallback
Codex ship-gate (SILENT): both disk loaders dropped `aliases` (the save path
never persisted them), so a disk-cache hit — strict cold-path OR the new stale
fallback — served /api/models with empty aliases, silently breaking `/model
<alias>` slash-command resolution (static/commands.js resolves slash aliases
only from /api/models.aliases). Add _model_aliases_from_config() helper and
reconstruct aliases from current config in BOTH loaders (fixes the pre-existing
strict-loader gap too). Adds a regression test.

Co-authored-by: starship-s <starship-s@users.noreply.github.com>
2026-06-20 19:25:51 +00:00
nesquena-hermes
fbafe1e623 harden(#4555): isolate stale-cache path in test fixture + enforce schema-version equality on stale fallback
Applies Opus ship-gate findings: (1) the autouse fixture now points
_get_models_cache_path at an isolated temp file so the over-budget static-fallback
test can't read a real ~/.../models_cache.json and become an order-dependent flake;
(2) _load_stale_models_cache_from_disk rejects a cross-schema cache (its groups/badge
shape may be incompatible with the current picker). Adds a cross-schema-rejection test.

Co-authored-by: starship-s <starship-s@users.noreply.github.com>
2026-06-20 19:19:52 +00:00
Hermes Agent
3d8f3a1f7b fix(models): use stale disk cache on catalog timeout 2026-06-20 19:11:17 +00:00
MinhoJJang
630fd26eec fix configured provider slash model routing 2026-06-20 17:42:18 +00:00
Rod Boev
7a0d868635 Keep profile-scoped reads from falling back to server credentials 2026-06-20 03:37:35 -04:00
Rod Boev
b59f58601e fix(#3961): block process-env fallback in readonly profile scopes 2026-06-20 02:48:24 -04:00
Rod Boev
321090efcc fix(#3961): preserve mirrored live-model profile reads 2026-06-20 02:33:54 -04:00
Rod Boev
846951304d fix(#3961): route provider and model credential reads through the thread-local profile channel 2026-06-20 02:24:07 -04:00
nesquena-hermes
e1e2bbc7ef fix(config): order ghost-home guard after ambient short-circuit (#4516 Codex gate)
Codex SILENT finding: the new 'if not target.exists(): return {}' guard ran
BEFORE the ambient-resolver short-circuit, so a matching ambient profile home
whose directory does not physically exist (fresh install / monkeypatched cfg)
returned {} instead of deferring to get_config() — breaking in-memory overrides
and changing the default-profile missing-home path. Move the guard below the
ambient short-circuit; add a regression test (matching-but-nonexistent ambient
home still defers to get_config()).
2026-06-20 03:57:56 +00:00
nesquena-hermes
acd2c186ef stage #4516: apply config defaults to non-default profile reads (#4465 / #4513) + v0.51.532 CHANGELOG 2026-06-20 03:51:55 +00:00
Rod Boev
f086158c10 fix(#2698): keep isolated deployments on their own state and profile roots 2026-06-20 00:48:19 +00:00
nesquena-hermes
9ade727e63 stage #4494: expose built-in personalities in WebUI config (#4465) + v0.51.525 CHANGELOG 2026-06-19 22:26:30 +00:00
nesquena-hermes
5a8d24c936 fix(#4343): default transcript virtualization OFF (experimental/opt-in) + force-off migration
Virtualization (#500/#4214) caused a scroll-up flicker on long sessions with
tall tool-call rows (variable-height anchor oscillation), distinct from the
stream-completion jump fixed in #4325. Per maintainer decision, flip the
'Virtualize long transcripts' preference to experimental/opt-IN (default OFF)
for everyone until the scroll behavior is reliable.

- api/config.py: virtualize_transcript default True->False; add
  virtualize_transcript_optin marker (both bool-allowlisted); load_settings
  force-off migration — a stored true WITHOUT the post-flip opt-in marker is
  reset to off, so 100% of existing users land on off; an explicit opt-in
  (marker present) is honored.
- boot.js / panels.js: default-off semantics (===true), persist the opt-in
  marker on enable, checkbox not pre-checked.
- index.html / i18n.js (en): relabel experimental + flicker warning.
- Tests: rewrite the #4325 toggle assertions for the opt-in contract + 4 new
  migration tests (unset/stale-true/explicit-optin/marker-without-true).

Phase B (root-cause scroll-stability) tracked separately. Thanks @b3nw.
2026-06-17 03:41:47 +00:00
nesquena-hermes
810414b438 feat(#4325): opt-out toggle to disable transcript virtualization
Adds a Preferences setting 'Virtualize long transcripts' (default ON, opt-out).
When disabled, _currentMessageVirtualWindow returns a full non-virtualized window
so the entire transcript renders at once (also short-circuits the virtual
re-measure loop). Server-persisted (virtualize_transcript bool, default True),
hot-applies via renderMessages on toggle. i18n in all 13 locales.

Pairs with @rodboev's #4328 stream-end anchor scroll fix (cherry-picked into
this branch) to fully resolve #4325.
2026-06-17 01:52:06 +00:00
nesquena-hermes
62a65a4ff1 review polish: surface plugin-provider check failures at warning (greptile P2) + fix #4253 comment mislabel
Independent review (APPROVED clean) + greptile both flagged one non-blocking nit:
_is_known_model_provider swallowed a transient _is_plugin_model_provider failure
at debug level, so a real plugin-backed provider could briefly vanish from the
picker with no visible log. Promoted to logger.warning(exc_info=True).

Also corrected the endpoint-error-hint comments in ui.js: they cited #4253, which
is actually a cron release (mislabel carried from #4279's body). Comment-only —
no logic change to the browser-verified collapsible-groups code.
2026-06-16 22:09:57 +00:00
nesquena-hermes
ea9cd16850 fix(providers): gate credential_pool detection to known model providers (#4324)
Fixes the Photon phantom-providers regression from #4247 (v0.51.461): the
credential-pool detection loop added every auth.json credential_pool key to
detected_providers without checking it names a model provider, so the Photon
iMessage plugin's photon/photon_project/photon_user messaging credentials each
rendered as a phantom provider carrying the full auto-detected model catalog
(251 models on the reporter's machine).

Gated on a new _is_known_model_provider() check (custom:* slug, static
_PROVIDER_DISPLAY/_PROVIDER_MODELS entry, or registered model-provider plugin)
on both the load_pool and raw-dict fallback paths; the group-builder's generic
fallback no longer paints an unrecognized id with the global catalog. Real
pool-keyed providers (copilot, aliased google->gemini) still surface.

Integrates the stronger of two independent same-fix PRs — #4330's implementation
(thorough docstring, try/except-hardened plugin check, preserved #1881 comment,
defensive else branch) + its 8-case test that mocks at the WebUI boundary
(stubs hermes_cli, never imports the bundled agent package that's absent in CI,
exercises both the load_pool and ImportError raw-dict paths) — over #4329's
2-case test. Both rodboev (#4329) and the maintainer agent (#4330) independently
found and fixed the same regression.

Reported by Nic.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-16 21:45:43 +00:00
nesquena-hermes
29d15d7360 fix review: profile-scope _CREDENTIAL_POOL_CACHE + getattr-safe runtime_api_key (#4247)
Codex gate caught a cross-profile leak: _CREDENTIAL_POOL_CACHE was keyed
by provider id alone, so a custom provider configured under profile A made
profile B report has_key=True from a stale cache hit (then 401 at request).
Key the cache by (active-profile auth-store path, provider id) at all
get/set/evict sites via _credential_pool_profile_tag(). Also make the two
new bare _entry.runtime_api_key reads getattr-safe (OAuth-only entries lack
the attr), matching the guard the author already applied at providers.py.
Adds a cross-profile isolation regression test.

Co-authored-by: akrhin <akrhin@users.noreply.github.com>
2026-06-16 18:22:07 +00:00
nesquena-hermes
b81a92ecd5 fix: credential pool fallback for custom providers (#4247)
Custom/aliased providers with keys in the credential pool (added via
hermes auth add) were reported unconfigured in the WebUI because provider
detection only checked env vars. Add _has_explicit_pool_credentials() that
checks the pool while filtering the ambient gh_cli entry (so Copilot doesn't
falsely show configured on any machine with gh installed), cache-routed to
avoid the cold per-provider load_pool latency, and resolve provider aliases
before load_pool() at every retrieval site so detection and retrieval read
the same pool (glm->zai, github->copilot, etc.). /api/models/live wrapped in
profile scope; getattr-safe runtime_api_key for OAuth-only entries.

Co-authored-by: akrhin <akrhin@users.noreply.github.com>
2026-06-16 17:44:10 +00:00
nesquena-hermes
ecf637c66d
Release PN (v0.51.453): bridge approval controls to the gateway runs API (#4229, #4203) (#4299)
Approve/deny on a remote-gateway backend now forwards to the gateway runs API
(run_id server-derived, existing auth+CSRF preserved, capability-probed + 60s
cached). Non-gateway/local approval path unchanged — relay only fires when a
session has an active gateway run (_STREAM_RUN_IDS). Old gateways get a clear
upgrade message instead of a silent no-op.

Co-authored-by: rodboev

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-06-15 22:25:24 -07:00
nesquena-hermes
0b0e2a3213
Release PG (v0.51.446): model picker show-all overflow for large catalogs (#3691) (#4282)
* stage-3712: model picker show-all overflow + fix per-row count leak (#3691)

Self-rebased rodboev's #3712 onto master. FIX (Nathan-caught): the per-row
provider chip stripped the '(N of M)' overflow count — that count belongs on the
group heading only, and was reading as nonsense post-expand (row 30 showing
'15 of 30'). Updated test to assert heading-carries-count + rows-do-not.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>

* stage-3712: + dedupe overflow expand (Codex SILENT) — reuse existing injected option

Codex caught: select hidden overflow model from search → reopen → Show all could
duplicate the row (_appendOverflowOptionsToGroup blindly appended every extra
model without checking the select for an already-injected option). Now moves/reuses
an existing same-value option instead of re-creating it.

* stage-3712: + fix expanded-heading double-count (Opus) — strip decoration, count rendered rows

Opus caught: post-expand the heading showed 'Nous (2 of 4) (4)' (appended its own
count onto the already-decorated backend label) and used modelCount (incl hoisted-
configured) vs rendered rows. Now strips the '(N of M)' suffix when expanded +
counts groupRows.length. Test asserts no double count on out[expanded].

* stage-3712: + include extra_models in Settings/Cron/Profile/Aux native selectors (Codex CORE)

Codex caught: the server overflow split moves models>15 into extra_models for ALL
providers, but the 4 native <select> builders in panels.js (settingsModel:6690,
cronFormModel:1112, profileFormModel:5784, _auxProviders:8287) read g.models only
— silently dropping the overflow tail for large providers. Now concat
models+extra_models at all 4 sites. Test guard added.

* Release PG: model picker show-all overflow for large provider catalogs (#3691)

Self-rebased rodboev's #3712 onto v0.51.445; full browser-tested (desktop+mobile,
before/after) + Nathan screenshot-approved. 4 gate-caught issues fixed inline:
per-row count leak (Nathan), overflow-expand dedup (Codex), heading double-count
(Opus), and native Settings/Cron/Profile/Aux selectors dropping the overflow tail
(Codex CORE). Codex SAFE + Opus SHIP + 41 picker tests green.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-15 17:16:36 -07:00
nesquena-hermes
e47f685b89
Release PA (v0.51.440): advanced per-model options + auxiliary model routing (#3097) (#4264)
* stage-3097: advanced model options (re-push fixes both no-ops), rebased onto master

* Release PA: advanced per-model options + auxiliary model routing (#3097)

ai-ag2026's #3097, re-pushed to fix both prior silent no-ops:
- base_url no longer clobbered (advanced applied after resolver write)
- extra_body now reaches the agent via request_overrides (+ cache-key); main
  modal hides unsupported timing fields (aux-only). Functionally verified live
  (gear→modal→save→persists). Codex SAFE + Opus SHIP + suite green.
Note: also adds 3 aux task slots (kanban_decomposer/profile_describer/triage_specifier).

Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-06-15 13:16:34 -07:00
nesquena-hermes
ed5afd2ae3
Release ON: show_cli_sessions default-on for new installs (#3988) + symlink delete/rename guard (#4217) (#4230)
* fix(#3988): default show_cli_sessions to true for new installs, grandfather existing OFF

Combines the best of #3997 (rodboev) and #4222 (Sanjays2402):
- config.py default flipped True so NEW installs surface CLI/TUI/Telegram/
  Discord/Hermes One sessions in the sidebar without hunting for the toggle.
- load_settings() backfill grandfathers ESTABLISHED installs OFF: when the saved
  settings predate the flip (onboarding_completed True + key absent), pin False so
  an existing user's sidebar doesn't silently change. Fresh installs get True.
- Default-true hydration at every read site (config.py / boot.js / panels.js
  checkbox) via the !==false idiom — no config-ON-but-UI-OFF mismatch (#4006 class).
- 10 tests: config default, bool-key round-trip, grandfather matrix
  (fresh/mid-onboarding/established-absent/explicit-true/explicit-false/
  save-preserves-grandfather), and both frontend hydration sites.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: Sanjays2402 <Sanjays2402@users.noreply.github.com>

* fix(#4217): guard /api/file/delete and /api/file/rename against symlink targets

* chore(release): stamp v0.51.427 (Release ON) for #3988 + #4217

* chore: drop unused imports (ruff F401) in absorbed test files

* fix(#4217,#3988): address review — dangling-symlink ordering + boot.js catch fallback

Greptile review on #4230 + the related-PR reviews surfaced two fixes on the
shipped code:
1. (#4217) _handle_file_delete/_handle_file_rename checked target.exists()
   BEFORE is_symlink(), so a DANGLING workspace symlink resolved to a missing
   target -> misclassified as 404 'File not found' and left permanently
   undeletable/unrenameable. Moved the no-follow is_symlink() guard before the
   follow-based exists() check in both handlers. +2 dangling-symlink tests.
2. (#3988) boot.js settings-load-FAILURE catch block hardcoded
   _showCliSessions=false, inconsistent with the new True default (the #4006
   catch-block-fallback class). Now defaults true. +1 test pinning the fallback.

Both confirmed by the new tests (20/20 in the two files).

* fix(#3988,#4217): address Opus review — broaden grandfather signal + move-handler parity

Opus #4230 ship-review surfaced 2 LOW findings, both now fixed in-line:
1. (#3988) The grandfather keyed only on onboarding_completed, so a CLI-configured
   user who tweaked a WebUI setting but never ran the wizard would get flipped ON
   (and onboarding.py later auto-persisting onboarding_completed=True would make it
   permanent). Now: established = show_cli_sessions absent AND (onboarding_completed
   OR any persisted setting other than a falsy onboarding flag). A genuinely new /
   still-mid-onboarding file still falls through to True. +1 test.
2. (#4217) _handle_file_move still checked exists() before is_symlink() — the same
   dangling-symlink-404 bug just fixed in delete/rename. Reordered for parity so a
   dangling symlinked source is rejected 400, not 404.

22 tests across the two files pass; ruff clean.

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: Sanjays2402 <Sanjays2402@users.noreply.github.com>
2026-06-14 23:06:22 -07:00
nesquena-hermes
e7e856cd79
fix(providers): keep custom provider when model id has known-provider prefix (#4210) (#4220)
* fix: keep custom provider when model id has known-provider prefix (#4210)

Siblings of #3872 (v0.51.349, custom provider with a configured base_url
preserves intrinsic routing prefixes). The base_url unset variant of
the same bug was missed: with provider=custom or custom:<slug> and no
base_url set, a model id like 'x-ai/grok-2' or 'google/gemma-2' was
hijacked to provider='openrouter', base_url=None. The backend then
failed to initialize with 'RuntimeError: No LLM provider configured'
because the user had no OpenRouter credentials configured.

resolve_model_provider() now applies the same _is_custom exemption the
config_base_url branch already carries to the cross-provider redirect
at the end of the slash branch. The custom provider keeps the request
and the full model id; real first-party providers pointed at an
OpenAI-compatible proxy (e.g. provider=openai + base_url=litellm) are
unaffected because their strip path is in the base_url branch and the
'prefix == config_provider' branch above the change.

Verified:
- test_model_resolver.py 32 passed (30 existing + 2 new RED tests)
- test_custom_provider_bare_model_reasoning.py 34 passed
- Pre-existing F401 (api/config.py:23,24,28) are not introduced by
  this change; same 3 errors exist on upstream/master.

Refs #4210

* chore(release): reposition #4210 CHANGELOG entry under v0.51.426 (OM)

---------

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-06-14 19:56:12 -07:00
nesquena-hermes
87fa0a2ace
feat(workspace): optional Todos tab in workspace panel (#3564) (#4209)
* feat(workspace): add Todos tab to workspace panel behind a setting (#2942)

* fix(workspace): use DOM attribute to check active tab across script scope

* Prefer the live todos snapshot in the workspace tab

* Fill the missing workspace Todos locale keys

* feat(workspace): add optional Todos tab to workspace panel (#3564)

Rebased rodboev's #3564 onto current master + CHANGELOG entry.

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* fix(#3564): apply workspace Todos tab visibility on cold boot

Codex gate: boot.js set window._workspaceTodosTab from saved settings but
never called _applyWorkspaceTodosTabVisibility(), so a previously-enabled
Todos tab stayed hidden on a normal page reload (only re-appeared after
opening Settings). Call the visibility helper after assigning the flag in
both the settings-apply and the fallback/default paths.

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* chore(release): stamp v0.51.422 (Release OI) for #3564

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-06-14 17:31:13 -07:00
nesquena-hermes
92ecf8edb4
fix(models): preserve colon-suffixed model IDs in normalization (#3959, ships #4196) (#4204)
* fix(models): preserve colon-suffixed model IDs in normalization (#3959)

* fix(models): align configured-model normalizer parity (#3959)

* chore(release): stamp v0.51.421 (Release OH) CHANGELOG for #3959/#4196

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-06-14 16:27:39 -07:00
nesquena-hermes
6c6444396d
fix(#3750): preserve LM Studio auth on reasoning probe (self-rebased #3837 + credential hardening) (#4202)
* fix(#3750): preserve LM Studio auth on reasoning probe

Rebased rodboev's #3837 onto current master (master reworked
resolve_model_reasoning_efforts via #3431 with a combined hermes_cli
import + _nested_route_reasoning_denied guard). Threaded the api_key
resolution + the _lmstudio_model_reasoning_options wrapper + the
HTTP-fallback probe into master's current structure; intent preserved
byte-identically (+164/-24, same as the original PR diff).

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* fix(#3837): gate LM Studio probe credential to configured endpoint + no-redirect opener

Codex security review (CORE) on the rebased #3837: GET /api/reasoning accepts
a caller-supplied base_url, and the new probe attached the configured LM Studio
API key as a Bearer token; urllib also re-sends Authorization across redirects.
Together that could exfiltrate the stored credential to an attacker-controlled
host. (master only escalates here because master probed WITHOUT a key.)

Fixes:
1. Only forward the LM Studio credential when the probe target normalizes to
   the configured LM Studio base URL (reuses _normalize_base_url_for_match);
   a caller-supplied non-matching base_url is probed keyless.
2. _lmstudio_reasoning_probe_options_fallback uses a no-redirect opener
   (_NoRedirectHandler) so a 3xx can't forward the Authorization header onward.
3. Two regression tests: caller-supplied base_url drops the key; redirect is
   not followed (credential never reaches the redirect target).

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* fix(#3837): route ALL credentialed LM Studio probes through the no-redirect probe

Codex re-gate caught that the first fix only hardened the built-in fallback;
in production hermes_cli IS importable, so the preferred path called
hermes_cli.models.lmstudio_model_reasoning_options(..., api_key=...) which uses
plain urlopen and still follows redirects with the credential.

Fix: _lmstudio_model_reasoning_options now short-circuits to the built-in
no-redirect probe whenever api_key is set; the hermes_cli path is used ONLY for
keyless probes (no credential to leak). Reworked tests accordingly:
- test_credentialed_probe_never_calls_hermes_cli: credentialed probe bypasses CLI
- test_keyless_probe_logs_signature_mismatch_before_fallback: keyless CLI degrade
- test_credentialed_probe_does_not_follow_redirects_end_to_end: E2E through
  resolve_model_reasoning_efforts with real hermes_cli; redirect target never hit

Verified both security tests fail without the guard and pass with it.

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* chore(release): stamp v0.51.420 (Release OG) in CHANGELOG

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-14 15:46:35 -07:00
b3nw
db3d22912e fix(config): reasoning toggle for nested Gemini custom provider routes (#3431)
Version-gated the nested-gateway allow to 2.5-series/3-era (reviewer fix:
gemini-1.5/1.0 have no thinking controls -> selector would fail on send) +
pre-2.5 exclusion regression tests. Stamped v0.51.408 (Release NU).

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-06-14 04:44:47 +00:00
nesquena-hermes
30cc15975e fix(#4130): coerce stale 'max' in get_reasoning_status read path too (Codex catch)
get_reasoning_status() returned the raw config reasoning_effort, so a stale saved
'max' surfaced on boot/status/chip even though streaming coerces it to xhigh. Now
routes the status value through coerce_reasoning_effort_for_model() with the resolved
model/provider/base_url, so all read paths agree. Added a stale-max->xhigh status
regression test.
2026-06-13 18:50:00 +00:00
Rod Boev
95bb9ebe66 Keep stale max configs on the supported path 2026-06-13 14:33:26 -04:00
Rod Boev
c7c313672c fix(reasoning): hide unsupported max effort (#4119) 2026-06-13 14:33:26 -04:00
nesquena-hermes
c02c31c837 Merge #4006 (scroll auto-follow setting + pin guards) onto master
# Conflicts:
#	api/config.py
#	static/panels.js
2026-06-13 03:54:11 +00:00
nesquena-hermes
92f7a24448 Merge #4024 Transparent Stream onto master (rebase)
# Conflicts:
#	CHANGELOG.md
2026-06-13 00:40:17 +00:00
nesquena-hermes
82fcf665eb harden(#4052): tolerate providers.*.models list-of-dicts keyed by model/name (Opus SHOULD-FIX)
The richer static-catalog builder's group-building loop used a strict item["id"]
extraction that would KeyError (caught → degrade to the minimal one-model catalog)
for legal config shapes where providers.<id>.models is a list of dicts keyed by
"model"/"name" rather than "id". Mirror the tolerant id-or-model-or-name resolution
the detection loop already uses, skip entries with nothing usable, and add a
regression test. No behavior change for the common list-of-strings / list-of-id-dicts
shapes.

docs(changelog): stamp #4052 model-picker budget fallback as v0.51.370 (Release MI)
2026-06-12 20:05:12 +00:00
Rod Boev
4d8fa0245e fix(#3928): stabilize the static fallback contract 2026-06-12 11:55:43 -04:00
Rod Boev
76f3083223 fix(#3928): keep fallback badge helper out of source-marker tests 2026-06-12 08:43:06 -04:00
Rod Boev
87183b9517 fix(#3928): return a real static catalog when /api/models times out 2026-06-12 08:35:22 -04:00
Mohamed Habib
306a148fbf merge: sync transparent stream branch with master 2026-06-12 05:09:01 +03:00
Mohamed Habib
fcfaa4686a feat: add transparent stream activity display 2026-06-12 04:45:41 +03:00
Rod Boev
a82eb1622f fix(#3958): keep reasoning effort status on the active session model 2026-06-11 18:46:17 -04:00
Rod Boev
4dc4786aae fix(#3979): treat malformed providers config as unconfigured 2026-06-11 16:55:14 -04:00
Ming
e05b99b7fd fix(scroll): guard _scrollAfterMessageRender and _finishDone against pin hysteresis snap-back
Two scroll-state bugs cause the viewport to snap to the bottom after
streaming completes, even when the user has scrolled up to read:

1. _scrollAfterMessageRender (ui.js): when preserveScroll=true and
   _scrollPinned=true (re-set by the 250px near-bottom hysteresis),
   _followMessagesAfterDomReplace() calls scrollToBottom(), overriding
   the user's position. Added !_messageUserUnpinned guard so explicitly
   scrolled-up users get their snapshot restored instead.

2. _finishDone (messages.js): the explicit scrollToBottom() at L3302
   fires when shouldFollowOnDone is true, but that flag only checks a
   120px near-bottom threshold. Added _isMessagePaneNearBottom(250)
   gate so users reading 250px+ above bottom keep their position.

Also adds a new 'auto_scroll_follow' setting (default: off) that gates
scrollIfPinned() and _shouldFollowMessagesOnDomReplace(). When disabled,
the viewport never auto-scrolls during streaming — the user controls
scroll position manually and uses the ↓ button to jump to bottom.

7-file setting-add pattern applied:
- api/config.py: _SETTINGS_DEFAULTS + _SETTINGS_BOOL_KEYS
- static/boot.js: both init paths
- static/ui.js: scrollIfPinned() + _shouldFollowMessagesOnDomReplace()
- static/index.html: checkbox in appearance settings
- static/i18n.js: en + zh-CN translations
- static/panels.js: payload, autosave, populate, full save
- messages.js: no additional changes (gated via _shouldFollowMessagesOnDomReplace)

Tests updated: test_tars_scroll_reset_regressions, test_issue1690,
test_issue3545. All 116 scroll-related tests pass.
2026-06-11 21:48:14 +02:00
Hermes Agent
85ee6ec2c8 fix(profiles): address greptile P2 flags on #3960
- _load_models_cache_from_disk: resolve _get_models_cache_path() once
  (was called twice — exists() then open()) (greptile P2).
- conftest: use a _MISSING sentinel for the _active_profile restore guard so
  it restores whenever api.profiles was importable pre-test, independent of the
  value (greptile P2; _active_profile defaults to 'default' so the prior
  is-not-None guard was functionally fine but technically value-dependent).

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-06-11 01:56:58 +00:00
Hermes Agent
b5d382a3bb fix(profiles): scope the detached models-rebuild worker to the active profile (#3957)
Codex + Opus both independently caught a CORE gap in the first cut: the
bounded /api/models rebuild runs on a detached 'models-catalog-rebuild' daemon
thread that inherits neither the request-profile thread-local (#798) nor
os.environ. So on a non-default profile the worker probed the DEFAULT profile's
credentials and, when the 4s budget was exceeded, published the rebuilt catalog
to the DEFAULT profile's disk cache (cross-contamination) — exactly the slow
path a non-default cold rebuild takes.

Fix:
- profile_scope_for_detached_worker(profile_name): sets the request-profile TLS
  AND applies the profile .env on the worker thread, restoring both on exit
  (no-op for default). Distinct from profile_env_for_active_request (which reads
  the current thread's TLS and must not clear it).
- get_available_models() captures the active profile on the request thread and
  wraps the rebuild worker body (probe + over-budget publish + disk save +
  fingerprint) in that scope; the legacy synchronous rebuild applies the profile
  env on the foreground. /api/models route no longer wraps (the work moved into
  get_available_models so ALL callers — chat/start, resolution — are fixed).
- 2 new regression tests incl. the worker-thread before/inside/after assertion.

Empirically verified: a fresh worker thread resolved models_cache.json/default
WITHOUT the scope (the bug) and models_cache.work.json + the work .env + the
work auth.json WITH it.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-06-11 01:41:24 +00:00
Hermes Agent
70d391c44a fix(profiles): scope /api/providers + /api/models to the active profile (#3957)
On a non-default profile, Settings → Providers timed out and the model
picker showed only the default profile's models. WebUI profile switching is
per-client/cookie-scoped (#798), but two read-only paths resolved from the
process-global default profile:

- Facet A: /api/providers + /api/models did not apply the active profile's
  .env around the read, so get_auth_status() / provider_model_ids() / custom
  key lookups resolved the default profile's credentials. On a non-default
  profile the auth probes could stall past the 30s frontend abort.
- Facet B: the /api/models disk cache was a single import-time
  STATE_DIR/models_cache.json shared across every profile, while the cache
  fingerprint is profile-specific -> a non-default profile rejected the shared
  snapshot every read and cold-rebuilt (the slow serial-probe path).

Fix:
- api.profiles.profile_env_for_active_request(): applies the active
  per-request profile's .env for the duration of the read (delegates to the
  existing profile_env_for_background_worker used by streaming). No-op for the
  default/root profile, so single-profile deployments are byte-identical.
- api.config._get_models_cache_path(): profile-keys the disk cache filename
  (models_cache.<profile>.json) derived from the default path; default profile
  keeps models_cache.json unchanged (no file migration).
- routes.py: wrap both GET handlers in profile_env_for_active_request.
- conftest: restore api.profiles._active_profile + clear request-profile TLS
  after each test (a pre-existing isolation hole that profile-keyed cache
  paths newly surface under sharding).

Tested: 10 new regression tests; live before/after on isolated servers shows
a non-default profile now surfaces its configured provider (deepseek) with
its own credentials + its own cache file, where master showed only the
default profile's providers and one shared cache.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-06-11 01:17:22 +00:00
nesquena-hermes
922efdf234 feat(outline): opt-in conversation outline panel (#2124)
Adds an opt-in, default-off, desktop-only floating panel that lists the user's
questions in a conversation as a numbered jump-list (click to scroll + flash the
message). Gated behind a Settings → Preferences toggle (show_conversation_outline)
wired through the existing show_* preference boot/load/autosave/save paths.

Review follow-ups applied on absorb:
- Outline is now strictly chat-only: leaving the chat view (Settings, Tasks,
  Insights, …) hides the toggle button AND closes the panel; returning to chat
  restores the toggle (panel stays closed until re-opened). Implemented by gating
  _outlineAllowed() on the active panel and re-evaluating via a MutationObserver on
  the <main> showing-<panel> class (switchPanel is a global fn declaration that
  can't be reliably wrapped from this script).
- Fixed a latent visual bug: #outlinePanelWrapper{display:flex} (id selector)
  outranked the UA [hidden]{display:none}, so wrapper.hidden=true never actually
  hid the panel — the × close button and auto-close had no visual effect. Added
  #outlinePanelWrapper[hidden]{display:none;}.
- Regression tests for both.

Closes #2124.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-10 23:46:39 +00:00
nesquena-hermes
cdd76c04b1 fix(model-resolver): preserve @provider:model picks across cold catalogs
_resolve_compatible_session_model_state() no longer reverts an explicit
@provider:model selection to the default when the provider's group is missing from
the cached catalog snapshot. explicit picks always honored; non-explicit (2nd+ turn
/ chat switch) preservation requires the provider to be KNOWN/CONFIGURED via the new
_provider_is_known_or_configured() (static registry + custom-provider config, NOT the
cold catalog) — so a cold live-discovery provider (ollama-cloud/deepseek/xai) is
preserved while a genuinely-unknown provider (@removed:...) falls through to
default-repair. A known-but-unconfigured builtin is deliberately preserved (surfaces
a clear runtime auth error rather than a silent swap; a cheap env/config credential
check would mis-classify OAuth/auth-store providers). Keeps the #3867 cached-catalog
hot path intact.

Co-authored-by: starship-s <starship-s@users.noreply.github.com>
2026-06-10 22:16:25 +00:00
nesquena-hermes
6988995389 fix(#3915): warn on STATE_DIR divergence at startup when session store is empty
When SESSION_DIR has no session .json files and the index is empty/absent,
print_startup_config() now scans sibling state directories and prints a diagnostic
pointing the user at the populated sibling + the HERMES_WEBUI_STATE_DIR to set —
fully fail-safe (try/except, warning-only, no behavior change).

Closes #3915.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-10 19:48:41 +00:00
nesquena-hermes
92e74c6cd3
fix(config): preserve intrinsic vendor prefix in model ID for custom proxy (#3872) (#3918)
A bare 'custom' provider with a remote base_url is a vendor-routing proxy
(LiteLLM, Bedrock gateway, etc.). A slashed model id like bedrock/opus-4-6 is
intrinsic — the proxy routes on the full string. The prior heuristic stripped
ANY known-provider prefix (bedrock is also a provider name), truncating it to
opus-4-6 and causing 403 'model not allowed'.

Fix: in the custom-base_url branch, strip the prefix only when (a) the configured
provider is a real first-party provider pointed at an OpenAI-compatible proxy
(provider=openai + proxy base_url — the #433 path), OR (b) for a bare 'custom'
provider, the bare id is genuinely a first-party model of that prefix's catalog
(openai/gpt-5.4 -> gpt-5.4, since gpt-5.4 IS an OpenAI model). An intrinsic
routing prefix whose bare id is NOT first-party of that namespace is preserved
(bedrock/opus-4-6 stays whole). Unknown prefixes (zai-org/...) preserved as before.

Both prior behaviors work in tandem with the fix:
  - #433 sprint40: bare custom + openai/gpt-5.4 -> gpt-5.4 (redundant, strip)
  - #433 model_resolver: provider=openai + google/gemma -> gemma (proxy strip)
  - #548: custom + zai-org/GLM-5.1 -> preserved (unknown prefix)
  - #1625: custom + loopback base_url -> preserved (local server)
  - #3872: custom + bedrock/opus-4-6 -> preserved (intrinsic vendor prefix)

Adds regression tests covering all of the above.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: haolf000 <haolf000@users.noreply.github.com>
2026-06-10 01:56:01 -07:00
nesquena-hermes
e8d71a2fb8
Release v0.51.347 — Release LK (streaming & render reliability cluster #3892 #3898 #3885 #3882 #3868) (#3902)
* stage v0.51.347: render/stream cluster (#3892 #3898 #3885 #3882 #3868) + 2 Opus SHOULD-FIX

* stage v0.51.347: trim #3885 error-guard comment to fit diagnostic-test window

* Stamp v0.51.347 — Release LK (streaming & render reliability cluster)

* Remove stray uv.lock accidentally staged (not part of any cluster PR)

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-09 17:02:35 -07:00
nesquena-hermes
26e133e3e8
[HELD — independent review pending] Release v0.51.340 — bg_task agent wakeup (trio #2968+#2971+#2979) (#3867)
* stage bg_task trio combined (#2979 superset) on master for deep review

* fix(bg_task): unsubscribe SessionChannel on header-write failure (Codex deep-review catch) + regression test

* test: realign on-subscribe-recovery anchor to subscribe_to_session_channel after leak fix

* CHANGELOG: bg_task trio as v0.51.340 LD (HELD pending independent review)

* bg_task trio: apply 3 independent-review (greptile) fixes

1. start_session_turn now threads the session PROFILE model defaults
   (_read_profile_model_config) into the wakeup model-resolve, so a brand-new
   session with an empty model falls back to the profile default not global
   DEFAULT_MODEL. Updated the white-box spy test signature accordingly.
2. /api/session/stream omits the Connection header (HTTP/1.1 keep-alive
   default) to match the #3103 long-lived-SSE pattern.
3. Reaper now prunes _LAST_EMIT_TS for collected sessions so the coalesce
   timestamp map can't grow one permanent entry per session forever.

nesquena APPROVED the PR; these are the 3 non-blocking greptile suggestions.

* test: realign _start_session_turn adapter stub lambda to new profile-defaults signature
2026-06-08 22:36:18 -07:00
nesquena-hermes
c7a389e0a7
Release v0.51.305 — Release JU (stage-p2b — dormant unified-SessionDB adapter) (#3759)
* refactor(sessions): add dormant JSON-backed SessionDB adapter (#3720, #3383)

First, lowest-risk slice of the unified-session-db migration: a SessionDB-shaped
adapter over the existing WebUI JSON store, behind a dormant experimental flag
(experimental.unified_session_db, default false). No runtime call site is rewired
— is_unified_session_db_enabled() has no live callers, so persistence behavior is
unchanged until a later migration PR opts in. Includes the adapter, the dormant
config flag + _apply_config_defaults wiring, an architecture doc, and adapter tests.

Also adds docs/architecture/ to the .gitignore docs allowlist (the docs/* rule
excludes subdirectories; the new architecture doc and its presence test would
otherwise be silently dropped on a clean checkout).

Co-authored-by: rodboev <[email protected]>

* docs(changelog): stamp v0.51.305 — Release JU (stage-p2b #3720)

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 18:16:07 -07:00
nesquena-hermes
4580f58496
Release v0.51.303 — Release JS (stage-p1a — cron toggle + config var expansion + git-discard hardening) (#3756)
* fix(cron): toggle run output rows instead of re-fetching when already open (#3732)

_loadRunContent() only ever expanded, so clicking an already-open cron run row
re-fetched its content pointlessly. It now toggles: an open row collapses (clears
the expansion state + resets the toggle button) and returns early, avoiding the
redundant API call.

Co-authored-by: mysoul12138 <mysoul12138@users.noreply.github.com>

* feat(config): expand ${VAR} references in config.yaml at load time (#3736)

hermes-agent already expands ${ENV_VAR} in config.yaml, but the WebUI's own
loader stored the raw dict, leaving literal ${...} strings. Recursively expand
${VAR} against os.environ on both config load paths (reload_config and
_load_yaml_config_file); unset vars are left untouched (${VAR} preserved).

Co-authored-by: Carry00 <Carry00@users.noreply.github.com>

* fix(security): anchor untracked-file deletes in git_discard (#3702)

git_discard(delete_untracked=True) used raw shutil.rmtree / Path.unlink after a
separate safe_resolve_ws validation, leaving a validation-to-use symlink-swap
window. Route untracked deletes through the anchored helpers (rmtree_anchored /
unlink_anchored) so a swapped path component is rejected at delete time; preserve
the prior missing_ok tolerance for benign concurrent-removal races. Adds
regression coverage for both the symlink-swap block and the concurrent-missing case.

Co-authored-by: Hinotoi-agent <Hinotoi-agent@users.noreply.github.com>

* docs(changelog): stamp v0.51.303 — Release JS (stage-p1a #3732 #3736 #3702)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: mysoul12138 <mysoul12138@users.noreply.github.com>
Co-authored-by: Carry00 <Carry00@users.noreply.github.com>
Co-authored-by: Hinotoi-agent <Hinotoi-agent@users.noreply.github.com>
2026-06-06 17:24:18 -07:00
nesquena-hermes
e3a7c93dc6
[HELD — independent review pending] Release v0.51.294 — stage-3401 (live-to-final redesign #3401 + 4 deep-review fixes) (#3741)
* Harden interrupted recovery control filtering

* Redesign live-to-final assistant replies

* Fix live activity anchor test fixture

* Fix CI lint issues for live reply tests

* Strengthen live progress prompt contract

* Recover PR #3401 refresh on origin/master

* Repair live-to-final refresh regressions

* Fix live worklog refresh regressions

* Show live footer timer on initial stream start

* Restore live stream shell after reload

* Preserve per-frame live SSE replay cursors

* Preserve reasoning as Worklog Thinking cards

* Quiet Worklog Thinking card styling

* Align Worklog Thinking card styling

* Scope live Worklog Thinking cards by segment

* Suppress exact duplicate settled Thinking

* Close #3401 merge review test gaps

* fix(#3401): resolve 4 deep-review regressions (inline-think, reconnect-dup, neon skin, busy-gate worklog)

Deep review (Codex diff-vs-master + live-browser drive) of the live-to-final refactor
surfaced 4 regressions vs master that the rewritten suite no longer guarded:

1. Inline <think>…</think>answer reasoning vanished — _assistantReasoningPayloadText
   used $-anchored regexes so a leading think block + visible answer extracted nothing
   and the Thinking card never rendered. Removed the 3 $ anchors to match the
   (non-anchored) display stripper. Live: inline-think thinking-only turn now renders.
2. (CORE) reconnect/reload duplicated the live reply — _rememberRunJournalCursor advanced
   a closure-local seq but never wrote INFLIGHT[activeSid].lastRunJournalSeq, so a reload
   replayed the journal from after_seq=0 over restored lastAssistantText. Now mirrors the
   cursor onto INFLIGHT + schedules a throttled persist.
3. Neon skin silently broke — PR deleted the :root[data-skin="neon"] CSS but left Neon in
   the picker. Restored the neon CSS block from master.
4. Settled tool-worklog rebuild gated purely on !S.busy — dropped every prior settled
   turn's worklog when renderMessages re-ran during an active stream (switch-back to an
   in-progress session). Restored master's !S.busy || (S.toolCalls && S.toolCalls.length).
   Live: busy re-render now preserves tool cards (4→4, was 4→0).

Live-verified all 4 + confirmed #3709/#3592 invariants still hold (1 thinking card, none
below footer; distinct siblings preserved). + tests/test_issue3401_deep_review_fixes.py (7).

* test(#3401): realign 3 stale source-shape assertions to the deep-review fixes

Fix commit changed two source literals that existing stage tests scanned for:
- test_live_activity_timeline.py (x2): split anchor 'if(!S.busy){' → the restored
  'if(!S.busy || (S.toolCalls&&S.toolCalls.length)){' guard (fix 4).
- test_run_journal_frontend_static.py: 'after_seq=0' not in source — fix 2's comment
  contained that literal; rephrased the comment to 'the zero floor (after_seq of 0)'.
Intent of all three assertions unchanged; only the matched string updated. No code
behavior change.

* docs(changelog): v0.51.294 — Release JJ (stage-3401, #3401 live-to-final redesign)

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: Nathan-Hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 12:12:37 -07:00
nesquena-hermes
2736f443b1
Release v0.51.286 — Release JB (stage-r21 — sidebar tab reordering #3067) (#3694)
* feat: allow sidebar tab reordering via drag (#3067)

Drag-reorder for sidebar tab chips in Settings, persisted via a sanitized
tab_order setting (collapses duplicates, rejects chat/settings, strips
non-strings).

Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>

* docs(changelog): v0.51.286 — Release JB (stage-r21)

---------

Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-05 18:39:42 -07:00
nesquena-hermes
988348682c
Release v0.51.284 — Release IZ (stage-w4 — sidebar status labels + cron-sessions toggle #3570 #3514) (#3692)
* feat(sidebar): add show_cron_sessions toggle to surface cron sessions (#3514, #2841)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* feat(sidebar): add manual session status labels (#3570)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* docs(changelog): v0.51.284 — Release IZ (stage-w4)

* fix(settings): persist show_cron_sessions in the explicit Save Settings path too (#3514)

Codex regression-gate follow-up: the autosave path (_preferencesPayloadFromUi)
included show_cron_sessions but the explicit saveSettings() button path read/saved
show_cli_sessions and dropped the cron checkbox — clicking Save Settings silently
omitted it. Read settingsShowCronSessions + add body.show_cron_sessions (gated on
CLI sessions, mirroring autosave).

* fix(settings): gate show_cron_sessions identically in BOTH save paths (#3514)

Codex round-2: my saveSettings() gate exposed that the autosave path
(_preferencesPayloadFromUi) posted the raw cron checkbox state ungated, so
show_cli_sessions=false + show_cron_sessions=true could persist via autosave.
Gate autosave on showCliCb too; update the regression test to assert both
paths gate on settingsShowCliSessions.

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-05 17:48:27 -07:00
nesquena-hermes
10413b4d65 Release v0.51.281 — Release IW (stage-verdigris — Verdigris emerald/bronze skin #3602)
Adds the Verdigris dark-only appearance skin (emerald/forest-green + bronze-gold),
renamed from the contributor's 'Hermes Agent' to a descriptive material name per
maintainer naming convention. Registered across all 5 skin sites (config allowlist,
boot.js swatch, index.html FOUC map, i18n in 12 locales, scoped CSS palette) + test.
Also fixes the zeus i18n test (zeus is no longer the trailing skin token).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-05 22:18:47 +00:00
nesquena-hermes
58528a4d88
Release v0.51.270 — Release IL (stage-u1 — un-hold batch: #3517 #3624 #3613) (#3674)
* feat(commands): add /use to force a skill for the next turn (#3517, #2977)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* fix(auth): cap pending passkey challenges by evicting oldest, not rejecting (#3624)

Co-authored-by: Hinotobi <paperlantern.agent@gmail.com>

* fix(providers): expose model-provider plugins in WebUI (#3613)

Co-authored-by: Pamnard <pamnard@users.noreply.github.com>

* docs(changelog): v0.51.270 — Release IL (stage-u1, 3-PR un-hold batch; #3448 + #3618 dropped)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: Hinotobi <paperlantern.agent@gmail.com>
Co-authored-by: Pamnard <pamnard@users.noreply.github.com>
2026-06-05 11:35:26 -07:00
nesquena-hermes
442b033e67
Release v0.51.268 — Release IJ (stage-b1 — low-risk perf + provider/clarify fixes) (#3671)
* perf(providers): O(1) codex cache merge membership checks (#3656)

Co-authored-by: Pamnard <pamnard@users.noreply.github.com>

* fix(models): add MiniMax-M3 to WebUI MiniMax fallback catalog test (#3627)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* fix(config): make DeepSeek reasoning-effort heuristic position-independent (#3650)

Co-authored-by: happy5318 <happy5318@users.noreply.github.com>

* fix(clarify): don't stash clarify draft while submission is in flight (#3651)

Co-authored-by: carryzuo00 <carryzuo00@gmail.com>

* perf(sessions): batch lineage report child fetch by parent id (#3659)

Co-authored-by: Pamnard <pamnard@users.noreply.github.com>

* perf(sessions): batch orphan sidecar state.db existence probes (#3657)

Co-authored-by: Pamnard <pamnard@users.noreply.github.com>

* test(streaming): pin DOM-INFLIGHT reattach invariant (#3572)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* docs(changelog): v0.51.268 — Release IJ (stage-b1)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Pamnard <pamnard@users.noreply.github.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: happy5318 <happy5318@users.noreply.github.com>
Co-authored-by: carryzuo00 <carryzuo00@gmail.com>
2026-06-05 10:44:02 -07:00
nesquena-hermes
3ca1188f64
Release v0.51.256 — Release HX (stage-r4) (#3596)
## Release v0.51.256 — Release HX (stage-r4)

Performance — bound WebUI memory growth & idle CPU on large installs.

### Fixed
| Issue | Author | Fix |
|-------|--------|-----|
| #3506 | @nesquena-hermes (reported w/ profiling by @djenttleman) | On a large install (~615 sessions / 40k messages / 454 MB state.db) the WebUI process climbed ~100 MB → ~1.5 GB RSS over days and held high idle CPU. Three root causes fixed: (1) `session_lifecycle._sessions` grew unbounded → new `discard_session()` drops the entry at agent-eviction boundaries, only when no in-flight commit / no uncommitted memory work (retry invariant preserved); (2) cache caps now operator-tunable (`HERMES_WEBUI_AGENT_CACHE_MAX` default 50→25, `HERMES_WEBUI_SESSIONS_MAX`); (3) GatewayWatcher computes a cheap fingerprint before the expensive per-session `MAX(messages.timestamp)` projection and only re-projects on change. |

### Rebase + review notes
- Rebased onto current master; the code diff was verified **byte-identical to the nesquena-APPROVED head** at rebase time (only CHANGELOG re-resolved).
- The Codex regression gate then surfaced **two correctness gaps** the approval didn't catch, both fixed here with regression tests:
  1. **Watcher fingerprint missed same-count transcript rewrites.** `/retry`,`/undo`,`/compress` (`SessionDB.replace_messages`) rewrite messages with new timestamps but can leave `message_count` unchanged → stale sidebar `last_activity`. Fixed with a **per-session** grouped message aggregate (`id, count, user_count, MAX(timestamp)`) over the same non-excluded sessions (a global MAX would miss a rewrite of an older, non-newest session); cron/webui stay excluded so idle churn still doesn't re-project.
  2. **LRU agent-cache eviction could close a live worker's agent** (`popitem(last=False)`, liveness-blind — pre-existing, but the lower 50→25 cap made it more likely). Eviction now snapshots `ACTIVE_RUNS` session_ids (before the cache lock — no nested lock) and skips live sessions, deferring (temporarily exceeding cap) rather than closing a live agent.

### Gate
- Full pytest suite: **7612 passed, 0 failed** (one boot-cascade flake re-run; clean on re-run)
- ruff: CLEAN · Codex (regression): 4 rounds → both gaps + a stale test fixed → **SAFE TO SHIP**

Co-authored-by: nesquena <nesquena@users.noreply.github.com>
2026-06-04 12:28:06 -07:00
nesquena-hermes
ba70926e51
Release v0.51.253 — Release HU (stage-r1) (#3591)
## Release v0.51.253 — Release HU (stage-r1)

Phase-1 low-risk batch — 7 PRs (no intervention beyond apply + one inline MUST-FIX).

### Fixed
| Issue/PR | Author | Fix |
|----------|--------|-----|
| #3525 | @TomBanksAU | Streaming DOM-replace "follow" window tightened 1200px→120px — a reader who scrolled up mid-stream no longer gets snapped to the bottom on completion. |
| #3556 | @ai-ag2026 | Topbar count distinguishes a partially-loaded transcript ("loaded of total" via server `message_count`); fully-loaded keeps the tool-row-filtered count. |
| #3502 follow-up | @rodboev | Sidebar messaging source badges (Telegram/Discord/…) render as chips, not just CLI ones. |
| — | @Karlineal | `.pre-header+pre` margin override scoped under `.msg-body` (removes a 10px gap above code blocks). |

### Tests
- `test_ctl_script.py` kills orphan fake-python trees on Windows; conftest `_discover_python` checks the Windows venv layout (`Scripts/python.exe`). (#3537, #3577, @rodboev)

### Docs
- Explicit WebUI–Agent compatibility policy + Docker pinning guidance. (#3232, @franksong2702)

### Dropped from this batch
- **#3538** (self-update stash-pop recovery) — the Codex regression gate found a **BRICK-class data-loss**: the recovery path runs `git reset --merge` then `git stash drop`, permanently discarding the user's local modifications while returning `ok:true` + scheduling a restart. Held with `changes-requested` + a repro and the fix (keep the stash, return `ok:false`, no restart). Concept is good; the destructive `stash drop` must go.

### Gate
- Full pytest suite: **7575 passed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN
- Codex (regression): SHIP-ONLY-WITH-FIXES (BRICK data-loss #3538 + tool-row count regression #3556) → #3538 dropped, #3556 fixed inline → **SAFE TO SHIP**

Co-authored-by: TomBanksAU <TomBanksAU@users.noreply.github.com>
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Co-authored-by: Karlineal <Karlineal@users.noreply.github.com>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-04 10:50:34 -07:00
nesquena-hermes
703aba3f3e
Release v0.51.250 — Release HR (stage-q22) (#3526)
## Release v0.51.250 — Release HR (stage-q22)

UX-approved (dark + light-fallback screenshots).

### Added
| PR | Author | Feature |
|----|--------|---------|
| #3328 | @heagandev | **Zeus appearance skin** — OLED-near-black dark surfaces that keep the default **gold accent** (a high-contrast "gold on black" look no existing skin offered). Selectable from Settings → Appearance or `/theme skin zeus`. Dark-focused; falls back to the default light palette in light mode. |

### Notes
- The PR was 2 days / ~19 releases stale and CONFLICTING; re-applied surgically onto current master (CSS palette + `zeus` registered at all 5 sites: config allowlist, boot.js swatch, index.html boot-map, i18n `cmd_theme` ×12 locales, picker) + THEMES.md doc row. The PR's own `test_zeus_skin.py` (6 tests) passes against the re-applied version.
- Fully scoped + additive: Codex verified every new CSS rule is under `:root.dark[data-skin="zeus"]` — no bleed into the default appearance or other skins.

### Gate
- Full pytest suite: **7563 passed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN · vision-verified dark (OLED+gold) + light (clean fallback)
- Codex (regression): **SAFE TO SHIP**

Co-authored-by: heagandev <heagandev@users.noreply.github.com>
2026-06-03 20:54:02 -07:00
nesquena-hermes
0501564098
Release v0.51.249 — Release HQ (stage-q21) (#3523)
## Release v0.51.249 — Release HQ (stage-q21)

Small opt-in feature. UX-approved (screenshot of the toggle in Settings → Preferences).

### Added
| Issue | Author | Feature |
|-------|--------|---------|
| #2974 | @rodboev | **"Auto-expand terminal on output"** preference (Settings → Preferences, **off by default**). When enabled, the collapsed embedded terminal panel expands automatically the first time a running command emits output. Fires once per stream (guarded on open && collapsed — not per chunk), and uses `expandComposerTerminal({focus:false})` so it doesn't steal focus from the composer. Backend-persisted boolean mirroring the `simplified_tool_calling` pattern; default-off = no behavior change on upgrade. |

### Gate
- Full pytest suite: **7557 passed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN · screenshot vision-verified (toggle renders cleanly under "Compact tool activity", unchecked by default)
- Codex (regression): **SAFE TO SHIP** (clean first pass) — no-arg `expandComposerTerminal` callers unchanged, default-off incl. settings-load-failure path, all 5 plumb sites mirror the existing pattern

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-03 20:15:53 -07:00
nesquena-hermes
81e748b455
Release v0.51.247 — Release HO (stage-q19) (#3521)
## Release v0.51.247 — Release HO (stage-q19)

Backend correctness fix.

### Fixed
| Issue | Author | Fix |
|-------|--------|-----|
| #3505 | @franksong2702 | **Reasoning effort is coerced to a level the active model/provider actually supports** before each request, instead of being sent verbatim and rejected. `openai-codex` `gpt-5` no longer gets `max` (→ `xhigh`); `o1`/`o3`/`o4` clamp to `low`/`medium`/`high`. Coercion only steps *down* (never escalates); `none`/unset preserved. The capability filter is applied across heuristic / models.dev / Copilot / LM Studio paths. |

This is the narrow, correct fix for the detection gap that #3431 tried to address by removing the chip-visibility gate (which we shelved). The chip-visibility gate is **untouched** (Codex confirmed) — `get_reasoning_status`/`_applyReasoningChip` still hide the chip for unconfirmed models.

### Review fix absorbed (Codex + self-flagged)
The first cut **dropped** a configured effort for *unrecognized* models, because capability detection returns `[]` for both "known-unsupported" and "simply-unknown" (custom providers, aggregator-rewritten ids, new releases) — that's a behavior change vs master (which sent it verbatim) and would silently disable reasoning. Fixed: an **empty** capability set now **preserves** the configured effort (provider stays the final authority; worst case = the same rejected request master already produces, i.e. no regression). Known-bad clamps return *non-empty* filtered sets, so they still degrade correctly. Nathan chose this "preserve-for-unknown" behavior. + regression test.

### Gate
- Full pytest suite: **7548 passed, 0 failed**
- ruff: CLEAN · 48 reasoning tests pass (incl. preserve-for-unknown + codex-clamp + never-escalate)
- Codex (regression): SHIP-ONLY-WITH-FIXES (unknown-model drop) → fixed → **SAFE TO SHIP**
- Verified empirically: gpt-5/codex max→xhigh, o3 max/xhigh→high, unknown high→high (preserved), none/unset preserved

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-03 19:21:26 -07:00
nesquena-hermes
c0d86db5a8
Release v0.51.242 — Release HJ (stage-q14) (#3500)
## Release v0.51.242 — Release HJ (stage-q14)

UX-approved via Telegram (Nathan — dark/light/mobile screenshots).

### Added
| PR | Author | Feature |
|----|--------|---------|
| #3440 | @t3chn0pr13st | **Graphite appearance skin** — a quiet, neutral-gray "workbench" alternative to the default gold/cream. Selectable from Settings → Appearance and `/theme skin graphite`. Both light + dark palettes on the existing CSS-variable token system; tightened typography, shadows, active-sidebar spacing, code-block framing. |

### Why it's safe
- **Fully scoped + additive**: every new CSS rule (and every `!important`) is under `[data-skin="graphite"]` — Codex verified zero bleed into the default appearance or other skins. `api/config.py` keeps the default skin as `default` and only *adds* `graphite` to the allowed set. No i18n keys dropped (only the `/theme` help string gains `graphite`).
- Opt-in; a user has to select it. Default experience unchanged.

### Test-robustness fix (absorbed)
The new graphite scoped selectors (e.g. `:root[data-skin="graphite"] .session-item.active .session-time{…}`) appear in `style.css` *before* the canonical unscoped rules, which broke 3 naive first-occurrence CSS-contract tests (`test_issue677` scroll-btn-overlay, `test_issue856_pinned_indicator_layout`, `test_workspace_panel_session_list`). Fixed those 3 to anchor on the canonical **unscoped** rule (start-of-line regex) instead of the first `.selector` match — robust against this and future skins. Verified they still pass on clean master CSS (invariant preserved, not weakened).

### Gate
- Full pytest suite: **7517 passed, 9 skipped, 3 xpassed, 0 failed**
- ESLint runtime gate: CLEAN · ruff: CLEAN · browser-smoke: CLEAN
- Codex (regression): **SAFE TO SHIP** (verified all CSS scoped, default skin unchanged, no i18n key loss)
- Vision-verified dark + light + mobile; UX-approved by Nathan via Telegram

Co-authored-by: t3chn0pr13st <t3chn0pr13st@users.noreply.github.com>
2026-06-03 16:46:53 -07:00
nesquena-hermes
9374e66855
Release v0.51.228 — Release GV (stage-p12 — workspace tree-drop #3411 + large-markdown preview #3378) (#3463)
* Release v0.51.228 (stage-p12): workspace tree-drop #3411 + large-markdown preview #3378

#3411 (@pamnard): stopPropagation on workspace file-tree OS-file dragenter/dragover/drop
so a tree drop uploads to the workspace WITHOUT also attaching to the composer.
#3378 (@starGazerK): raise md rich-render ceiling 64KB/1500L->256KB/5000L + backend
file read 200KB->400KB, add 'Render as markdown anyway' force button (reuses cached
raw content, no extra fetch).

* fix(workspace): force-render uses fresh path-scoped cache, blocked while dirty (#3378 Codex follow-up)

Codex review of #3378 found the markdown force-render path had two SILENT bugs:
(1) saving a md file from the plain-text fallback didn't update _previewRawContent,
so a later force-render showed stale pre-save content; (2) the cache-reuse check
'path===_previewCurrentPath' was tautological (var just assigned), so a force-render
after a file switch could render the previous file's cached content. Fixes: track
_previewRawContentPath (set on fetch AND save), reuse cache only when it matches the
requested path, and block force-render while the editor is dirty/open. +3 regression
tests. (#3411 was cleared clean by Codex.)

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-02 20:58:22 -07:00
nesquena-hermes
0b5458f3da
Release v0.51.226 — Release GT (stage-p9 — mobile composer context-usage ring #3062 + activity-feed default-expand setting #3080) (#3459)
* feat(composer): replace mobile config-button sliders icon with context-usage ring (#3062, @NottheGuy007)

Squashed net diff of #3062 (the PR branch's tip commits were deletes of files not
present in our tree). Replaces the composerMobileCtxBadge text badge with an SVG
progress ring (ctx-arc + ctx-num) showing real-time context-window usage: ring
fill via stroke-dashoffset, centered percentage, color-coded green<=50%
orange<=85% red>85%. Ring resets to 0%/green on new session.

* feat(activity): add 'expand activity feed by default' appearance setting (#3080, @AJV20)

Squashed net diff of #3080. Adds a Settings -> Appearance checkbox
(activity_feed_expanded_default, default off) to expand new Activity disclosures
by default; preserves manual per-turn collapse/expand (explicit toggle still
wins); live 'Waiting on model' rows explain what the agent is doing before/after
tool calls. i18n keys for all locales.

* docs(changelog): v0.51.226 — context-usage ring (#3062) + activity-feed default-expand setting (#3080)

* test(mobile): update touch-target test for #3062 ring (badge removed)

#3062 replaced the composerMobileCtxBadge text badge with the SVG context-usage
ring (composerMobileCtxRing) but left 3 stale references in tests/test_mobile_layout.py.
The full suite caught test_mobile_composer_primary_controls_keep_touch_friendly_sizing
asserting the removed .composer-mobile-ctx-badge CSS rule + #composerMobileCtxBadge
element. Updated the assertion to the new ring: confirm composerMobileCtxRing exists,
the old badge is fully gone (not dangling), and the ring SVG is aria-hidden so it
stays a decorative overlay that doesn't steal the config button's 44px touch target
(which is still asserted via .composer-mobile-config-btn above). 56/56 mobile-layout
tests pass. Codex: no production JS dereferences the removed badge.

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-02 19:48:01 -07:00
nesquena-hermes
bb5aa02523
Release v0.51.224 — Release GR (stage-p6 — profile tool/skill config authoritative on streaming worker #3294) (#3456)
* fix: respect profile toolset/skill config on WebUI streaming worker (#3294)

The streaming agent runs on a detached worker thread that does not inherit
the per-request thread-local profile context (set from the hermes_profile
cookie on the HTTP handler thread). On that worker, the ambient get_config()
resolves via get_active_profile_name() which falls back to the process-global
_active_profile (usually 'default'). A session under a non-default profile
with an empty platform_toolsets.cli therefore loaded the DEFAULT profile's
full toolset list, inflating a tools-disabled profile's prompt from ~400 to
~15K input tokens.

Add api.config.get_config_for_profile_home() — a race-free direct disk read
of an explicit profile home's config.yaml (no shared-cache mutation), which
defers to get_config() when the requested home matches the ambient path so
in-memory test overrides are preserved. The streaming worker now resolves
_cfg from the session's own profile home, fixing toolsets, prefill context,
and fallback chains in one place.

Closes #3294

Co-authored-by: gottipx <gottipx@users.noreply.github.com>

* docs(changelog): v0.51.224 — #3294 profile toolset config (dropped #3405 to hold per Codex stale-model-repair finding)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: gottipx <gottipx@users.noreply.github.com>
2026-06-02 18:33:22 -07:00
nesquena-hermes
b850a88e8b
Release v0.51.223 — Release GQ (stage-p5 — openai-api first-class picker provider #3443 + MiniMax-M3 #3374) (#3453)
* feat: upgrade MiniMax default model to M3

Add MiniMax-M3 as the new default and prune deprecated older
versions (M2.5/M2.5-highspeed/M2.1/M2) from the model catalog.
M2.7 (and M2.7-highspeed) is retained as the legacy compatible
option for users who pin to it.

Updated:
  - api/config.py: _FALLBACK_MODELS adds minimax/MiniMax-M3 (placed
    before M2.7 so the dropdown surfaces it first)
  - api/config.py: _PROVIDER_MODELS['minimax'] adds M3 first, removes
    M2.5/M2.5-highspeed/M2.1
  - api/config.py: _PROVIDER_MODELS['minimax-cn'] adds M3 first,
    removes M2.5/M2.1/M2
  - tests/test_minimax_provider.py: updated CN catalog assertions
    to match the new {M3, M2.7} list

API URL and TTS configuration are unchanged.

Co-Authored-By: Octopus <liyuan851277048@icloud.com>

* fix(models): register openai-api as first-class picker provider

* fix(models): detect OPENAI_API_KEY as openai-api, not bare openai (#3443 Codex follow-up)

Codex review found #3444 added the openai-api picker entry but the env-detection
side still did detected_providers.add('openai') for OPENAI_API_KEY. The agent
registry has only openai-api and openai-codex (no bare openai), so a env-only
OPENAI_API_KEY setup emitted @openai: picker entries the agent can't resolve on
the send path. Detect openai-api to match the registry. Adds a regression test.

* docs(changelog): v0.51.223 — re-stamp keep-set (#3443 openai-api + #3374 MiniMax-M3); dropped #3289 + #3264 to hold per Codex

---------

Co-authored-by: octo-patch <octo-patch@github.com>
Co-authored-by: Octopus <liyuan851277048@icloud.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-02 18:08:01 -07:00
nesquena-hermes
264863cc37 fix(config): extend #3429 URI-scheme fix to backend normalization + model-key matching (#3436, @b3nw)
v0.51.218 fixed getModelLabel() (the visible chip) for URI-scheme model IDs but
left the same first-segment-slash-strip bug in the matching/dedup paths:
api/config.py _norm_model_id + _get_label_for_model, and static/ui.js
_normalizeConfiguredModelKey. For gpt://folder/model/latest those treat the
path slashes as provider delimiters, mis-normalizing the model-identity key
(the #3360 collision class, for URI ids). Adds a URI-scheme guard to all three
so the scheme is not stripped. Kept v0.51.218's getModelLabel (more thorough
than #3436's variant — it extracts the model name vs returning the whole id);
took #3436's backend + _normalizeConfiguredModelKey halves with backend/frontend
parity tests.

Co-authored-by: b3nw <b3nw@duck.com>
2026-06-02 21:04:02 +00:00
b3nw
68716b3e4f fix: generalized reasoning capability heuristics for custom providers (#3377) 2026-06-02 17:11:32 +00:00
happy5318
2892d3d6c0 fix: also filter GH_TOKEN env var from Copilot credential pool
Follow-up to PR #3382 review feedback. There are three GitHub-related
env vars that can be auto-detected:

- COPILOT_GITHUB_TOKEN: user-specific config, should NOT be filtered
- GITHUB_TOKEN: classic PAT, should be filtered (already done)
- GH_TOKEN: another env var, should also be filtered (this commit)

Use a frozenset (_AMBIENT_GH_ENV_SOURCES) to clearly define which
env var sources are ambient vs user-configured.
2026-06-02 17:11:32 +00:00
happy5318
02a1849ca4 fix: filter GITHUB_TOKEN env var from Copilot credential pool
Copilot API does not support classic PATs (ghp_*), only OAuth tokens (gho_*)
or fine-grained PATs (github_pat_*). When GITHUB_TOKEN env var contains a
classic PAT, it should be filtered from the credential pool to prevent
Copilot from appearing in the model picker dropdown.

The _is_ambient_gh_cli_entry() function now also filters entries with
source='env:GITHUB_TOKEN', treating them as ambient/auto-detected rather
than user-explicitly-added credentials.

Fixes issue where Copilot group appeared in WebUI model picker even when
gh auth token was an unsupported classic PAT.
2026-06-02 17:11:32 +00:00
b3nw
d06776a4c8 fix: model picker snaps to wrong model with multi-slash IDs (#3360)
When a custom/proxy provider serves models whose IDs share the same base
name across vendor prefixes (e.g. vendor_a/deepseek/deepseek-v4-pro vs
vendor_b/deepseek/deepseek-v4-pro), several normalization functions use
split('/').pop() (or split('/')[-1]) which discards all segments except
the last.  This causes three user-facing symptoms: (1) clicking one
model selects a different colliding model, (2) configured-model badges
attach to the wrong dropdown entry, and (3) the model-chip label in the
composer bar is truncated to just the base model name.

Root cause: all three callers take only the last slash-segment instead
of stripping only the first (provider) segment and preserving the
remaining vendor hierarchy.

Fix 1 — _findModelInDropdown (static/ui.js): Move the exact string match
before the provider-aware normalized match.  Previously, when all models
share the same provider ID (common with LLM proxy setups), the normalized
match returned whichever colliding option appeared first in DOM order,
even though an exact match existed.

Fix 2 — _normalizeConfiguredModelKey (static/ui.js) and _norm_model_id
(api/config.py): Replace split('/').pop() / split('/')[-1] with a first-
segment-only strip (regex on frontend, split('/',1) on backend), matching
the strategy already used by _findModelInDropdown's norm lambda.  This
prevents multi-slash IDs from colliding in badge assignment and the
configured-entry dedup set.  Additionally, strip colon-qualified provider
prefixes (e.g. custom:name/) before the slash strip so badge-key variants
like 'custom:llm-proxy/opencode_go/model' merge correctly with the bare
'opencode_go/model' in the configured section dedup.

Fix 3 — getModelLabel (static/ui.js) and _get_label_for_model
(api/config.py): Same split('/').pop() to first-segment-strip change so
the composer-bar model chip and backend label preserve vendor context
(e.g. shows 'opencode_go/deepseek-v4-pro' instead of 'deepseek-v4-pro').

Verification: 9 new regression tests (test_issue3360) covering exact-
match priority, multi-slash normalization, and backend/frontend parity.
Updated 1 existing test (test_norm_model_id_trailing_empty_guard) that
asserted the old split('/').pop() pattern.  All 25 related tests pass.

AI Usage: Gemini (gemini-2.5-pro), via Antigravity IDE, pair-programmed.

(cherry picked from commit a454fecd2b3a83f7da34473c883be069171aeac9)
2026-06-02 05:08:30 +00:00
nesquena-hermes
816a4a93f9 harden(#2622): DOM-bound plugin handlers + tab.path validation (Codex round-2)
Codex found two more once the config-guard bug was fixed (Opus concurred on #2):
1. panels.js _buildPluginCard built the Open button + enable toggle with inline
   onclick/onchange that interpolated tab.path / plugin.key into a JS-string-in-
   attribute context — HTML-escaping is insufficient there (quote breakout).
   Now rendered inert + bound via addEventListener with RAW closure values.
2. tab.path was unvalidated. Added _VALID_PLUGIN_TAB_PATH (^/[A-Za-z0-9._~/-]{0,255}$)
   in load_plugins() — absolute, no quotes/query/fragment/control chars.
Also Opus nit: deep-merge now coerces dashboard_plugins values to bool + str keys.
Regression tests added for both.
2026-06-02 03:39:41 +00:00
nesquena-hermes
944e739b40 fix(#2622): restore settings allowlist guard (Opus HALT) + validate plugin name
CRITICAL (Opus HALT on prior commit): the PR's edit to save_settings() replaced
'if k in _SETTINGS_ALLOWED_KEYS' with 'if k=="dashboard_plugins": continue' and
orphaned the whole validation body under the continue. Effects: (a) settings save
broken for every key except dashboard_plugins; (b) the allowlist security boundary
gone -> any client key (password_hash, signing_key_*) became settable. Restored the
guard + correct indentation; dashboard_plugins handled by the deep-merge above.
Verified in-process: language persists, password_hash/signing_key injection
rejected, dashboard_plugins still deep-merges.

Also (Opus SHOULD-FIX #3): validate plugin name against ^[a-z][a-z0-9_-]{0,63}$
so a manifest name like '../foo' can't make the URL-space ambiguous.

Regression tests added for both (the allowlist bug had ZERO coverage).
2026-06-02 03:22:25 +00:00
nesquena-hermes
bf3ff69c5e feat(plugins): add WebUI dashboard plugin system with iframe isolation (#2622, @pix0127) 2026-06-02 02:48:11 +00:00
nesquena-hermes
de22c607bc stage-batch10: #3327 model-id normalize + #3334 RFC slice doc + #3341 profile skill counts
#3327 fix(reasoning): normalize custom-provider model ids for fallback heuristics
Co-authored-by: Carry00 <Carry00@users.noreply.github.com>

#3334 docs(rfc): mark run-adapter Slice 4f shipped, define Slice 4g gate
Co-authored-by: Michaelyklam <Michaelyklam@users.noreply.github.com>

#3341 fix(profiles): show enabled vs compatible skill counts
Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-06-01 18:24:05 +00:00
AJV20
d8471a7d87 fix(profiles): avoid config import cycle 2026-06-01 05:05:16 +00:00
nesquena-hermes
5742a23836 fix: apply Opus+Codex gate findings — stale-stopped not configured (#1944), narrow #2905 markers, scope except
Both pre-release reviewers (Opus advisor + Codex regression gate) converged on
the same MUST-FIX:

- #3194: treating gateway_stale_stopped_state as 'configured' contradicted
  #1944 (a stopped root gateway should read like 'not configured' so the
  banner doesn't nag). Now ONLY stale-RUNNING metadata
  (reason=gateway_stale_running_state or gateway_state=='running') flips
  configured=True; stale-stopped falls through to bool(identity_map) like the
  genuinely-unconfigured case. Updated the test accordingly + added a
  stale-stopped no-regression test.

Opus follow-ups also applied:
- #2905: narrowed the populated-home markers to WebUI-only artifacts
  (webui/, webui/sessions, webui/settings.json), dropping config.yaml/auth.json
  so a long-time agent user installing WebUI fresh isn't wrongly diverted to
  the legacy %USERPROFILE%\.hermes (auth.json predates #2897 there).
- profiles._resolve_base_hermes_home(): narrowed except Exception -> ImportError
  so a real bug in the config helper still surfaces.

Adjacent suites green: #2840, #1879, gateway_status_agent_health (66 tests).
2026-05-31 19:12:30 +00:00
nesquena-hermes
5dc11ef977 fix: Windows upgrade state stranding (#2905) + gateway-configured banner on first deploy (#3194)
Two urgent breaking bugs that had no PR, combined into one hotfix.

#2905 (data-loss-class): v0.51.134 moved the Windows default Hermes home
from %USERPROFILE%\.hermes to %LOCALAPPDATA%\hermes (PR #2897) with no
migration, so upgrading Windows users opened the app to empty
sessions/pins/settings (data intact on disk, at an address the new build
no longer read). _platform_default_hermes_home() now prefers the populated
legacy home ONLY when the new location is not yet established —
non-destructive (no file moves) and self-healing on next launch.
profiles._resolve_base_hermes_home() delegates to the same config helper so
the active-profile pointer can never drift from STATE_DIR.

#3194: GET /api/gateway/status reported 'Gateway not configured' on a fresh
two-container Docker deploy because an alive=None + gateway_stale_running_state
health payload with an empty identity_map fell through to
configured=bool(identity_map)=False. The alive=None branch now treats a
payload carrying gateway metadata (gateway_state detail, or a stale-running/
stale-stopped reason) as configured.

Tests: +17 regression tests (11 for #2905 incl. full truth table + non-destructive
guard + POSIX no-op; 6 for #3194 incl. 5 no-regression guards). Full suite
7090 passed, 0 failed.

Closes #2905
Closes #3194
2026-05-31 19:12:30 +00:00
nesquena-hermes
e58fb6a201 ci: forward-looking ruff lint gate (E9+F+B, new-code-only) (#3275)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-05-31 18:26:37 +00:00
carryzuo00
d6c1b696f5 fix(reasoning): expose effort levels for bare/dot-separated model names on custom providers
Custom API aggregators (New API, One API, etc.) route requests using their
own naming conventions — bare names like ``deepseek-v4-flash`` or dot-separated
names like ``moonshotai.kimi-k2.5`` — rather than the OpenRouter-style
``vendor/model`` slash format that ``_heuristic_reasoning_efforts``'s prefix
list was written for.

Because none of these names match the slash-prefixed patterns, the function
returned ``[]``, hiding the reasoning effort selector in the WebUI even for
models that fully support thinking/reasoning.

Fix: add a secondary check in ``_heuristic_reasoning_efforts`` that strips an
optional dot-vendor prefix and matches the remaining model name against a list
of known thinking-capable bare-name prefixes (``deepseek-v4``, ``deepseek-r1``,
``kimi-k2``, ``qwen3``, ``claude-3/4``, ``o1/o3/o4``), plus a keyword catch
for model names containing ``thinking`` or ``reasoning``.

The slash-prefix path and all provider-specific fast paths (copilot, lmstudio,
models.dev metadata) are unchanged; this block only runs as a final fallback.

Regression tests added in ``tests/test_custom_provider_bare_model_reasoning.py``.
2026-05-31 02:51:26 +08:00
zapabob
1f51ef14d0 fix: detect shared OpenCode API key 2026-05-29 23:22:15 +00:00
theseussss
b554fd8078 fix(reasoning): 推理强度补齐 max 档,与 hermes_constants 对齐
WebUI 镜像的 VALID_REASONING_EFFORTS 此前止于 xhigh,漏了底层
hermes_constants 已有的 max 档。补齐后端常量、前端命令/下拉/帮助文本,
并同步快照测试。支持 max thinking 的 provider(如 Anthropic)现可在
composer 下拉和 /reasoning 命令中选到 Max。
2026-05-29 03:05:02 +08:00
Hermes Agent
b2220a54b5 fix: route openai-codex through models.dev so GPT-5.5 exposes xhigh
- Remove openai-codex special case that called github_model_reasoning_efforts()
- Codex now falls through to _models_dev_reasoning_efforts() (full efforts)
- GitHub/Copilot still use the GitHub helper (caps at high)
- Added regression tests for both behaviors
2026-05-27 21:38:06 -06:00
nesquena-hermes
424a55036f Merge pull request #3017 2026-05-28 02:16:34 +00:00
AJV20
fb6dd28ac3 fix: cap custom models endpoint probes 2026-05-27 12:19:42 -04:00
Hermes Agent
74fe79ddc6 fix(reasoning): use agent metadata for effort support 2026-05-27 03:54:45 -06:00
Isla-Liu
20f49e7bda docs(config): address PR #2964 review nits
* Comment alongside _AUTH_FINGERPRINT_VOLATILE_KEYS notes client_secret
  is treated as rotation-only on purpose, not a model-cache
  differentiator (maintainer §Concerns 3).
* _write_auth docstring at tests/test_issue_t16551f61_auth_token_churn_
  fingerprint.py:108 now describes actual behavior (writes + monkeypatch,
  no sleep+restat) — Copilot r3302471228.

Refs #2242. PR #2964 review.
2026-05-27 09:51:32 +08:00
nesquena-hermes
e0dfa0c586 Merge pull request #2792 2026-05-27 00:37:48 +00:00
Isla-Liu
ecfa580660 fix(config): exclude credential-rotation fields from models-cache auth.json fingerprint
auth.json is rewritten by credential-pool/OAuth token refresh roughly every
14 minutes. _models_cache_source_fingerprint() hashed it via mtime/size
(#1699 _models_cache_file_fingerprint), so every token refresh churned the
fingerprint and the 24h /api/models cache was effectively dead -- the hot
GET /api/session?resolve_model=1 path paid a cold ~11.5s rebuild every few
minutes (RCA t_d127953d residual #2, t_16551f61).

Add _auth_store_semantic_fingerprint(): content-hash auth.json with a
DENY-list of known credential-rotation-only keys (access/refresh token,
expiry, per-credential status/telemetry, request_count, save updated_at)
stripped. Deny-list (not allow-list) is deliberate -- any unknown field, or
a real provider/endpoint/model-set change (active_provider, a new
credential_pool entry, base_url, source, label, auth_type, the providers{}
block, ...) stays in the fingerprint and still correctly busts the cache.
Conservative fallbacks: missing file -> marked; unreadable/corrupt ->
stat-based fallback (never less safe than pre-fix). config.yaml keeps the
cheap stat fingerprint (deliberate edits, no timer churn).

Bidirectional invariant regression test (non-tautological -- the
end-to-end churn test flips RED when the auth_json axis is reverted to
stat-based): token-only churn keeps fingerprint byte-identical AND keeps a
valid disk cache loadable; active_provider change / new credential_pool
entry / changed base_url each flip the fingerprint AND reject the stale
disk cache. Measured: 5/5 cold rebuilds per 5 refresh cycles -> 0/5.

Tests: 9 new pass; 28 adjacent (#1699/#1633/display-resolver) pass;
54 models_cache/fingerprint suite pass.
2026-05-26 12:46:59 +08:00
Harlan Zhou
e8b426d825 test: avoid global env-coupled defaults regression 2026-05-25 01:58:32 +00:00
Harlan Zhou
f8a7726e09 fix(windows): align WebUI defaults with Hermes Agent home path 2026-05-25 01:58:32 +00:00
Frank Song
826e719a29 fix: validate auxiliary model task slots 2026-05-25 08:51:44 +08:00
Roberto Villegas
d1471fbed7
fix(webui): resolve reasoning efforts for routed codex models 2026-05-24 18:42:44 -06:00
Roberto Villegas
a9ce2889af
fix(ui): hide reasoning chip when model lacks effort levels
Resolve supported reasoning efforts per active model/provider and pass
that context through /api/reasoning so Composer and other non-configurable
models no longer show a misleading effort picker.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 18:42:31 -06:00
Roberto Villegas
ef5eafcceb
fix(cursor-acp): route slash models and honor picker on new chat
Ensure cursor/composer IDs always resolve via @cursor-acp:, carry the
visible picker selection into POST /api/session/new, persist model
changes before a session exists, and evict cached agents on model switch.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 18:42:20 -06:00
Roberto Villegas
aee376323f
feat(cursor-acp): add cursor-acp to WebUI model picker
- Add cursor-acp to _PROVIDER_DISPLAY with label 'Cursor ACP'
- Add cursor-acp static model list to _PROVIDER_MODELS
- composer-2.5, composer-2, default, cursor-acp
2026-05-24 18:42:04 -06:00
hermes-agent
a86b378036 Stage 405: PR #2680 — feat: add Auxiliary Models settings card by @mccxj
Cherry-picked via 3-way apply (rebase had failed on static/index.html
conflict when applied via rebase commit chain; 3-way of the net delta
against stage HEAD applied cleanly).

Co-authored-by: mccxj <mccxj@github.users.noreply.github.com>
2026-05-24 18:28:26 +00:00
AJV20
b6f7412b53 Add option to ignore agent updates 2026-05-24 15:52:34 +00:00
Abdul Munim
d7f1514d96 fix(models): surface bedrock provider in WebUI model picker (#2720)
Bedrock was silently dropped from the picker because:
1. 'bedrock' absent from _PROVIDER_DISPLAY — group header fell back to
   title-cased id; more critically the group fell to the else branch
2. 'bedrock' absent from _PROVIDER_MODELS — else branch has no
   auto-detected models, so the group was never appended
3. Fallback env-var detection (hermes_cli unavailable) never checked
   AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY

Fix:
- Add 'bedrock': 'AWS Bedrock' to _PROVIDER_DISPLAY
- Add static fallback model list to _PROVIDER_MODELS['bedrock'] with
  global Anthropic Claude 4.x cross-region inference profile IDs;
  live discovery via hermes_cli.models.provider_model_ids('bedrock')
  is used first (existing _read_live_provider_model_ids machinery)
- Detect bedrock in env fallback path when both AWS_ACCESS_KEY_ID and
  AWS_SECRET_ACCESS_KEY are present

Tests: tests/test_issue2720_bedrock_model_picker.py (5 new tests)
2026-05-24 03:42:13 +00:00
george-andraws
b2477974c5 fix: make in-flight recovery storage quota-safe 2026-05-22 19:49:20 +00:00
fxd-jason
56575bd393 feat: sort configured/custom providers to top in model picker and settings 2026-05-22 16:13:46 +00:00
Hermes Agent
905b3eba5e Stage 398: PR #2700 — feat: make pinned session limit configurable (builds on shipped #2614 3-cap)
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-05-21 17:43:56 +00:00
Hermes Agent
7d3013245a Stage 398: PR #2687 — feat: hide suggestions preference (closes #2679)
Closes #2679

Co-authored-by: Michaelyklam <Michaelyklam@users.noreply.github.com>
2026-05-21 17:43:48 +00:00
nesquena-hermes
38933b288d Stage-394 follow-up: profile-switch reconciliation + a11y switch role + server-side chat/settings filter
Per deep-review verdict SHIP-WITH-FIXES on PR #2636:

1. Profile-switch reconciliation: _refreshProfileSwitchBackground now re-fetches
   /api/settings and re-applies hidden_tabs for the new profile. Without this,
   Profile A's hidden-tabs choice stayed in effect under Profile B until the
   user opened Settings → Appearance.

2. A11y: switched chips from role=button + aria-pressed to role=switch +
   aria-checked. The pressed/not-pressed wording confused screen-reader users
   because chip-off looks like the off state. Added role=group +
   aria-labelledby on the container, and a :focus-visible style on the chips.

3. Server-side belt-and-suspenders: api/config.py now strips 'chat' and
   'settings' from hidden_tabs at validation time, matching the client's apply-
   time filter. A tampered POST can no longer persist the forbidden values.

3 new regression tests added (chat/settings rejection, profile-switch wiring,
chip a11y attributes).

Co-authored-by: FrancescoFarinola <francesco.farinola@example.com>
2026-05-20 23:05:19 +00:00
Francesco Farinola
7f1feca3fe feat: sidebar tab visibility toggle in Settings > Appearance
Add chip row in Settings > Appearance that lets users toggle individual
sidebar/rail tabs on or off. Chat and Settings are always visible.

- Backend: hidden_tabs list setting with validation (no bool coerce)
- Frontend: pill chips that scan rail buttons, autosave via appearance
- Boot: _restoreTabVisibility IIFE applies hidden tabs before first paint
- i18n: 11 locales (label + description)
- Tests: 5 regression tests covering backend, frontend contracts,
  boot restore, i18n coverage, and settings session tracking
2026-05-20 22:57:36 +00:00
nesquena-hermes
4d8b8d0ffe Stage 393: PR #2633
# Conflicts:
#	CHANGELOG.md
2026-05-20 22:23:53 +00:00
Colin Chang
9c3e37d2ee fix: custom_providers models allowlist takes priority over live /v1/models fetch
Custom providers that have a curated models: list in config.yaml
(e.g. ZenMux gateways) should show ONLY those configured models in
the picker dropdown, not the full /v1/models catalog.

Before this fix, _named_custom_groups unconditionally called
_read_custom_endpoint_models() which would pull hundreds of models
from aggregator gateways and overwrite the user's curated list.

Now the build checks if the custom_provider entry has a non-empty
models dict/list in config.yaml — if so, it skips the live fetch
and uses only the configured models (same behavior as hermes-agent
model_switch.py Section 4 patch).

Closes: configure-model-list-should-be-authoritative
2026-05-20 20:22:11 +00:00