Commit graph

488 commits

Author SHA1 Message Date
nesquena-hermes
14ad45bd1f fix(#5311): skip live /v1/models probe for opencode-go — use curated static list
OpenCode Go's /v1/models endpoint returns models from the full public catalog that
are NOT enabled on the Go tier; selecting any such probe-only model 404s
('model not found') when the chat request is sent. get_available_models now skips
the live probe for opencode-go (bare branch) so the next `if not raw_models` falls
through to the curated static _PROVIDER_MODELS['opencode-go'] list — the actual
Go-tier models. Narrow, provider-scoped; no other provider affected.

Contributor stage; gate-pass. Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
2026-07-05 05:05:30 +00:00
nesquena-hermes
d27ef5d196 harden #5511: active provider owns its OWN providers: allowlist (complete ownership model)
Codex 5th CORE: the ownership set only covered static _PROVIDER_MODELS, not the
active provider's own config.yaml providers.<active>.models allowlist. So an
active provider defined purely via providers: (no static catalog) could be
hijacked by another slug's overlapping entry earlier in config order. Fix folds
the active provider's own providers: allowlist (list + dict shapes, copilot
excluded) into _provider_models_set — completing the ownership model so the
active provider owns its declared models from EITHER the static catalog OR its
own providers: entry. Regression test (fail-without-fix verified). 40 model_resolver
tests green.

Co-authored-by: akay64 <akay64@users.noreply.github.com>
2026-07-04 07:28:44 +00:00
nesquena-hermes
f2604a22bd harden #5511: _canonicalise_provider_id accepts alias targets in _PROVIDER_MODELS (Gemini class)
Codex 4th CORE (root latent bug in the shared canonicalizer): _canonicalise_provider_id
only accepted an alias resolution if the target was in _PROVIDER_DISPLAY. But some
canonical targets (e.g. 'gemini') are indexed in _PROVIDER_MODELS and NOT
_PROVIDER_DISPLAY, so 'google-gemini'->'gemini' was rejected and returned raw
'google-gemini' — leaving the ownership guard empty and letting providers.openai.models
hijack an active Gemini-owned model. Fix mirrors the direct-hit check (line 1445):
accept alias targets in EITHER _PROVIDER_DISPLAY OR _PROVIDER_MODELS. This is the
root fix for the whole alias-canonicalization class (gemini/google-gemini/
google-ai-studio etc.), not just the reported site. Blast-radius verified: 81
canonicalise/provider-alias tests across 5 files still green; x-ai still stays x-ai,
opencode_go still folds. Gemini regression test added (fail-without-fix verified).

Co-authored-by: akay64 <akay64@users.noreply.github.com>
2026-07-04 07:23:45 +00:00
nesquena-hermes
4399ff02e0 harden #5511: canonicalise active provider for ownership set (latent alias bug)
Codex 3rd CORE (pre-existing latent bug the providers: scan exposes): the
_provider_models_set ownership lookup used the RAW config_provider (e.g. 'z-ai')
but _PROVIDER_MODELS is keyed by canonical slug ('zai'), so an aliased active
provider got an empty ownership set -> _skip_custom_providers guard-2 silently
failed -> another providers.<slug>.models entry could hijack an active-owned
model (verified: active z-ai + glm-5 + providers.openai.models.glm-5 -> openai).
Fix: canonicalise config_provider before the _PROVIDER_MODELS lookup (and reuse
the canonical slug for the scan's ownership guard). Regression test with an
aliased active provider (verified fail-without-fix).

This is the root-cause fix for the alias class — the ownership guard now holds
for every aliased provider (zai/kilo/etc.), not just the reported site.

Co-authored-by: akay64 <akay64@users.noreply.github.com>
2026-07-04 07:17:05 +00:00
nesquena-hermes
f8c9f9e08c harden #5511: providers: scan honors active-provider ownership guard
Codex 2nd CORE: the new providers: scan didn't honor the _skip_custom_providers
active/default ownership guard (config.py:2535) that the custom_providers scan
uses. So with an active provider (e.g. ai-gateway) + model.default=gpt-5 AND a
providers.openai.models.gpt-5 entry, the scan hijacked gpt-5 to openai instead of
the active ai-gateway path. Fix: when _skip_custom_providers is true, the
providers: scan only considers the active provider's own canonical slug (skip all
other slugs). Regression tests: active-provider-owns-default not hijacked (fails
without fix); active provider's OWN providers: entry still matches.

