Commit Graph
491 Commits
Author SHA1 Message Date
Ruby Hartono d57fa7ba3b fix(stream): retry (not break) on queue.Empty so terminal frames survive a concurrent drain
Reviewer regression (reproduced): the broadcast drop-oldest loop did
put_nowait -> on Full, get_nowait -> on Empty, break. But Empty here means a
concurrent consumer (the SSE handler thread) DRAINED the queue between our Full
and get_nowait — the queue now has space, which is exactly the condition to
retry the put. `break` silently discarded `item`, and when `item` was a terminal
frame (stream_end/error/cancel) the subscriber never received it, leaving the
client attached indefinitely (spinner-forever). The "# cap 0 defensive guard;
never reached" comment was the flawed assumption: any concurrent drain makes
the Empty branch reachable at any cap.

Fix: on queue.Empty, `continue` (retry the put into the now-empty queue) in
both the broadcast path and the replay path (for consistency; the replay path
runs under self._lock with a fresh queue so it's unreachable in practice, but
the same race-safe rule applies). No infinite-loop risk: each iteration either
succeeds a put (break) or succeeds a get (progress); the continue only fires on
Empty, after which the next put has space.

Test: test_terminal_frame_survives_concurrent_drain_during_drop_oldest
deterministically simulates the interleaving with a custom queue whose first
post-Full get_nowait raises Empty (as if a concurrent consumer drained it),
then runs BOTH the pre-fix `break` logic (drops the terminal frame — proving
the test bites) and the post-fix `continue` logic (delivers it).

