Commit graph

8308 commits

Author SHA1 Message Date
Bartok9
70679ada61 fix(config): validate config-key schema, refuse unknown keys (#34067)
Fixes #34067. 'hermes config set <unknown.key.path> <value>' silently
accepted arbitrary key paths, wrote them to config.yaml, and reported
success — but the runtime/gateway never read them.

The headline case from the issue: a user typing
  hermes config set gateway.discord.gateway_restart_notification false
gets success, but the value lands at config.yaml:gateway.discord.* where
nothing reads it. The correct path is discord.gateway_restart_notification
(platform configs live at the top level of DEFAULT_CONFIG, not under
a 'platforms' namespace). The user reasonably believes the change took
effect, then loses time debugging behavior that hasn't changed.

Fix: schema-validate the dotted key path against DEFAULT_CONFIG before
writing. Walk DEFAULT_CONFIG along the user's segments and:

  - Reject unknown top-level keys with a fuzzy-match suggestion
  - Reject unknown sub-keys by suggesting the closest sibling
  - Accept anything below open-dict shapes (mcp_servers.<name>.command,
    providers.<openrouter>.api_key, etc.)
  - Accept anything below schema-defined-extensible shapes (platform
    configs like discord.*, telegram.* — PlatformConfig has dynamic
    'extra' fields, so deep validation is unsafe)
  - Special-case 'platforms.X' → suggest 'X' (the actual top-level layout)

Bypass with --force for forward-compatibility with keys a newer Hermes
version adds but the running version doesn't recognize yet:
  hermes config set --force brand_new_future_key value

API-key style names (OPENROUTER_API_KEY, *_TOKEN, etc.) still route to
.env before schema validation runs, so this is non-breaking for that path.

Adds 21 regression tests across TestSchemaValidation + TestValidateConfigKey
covering: unknown top-level keys, unknown sub-keys (the headline bug),
platforms.* prefix suggestions, fuzzy-match top-level typos, sibling-
suggestion sub-key typos, --force bypass, and that known config keys
(simple, platform-extensible, open-dict) still work.

Also updates 2 pre-existing tests that used non-canonical paths
(platforms.telegram.* and 'verbose') which schema validation correctly
flags — switched to canonical paths (telegram.* and agent.gateway_timeout).

All 53 tests in test_set_config_value.py pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 05:38:59 -07:00
Avi Fenesh
77ba81f75f fix(bedrock): add Fable + Claude 4.6/4.7/4.8 1M entries to context table, drop stale cached values
BEDROCK_CONTEXT_LENGTHS was missing entries for current 1M-context Claude
models, and the resolution path in get_model_context_length() short-circuits
to that table (step 1b) before DEFAULT_CONTEXT_LENGTHS is ever consulted, so
the catalog's correct values could never apply on Bedrock:

- claude-fable-5 (no entry at all) fell through to
  BEDROCK_DEFAULT_CONTEXT_LENGTH and reported 128K for a 1M model.
- opus-4-7 / opus-4-8 substring-matched the generic 'anthropic.claude-opus-4'
  key and reported 200K.
- opus-4-6 / sonnet-4-6 had explicit 200K entries predating their 1M windows.

The practical symptom: the agent compresses context prematurely (at ~128K or
~200K of a 1M window) on every Bedrock-hosted current Claude model.

Fixing the table alone is not enough for existing installs: a previously
persisted 128K/200K value in the context-length cache wins at step 1 and
masks the corrected table forever. Step 1 now reconciles Bedrock-context
cache hits against the static table (the table is authoritative for Bedrock
— there is no live probe to reconcile against), invalidating stale entries
so existing users converge to the right window without manual cache surgery.

Tests cover the new table entries (incl. inference-profile and versioned ID
forms), the 128K-default regression for Fable, the stale-cache invalidation
path, and that pre-4.6 models keep their 200K entries.
2026-07-20 05:38:55 -07:00
Drexuxux
487574c5b6 fix(pricing): strip apac./au. Bedrock region prefixes so cost isn't unknown
_normalize_bedrock_model_name stripped ("us.", "global.", "eu.", "ap.",
"jp.") before the pricing lookup, but AWS Bedrock's Asia-Pacific
cross-region inference profiles are prefixed "apac." (and Australia
"au."), not "ap.". A bare "ap." never matches an "apac.*" id
(str.startswith stops at the 'a' where "ap." expects '.'), so
"apac.anthropic.claude-*" and "au.anthropic.claude-*" fell through with
the prefix intact, missed the bare "anthropic.claude-*" pricing key, and
every Asia-Pacific / Australia Bedrock session priced as "unknown" — no
cost estimate or tracking for two whole geographies, while us./eu./global.
worked.

Add "apac." and "au." to the strip list (mirrors the same fix landing in
bedrock_adapter.is_anthropic_bedrock_model via #46297, which covers the
prompt-caching capability gate but not this duplicated cost-lookup copy).

Extends the existing cross-region pricing test to cover apac./au.; without
the fix it fails with scoped == None for "apac.".
2026-07-20 05:38:45 -07:00
Jake Tracey
4e4904c379 fix(bedrock): recognise au./apac. inference profiles to enable prompt caching
is_anthropic_bedrock_model() strips a regional prefix before checking for
"anthropic.claude" to route Claude through the AnthropicBedrock SDK path
(prompt caching, thinking budgets) instead of the Converse path. The prefix
list was missing "au." and "ap." does not match "apac.", so AU/APAC Claude
inference profiles silently lost prompt caching. Add "apac." and "au.".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 05:38:45 -07:00
Teknium
a1813c1ef4
feat(desktop): inline TTS voice/model settings in the Capabilities tab (#68017)
* feat(desktop): inline TTS voice/model settings in the Capabilities tab

The Capabilities > Tools > Text-to-Speech panel only surfaced API keys per
provider — voice and model settings (e.g. tts.openai.voice) lived exclusively
in Settings > Voice, so users couldn't select or type a voice/model name where
they configure the backend.

- web_server: TTS provider rows now carry their tts_provider config key
  (the section holding that backend's voice/model settings)
- desktop: new VoiceProviderFields renders the provider's config fields
  inline in the toolset panel, deriving the key list from the curated
  Settings > Voice section so the two surfaces can't drift; shared
  ConfigField extracted to config-field.tsx
- voice/model name fields are now free-input comboboxes (Input + datalist)
  instead of closed Selects — custom voice IDs (ElevenLabs cloned voices,
  xAI custom voices, Edge's 400+ catalog) are typeable, known values remain
  suggestions
- refreshed the stale OpenAI voice list (adds ash/ballad/cedar/coral/marin/
  sage/verse) and added suggestion lists for edge/gemini/minimax/mistral/
  kittentts/piper/neutts models and voices
- config.py: added missing tts.minimax and tts.kittentts default blocks and
  deepinfra model/voice fields to the Voice section so those providers are
  configurable from the GUI at all

* test(desktop): await effect-driven panel content in post-setup CTA tests

The auto-expand effect renders the provider's inner panel one re-render
after the row; with the QueryClientProvider wrapper the extra provider
tick made the synchronous getByRole race it (~10% local flake, failed on
CI). Await the panel content with findBy* instead.
2026-07-20 05:38:38 -07:00
Teknium
369afc60be fix: migrate CLI kanban gate + remaining mocks to 5-value judge contract
Follow-up to the salvaged transport-failure auto-pause (#54387): the PR
branch predates the CLI completion gate merged in #67985, so that new
judge_goal consumer (hermes_cli/kanban.py) needed the 5-value unpack too
— otherwise it would fail open again via the swallowed ValueError.

Also migrates the two remaining 4-value mocks in
tests/cli/test_cli_goal_interrupt.py flagged on the earlier PR #27760.
2026-07-20 05:38:25 -07:00
João Vitor Cunha
d401fd7251 fix(goals): update judge consumers for transport result 2026-07-20 05:38:25 -07:00
João Vitor Cunha
3b4f96ce94 fix: update mocks in test_goal_verdict_send.py to match 5-tuple judge_goal return value 2026-07-20 05:38:25 -07:00
João Vitor Cunha
48fc1d780b fix(goals): auto-pause goal loop on consecutive transport failures
When a goal_judge model has a broken API key (401), DNS failures, or
timeouts, the judge falls through to 'continue' (fail-open) but the
consecutive transport failures were not counted — only parse failures
were tracked.  With 3 consecutive parse failures the loop auto-pauses,
but with transport failures it looped forever (the Xiaomi 401 bug).

Changes:
- Add DEFAULT_MAX_CONSECUTIVE_TRANSPORT_FAILURES = 5
- Add consecutive_transport_failures counter to GoalState
- judge_goal() now returns a 5-tuple (verdict, reason, parse_failed,
  wait_directive, transport_failed) instead of 4-tuple
- Transport errors (API 401/5xx, timeouts) set transport_failed=True
- evaluate_after_turn() auto-pauses when consecutive transport failures
  reach the threshold, with a clear message naming the failing model
- All 101 tests updated and passing
2026-07-20 05:38:25 -07:00
Teknium
b9dba7eff5
fix(env-probe): stuck Windows probe can no longer deadlock system-prompt builds (#67999)
An orphaned pip descendant holding the probe's inherited stdout/stderr
pipe handles wedged subprocess.run's post-timeout communicate() (which
joins the pipe reader threads with NO timeout on Windows). The warm
probe thread then hung holding the module-level _CACHE_LOCK, so every
new session's prompt build blocked indefinitely (#67964).

Two layers:

- _run(): replace subprocess.run with Popen + communicate(timeout); on
  TimeoutExpired kill the process TREE (taskkill /T on Windows) and
  reap the direct child bounded — never re-read the pipes, so an
  orphaned descendant holding them open can't block us.
- get_environment_probe_line(): the probe now runs in a single
  background worker publishing via a threading.Event; callers wait at
  most _PROBE_WAIT_TIMEOUT (10s) then fail open with "". After one
  full timeout, later callers only peek. If the stuck worker ever
  finishes, its line resumes appearing in new prompts.

Regression tests: hung probe with 4 concurrent callers returns bounded;
late recovery publishes; repeat callers skip the wait; _run() returns
promptly despite a pipe-holding descendant (real subprocess E2E).

Fixes #67964
2026-07-20 05:38:20 -07:00
Teknium
34a304abb3 fix: unpack judge_goal 4-tuple in salvaged CLI gate; harden tests
Follow-up to the cherry-picked CLI judge gate (#55854): the gate carried
the same 3-value unpack bug just fixed on the tool path in PR #67973 —
the ValueError would have been swallowed by the fail-open handler,
silently disabling the gate.

Also: test mock now returns the real 4-value judge contract (the old
3-value mock masked the bug), tests track complete_task invocations and
assert the rejection path never writes, and the unused _make_goal_task
helper is dropped.
2026-07-20 03:36:44 -07:00
srojk34
aa32154e4b fix(kanban): apply goal_mode judge gate to CLI complete command
The three-commit hardening series (Issue #38367, PR #55408) added a
pre-completion judge gate to `tools/kanban_tools.py:_handle_complete`
(the kanban_complete tool used by agent tool-calls).  The structurally
identical `hermes_cli/kanban.py:_cmd_complete` (the `hermes kanban
complete` CLI subcommand) was left unguarded.

A goal_mode worker with terminal tool access — the overwhelming default
for coding agents — can bypass the judge entirely by running:
    hermes kanban complete <task_id>
This transitions the task to `done` status with no judge verdict, making
the acceptance-criteria enforcement worthless on that path.

Fix: apply the same gate in _cmd_complete before calling kb.complete_task.
When a judge is reachable and returns anything other than "done", the
command prints an actionable rejection message and exits non-zero without
modifying the task.  The fail-open policy (no judge configured → allowed)
is preserved to match the tool-call path.
2026-07-20 03:36:44 -07:00
Teknium
73543744bc fix(gateway): key-presence precedence for session_reset/stt nested fallback
Follow-up for salvaged PR #59779: the session_reset and stt fallbacks
used truthiness/type checks, so a present-but-empty top-level value was
silently replaced by the nested gateway.* form — inconsistent with the
key-presence precedence every other key in the block uses. Switch both
to 'key not in yaml_cfg' gating and add precedence regression tests.
2026-07-20 03:34:58 -07:00
pierrenode
e9bd3b6eeb fix(gateway): honor nested gateway.* form for 9 more top-level keys
load_gateway_config() already accepted both the top-level key and the
nested gateway.<key> form (written by `hermes config set gateway.<key>
...`) for multiplex_profiles, max_concurrent_sessions, streaming, and
write_sessions_json — each fixed one at a time as users hit it (most
recently #59320 for multiplex_profiles). Nine sibling top-level keys
never got the same nested fallback: session_reset, quick_commands, stt,
stt_echo_transcripts, group_sessions_per_user, thread_sessions_per_user,
reset_triggers, always_log_local, and unauthorized_dm_behavior.

`hermes config set gateway.<any-of-these> ...` builds exactly this nested
shape (hermes_cli/config.py's _set_nested has no schema, so it accepts
any dotted path), so a user following the same pattern that legitimately
works for gateway.multiplex_profiles/gateway.streaming gets a silent
no-op for these nine keys instead.

Fix: read `gateway: {...}` into a single `gateway_section` variable once
(consolidating three separate `yaml_cfg.get("gateway")` calls already in
the function) and add the same top-level-wins/nested-fallback check for
each of the nine keys, mirroring the existing write_sessions_json
precedent exactly.

Note: because every fallback here is guarded by
`isinstance(gateway_section, dict)`, this also makes the streaming
fallback tolerate a scalar `gateway:` block (e.g. `gateway: disabled`)
without crashing — the same crash #40837 (open) targets specifically for
streaming. This change doesn't set out to fix that PR's issue, but the
consolidated guard covers it as a side effect; flagging it for the
reviewer rather than leaving it to be found in review.
2026-07-20 03:34:58 -07:00
polyhistor
a7e911150c fix(bedrock): address review — route list-string items through _safe_text, update stale placeholder assertions
Addresses hermes-sweeper review on PR #66167:

1. _convert_content_to_converse() still emitted {"text": part} directly
   for plain-string items inside a content list (as opposed to
   {"type": "text"} dicts), bypassing _safe_text() entirely. A
   whitespace-only string item (e.g. ["   "]) could still reach Bedrock
   as a blank block. Now routed through _safe_text().

2. tests/agent/test_bedrock_adapter.py::TestEmptyTextBlockFix asserted
   the pre-fix behavior (whitespace -> literal space " "), contradicting
   the new _safe_text()/_EMPTY_TEXT_PLACEHOLDER behavior added in
   4618095a. Updated assertions to expect the non-whitespace placeholder,
   plus added a regression test for the list-string-item case above.
2026-07-20 03:27:38 -07:00
polyhistor
9172048a2f fix(bedrock): use non-whitespace placeholder for empty text blocks
Bedrock Converse rejects text content blocks that are empty OR
whitespace-only (ValidationException: "text content blocks must
contain non-whitespace text"). The prior fix attempt substituted a
single space (" ") for missing content -- but a lone space IS
whitespace, so it was rejected by the exact same validation rule it
was meant to satisfy. This caused a deterministic, unrecoverable
retry-loop failure once any blank/whitespace assistant, tool, or user
turn entered history (most commonly via context-compaction rewriting
a turn to a blank string).

Adds _safe_text()/_EMPTY_TEXT_PLACEHOLDER ("(empty)") and applies it
everywhere a blank text block could reach the wire: user/assistant
content conversion, tool results, the assistant-empty-turn fallback,
and the first/last-message user-alternation padding. System-prompt
blocks are the one exception: blank parts are dropped entirely rather
than placeholder-filled, since a system prompt block should never
carry meaningless placeholder text.

Adds tests/agent/test_bedrock_empty_text_blocks.py (11 tests, was
already present uncommitted -- codifies the exact failing history
from issue #9486 and asserts no blank block ever reaches Bedrock).

Verified against the actual failed request dump from this session
(27-message payload) -- replaying it through the fixed converter now
produces zero blank/whitespace-only blocks.
2026-07-20 03:27:38 -07:00
iizotov
81d4b3654a fix(bedrock): map Claude opus/sonnet 4.6+ to 1M context window
The Bedrock static context table only had entries up to the 4-6
generation and capped all Claude models at 200K. Newer model IDs
(opus/sonnet 4-7, 4-8) silently inherited 200K via the generic
"anthropic.claude-opus-4" / "...-sonnet-4" substring fallback,
contradicting the native Anthropic table in model_metadata.py which
already maps these to 1M.

Map opus/sonnet 4-6/4-7/4-8 to 1_000_000 on Bedrock. Haiku 4.5,
sonnet 4.5, legacy Claude 4 and 3.x stay at 200K (no 1M window).

Add opus-4-8 coverage and haiku/sonnet-4-5 200K guards.
2026-07-20 03:27:28 -07:00
Patrick Muller
a8602e4afa test(bedrock): update context-length tests to match Anthropic docs
Update TestBedrockContextLength to assert the corrected values from
BEDROCK_CONTEXT_LENGTHS:

- test_claude_opus_4_6: 200_000 -> 1_000_000 (1M GA for Opus 4.6)
- test_claude_sonnet_versioned: 200_000 -> 1_000_000 (1M GA for Sonnet 4.6)
- test_inference_profile_resolves: 200_000 -> 1_000_000
  (us.anthropic.claude-sonnet-4-6 resolves to Sonnet 4.6's 1M value)

Also adds three new test cases that document Anthropic's published
context windows explicitly and guard against future regressions:

- test_claude_opus_4_7: asserts 1_000_000 and cites the models overview
- test_claude_sonnet_4_5_is_200k: asserts 200_000 and cites the
  April 30, 2026 release note retiring the 1M beta for Sonnet 4.5
- test_claude_haiku_4_5_is_200k: asserts 200_000 for Haiku 4.5

All 178 tests in test_bedrock_adapter.py pass after the change.
2026-07-20 03:27:28 -07:00
Teknium
47fb20c0bd
fix: session-scoped /fast + full /new reset to config defaults (#67979)
* fix(fast): default /fast to session scope on CLI and gateway

Completes the session-first policy from #67946 for the /fast toggle
(the remaining half of #54084). A bare /fast fast|normal now applies to
the current session only; --global persists agent.service_tier to
config.yaml.

Gateway: new _session_service_tier_overrides dict (registered in
_CONVERSATION_SCOPED_STATE so /new clears it) resolved at both agent
turn sites via _resolve_session_service_tier(); the /fast handler and
its choice picker apply session overrides and evict the cached agent.
The TUI config.set fast path was already session-scoped.

CLI: /fast parses --global (parity with /reasoning); bare toggles
mutate self.service_tier only.

* fix(sessions): /new resets model, reasoning, and fast to config defaults

/new and /reset are full conversation boundaries: session-scoped
runtime overrides do not carry into the next session (#48055, #23131).

CLI new_session(): clears the one-turn model restore, re-derives
service_tier from config, and — when the session's model differs from
the config default — switches back via the shared switch_model()
pipeline (live agent swap included; best-effort so an unreachable
default never blocks /new).

TUI _reset_session_agent(): stops forwarding model_override /
create_reasoning_override / create_service_tier_override into the
rebuilt agent and pops the pins so later rebuilds can't resurrect
them. The gateway already cleared its per-session overrides via
_clear_conversation_scope on /new.

Cross-session contamination stays impossible: nothing here touches
process-global env or other sessions' pins.
2026-07-20 03:27:22 -07:00
Teknium
3441b80f4f feat(pricing): add Bedrock rows for Opus 4.8/4.7, correct Opus 4.6 to $5/$25
Adds current-gen Claude Opus pricing rows on Bedrock keyed to Anthropic's
published list price, which commercial Bedrock on-demand mirrors. Also
corrects the existing Opus 4.6 row: it carried Claude-3-era Opus pricing
($15/$75); Opus 4.5+ list at $5/$25 with cache write 1.25x / read 0.1x.

The AWS Price List API had not published these SKUs machine-readably as
of 2026-07, so these are commercial-list snapshots pending an
authoritative machine source.

Reapplied from PR #62327 (commit authored under a placeholder identity,
so cherry-pick was not usable; sonnet-5 row from that PR already landed
via #67932).

Co-authored-by: pgregg88 <4943027+pgregg88@users.noreply.github.com>
2026-07-20 03:27:16 -07:00
Osraka
e101bbaebb fix(pricing): restrict Bedrock profile normalization 2026-07-20 03:27:16 -07:00
Osraka
54418a888e fix(pricing): resolve versioned Bedrock profile IDs 2026-07-20 03:27:16 -07:00
Teknium
9ca8ce4335
fix(kanban): unpack judge_goal's 4-tuple at the completion gate (#67973)
judge_goal() returns (verdict, reason, parse_failed, wait_directive) since
the goals.py wait-directive change, but the kanban goal-mode completion gate
at tools/kanban_tools.py still unpacked 3 values. Every judge call raised
ValueError, the defensive except swallowed it, and the pre-initialized
verdict='done' let every completion through — the acceptance gate was
silently disabled.

Now unpacks all 4 values; the test mock is updated to match the real
contract. The other two judge_goal consumers (hermes_cli/goals.py) already
use 4-value unpacks.

Reported and diagnosed by @bill3wits in PR #57276; reimplemented under
project authorship because the original commit was authored under a
non-existent local identity (bash@hermes.local) that cannot be carried
into history. Also fixes #58066 (duplicate report by @Gibcity).
2026-07-20 03:13:44 -07:00
nnnet
305ecac8b2 feat(providers): extend `enabled: false` gate to built-in resolution
The first commit's gate sat inside ``_get_named_custom_provider`` —
which only handles user-defined custom blocks. Built-in provider names
(``openai`` / ``anthropic`` / ``openrouter`` / ``gemini`` / ...) have
their own resolution paths in ``resolve_runtime_provider`` (pool /
explicit / generic / ``resolve_provider``) and bypass that gate.

So a user who flipped ``providers.openrouter.enabled: false`` would
still see OpenRouter resolved when something explicitly requested it
(e.g. a fallback chain entry). That defeats the point of the flag.

This commit moves the gate one level up: right after
``requested_provider`` is computed, before any custom / built-in /
Azure short-circuit. It now raises a typed ``ValueError`` referencing
the YAML path, so callers can recognise it and advance to the next
fallback instead of silently using a disabled provider.

3 new tests cover:
* disabled custom provider raises
* disabled built-in provider raises
* enabled provider doesn't hit the gate

All 20 tests in the providers suite pass.
2026-07-20 03:06:02 -07:00
nnnet
7de06f700e feat(providers): add `enabled: false` flag to hide a provider
A ``providers.<name>`` block in ``config.yaml`` can now opt out of being
listed anywhere by setting ``enabled: false`` — without removing the
block, so re-enabling it stays a one-line edit. Missing or ``true`` keeps
the previous behaviour (enabled), so this is fully backwards-compatible.

The flag is honoured in four places:

* ``hermes_cli/model_switch.py`` — model-override validation (the
  allow-list that the picker consults to accept a non-public model id)
  and the picker's own endpoint iteration. A disabled provider no longer
  appears as a row and its models can't be silently accepted via
  override.
* ``hermes_cli/runtime_provider.py`` — the runtime resolver skips
  disabled blocks, so an explicit ``--provider X`` against a disabled
  entry fails fast instead of using stale base_url / api_key from the
  ignored block.
* ``hermes_cli/doctor.py`` — the doctor's "configured providers" set
  excludes disabled entries, so health checks don't flag missing API
  keys for providers the user has turned off.

Motivation: when a user has 20+ providers wired up in ``config.yaml``
(many of them only used occasionally) the picker becomes noisy and the
runtime resolver may pick a suboptimal one on ambiguous --provider names.
There's currently no way to hide a provider short of deleting its block
— which loses the api_key + base_url + custom routing config the user
spent time wiring. ``enabled: false`` lets them keep the config but get
it out of the way.

The helper ``is_provider_enabled()`` in ``hermes_cli/config.py``
centralises the gate (and accepts YAML-stringified booleans like
``"false"`` for hand-edited configs). 17 unit tests cover the defaults
and edge cases.

A follow-up PR can wire ``hermes provider enable/disable <name>`` and a
dashboard toggle on top of this primitive — they reduce to mutating the
flag.
2026-07-20 03:06:02 -07:00
Craig French
b239ee2123 feat(model-switch): excluded_providers config to hide providers from /model picker 2026-07-20 03:06:02 -07:00
Deepak Jain
1b56d0d1a2 test(cli): isolate model picker Ollama probes
Fixes #30604
2026-07-20 03:06:02 -07:00
Jan-Stefan Janetzky
766c617e83 fix(compression): detect semantic no-op results 2026-07-20 02:25:57 -07:00
Jan-Stefan Janetzky
75af6dc57c fix(redaction): normalize URL credential key aliases 2026-07-20 02:25:57 -07:00
Jan-Stefan Janetzky
763c7f79d4 test(compression): isolate provider handoff setup 2026-07-20 02:25:57 -07:00
Jan-Stefan Janetzky
46e4891c64 fix(compression): close post-dispatch lock scope 2026-07-20 02:25:57 -07:00
Jan-Stefan Janetzky
62a00a7391 fix(redaction): cover strict URL reference forms 2026-07-20 02:25:57 -07:00
Jan-Stefan Janetzky
a48315e322 fix(compression): guard lock refresher startup 2026-07-20 02:25:57 -07:00
Jan-Stefan Janetzky
34bab1c6ac fix(compression): harden provider context handoff 2026-07-20 02:25:57 -07:00
Jan-Stefan Janetzky
192ef93ad5 fix(agent): harden pre-compress context handoff 2026-07-20 02:25:57 -07:00
Teknium
8decd39844 test(file-sync): patch module-level _monotonic alias instead of shared stdlib time module
Follow-up for salvaged PR #39946: file_sync.py already aliases time.sleep
as _sleep specifically to avoid tests mutating the shared stdlib module
object. Apply the same convention to the rate-limit clock (_monotonic)
and point the new regression test at it.
2026-07-20 02:25:53 -07:00
Taylor H. Perkins
f9158b818b fix(file-sync): don't rate-limit retry after a failed sync cycle
FileSyncManager.sync() is rate-limited to once per _sync_interval via
_last_sync_time, and its docstring promises that on failure "state rolls
back so the next cycle retries everything". But the except handler also
set _last_sync_time = time.monotonic() on failure, so the next non-forced
sync() within the interval hit the rate-limit guard and returned early —
suppressing the retry the rollback had just prepared.

Because the non-forced sync() runs before every command on the SSH, Modal
and Daytona backends, a single transient upload failure (network blip,
dropped channel) left the remote with stale files for the next command
(up to _sync_interval, default 5s). Forced syncs bypass the guard, which
is why it was intermittent.

Remove the failure-path timestamp bump so the clock only advances on a
successful or no-op cycle, matching the documented contract. Add a
regression test that fails before this change and passes after.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 02:25:53 -07:00
liuhao1024
07f39cf9a6 fix(models): add Claude Sonnet 5 to curated model lists
Add claude-sonnet-5 to the static curated lists for Anthropic, OpenRouter,
Nous Portal, Copilot, GMI, OpenCode Zen, and AWS Bedrock so the model
appears in hermes model / /model picker discovery.

Fixes #55846
2026-07-20 02:25:44 -07:00
Ariel Bravy
e2561466c7 feat(models): add Claude Sonnet 5 support 2026-07-20 02:25:44 -07:00
Teknium
9bda6438d4
fix(config): remove unknown-top-level-key warning — top-level keys bridge to env (#67924)
The 'Unknown top-level config key' warning (f5bacee27) assumed a
closed-world allowlist of valid roots, but top-level scalars in
config.yaml are deliberately bridged into os.environ (gateway/run.py,
hermes send) so skills and external apps Hermes drives can read
arbitrary env-style keys (DISCORD_HOME_CHANNEL, MY_APP_TOKEN, ...).
An allowlist can never enumerate those — two widening follow-ups
(7c2ece53c, 3c7217706) already proved the whack-a-mole. Drop the
generic warning entirely; keep the targeted provider-like-field
misplacement hint (base_url/api_key at root).
2026-07-20 02:25:33 -07:00
Craig French
1c3a48965b fix(model-switch): keep same-endpoint custom providers with different names as separate picker rows 2026-07-20 02:22:37 -07:00
Alex López
7ed18dae90 test(model): isolate custom-provider grouping tests from live discovery 2026-07-20 02:22:26 -07:00
Teknium
dc0dbc9387 fix(reasoning): default /reasoning <level> to session scope in CLI and TUI
Parity with the gateway /reasoning handler and the new /model default:
a bare /reasoning <level> now applies to the current session only;
--global persists agent.reasoning_effort to config.yaml. --session is
still accepted as an explicit alias for the default. Display toggles
(show/hide/full/clamp) remain persistent as before — they are user
preferences, not conversation state.

Builds on YAMAGUCHI Seiji's #51158 (session-scope plumbing + /new reset,
cherry-picked as the previous commit) with the default flipped to match
the session-first policy. Fixes the CLI half of #54084.
2026-07-20 02:22:22 -07:00
YAMAGUCHI Seiji
8590c2d0d9 feat(cli): make reasoning effort session-scoped 2026-07-20 02:22:22 -07:00
Teknium
8b6fde3a35 fix(model): default /model switches to session scope everywhere
Flip the resolve_persist_behavior() fallback from persist-to-config to
session-only. A plain /model <name> (typed or via any picker — CLI,
TUI/Desktop, gateway) now affects only the current session; --global
persists explicitly, and model.persist_switch_by_default: true restores
the old opt-out behavior for users who want switches to stick.

This is the root cause behind the recurring 'session switch applied
globally' bug class (#61458, #63083, #58290, #61190): every surface
funnels its no-flag default through this one function, so per-surface
patches kept missing paths. Fixing the default fixes all surfaces at
once: CLI typed + picker, TUI/Desktop config.set + slash, gateway typed
+ inline picker.

Builds on liuhao1024's #58371 (--provider session scoping, cherry-picked
as the previous commit) and supersedes the per-surface #61488.
2026-07-20 02:22:22 -07:00
liuhao1024
0d6d73525d fix(model): default --provider switches to session-only persistence
When /model is called with --provider but without --global or --session,
the switch now defaults to session-only instead of persisting to
config.yaml. Provider switches are typically exploratory — the user is
trying a different backend for this conversation, not reconfiguring the
default. --global can still force persist when desired.

This addresses a regression from fad4b40d9 where /model switched to
persist-by-default, causing /model xxx --provider xxx to overwrite the
global config when the user only intended a temporary switch.

Fixes #58290
2026-07-20 02:22:22 -07:00
ajzrva-sys
65bb16c8ce fix(streaming): detect text-only stream drops with no finish_reason (#32086)
When a streaming response ends cleanly (HTTP 200) with no finish_reason
after delivering text but no tool calls, the chunk collector silently
stamps finish_reason='stop' and the conversation loop presents truncated
text as a complete response.

Three stream-drop paths now exist after chunk collection:

1. Zero-chunk → EmptyStreamError, retried (existing)
2. Tool-call in progress, no finish_reason → partial-stream-stub (existing)
3. Text-only, no finish_reason → partial-stream-stub (NEW — this fix)

Path 3 routes through the same PARTIAL_STREAM_STUB_ID + FINISH_REASON_LENGTH
machinery as path 2. The conversation loop shows 'Stream interrupted —
requesting continuation' and injects a continue prompt, giving the model
a chance to resume where the stream dropped.

Observed with DeepSeek provider where CloudFront drops SSE streams
mid-response after delivering partial text.
2026-07-20 13:31:02 +05:30
kshitijk4poor
86e603e7d6 fix(compression): verify cached prompt embeds current memory before retaining
The salvaged retention check compared the built-in memory snapshot
before vs after the disk reload. That holds for a long-lived CLI agent,
but on fresh-agent surfaces (gateway per-turn agents, TUI) the cached
prompt is restored from the session DB and can predate mid-session
memory writes that the fresh MemoryStore already absorbed at init: the
snapshot is then identical on both sides of the reload while the prompt
itself is stale, so compression would retain (and re-persist via
update_system_prompt) a prompt missing the new memory for the life of
the session.

Replace the equality check with a containment check
(_cached_prompt_reflects_builtin_memory): retain the cached prompt only
when the freshly-reloaded rendered blocks appear verbatim inside it,
and rebuild when a leftover block header remains for a target whose
entries have since been emptied or disabled. Block headers are shared
via MEMORY_BLOCK_HEADERS in tools/memory_tool.py so the check stays in
lockstep with MemoryStore._render_block.

Adds regression guards for the gateway stale-restore path and the
emptied-memory leftover-block path; verified with a real-MemoryStore
E2E matrix (9 scenarios) against a temp HERMES_HOME.
2026-07-20 13:13:02 +05:30
konsisumer
54c3f589ad fix(compression): retain prompt cache when memory is unchanged 2026-07-20 13:13:02 +05:30
Teknium
463c2ae255 fix: section-3 grouping follow-ups for salvaged PR #36998
- extra_headers participates in the section-3 group identity (mirrors
  section 4 — header-routed tenants behind one proxy URL stay distinct)
- model declarations go through _declared_model_ids() so
  models: [{id: ...}] rows keep working
- gateway model-switch handler moved to gateway/slash_commands.py since
  the PR branched — re-applied the display-form edits there (both the
  legacy picker closure and the current typed path)
- regression tests: same-endpoint fold, api_mode separation,
  header-routed separation, list-of-dict models, RID display stripping
2026-07-20 00:41:21 -07:00