Commit Graph
343 Commits
Author SHA1 Message Date
nesquena-hermesandstarship-s 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-hermesandstarship-s 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
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-hermesandakrhin 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-hermesandakrhin 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
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
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
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
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
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
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
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
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 Agentandnesquena-hermes 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 Agentandnesquena-hermes 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 Agentandnesquena-hermes 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-hermesandRod Boev 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-hermesandstarship-s 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-hermesandRod Boev 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
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-hermesandnesquena-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-hermesandrodboev 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
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
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
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