Model Used: builtin:zai-coding-plan/GLM-5.2 (ZCode agent).
2026-07-17 07:04:04 +07:00
Ruby Hartono bbda885aa6 fix(stream): cap StreamChannel per-subscriber queues (drop-oldest on full)
Memory: each connected tab's subscriber queue was queue.Queue() with no
maxsize. A slow/backpressured/backgrounded tab accumulated every coalesced
token frame for the WHOLE turn (producer = agent token stream) — unbounded
per-tab growth, OOM risk with many tabs x long agentic turns. The offline
buffer was already capped (#4633), but once a subscriber attached the
live-broadcast path flowed into an unbounded per-subscriber queue.

Cap per-subscriber queues at _SUBSCRIBER_QUEUE_MAXSIZE (== _OFFLINE_BUFFER_MAXLEN)
and drop the OLDEST frame on queue.Full in both the broadcast (put_nowait) and
offline-buffer replay (subscribe_with_snapshot) paths, so a slow tab keeps the
most recent tail (older frames stay recoverable via the run journal by
last_event_id). Mirrors SessionChannel.emit's drop-on-full contract.

The cap intentionally equals the offline buffer bound, not SessionChannel's
smaller 16: StreamChannel carries the live token stream and has a #4633
reconnect-replay contract (a resubscribing tab must receive the full retained
offline tail), so capping below the buffer would force every flaky-network
reconnect through the run journal. Matching the buffer bound preserves that
contract while bounding live-broadcast memory to the same worst-case #4633
already accepts. The SSE write deadline (SSE_WRITE_DEADLINE_SECONDS) breaks a
stuck socket within ~20s, so the overflow window is short; this also bounds it
by frame count.

Adds a cumulative subscriber_dropped_events diagnostic counter (ops visibility).

Test: tests/test_stream_subscriber_queue_cap.py — bounded queue never exceeds
cap, drop-oldest keeps newest tail, draining vs non-draining subscribers, and
the #4633 reconnect-replay contract still delivers the full offline tail.
Existing offline-buffer/gap-recovery/multitab tests unchanged.

Contract Routing
- Task type: memory-bound fix (unbounded per-subscriber queue).
- Touched areas: live chat SSE broadcast (StreamChannel).
- Relevant public docs: AGENTS.md, CONTRIBUTING.md, docs/CONTRACTS.md,
  docs/rfcs/webui-run-state-consistency-contract.md (Live stream/SSE layer),
  docs/rfcs/session-sse-contract-v1.md (distinct: that RFC governs the PROPOSED
  per-session endpoint /api/sessions/{id}/events, NOT StreamChannel, which is
  the /api/chat/stream live token broadcast; no text in that RFC is changed).
- State layer mutated: Live stream / SSE layer — specifically the per-subscriber
  queue inside StreamChannel (api/config.py). NOT the durable run journal, NOT
  the transcript, NOT replay.
- Invariant preserved: the #4633 reconnect-replay contract — a resubscribing
  tab must receive the full retained offline tail. The cap is set EQUAL to
  _OFFLINE_BUFFER_MAXLEN so replay delivery is unchanged; only the previously-
  unbounded live-broadcast-to-slow-tab growth is bounded. Regression:
  test_reconnect_replay_still_delivers_full_offline_tail (and the pre-existing
  test_stream_offline_buffer_cap.py suite) pass unchanged.
- Scope boundaries: does not touch replay idempotency (Invariant 5), the turn
  journal, compression, or sidebar metadata. Slow-tab frames dropped at the cap
  stay recoverable via the run journal by last_event_id.

Release note: StreamChannel per-subscriber SSE queues are now bounded
(drop-oldest), capping memory for slow/backgrounded tabs during long turns.

Model Used: builtin:zai-coding-plan/GLM-5.2 (ZCode agent). No AI-specific tool
use mattered beyond the repo's standard read/edit/test workflow.
2026-07-16 20:18:39 +07:00
6ed474ca54 fix(models): route list-shaped custom provider models (#6127) (#6128)
Centralize custom-provider model-ID extraction into _configured_model_ids()
+ _configured_model_options(), used by both catalog construction and runtime
routing so the picker and resolver can't drift. Fixes list-shaped
custom_providers[].models allowlists silently staying on the default provider
instead of routing to their own (#6121). Existing mapping / list-of-strings
configs unaffected.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-07-16 00:22:50 -07:00
nesquena-hermes 45cb5bd3e6 feat(#5979): persisted explicit-pick provenance for cold custom-proxy resolution
Nathan's call: adopt Codex's complete mechanism instead of the unconditional
cold-preserve. The cold-catalog custom-proxy decision is now gated on whether the
user DELIBERATELY selected the model this session, closing the residual where a
stale #433 leftover would preserve+400 on a cold+no-disk send.

Mechanism:
- resolve_model_provider(model_id, *, explicitly_picked=False): in the custom-arm
  cold branch (no warm provenance, not config-declared), explicitly_picked=True
  preserves the vendor namespace verbatim (deliberate pick, proxy routes on it);
  otherwise the legacy redundant-prefix strip runs (stale cross-provider leftover,
  #433). Warm endpoint-advertised provenance still wins over the flag.
- Session.model_explicitly_picked: new persisted field (default False, restored
  from metadata via **kwargs, listed in METADATA_FIELDS so it survives a restart
  — b3nw's exact cold scenario).
- /api/chat/start stamps it: True on a fresh explicit_model_pick; cleared only
  when the resolved model actually CHANGED without a pick (real switch away);
  preserved across plain follow-up sends of the same model (the onchange marker
  is one-shot, so per-send explicit_model_pick alone would drop the intent).
- streaming.py reads s.model_explicitly_picked and passes explicitly_picked= into
  resolve_model_provider (inside the profile scope).

Independent end-to-end matrix (all pass): ben default cold, ben picked non-default
cold+warm, ben stale cold (strips), #433 warm+cold, #3872 bedrock, #548 zai-org.
67 resolver+sprint40 tests pass incl. new explicit-pick + stale-companion +
persistence round-trip.
2026-07-13 01:43:17 +00:00
nesquena-hermes f42b4d14f5 harden #5979 follow-up round 2 (Codex): profile-fingerprint-gated warm no-op
Codex finding: the warm helper's fast no-op (if _models_cache_provenance is not
None: return) ignored WHICH profile the resident provenance belongs to. The
catalog globals are process-wide, so profile B's resident provenance could block
profile A from loading A's own valid disk cache — A would then resolve against
B's advertised ids (verified: profile A provenance resident + profile B bare-only
disk cache -> A's openai/gpt-5.4 wrongly preserved).

Fix: gate BOTH the fast no-op and the post-lock recheck on a current-profile
fingerprint match (_provenance_is_current: prov[1] == _models_cache_source_fingerprint()).
On mismatch the helper falls through, loads THIS profile's disk snapshot, and
republishes provenance stamped with the current fingerprint.

Tests: rewrote noop-when-warm to use the CURRENT runtime fingerprint (genuinely
hits the fast path) and added ignores-foreign-profile-resident (foreign fp -> falls
through -> loads this profile's disk snapshot). 66 resolver+sprint40 tests pass.

NOTE: Codex also re-raised the cold+no-disk #433 case (a stale openai/gpt-5.4 on a
bare-only relay preserves instead of strips when no catalog exists at all). Fable
weighed this exact residual and judged cold-preserve strictly better than master
(loud-fail-and-heal vs silent-recurring-truncation). The two advisors diverge;
surfacing to Nathan rather than auto-adopting the heavier persisted-explicit-pick
mechanism.
2026-07-13 01:43:17 +00:00
nesquena-hermes e3152572aa harden #5979 follow-up per Codex gate: non-blocking disk-only warm + profile scope
Codex gate found two real issues in the send-path warm:

1. Blocking lock wait (CORE): get_available_models(prefer_cache=True) still
   acquires _available_models_cache_lock and can wait up to ~60s (unbounded in
   synchronous rebuild mode) on an in-flight rebuild — unacceptable on the send
   hot path. Rewrote warm_models_catalog_provenance_if_cold as a genuinely
   non-blocking, disk-only publish: try the cache lock NON-BLOCKING (return
   immediately if busy — a concurrent build will publish provenance itself),
   read ONLY the on-disk cache via _load_models_cache_from_disk (no network, no
   rebuild), publish snapshot+fingerprint + _sync_models_cache_provenance().
   Verified: returns in ~3.5ms while the lock is held (was a 60s block).

2. Profile scope (CORE): the streaming worker is a separate thread that does NOT
   inherit the HTTP handler's request-profile TLS. Both the warm and the resolve
   read profile-keyed config (cache path + source fingerprint via
   get_active_profile_name), so a cold send from a NAMED profile could resolve
   against the DEFAULT profile's config and route to the wrong provider/base_url
   (get_available_models reloads config on path mismatch). Wrapped warm + resolve
   in profile_scope_for_detached_worker(_resolved_profile_name) so both see the
   owning session's profile (no-op for default/root).

Tests: rewrote never-live-rebuilds (now asserts warm NEVER calls
get_available_models — reads disk directly) and added a non-blocking regression
(warm returns <2s while the cache lock is held). 65 resolver+sprint40 tests pass;
b3nw end-to-end + non-blocking + disk-warm all verified.
2026-07-13 01:43:17 +00:00
nesquena-hermes 6745827eb5 fix(#5979 follow-up): preserve custom-proxy vendor namespace on cold-catalog sends
The earlier #5979 fix (PR #5980, exp-v0.52.53) resolved the WARM-catalog and
config-declared cases, but b3nw confirmed truncation persists on exp-v0.52.55/56.

Root cause (reproduced on master, confirmed by Codex + Fable):
b3nw's config is provider: custom:llm-proxy, model.default: x-ai/grok-4.5, no
models[] allowlist. His DEFAULT (x-ai/grok-4.5) preserves via config-declaration,
but a model he SELECTS in the picker that isn't the default (x-ai/grok-composer-2.5-fast)
truncates on every COLD send. The send path (api/streaming.py:7857) calls
resolve_model_provider without warming the catalog, and the #1855 chat/start fast
path deliberately skips the catalog when a session already has a persisted
model+provider -- so 'cold at send' is the designed state after any restart /
settings-save / cache-expiry, not a rare race. With provenance None and no config
declaration, the #5980 tri-state fell through to the LEGACY family strip
(if prefix in _PROVIDER_MODELS and _is_first_party_model: return bare), and
grok-composer-2.5-fast had graduated into the x-ai first-party catalog -> stripped.

Fix (two changes, both advisor-verified):
1. Flip the custom-arm cold default from family-strip to PRESERVE-verbatim
   (api/config.py). For an explicit custom / custom:<slug> proxy the strip now
   requires POSITIVE provenance (endpoint advertises only the bare id, #433). A
   wrong strip destroys routing info the user can't recover; a wrong preserve
   fails loudly and self-heals on warm. This also honours _endpoint_advertised_model_ids's
   own documented 'None -> preserve verbatim' contract and removes the data-driven
   flaw where a model graduating into the static catalog silently flips routing.
2. Network-free provenance warm on the send path
   (warm_models_catalog_provenance_if_cold, called in streaming.py before the
   resolve). prefer_cache=True republishes provenance from the durable disk cache
   (no live probe, no _cfg_lock held on the worker thread -> no lock-order
   deadlock), so #433's bare-only-strip stays exact whenever a disk cache exists.

Residual (accepted, strictly better than master): a #433-shaped wrong-preserve
now requires memory cold AND disk cache absent/invalid (first run, post-config-save
window, schema/version bump). It fails loudly and heals on the next /api/models or
session-visit rebuild. Under master b3nw's failure was the mirror image but silent,
recurring every send, and unfixable short of declaring every model in config.

Supersedes rodboev's #5996, which targeted the providers.<slug> arm and does not
cover b3nw's actual custom:llm-proxy config (confirmed by Codex).

Tests: rewrite the cold-fallback test (strip -> preserve), sharpen the
foreign-profile test so trusted-strip vs ignored-preserve genuinely diverge, add
b3nw's exact cold non-declared custom:slug case, and add three warm-helper tests
(publishes-from-disk, noop-when-warm, never-live-rebuilds). 64 resolver+sprint40
tests pass; b3nw end-to-end verified; #433 positive-provenance strip verified.

Thanks @b3nw for the CLI-vs-WebUI comparison that localized it.
2026-07-13 01:43:17 +00:00
nesquena-hermes 1443288dee harden #5979 round 3 (Codex): lock-free atomic (snapshot,fingerprint) provenance
The round-2 fix acquired _available_models_cache_lock in the resolver to read
the snapshot+fingerprint consistently. Codex found that introduces a DEADLOCK:
config-save takes _cfg_lock -> _available_models_cache_lock while catalog-refresh
takes _available_models_cache_lock -> _cfg_lock (lock-order inversion), and it
also makes every send wait behind a catalog rebuild.

Fix: publish an immutable (snapshot, publisher_fingerprint) tuple atomically via
_sync_models_cache_provenance() at every cache publish/invalidate site (9 sites),
and have _endpoint_advertised_model_ids read that single global with ONE lock-free
load. Reading one tuple can't tear (GIL), acquires no lock (no ordering edge, no
deadlock), and never blocks behind a rebuild. A reader racing a publish sees the
previous consistent pair, never a torn snapshot/fingerprint.

Verified: two-thread probe — resolver completes in 0.0001s while another thread
holds _available_models_cache_lock (was the deadlock). Real catalog build publishes
the provenance tuple and closes the P0 end-to-end. Adds a deadlock-regression test.
2026-07-12 05:41:08 +00:00
nesquena-hermes 036c671f14 harden #5979 round 2 (Codex): atomic snapshot+fingerprint read + tri-state cold fallback
Two more real issues from the second Codex gate:

1. Atomic provenance read: _endpoint_advertised_model_ids read the global
   snapshot and its source fingerprint as two unlocked steps, while publishers
   assign the new snapshot BEFORE computing the fingerprint (streaming.py
   publish sites). In that window profile B's freshly-published catalog could
   validate against profile A's still-current fingerprint and strip against the
   wrong catalog. Fix: acquire _available_models_cache_lock (RLock) across the
   snapshot read + fingerprint check + memo build, so the pair is always
   consistent. The lock is held only for the two-assignment publish (the network
   rebuild runs before it), so the per-send acquisition never blocks on a probe.

2. Tri-state cold fallback (was: unconditional cold->preserve): an unconditional
   verbatim fallback newly broke the #433 relay cold path (openai/gpt-5.4 sent
   to a relay that only accepts gpt-5.4; master stripped it). Now:
     (1) config declares the full id verbatim (model.default / model.models /
         custom_providers[].models) -> preserve (network-free, survives a cold
         restart -> #5979 works cold via b3nw's model.default)
     (2) endpoint catalog advertised it -> full=preserve / bare-only=strip
     (3) genuinely unknown (cold + not declared) -> LEGACY family heuristic, so
         the edge is never worse than pre-fix: first-party redundant prefix
         still strips (#433 cold), intrinsic/unknown preserved (#3872/#548).
   The #5979 active-user path can't reach (3): a selected id is either
   config-declared or in the catalog the dropdown was built from.

New _model_id_declared_in_config() provides the config-provenance signal.
Tests updated: cold-fallback-to-legacy, config-declared-cold-preserve,
foreign-profile-catalog-ignored (proves untrusted snapshot is not used).
2026-07-12 05:41:08 +00:00
nesquena-hermes 0bdf555779 harden #5979 per Codex gate: union extra_models bucket + profile-isolation fail-safe
Codex regression gate found two real issues on the first cut:

1. extra_models bucket: a provider's advertised ids can be split across the
   catalog's 'models' (visible) and 'extra_models' (picker overflow) buckets.
   Reading only 'models' could miss a bare id that overflowed, leaving the
   #433 stale prefix unstripped. _endpoint_advertised_model_ids now unions
   both buckets.

2. Profile isolation (profiles are islands): _available_models_cache is a
   process global, so a concurrently-active foreign profile could publish the
   snapshot a different profile then reads for provenance — potentially
   stripping an advertised full id against another profile's catalog. The
   accessor now rejects the snapshot unless its published source fingerprint
   (whose config_yaml axis is the profile-specific config path) matches the
   current runtime fingerprint; any mismatch fails safe to verbatim preserve.

Adds tests: extra_models-counts-as-advertised (#433 via overflow bucket) and
foreign-profile-catalog-preserves-verbatim (isolation fail-safe). Test helpers
now stamp the source fingerprint exactly as the real publish sites do.
2026-07-12 05:41:08 +00:00
nesquena-hermes 4349ff4302 fix(config): custom/proxy providers keep the full vendor namespace (#5979)
Resolve the model id for custom/proxy providers by ENDPOINT PROVENANCE
instead of a catalog-family guess. The old gate stripped a vendor prefix
whenever the bare id was first-party of the prefix's home vendor, which
could not distinguish a namespace the proxy actually routes on
(x-ai/grok-4.5 -> keep) from a stale cross-provider leftover on a relay
that only serves bare ids (openai/gpt-5.4 -> strip). A model graduating
into the first-party catalog (agent commit adding grok-4.5) silently
flipped a working custom-proxy id from preserved to stripped -> HTTP 400.

New _endpoint_advertised_model_ids() reads the already-published catalog
snapshot (never builds/probes/touches disk; O(1) per send via a
snapshot-identity memo) and answers what the endpoint advertised:
  - full vendor/model advertised -> preserve (#5979, #3872, #548)
  - only the bare id advertised   -> strip the redundant prefix (#433)
  - cold/unbuilt catalog          -> preserve verbatim (fail-safe toward
                                     the id the user actively selected)
The non-custom first-party-proxy strip arm is unchanged.

Rewrites the two pinned #433 tests to model provenance (bare-only
advertised) and adds #5979 coverage: advertised-full preserve (bare +
named custom), cold-catalog preserve-verbatim, and both-advertised
prefers-full.

Thanks @b3nw for the report and precise root-cause analysis.
2026-07-12 05:41:08 +00:00
20d258d552 Release exp-v0.52.40: ACP sessions in sidebar (#5939) + date-stamped Claude 3.0 reasoning fix (#5934) (#5951)
* fix(reasoning): stop date-stamped Claude 3.0 ids from reading reasoning-capable

_candidate_supports_reasoning captured a trailing date stamp as the minor
version so bare date-stamped Claude 3.0 ids like claude-3-opus-20240229
satisfied the major==3 and minor>=7 gate and wrongly surfaced reasoning-effort
controls. Cap the minor group to 1-2 digits with a (?!\d) guard so the date
stamp is not read as a minor version -- the same date-stamp defense
_is_pre_adaptive_anthropic already uses. Claude 3.0/3.5 now correctly report
no reasoning support; 3.7+ and 4.x (incl. date-stamped builds) are unaffected.

* test(reasoning): cover date-stamped Claude 3.0 not reasoning-capable

Locks the heuristic so claude-3-opus-20240229 (and sibling date-stamped 3.0
ids) report no reasoning support while 3.7+/4.x stay capable.

* fix(session): classify ACP adapter sessions into the CLI sidebar family

Gateway ACP adapter sessions (source='acp' in state.db — Zed, external
device bridges like a Rabbit R1) normalised to session_source 'other',
which fell through BOTH sidebar buckets: sidebar_source=webui skips the
state.db projection entirely (and ACP rows have no WebUI sidecar), while
sidebar_source=cli keeps only CLI-classified rows. Result: ACP
conversations were invisible and could never be clicked/imported.

Classify 'acp' as a CLI-family source, mirroring 'tui' (a local
interactive agent client): normalize_agent_session_source maps it to
session_source 'cli' with label 'ACP', is_cli_session_row recognises the
raw source, is_cli_session_row_visible keeps ended/untitled ACP rows
visible (they are always user-driven), and frontend _isCliSession
accepts raw === 'acp'. Zero-message ACP connect/reconnect stubs stay
hidden via the existing message-count guard. Claim policy is untouched:
ACP stays claimable like CLI/TUI per the documented 'future local agent
sources' allowance in _is_claimable_cli_source.

Rollback: revert this commit (pre-change file copies also in
~/.hermes/backups/acp-sidebar-fix-20260711/), then restart WebUI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(session): require a user turn before surfacing ACP rows

An ended ACP connection can record only assistant/tool/system messages
(replayed or aborted turn), which the blanket TUI-style visibility
exception would surface despite not being user-driven. Gate the ACP
branch on _count_user_turns(row) > 0, per Greptile review on #5939.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Release exp-v0.52.40: ACP sessions in sidebar (#5939) + date-stamped Claude 3.0 reasoning-control fix (#5934)

---------

Co-authored-by: 黄云龙 <76432572+nankingjing@users.noreply.github.com>
Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 15:20:41 -07:00
nesquena-hermes 7f88b66ff9 Merge salvage/5461-plugin-catalog 2026-07-11 05:27:11 +00:00
nesquena-hermes 7176b3a756 fix(config): route plugin providers before config_provider bare-passthrough
Gate found the plugin-routing branch ran AFTER the provider==config_provider
early-return, so when an ACTIVE plugin provider was also the configured default,
model_with_provider_context returned a bare model and dropped the '@plugin:'
hint -> wrong backend. Moved the _is_plugin_model_provider() branch above the
config_provider passthrough. Regression test (GMI + anthropic/claude-sonnet-4.6
as the active plugin+configured provider) added. Co-authored-by: alexfoxtm.
2026-07-11 05:18:27 +00:00
nesquena-hermes bd1cdf24c7 Merge #5910: add 'max' reasoning effort level (opt-in, availability-gated) 2026-07-11 04:47:37 +00:00
nesquena-hermesandperejaslav 742ccd59b5 feat(reasoning): make 'max' selectable only when actually available (#3505 refinement)
Maintainer call (2026-07-11): 'set max when available, don't when not.' Two layers:
- resolve_model_reasoning_efforts() now uniformly passes its sourced list through
  the provider ceiling filter (preserving any 'none' sentinel), so the UI dropdown
  offers 'max' ONLY for models whose native ladder genuinely includes it (adaptive
  Claude 4.6+, DeepSeek) and hides it everywhere it would be rejected/mishandled
  (GPT-5, o-series, Gemini, legacy/cloud-hosted Claude, unknown providers).
- coerce_reasoning_effort_for_model() default-denies a stale/CLI 'max' to xhigh on
  an UNRECOGNIZED provider (empty capability list) so it can never 400, while a
  RECOGNIZED reasoning provider whose exact model id we couldn't resolve (e.g.
  claude-opus-latest) still preserves 'max'. All other levels keep #3505
  preserve-verbatim. New _provider_known_reasoning_capable helper + regression tests.

Co-authored-by: perejaslav <perejaslav@users.noreply.github.com>
2026-07-11 04:42:35 +00:00
nesquena-hermes b779f0b777 fix(reasoning): extend max ceiling to OpenAI-family + cloud-hosted Claude lanes
Re-gate found 'max' still reached provider adapters that reject/mis-handle it on
lanes beyond openai-codex/native: direct openai + openai-api + azure-foundry
GPT-5 (APIs top out at xhigh), and Azure-Foundry/Bedrock-hosted LEGACY Claude
(the pre-adaptive classifier only matched native Anthropic provider names, so max
survived to the 8k manual-thinking budget). Extended the GPT-5/o-series ceiling
to all OpenAI-family lanes and the pre-adaptive-Claude ceiling to azure/bedrock/
vertex lanes (gated on 'claude' in the model id so non-Claude models on those
lanes are untouched). Regression tests for both lanes.
2026-07-11 04:06:36 +00:00
nesquena-hermes 381bcb4822 fix(reasoning): classify date-stamped + claude-3 IDs as pre-adaptive
Re-gate found _is_pre_adaptive_anthropic misclassified date-stamped legacy IDs
(claude-sonnet-4-20250514, claude-opus-4-20250514) and the claude-3 family
(claude-3-opus-20240229) as adaptive, so 'max' survived to the 8k manual-thinking
fallback instead of degrading to xhigh (32k-equivalent). Rewrote the helper:
claude-3* always pre-adaptive; a 6+ digit date stamp is not a minor version so
'4-20250514' is a pre-adaptive 4.0 build; 4.6+/latest stay adaptive. Extended
regression tests to both ID shapes.
2026-07-11 03:52:10 +00:00
nesquena-hermesandperejaslav 96b04359ab fix(reasoning): degrade 'max' via provider ceiling, not sourced list
Gate found removing the max->xhigh coercion let 'max' reach provider adapters
whose native ladder tops out lower, degrading WORSE than before: Gemini treats
unknown 'max' as medium; pre-adaptive Claude (3.7/4.5) manual-thinking lacks a
'max' budget and falls to 8k. Fix: coerce_reasoning_effort_for_model now applies
the hard provider ceiling (_filter_reasoning_efforts_for_provider) FIRST and
degrades down the ladder when the ceiling excludes the requested level — even
when the sourced capability list is empty (unrecognized) or wrongly advertises
the level. Gemini + all Google aliases and pre-4.6 Anthropic now cap 'max' at
xhigh; adaptive Claude 4.6+/DeepSeek/etc. preserve it. Regression tests added.

Co-authored-by: perejaslav <perejaslav@users.noreply.github.com>
2026-07-11 03:40:11 +00:00
nesquena-hermesandruizanthony 54efd6078e feat(workspace): opt-in 'start new chat when switching workspace' (#5473)
Salvage-rebuild of #5473 (@ruizanthony), reworked to OPT-IN per maintainer
decision. New default-OFF setting new_chat_on_workspace_switch: when enabled,
selecting a DIFFERENT workspace starts a fresh chat bound to that workspace
(leaving the current conversation on its original workspace, avoiding stale
cross-workspace context) instead of mutating the current session in place.
Same-workspace selection stays an in-place refresh. Default off preserves the
shipped behavior for everyone.

Wiring: config default + bool key; window._newChatOnWorkspaceSwitch set on
boot + autosave; Settings checkbox (Preferences); gated branch in
switchToWorkspace() that runs before the in-place /api/session/update; 3 i18n
keys across all 15 locales; regression tests (default-off, gated-branch-present,
flag-wiring, checkbox+i18n).

Co-authored-by: ruizanthony <ruizanthony@users.noreply.github.com>
2026-07-11 03:13:48 +00:00
nesquena-hermesandperejaslav d56123f796 feat: add max reasoning effort level (salvage of #4627)
Salvaged from #4627 by @perejaslav. Core Hermes already supports
reasoning_effort=max; this closes the WebUI parity gap (Anchor-A) — adds
max to VALID_REASONING_EFFORTS + dropdown/chip/reasoning-command, keeps
the degrade ladder for capped models.

Co-authored-by: perejaslav <perejaslav@users.noreply.github.com>
2026-07-11 01:13:36 +00:00
nesquena-hermesandalexfoxtm 553ad8146e fix(api/config): surface + route plugin-only providers in static catalog
Salvaged from #5461 by @alexfoxtm — the catalog-surfacing half was correct; this rebuilds it fresh on master and adds the missing routing (model_with_provider_context -> @plugin:model) + a routing regression test.

Co-authored-by: alexfoxtm <alexfoxtm@users.noreply.github.com>
2026-07-11 01:11:37 +00:00
nesquena-hermes 0af0a5fcf0 Merge #5746: final answer only activity display mode 2026-07-10 23:50:28 +00:00
Frank Song 3c95b3a9d5 Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-11 02:07:37 +08:00
igorroncevic dea5e8a96d fix: fail closed on incomplete OpenRouter pricing 2026-07-10 15:18:39 +02:00
Frank Song e604b76720 Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-10 11:39:50 +08:00
de0e6eb452 Release: centralize auxiliary-model task catalog (#5865) (#5870)
Auxiliary-model task definitions were maintained in BOTH Python and JS and could
drift (the retired session_search task lingered in one). Centralize to a single
backend catalog that panels.js renders+saves from exclusively; purge the retired
slot on reset while preserving other custom bindings; old configs still load.

Gate-pass: Codex SAFE (only session_search removed, 11 other tasks + defaults
unchanged, 22 keys match across 15 locales, legacy migrate preserves custom
bindings, executable DOM harness rendered all rows). 53 own + 887 regression green.

Co-authored-by: t <a@b>
Co-authored-by: starship-s <starship-s@users.noreply.github.com>
2026-07-09 20:31:54 -07:00
Frank Song 4af5334ebb Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-10 03:33:07 +08:00
2b457f1646 Release: cap StreamChannel offline replay buffer (#5788) (#5849)
Bounded deque(maxlen) drop-oldest so a disconnect-without-cancel can't grow the
per-turn offline replay buffer unbounded (per-abandoned-turn OOM risk, #4633 family).
Dropped frames recovered via run-journal last_event_id replay path.

Deep-review gate: Codex SAFE (journal-recovery verified end-to-end), 20 own + 1106
regression tests green. Crown-jewel streaming path.

Co-authored-by: t <a@b>
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-07-09 12:26:05 -07:00
Frank Song 61d32671ed Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-10 00:07:14 +08:00
0d3ee7de9f Release Batch A: worktree cli-shadow + atomic settings.json + state.db read-only (#5832, #5829, #5833) (#5837)
Phase 1 low-risk backend reliability batch. Three independent contributor fixes
on distinct files, each byte-identical to its PR head, Codex SAFE, 691+16 tests pass.

- #5832 (silent-reader-cn): importlib-load agent cli.py by absolute path so a
  shadowing cli/ namespace package can't brick worktree creation
- #5829 (ai-ag2026): atomic settings.json writes (temp+fsync+os.replace, mode-preserving)
- #5833 (ai-ag2026): open the remaining 8 state.db read projections read-only

Co-authored-by: t <a@b>
Co-authored-by: silent-reader-cn <silent-reader-cn@users.noreply.github.com>
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-07-09 09:04:59 -07:00
Frank Song 96af63a95e Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-09 14:41:48 +08:00
g 9a1fdabfda Merge remote-tracking branch 'origin/master' into pr-5557 2026-07-09 06:12:41 +00:00
Frank Song 24053bf025 Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-09 11:23:11 +08:00
Hermes Agent 69d0fc73c9 fix: derive service tier support from agent fast-mode metadata 2026-07-09 03:16:53 +00:00
Frank Song 33859b2704 Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-09 09:43:54 +08:00
sbe27 68d406f7ff fix(models): refresh OpenCode Go static catalog 2026-07-09 01:23:08 +00:00
Frank Song ff0e6347cb Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-09 08:22:26 +08:00
t 2cf43527a3 feat(updates): stable/experimental release channels
Add an update_channel setting (stable|experimental) that selects which git-tag
stream the self-updater tracks on the single linear master line:

  stable       -> 'v*'      promoted, soaked releases (default; unchanged glob)
  experimental -> 'exp-v*'  every release batch (opt-in testers)

A channel is only a tag glob — no branches, no divergence — so every ff-only
guarantee (#2653/#2846/#3140) is preserved. Channel governs the WebUI repo only;
the Agent repo keeps its historical branch fall-through.

Correctness (advisor-reviewed, Codex + Fable):
- describe uses --match <glob> so a commit tagged both v* and exp-v* resolves
  to the channel-correct tag
- stable never falls through to origin/master when HEAD contains the latest
  stable tag (the firehose-suppression that makes channels work)
- apply_force_update refuses to reset --hard onto an ancestor ref (rewind guard)
- update cache + in-progress guard keyed by (channel, include_agent)
- channel display badge is a SEPARATE field; WEBUI_VERSION stays channel-neutral
  (asset cache-busting / SW cache / stale-client skew all do exact-string equality)

Settings UX: channel dropdown with risk-forward copy + the ff-only asymmetry
helper ('switching back to Stable keeps your current version until Stable catches
up'), channel chip on the version badge, i18n keys, re-check on switch.

Tests: new tests/test_update_channels.py (12 real-git-fixture tests) + existing
update suite updated for the channel-aware signatures.
2026-07-08 19:25:27 +00:00
Frank Song 9b352e3acf Merge remote-tracking branch 'origin/master' into paperclip/pr-5732 2026-07-09 00:28:30 +08:00
Rod Boev f4172f73b0 fix(mcp): preserve config overrides for profile reads (#5619) 2026-07-08 16:18:52 +00:00
nesquena-hermesandrodboev 2717ea0ac5 feat(chat): add final answer only activity display mode (#5706, picker grid fixed)
Clean rebase of rodboev's #5746 (rebase-first).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-07 14:38:08 +00:00
Frank Song 5764df66f3 fix(wakeup): share process pause lane resolution 2026-07-07 17:00:36 +08:00
Frank Song 1ab9c47e9b fix(wakeup): canonicalize paused route model lanes 2026-07-07 15:09:30 +08:00
t 2454719f2e gate 5696 + fold-in: non-throwing HERMES_DEBUG_SLOW parse (Codex CORE fix) 2026-07-06 20:15:42 +00:00
t 06590556f5 gate 5696 on current master 2026-07-06 20:05:17 +00:00
Konstantin M 60e1b5277d perf(webui): bound _last_message_timestamp to a tail window
Session.compact() called _last_message_timestamp(self.messages) which
reversed-walked all 2,730 messages on every /api/session response.
The messages array is chronologically ordered, so the last non-tool
message sits at the very end. Scan only the last 8 messages; fall
back to a full scan only if no timestamp is found in the window
(unusual sessions with >8 trailing tool rows).

Reverts to identical behavior when the window hits, since the most
recent user/assistant message's timestamp is what compact() emits.

Bench (single-shot, live Chromebook eMMC):
  before: get_session=1107ms compact=1725ms total=2880ms
  after:  get_session= 648ms compact= 288ms total=1313ms
  (-80% on compact, -54% on total)

Revert: git checkout master && systemctl --user restart hermes-webui
2026-07-06 16:53:01 +00:00
Konstantin M 1018732a26 perf(webui): Phase 0 instrumentation for session-load hot path
The slow-request journal showed /api/profiles, /api/models, and /api/session
fire on every session click but had no per-stage timing. Add:

- RequestDiagnostics coverage for GET /api/profiles, /api/models, /api/session
- Stage markers inside /api/profiles (list / active lookup / isolated check)
- Stage markers inside /api/models (freshness routing, serialize)
- Per-stage stage-log inside get_available_models_for_session_visit that
  emits a [SLOW] line when total wall time crosses HERMES_DEBUG_SLOW ms
  (default 500ms) so we can pinpoint which sub-step is blocking

No behavior change. Existing _t0.._t6 timing on /api/session and the
2.5s/10s TTL on /api/sessions are unchanged. Reversible: revert commit.
2026-07-06 14:23:34 +00:00
Rod Boev 624e03c60a refactor(#2695): centralize WebUI static asset resolution 2026-07-06 06:45:36 -04:00
Rod Boev 29e9a27299 fix(#5619): preserve same-path cfg overrides on reload 2026-07-05 17:08:20 -04:00