Co-authored-by: akay64 <akay64@users.noreply.github.com>
2026-07-04 07:10:47 +00:00
nesquena-hermes
cca3b6c279 release #5511: exclude Copilot from providers: routing scan + regression tests + CHANGELOG
Gate-cert (nesquena-hermes) + Codex found 1 CORE: akay64's new providers: scan in
resolve_model_provider() iterated ALL providers.<slug>.models INCLUDING
providers.copilot.models — but that key is a documented per-model SETTINGS map
(reasoning_effort/limits), NOT a routing allowlist (config.py exception). So a
Copilot per-model settings entry hijacked that model's routing to Copilot
(Codex reproduced gpt-5 -> copilot). Fix (Codex-exact): skip copilot in the scan.

Added the regression tests the PR shipped 0 of: user-defined allowlist routes;
active-openai + providers.copilot.models (dict + list shapes) stays openai
(both verified to fail without the exclusion).

Co-authored-by: akay64 <akay64@users.noreply.github.com>
2026-07-04 07:06:05 +00:00
nesquena-hermes
ec9f77c319 Merge commit '87404d76f036' into release/stage-5511 2026-07-04 06:57:54 +00:00
akay64
87404d76f0 fix: scan providers: in resolve_model_provider() for user-defined provider matching
Mirrors the existing custom_providers: scan immediately above. User-defined
providers declared under config.yaml's providers: section with explicit
models: lists were invisible to model-to-provider resolution — bare model
IDs fell through to the stale config_provider, routing through the wrong
endpoint.

The insertion site sits after custom_providers: (unchanged) and before
all heuristics (@provider:model, slash, fallback — all unchanged).
2026-07-04 08:59:21 +04:00
Rod Boev
fdacb4b922 fix(#5435): stop startup workspace repair from persisting speech defaults 2026-07-02 21:28:46 -04:00
Rod Boev
8f7b9c1d0d fix(#5435): drop dead persisted-key state from config load 2026-07-02 19:38:35 -04:00
Rod Boev
3b344b1d9a fix(#5435): preserve local speech prefs until settings.json owns them 2026-07-02 19:33:13 -04:00
Rod Boev
91ae1dede9 fix(#5435): persist TTS and voice preferences server-side 2026-07-02 15:43:24 -04:00
nesquena-hermes
b931ae2f9b fix(settings): reconcile #5145 rename+steer-flip onto master's #5170 mirror
Rebase PR #5162 (rename busy_input_mode -> default_message_mode; flip the
default from 'queue' to 'steer') onto current origin/master WITHOUT dropping
the shipped #5170 localStorage persistence mirror.

Rename the mirror machinery to the new setting name for consistency:
  _BUSY_INPUT_MODES        -> _DEFAULT_MESSAGE_MODES        (values unchanged)
  _normalizeBusyInputMode  -> _normalizeDefaultMessageMode  (fallback now 'steer')
  _persistBusyInputMode    -> _persistDefaultMessageMode
  _readPersistedBusyInputMode -> _readPersistedDefaultMessageMode
  window._busyInputMode    -> window._defaultMessageMode (+ renamed exports)

localStorage: write the new 'hermes-default-message-mode' key; read it with a
fallback to the legacy 'hermes-busy-input-mode' key so an existing user's
persisted preference survives the rename.

Preserve #5170 behavior at every mirror site under the new names:
  - boot success  -> window._defaultMessageMode=_persistDefaultMessageMode(...)
  - boot FAILURE  -> window._defaultMessageMode=_readPersistedDefaultMessageMode()
    (NOT a hardcoded 'steer' — a saved 'interrupt'/'queue' must still apply when
    the server is unreachable; do not regress #5167/#5132)
  - preferences autosave, settings-panel load, and _applySavedSettingsUi all
    persist through _persistDefaultMessageMode(...)

Tests updated for the rename while keeping the persistence-behavior assertions
(test_1062, test_5145, test_5167); test_5167 gains explicit guards that the
load-failure path reads the persisted pref and never hardcodes a literal mode,
plus autosave/panel-load mirror-write coverage.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-07-02 05:44:48 +00:00
nesquena-hermes
4bbb64bcc1 #5213: Claude Code sidebar visibility toggle (#4714), rebased on master 2026-07-02 01:40:19 +00:00
nesquena-hermes
d43618beca stage #5301 2026-07-01 22:42:35 +00:00
nesquena-hermes
17b0998c0b stage #5313 2026-07-01 19:35:42 +00:00
nesquena-hermes
6caaced8eb fix(webui): bound in-memory SESSIONS cache with lazy reload (#4765)
Root cause of the silent-crash-after-hours cluster (#4765/#2233/#4633): the
global in-memory SESSIONS LRU evicted with a blind popitem(last=False), which
could drop an actively streaming or not-yet-persisted session (data loss) and
was capped only via an env var. On long-running installs the effective result
was unbounded RAM growth until segfault.

- Add _session_is_evictable(): a session is evictable ONLY when it is not
  streaming (no active_stream_id), has no in-flight turn (no pending_user_message
  / pending_started_at), and its full state is proven on disk (sidecar
  message_count >= in-memory count; metadata-only stubs and zero-message shells
  are trivially safe).
- Add _evict_sessions_over_cap(): replaces all 8 blind popitem loops across
  models.py, routes.py, streaming.py. Walks the LRU oldest-first and removes only
  provably-safe entries; never acquires LOCK/stream locks itself (caller holds
  LOCK) so no lock-ordering deadlock. May briefly exceed the cap rather than ever
  evict an active/unsaved session.
- Make the cap configurable via config.yaml webui.sessions_cache_max
  (get_sessions_cache_max()); precedence config.yaml -> HERMES_WEBUI_SESSIONS_MAX
  (legacy) -> DEFAULT_SESSIONS_CACHE_MAX=300. No new HERMES_* env var. Invalid or
  <1 values fall back so a typo can never disable the bound.
- Evicted sessions lazily reload from their JSON sidecar via the existing
  get_session() accessor; no call sites changed. _index.json sidebar behavior
  unchanged (the sidebar reads the index, not SESSIONS).
- Add tests/test_issue4765_sessions_lru_eviction.py (8 tests): eviction past
  cap, active/streaming never evicted, unsaved/stale-tail never evicted, lazy
  reload with identical content, and no-data-loss under heavy churn.
- README: document the config.yaml key + safety semantics.

Fixes #4765.
2026-07-01 18:14:39 +00:00
Charles Inglis
f9de93f42e refactor: make nested-route reasoning deny boundary-based, not prefix-based
Structural hardening per user request, following the 3rd round of the
same bypass class on PR #5313's reasoning_efforts feature:

Round 1: plain-ordering regression (provider-config short-circuit ran
before the nested-route deny).
Round 2: a provider-qualified hint (@custom:<slug>:vertex/gemini-...)
left a slug fragment that the deny's prefix-match missed.

Both were legitimate, narrowly-targeted fixes, but the pattern —
_nested_route_reasoning_denied() requiring the model string to START
WITH the route prefix — meant every future wrapper/nesting scheme
would need the strip logic to be updated in lockstep, and a missed
case fails OPEN (reasoning re-enabled on a route that must never show
it), which is the wrong failure mode for a security-adjacent guard.

This commit removes that class of bug instead of patching its latest
instance: _nested_route_reasoning_denied() now searches for the
vertex/gemini- or gemini_cli/gemini- pattern ANYWHERE in the string at
a non-alphanumeric boundary, rather than requiring it at position 0.
Correctness no longer depends on _strip_provider_hint_for_reasoning()
having stripped exactly the right prefix first — any number of opaque
wrapper layers (@provider:, a named custom-provider slug, or any
nesting scheme not yet invented) can precede the route and the deny
still fires.

Verified: all historical bypass strings (round 1 and round 2, plus a
hypothetical deeper double-wrapped case) now correctly deny; embedded
substrings that must NOT match (e.g. 'notvertex/gemini-x') correctly
don't, thanks to the boundary lookbehind.

Added test_nested_route_deny_is_boundary_based_not_prefix_based
locking in the structural invariant directly, independent of any
particular wrapper scheme.

Verification:
./scripts/test.sh tests/test_reasoning_effort_model_capabilities.py tests/test_custom_provider_bare_model_reasoning.py
# 54 passed in 1.31s

Full suite: 11257 passed, 64 pre-existing failures (identical file/test
set as the prior commit's baseline — network/credential/profile-isolation/fd-leak
tests, unrelated to reasoning_efforts).
2026-07-01 10:02:45 -05:00
Charles Inglis
0109baa142 fix: strip named-provider slug before nested-route reasoning deny
Resolve gate certifier feedback on PR #5313 (deeper bypass, round 3):

A provider-qualified hint like `@custom:<slug>:vertex/gemini-image-1.0`
still bypassed `_nested_route_reasoning_denied()`. The old
`_strip_provider_hint_for_reasoning()` did a naive first-colon split,
which only removed the leading `@custom:` wrapper and left the named
provider's slug (e.g. `agg:vertex/gemini-image-1.0`) attached to the
model id. That leftover slug fragment no longer starts with
`vertex/gemini-`, so the nested-route deny missed it and a configured
`providers.<name>.reasoning_efforts` / `custom_providers[].reasoning_efforts`
allowlist re-enabled reasoning controls on Gemini image/embedding routes
that must never expose them.

Fix: `_strip_provider_hint_for_reasoning()` now accepts the resolved
provider id and strips the exact `@{provider}:` prefix first (e.g.
`@custom:agg:`), so both the wrapper and the slug are removed in one
pass before the nested-route deny check runs. Falls back to the
original first-colon split when no provider is supplied, preserving
existing behavior for plain `@provider:model` hints.

Verified in-process: both
`@custom:agg:vertex/gemini-image-1.0` and
`@custom:agg:vertex/gemini-embedding-001` now correctly resolve to []
instead of leaking the configured ["low", "high"].

Added regression test extending the existing nested-route-deny test to
cover provider-qualified hinted models.

Verification:
`./scripts/test.sh tests/test_reasoning_effort_model_capabilities.py tests/test_custom_provider_bare_model_reasoning.py`
-> 53 passed in 1.23s
Full suite: 11258 passed (63 pre-existing failures unrelated to this
change — network/credential/profile-isolation/fd-leak tests; confirmed
identical failure set on unmodified branch).
2026-07-01 09:35:14 -05:00
Charles Inglis
4f4c8fd183 fix: respect custom provider config and nested route denies
Resolve review feedback on PR #5313:
- Read reasoning_efforts from named custom_providers entries for custom:<name>
  providers instead of looking only in providers:<name>.
- Preserve the nested-route deny ordering before the provider-config shortcut
  so Gemini image/embedding routes cannot be re-enabled by config.
- Deduplicate configured effort values before returning them.

Add focused regression tests for bare provider config, named custom provider
config, all-invalid fallthrough, ACP hard guards, and nested route denies.
2026-07-01 07:35:08 -05:00
Gordie
fb8bf2f5bd fix(webui): keep #1855 fast-path window and Copilot gpt-4o regression tests green
- Extract the MoA @moa:/moa/ prefix-stripping branch of
  _resolve_compatible_session_model_state into a new
  _moa_fast_path_model_state() helper. Inlining it had pushed
  `catalog = get_available_models()` just past the 6000-char
  source window that
  tests/test_issue1855_resolve_model_provider_fast_path.py::
  TestFastPathSourceShape scans to guard the #1855 fast-path/
  catalog-call ordering, breaking that regression test after
  rebasing onto current master.
- Re-add gpt-4o to the refreshed Copilot static fallback list.
  It's a real Copilot-served model and
  tests/test_issues_373_374_375.py::TestStaleModelListCleanup::
  test_copilot_list_unchanged asserts the Copilot list keeps it
  even as #374 removes it from the generic OpenAI list. The live
  Copilot catalog probe remains authoritative; this only affects
  the cold-start/probe-miss fallback.

Verification:
  ./scripts/test.sh tests/test_issue1855_resolve_model_provider_fast_path.py tests/test_issues_373_374_375.py tests/test_issue5057_moa_webui_route.py tests/test_copilot_provider_model_settings_not_allowlist.py tests/test_moa_model_picker_provider.py tests/test_pr1970_lmstudio_base_url_fallback.py -q
  -> 73 passed in 3.60s

  ./scripts/test.sh tests/ -k "config or provider or model or copilot or moa or lmstudio" -q
  -> 1745 passed, 2 skipped, 9775 deselected, 1 xpassed in 68.57s
2026-07-01 07:01:51 -05:00
Gordie
0b8e67c20d fix(#5301): admit active models-only custom provider without reopening dedup regression
Maintainer review found regression 2 on commit 575996a7: the new
_has_provider_route gate (api/config.py) required api/base_url/api_key/key_env
before admitting a configured provider into the picker, which dropped a
models-only custom provider config (the lmstudio-style shape from #1970,
tests/test_pr1970_lmstudio_base_url_fallback.py::test_provider_catalog_preserves_dict_shaped_raw_key_lookup).

A naive fix (admit any config with models) would re-break
test_unknown_duplicate_copilot_provider_config_is_not_rendered, since a
spurious alias like copilot-2: {name: copilot, models: {...}} must stay
rejected.

Fix: admit a models-only provider config as evidence only when its canonical
id matches the active/configured provider (threaded via active_provider,
already in scope). Non-active models-only configs (including duplicate
aliases of known providers) are still rejected.

Added a regression test:
tests/test_pr1970_lmstudio_base_url_fallback.py::test_provider_catalog_rejects_non_active_models_only_custom_provider
covering both the active-admits and non-active-rejects cases side by side.

Verification:
./scripts/test.sh tests/test_issue5057_moa_webui_route.py tests/test_copilot_provider_model_settings_not_allowlist.py tests/test_moa_model_picker_provider.py tests/test_pr1970_lmstudio_base_url_fallback.py -q
26 passed, 1 skipped in 5.58s

Broader sanity pass: ./scripts/test.sh tests/ -k "config or provider or model or copilot or moa or lmstudio" -q
1728 passed, 5 skipped, 9654 deselected (4 pre-existing failures confirmed unrelated: 2 reproduce identically on the pre-fix commit via git stash, 2 are order-dependent and pass in isolation).
2026-07-01 07:01:51 -05:00
nesquena-hermes
cf1ab5bbbc fix(#5301): scope models-as-settings-map guard to Copilot only (was breaking providers.<id>.models allowlist for all built-ins, #644 regression) 2026-07-01 07:01:51 -05:00
Gordie
07a1509feb fix(webui): harden MoA picker preset fallbacks
- Avoid an unguarded resolve_moa_preset fallback call when preset resolution fails.
- Populate the MoA picker directly from configured presets so older Hermes Agent installs that lack provider_model_ids('moa') still render the virtual provider.
- Add regression coverage for both review findings.

Verification:
./scripts/test.sh tests/test_issue5057_moa_webui_route.py tests/test_copilot_provider_model_settings_not_allowlist.py tests/test_moa_model_picker_provider.py -q
12 passed, 1 skipped
2026-07-01 07:01:51 -05:00
Gordie
f29441bf09 fix(webui): split MoA picker and Copilot catalog fixes
- Keep resolve_moa_preset optional so older hermes-agent installs still degrade gracefully.
- Surface MoA presets in the WebUI model picker as a virtual provider.
- Keep configured Copilot per-model settings from collapsing the built-in catalog.
- Refresh the Copilot static fallback used when the live catalog probe misses.

Verification:
./scripts/test.sh tests/test_issue5057_moa_webui_route.py tests/test_copilot_provider_model_settings_not_allowlist.py tests/test_moa_model_picker_provider.py -q
11 passed, 1 skipped
2026-07-01 07:01:51 -05:00
Charles Inglis
b80ea016b1 fix: guard cursor-acp/copilot-acp before config lookup, fall through on all-invalid list
Addresses Greptile review feedback on PR #5313:
1. Move cursor-acp/copilot-acp guard before step 0 so a stray config
   entry can't surface unsupported effort options for those providers.
2. Only short-circuit when the filtered list is non-empty; an all-invalid
   list (e.g. typos) falls through to heuristics instead of hiding
   reasoning support from the UI.
2026-06-30 23:28:03 -05:00
Charles
26fb3745f9
Update api/config.py
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-06-30 23:25:43 -05:00
Charles Inglis
b84832e999 feat: support per-provider reasoning_efforts in config.yaml
Add a config-driven path to resolve_model_reasoning_efforts() that reads
providers.<name>.reasoning_efforts from config.yaml. This lets users
explicitly list valid reasoning effort levels per provider, so the
WebUI dropdown only shows options the model actually supports.

Handles both custom:<name> and bare registered provider names.
Falls through to existing heuristics (Copilot per-model lists, LM Studio
live API, models.dev) when no config entry is present.
2026-06-30 23:06:09 -05:00
Paladin173
d8f0f8f97f Fix composer control reorder rebase collision
Reapply footer control ordering on current upstream/master while preserving the required situational chip renderer. Persist composer_control_order with backend validation, keep the settings descriptions reorder-aware, and make primary/situational chip renderers participate in same-group drag ordering.

Verified with: node --check static/boot.js; node --check static/panels.js; git diff --check; ./scripts/test.sh tests/test_issue4598_composer_control_visibility.py
2026-06-30 21:11:54 -05:00
nesquena-hermes
3e84d7bf11 Merge #4738 (savagebread neon skins + skin-persistence fix) into stage 2026-06-30 22:21:28 +00:00
nesquena-hermes
0b2507ac9f fix(#4738): register neon-soft/neon-paint in _SETTINGS_SKIN_VALUES (server-side skin persistence) 2026-06-30 22:13:36 +00:00
nesquena-hermes
78ca4d43a1 Merge #5005 (futureworld678 Shift+Enter) into stage 2026-06-30 21:51:42 +00:00
nesquena-hermes
0b74397fa6 Merge #5120 (rodboev budget limit) into stage 2026-06-30 21:28:51 +00:00
nesquena-hermes
b098e5c658 feat(#4676): gate per-project quick-create buttons behind opt-in setting
Add a default-OFF 'project_quick_create_buttons' setting that gates the
per-project '+' quick-create buttons on sidebar project chips. The chip
render site only calls _attachProjectQuickCreateButton when the new
window._projectQuickCreate runtime flag is enabled, so the buttons do
not appear unless the user opts in via Settings.

Mirrors the large_text_paste_as_attachment opt-in plumbing (defaults,
allowlist, settings collect/restore/save, window hydration at both sites,
checkbox row, i18n across all locales).
2026-06-30 19:49:07 +00:00
Lester
b1c11939aa Add Shift+Enter send key option 2026-06-30 18:04:29 +00:00
nesquena-hermes
121059b384 Merge #5161 (pr/5144-busy-composer-placeholder-hint) into stage-w5 2026-06-30 17:25:03 +00:00
Maude Bot
5263b9acf7 feat: add Webhooks sidebar filter parity
Co-authored-by: Kilian Tyler <kilian@kil.dev>
2026-06-30 17:06:31 +00:00
justanotherAIcontributor
91d32c40c5
Merge branch 'master' into fix/large-text-paste-toggle-upstream 2026-06-30 16:13:49 +10:00
Hermes Agent
fabd461dcd fix(auth): guard profile-owned stream IDs 2026-06-30 04:28:15 +00:00
Stacey2911
6b0d8eaadf fix(webui): make large text paste-as-attachment toggleable via settings
Add a user setting 'large_text_paste_as_attachment' (default: on) that lets
users paste large text inline instead of having it auto-converted to .md file
attachments. The toggle lives in Control Center → Chat tab and persists via
settings.json, following the established upstream pattern for boolean settings.
2026-06-30 14:25:53 +10:00
Rod Boev
affdb40a1c fix(config): route stale model reads through shared reload gate (#5220) 2026-06-29 20:50:34 +00:00
Rod Boev
6653b7bd2f fix(config): collapse post-restart config reload stampede (#5220) 2026-06-29 20:50:34 +00:00
Rod Boev
04d0cde843 fix(#692): keep budget validation consistent across save and read 2026-06-29 01:59:51 -04:00
Rod Boev
9a1f27a48b feat(settings): add opt-in busy placeholder hint (#5144) 2026-06-28 18:47:42 -04:00
Rod Boev
234fefe6ee fix(#5139): keep timeout capability probes on the unsupported path 2026-06-28 18:29:37 -04:00
Rod Boev
23b7a7935a fix(#5139): keep older gateways on the unsupported warning path 2026-06-28 18:18:47 -04:00
Rod Boev
343a8b50bf fix(#5139): distinguish offline gateway approval warnings 2026-06-28 17:53:37 -04:00
nesquena-hermes
2d249089fc fix(session): guard _evict_session_agent against closing a live session's _session_db
Mirror the worker's LRU-eviction ACTIVE_RUNS liveness check in
_evict_session_agent so truncate/clear/model-switch can't close the SessionDB
out from under an in-flight turn on the same session (#5096 Bug D follow-up).
The cache handle still drops (harmless — worker holds a local ref); only the
lifecycle commit + _session_db.close() are skipped while a run is active.

Co-authored-by: claw-io <claw-io@users.noreply.github.com>
2026-06-28 19:36:59 +00:00
Rod Boev
62e25ad9c1 feat(#692): add configurable budget limit with percentage-used tracking 2026-06-28 10:10:35 -04:00
nesquena-hermes
20d0313d0c feat(settings): expose max_tokens override in Settings (#5010)
The streaming path already honored a max_tokens cap from the active profile's
config.yaml, but there was no UI to manage it. Settings > Preferences now has a
"Max output tokens" field that writes a root-level max_tokens override through
the profile config (not settings.json, which the streaming worker never reads
for this), preserving unrelated YAML keys under _cfg_lock. Blank clears the
override so agent.max_tokens fallback resumes; get_max_tokens_status() surfaces
root / effective / fallback so the field shows what a new turn would use.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-27 20:27:18 +00:00
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