Commit Graph
100 Commits
Author SHA1 Message Date
nesquena-hermesandrh-id b537be3008 test(core): accept new limit kwarg in get_state_db_session_messages stub
Co-authored-by: rh-id <rh-id@users.noreply.github.com>
2026-07-16 19:47:36 +00:00
nesquena-hermes d9f6a4ecb6 Release exp: cache GET /api/providers per-profile with short TTL (#6013). Thanks @SeanWit. 2026-07-16 02:45:50 +00:00
nesquena-hermes 500fa485d2 Merge commit 'refs/pull/6013/head' of github.com:nesquena/hermes-webui into stage/6013 2026-07-16 02:28:15 +00:00
nesquena-hermes a69bc3a264 fix(#6105): close the drawer-over-picker stacking class via z-index, not per-site guards
Codex found the body-reparented picker (z-200, later body child) could be revealed
UNDER a full-screen drawer/panel via multiple paths (swipe, breakpoint resize,
profile-switch). Rather than guard each open-site (whack-a-mole + my resize guard
over-fired on keyboard reflows), fix the class once: .model-dropdown--floating gets
z-index:190 — above the composer-mobile-config-panel (z-180) it opens from, below
the mobile sidebar + workspace panel (both z-200) — so any drawer/panel that opens
covers the picker, matching master's stacking intent. Reverted the two boot.js
guards. Co-authored-by webtecnica.
2026-07-16 01:15:11 +00:00
nesquena-hermes 7ed58510bc fix(#6105): also close floating picker on breakpoint-crossing resize that reveals a drawer
Codex found a sibling of the swipe-stacking edge: a direct landscape/tablet->phone
resize can re-reveal an already-open mobile sidebar or workspace panel under the
body-reparented picker (z-200 siblings, no click to trigger outside-close). The
window resize handler now guardedly closes+restores the picker before re-syncing
drawer state, gated on entering phone width AND a drawer/panel actually being open,
so ordinary keyboard/URL-bar reflows don't dismiss the picker mid-use.
2026-07-16 00:59:01 +00:00
nesquena-hermes 7091c4aa84 fix(#6105): close floating model picker before gesture-opening mobile sidebar
Codex re-gate found the body-reparented picker (z-index:200, body child) would
paint on top of the mobile sidebar drawer when a left-edge swipe opened the
sidebar while the picker was open. _openMobileSidebarFromGesture() now calls
guarded closeModelDropdown() first (which also restores the dropdown to the
footer), so the swipe reveals the session drawer cleanly. Co-authored-by webtecnica.
2026-07-16 00:48:27 +00:00
nesquena-hermes 6e30fd21f8 docs: CHANGELOG for #6105 model-picker reparent 2026-07-16 00:29:07 +00:00
nesquena-hermesandwebtecnica fca5056589 fix: reparent mobile model picker to body so fixed positioning escapes footer (#6080)
The composer model dropdown (#composerModelDropdown) is a child of
.composer-footer, which sets container-type:inline-size (and, under the Geist
Contrast skin, a backdrop-filter). Both establish a fixed containing block, so
the phone position:fixed dropdown resolved against the FOOTER (pinned to the
bottom of the screen) instead of the visual viewport — dropping the menu below
the fold on all skins (the "tapping the model picker does nothing / opens
off-screen" symptom in #6080).

Fix (Fable UX gate recommendation on PR #6105): on the phone path
(<=640px) reparent the dropdown to <body> and switch it to position:fixed via a
.model-dropdown--floating class, exactly the working #profileDropdown idiom, so
fixed positioning is viewport-relative on every skin. The visualViewport math
from PR #6105 is preserved (viewport-relative coordinates, above/below flip
preferring the larger space, width = viewport - 16px, titlebar-bottom clamp) and
so are the rAF-coalesced visualViewport resize/scroll listeners.

Desktop (>640px) is unchanged: closeModelDropdown() and the desktop branch of
_positionModelDropdown() restore the element into its original .composer-footer
slot and clear the phone inline styles, so the desktop popover stays an
absolutely positioned footer child, byte-for-byte identical to master. Because
the menu now leaves the footer on phones, no per-skin backdrop-filter override
is needed.

Adds tests/test_issue6080_model_picker_reparent.py, including a node-executed
DOM-parentage guard that runs _positionModelDropdown()/closeModelDropdown()
against a stub DOM and asserts the open phone dropdown has NO .composer-footer
ancestor (escaping the container-type trap) and is restored into the footer on
close and on desktop.

Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
2026-07-16 00:27:47 +00:00
nesquena-hermes ee7ec5e5d7 Release exp batch: narrow-screen CSS fixes — approval/clarify popup min-width (#6104) + stream-timestamp foldable layout (#6106).
Scoped to the two stated fixes only: strips the #composerModelDropdown position:fixed
mobile override that #6104's branch had bundled in (that's the #6080 model-picker
mechanism, being reworked separately to reparent to body level — the fixed override
is trapped by .composer-footer's container-type so it doesn't belong here), and drops
the <=360px tool-name-hiding rules (header overflow-clip prevents the spill without
losing tool identity). Thanks @webtecnica.
2026-07-16 00:10:25 +00:00
nesquena-hermes 928646a57c Merge commit 'refs/pull/6106/head' of github.com:nesquena/hermes-webui into stage/css2-batch 2026-07-15 23:58:12 +00:00
nesquena-hermes 89e443674e Merge commit 'refs/pull/6104/head' of github.com:nesquena/hermes-webui into stage/css2-batch 2026-07-15 23:58:10 +00:00
nesquena-hermes 9c0e0986c3 Release exp: pinch-to-zoom for Mermaid diagrams on touch devices (#5913). Thanks @silent-reader-cn. 2026-07-15 23:02:13 +00:00
nesquena-hermes a2d70b9e53 Release exp: hold streaming CLI-cache TTL above sidebar poll cadence (#6102). Thanks @starship-s. 2026-07-15 22:40:48 +00:00
nesquena-hermes b877c9aa40 Merge commit 'refs/pull/6102/head' of github.com:nesquena/hermes-webui into stage/6102-cachettl 2026-07-15 22:40:21 +00:00
nesquena-hermes 38119fac25 fix(#6056): extend sidebar title-field redaction to /api/sessions/search (Codex class-fix)
Codex re-gate found the same derived-title leak on the sibling /api/sessions/search
endpoint (3 branches redacted only 'title'). Extract _redact_sidebar_title_fields()
shared helper, apply it in both _sidebar_session_response_item and all 3 search
branches, read api_redact_enabled once per search response (mirrors #4662). +helper
test + source-guard that every search branch calls the redactor.
2026-07-15 21:06:36 +00:00
nesquena-hermes ffbc91343a fix(#6056): redact display_title/_state_db_title/parent_title on the sidebar API
Codex gate found the #6056 derived display_title (from raw user-message content)
bypassed sidebar redaction — the serializer redacted only 'title', so a credential
in a delegated goal leaked to the sidebar even with api_redact_enabled=True.
Redact display_title + _state_db_title + parent_title on the same path; +regression
test (proven to fail on the unfixed serializer).
2026-07-15 20:57:31 +00:00
nesquena-hermes 80cb79c7f7 Release exp: show delegated goal titles for generic subagent child sessions (#6056). Thanks @rodboev. 2026-07-15 20:47:11 +00:00
nesquena-hermes b96405ef29 Merge commit 'refs/pull/6056/head' of github.com:nesquena/hermes-webui into stage/6056-delegtitles 2026-07-15 20:46:34 +00:00
nesquena-hermes 6efbf899c9 Release exp: show requested line range in collapsed read_file tool labels (#6062). Thanks @franksong2702. 2026-07-15 20:37:32 +00:00
nesquena-hermes 2e93d04a57 Release exp: allow manual update check when auto-update is disabled (#6088) + regression test. Thanks @webtecnica. 2026-07-15 20:02:35 +00:00
nesquena-hermes 754dcca048 Merge commit 'refs/pull/6088/head' of github.com:nesquena/hermes-webui into stage/6088-manualupd 2026-07-15 20:02:02 +00:00
nesquena-hermes e41507d164 Release exp: suppress false no-response after tool-limit turn with a final answer (#6081). Thanks @coyaSONG. 2026-07-15 19:39:02 +00:00
nesquena-hermes 30059085c2 fix(#6083): only grace-evict CONFIRMED never-persisted shells (Codex finding #2)
Codex re-gate asked to distinguish a confirmed-absent sidecar from an
indeterminate/unreadable one before entering the abandoned-shell grace path.
Add _session_sidecar_exists() (True/False/None) and only grace-evict when the
sidecar is confirmed absent (False); a present-or-indeterminate sidecar keeps
the session resident. Age-window bound retained (fully closes the leak, no
data-loss edge); an SSE-mounted-lease refinement is noted as optional follow-up.
2026-07-15 19:06:10 +00:00
nesquena-hermes 02e5acb890 fix(#6083 follow-up): bound never-persisted empty session shells
Codex gate found the eviction-reorder made never-saved empty shells immortal
(abandoned /api/session/new tabs accumulate unbounded past sessions_cache_max).
Add a grace-window bound: protect a never-persisted empty shell only while fresh
(<30min old) OR while it carries a composer draft; a stale, empty, draftless,
never-saved shell is treated as abandoned and becomes evictable again. A real
compose persists a draft on the first keystroke (disk-backed + reloadable), so
the brick fix is preserved while the leak is closed. +2 regression tests.
2026-07-15 18:44:21 +00:00
nesquena-hermes 0881b8ec0c Release exp: fix new-conversations-unstartable (password-manager autofill evicts unsaved session) (#6083). Thanks @ai-ag2026. 2026-07-15 18:24:11 +00:00
nesquena-hermes 078a64cabe Merge commit 'refs/pull/6083/head' of github.com:nesquena/hermes-webui into stage/6083-brick 2026-07-15 18:23:03 +00:00
nesquena-hermes f803eb2fcf Release exp: preserve visible reasoning on deferred anchor paint (#6048). Thanks @franksong2702. 2026-07-14 23:44:11 +00:00
nesquena-hermes 4f5f197f9a Merge commit 'refs/pull/6048/head' of github.com:nesquena/hermes-webui into stage/6048 2026-07-14 23:43:22 +00:00
nesquena-hermes 74fd7ed3e1 docs: add change guidelines (GUIDELINES.md) + wire into AGENTS.md and CONTRIBUTING.md
Distilled habits that get a PR merged in one review round instead of several:
four root causes + ten rules + show-your-work PR-description contract. Full doc
in docs/GUIDELINES.md, compressed 10-point form in AGENTS.md (agent entry point),
pointer added to CONTRIBUTING.md reading list.
2026-07-14 22:40:52 +00:00
nesquena-hermes 0a85a842c6 Release exp: cascade delegated subagent sessions on parent deletion (#5926). Thanks @pxxD1998. 2026-07-14 19:10:25 +00:00
nesquena-hermes b8605e5561 Merge branch 'rebase-5926' into stage/5926 2026-07-14 19:09:29 +00:00
nesquena-hermes 132bd8b270 Release exp: fail-closed gateway-restart recovery for agent updates (#6054). Thanks @franksong2702. 2026-07-14 08:58:53 +00:00
nesquena-hermes b49c93c443 Merge branch 'rebase-6054' into stage/6054 2026-07-14 08:57:38 +00:00
nesquena-hermes ce81d8acfd Release exp: short-circuit impossible state-db prefix reconciliation (#6014). Thanks @Isla-Liu. 2026-07-14 05:57:38 +00:00
nesquena-hermes 93a1e41675 Merge branch 'rebase-6014' into stage/6014 2026-07-14 05:46:27 +00:00
nesquena-hermes 07da3af768 Release exp: honor agent worktree config default in new chats (#6029). Thanks @ayxuerui. 2026-07-14 04:44:19 +00:00
nesquena-hermes 58129b9e84 Merge branch 'rebase-6029' into stage/6029 2026-07-14 04:43:53 +00:00
nesquena-hermes c690c0a7d3 Release exp: #5806 profile-concept help localization + example (Binding row dropped per maintainer). Thanks @rodboev. 2026-07-14 04:05:14 +00:00
nesquena-hermes 1a637f8142 Merge branch 'rebase-5806' into stage/5806 2026-07-14 04:04:49 +00:00
nesquena-hermes 503a369d0c refine(#2147): drop confusing Binding row, keep localization + Example
Maintainer revision of #5806: the localization refactor (all profile-concept
help text now goes through t() so it's translatable via the documented
English fallback) is the core value, and the concrete Example row genuinely
clarifies how profiles + workspaces work together. The Binding row restated
the Together row + added scheduled-task edge-case trivia that muddied a concept
explainer, so it's dropped (row + its 2 i18n keys + its test).

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-14 04:04:39 +00:00
nesquena-hermes 5d97c18242 fix(#2147): correct two inaccurate help-copy claims
- workspace 'scopes file access' -> 'keeps chat, terminal, and file browsing
  pointed at that project' (terminal/agent tools can access outside the workspace).
- scheduled tasks 'run under the profile that created them' -> 'run under the
  profile selected on the task, or the WebUI server default when none selected'.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-14 03:40:21 +00:00
nesquena-hermes ca9444292d Release exp: Nix flake/NixOS module (#5431) + composer-focus shortcut (#5795). Thanks @rodboev.
#5431 (#2422): optional Nix flake + NixOS systemd module. Codex SAFE — zero non-Nix
  impact (only guarded bootstrap.py fallback touches core), secure-by-default
  (loopback, 0700/0077, non-root, EnvironmentFile secrets), 100 tests.
#5795 (#5759): Cmd/Ctrl+/ focuses composer. Codex SAFE after maintainer fix
  (matched e.key==='/' not physical Slash code so QWERTZ Ctrl+- zoom isn't stolen
  + shifted-/ works); focus-only, guarded, no conflict, 4 tests.
2026-07-14 01:25:40 +00:00
nesquena-hermes 7c3eae400a Merge branch 'rebase-5431' into stage/nix-slash 2026-07-14 01:24:31 +00:00
nesquena-hermes ccbaa4ba61 Merge branch 'rebase-5795' into stage/nix-slash 2026-07-14 01:24:31 +00:00
nesquena-hermes 7b210baca4 fix(#5759): match composer-focus chord on '/' char not physical Slash code
The Cmd/Ctrl+/ predicate keyed off e.code==='Slash' + !e.shiftKey, which on
QWERTZ layouts (a) stole Ctrl/Cmd+- browser zoom-out (physical Slash there is
the minus/zoom key) and (b) missed the real shifted-'/' chord. Match e.key==='/'
instead — layout-correct on every keyboard. Tests updated to assert the physical
code isn't used and a layout-shifted '/' still matches.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-14 01:20:12 +00:00
nesquena-hermes 7fb3d412f5 Release exp: instrument send-to-first-token latency (#6042, closes #6006). Thanks @rodboev.
Measure-only TTFT (ttft_ms) on the metering session + _firstTokenMs diagnostic. Computed once
on first token (no hot-path latency), all mutation under existing GlobalMeter._lock, existing
token/usage/cost metering unchanged. Codex SAFE (16-stream concurrency probe, no leak/None-deref)
+ stream-gate GREEN all modes + 22 tests.
2026-07-13 22:52:15 +00:00
nesquena-hermes f1480ad407 Merge branch 'rebase-6042' into stage/6042-ttft 2026-07-13 22:51:55 +00:00
nesquena-hermes ea5af7b6c8 Release exp: fix 'approve once' persisting as session approval (#6035, closes #6017). Thanks @rudidev08.
Local (non-gateway) _resolve_approval_legacy collapsed 'once'+'session' into approve_session()
→ 'approve once' silently persisted session-wide. Narrowed to choice=='session' only; 'once'
still resolves the current call but doesn't persist. Codex SAFE (adversarial: once resolves
current+no residue+next re-prompts, session/always/deny unregressed, gateway consistent, tests
non-vacuous + parked-thread probe of all 4 choices).
2026-07-13 20:16:26 +00:00
nesquena-hermes b86922ae38 Merge branch 'rebase-6035' into stage/6035-approval 2026-07-13 20:15:59 +00:00
nesquena-hermes d6684c091f Release exp: keyboard/screen-reader access for conversation actions menu (#6019). Thanks @sheldon-im.
ARIA (aria-expanded/controls, role=menu/menuitem) + focus-to-first-action on open +
Arrow/Home/End/Escape nav + focus-restore-to-trigger on close; existing click/touch unchanged.
Codex SAFE (isConnected-guarded restore, menu-scoped bounded arrow-nav, no listener leak
[25-cycle probe], 49 tests) + stream-gate GREEN.
2026-07-13 19:37:56 +00:00
nesquena-hermes e02636c58c Merge branch 'rebase-6019' into stage/6019-a11y 2026-07-13 19:24:30 +00:00
nesquena-hermes 735d74df46 Release exp: Round-2 batch — model-picker dedup (#5997) + zh-CN localization fill (#6024)
#5997 (rodboev, closes #5989): proxy/catalog model-picker dedup, 4-round convergence.
  Colon-suffix collision closed (provider-context exact-prefix strip, :free/:thinking/:latest
  survive), 5 prior findings stay fixed. Codex SAFE r4 (73 tests) + stream-gate GREEN.
#6024 (Loukky): zh-CN localization fill (+55/-55, pure value replacement on existing keys,
  no key add/remove, no value-swap [en block 0 CJK], placeholders preserved).
Disjoint (static/ui.js picker logic / static/i18n.js locale data).
2026-07-13 19:17:13 +00:00
nesquena-hermes 4016130c24 Merge branch 'rebase-6024' into stage/r2-batch 2026-07-13 19:04:13 +00:00
nesquena-hermes 54a7bb2058 Merge branch 'rebase-5997' into stage/r2-batch 2026-07-13 19:04:12 +00:00
nesquena-hermes 996705b7b1 Release exp: worktree unlock-before-remove so WebUI worktrees are reapable (#6028, #6023). Thanks @ayxuerui.
Fail-soft git worktree unlock before remove (agent locks worktrees it creates; git refuses
--force remove on locked → orphan accumulation). Dirty-guard still runs first. Codex SAFE,
13 tests pass. Shipped solo (unbundled from #6013 which has a full-suite test-interaction).
2026-07-13 18:27:44 +00:00
nesquena-hermes 1e2a01102a Merge branch 'verify-6028' into stage/6028-solo 2026-07-13 18:27:23 +00:00
nesquena-hermes 42d62e0793 Release exp: render MEDIA: tokens inline during streaming (#5802). Thanks @silent-reader-cn.
MEDIA tokens (image/video/audio/pdf/diff/csv/excalidraw) render inline in real time
during streaming incl split-across-chunk markers. Codex SAFE r4 (both prose regressions
closed — ownership preserved + fade retained, normal prose byte-identical to master, all
4 original findings fixed, security clean, no ReDoS), stream-gate GREEN all modes (hide_all
RED was load flake, isolated re-run clean). 227 tests + 18 real-SMD subtests pass. 4-round
convergence on the crown-jewel render + security path.
2026-07-13 08:17:52 +00:00
nesquena-hermes 2d1a2d15a0 Merge branch 'rebase-5802' into stage/5802-media 2026-07-13 08:06:28 +00:00
nesquena-hermes ab1d909aaa Release exp: gateway terminal errors + persist pending turn (#5969, closes #5940). Thanks @rodboev.
Gateway terminal provider errors classified+surfaced (not generic empty turn), session_id-bound,
pending turn persisted through reload incl repeated-prompt (is_exact_checkpoint). Codex SAFE r4 +
stream-gate GREEN all modes. Shipped solo (unbundled from #6002 which has a test-isolation CI fail).
2026-07-13 03:42:36 +00:00
nesquena-hermes f777175cf9 Merge branch 'rebase-5969b' into stage/5969-solo 2026-07-13 03:41:39 +00:00
nesquena-hermes 4b72dea9ab Release exp: fork-from-here during active streaming (#5994, closes #5993). Thanks @kertisalex.
Past committed messages fork during a live response; only the streaming message +
just-sent uncommitted prompt are blocked (toast). Codex SAFE round2 (off-by-one fixed,
_isLastMsg busy-gated, _pending not carried forward), stream-gate GREEN (hide_all RED
was load flake, isolated re-run clean), 99 tests.
2026-07-13 02:25:30 +00:00
nesquena-hermes 6d0fdcf9e2 test(#5979): add streaming explicitly_picked lifecycle test (Fable fast-follow)
Fable's one recommended fast-follow: the streaming glue that computes
_explicitly_picked from the persisted signature (streaming.py:7905-7931) had no
direct coverage. Adds test_streaming_explicitly_picked_computation_5979 mirroring
that exact comparison across deliberate-pick (honored), stale-after-model-change
(ignored), stale-after-provider-change (ignored), and never-picked (unmarked).

Both advisors passed on the signature mechanism: Codex SAFE TO SHIP (session-update
hole verified closed: update->reload->cold openai/gpt-5.4 strips to gpt-5.4;
b3nw's deliberate pick preserves), Fable 'ship it' (clean data-model invalidation,
correct layering, no #433/#3872/#548 regression). Independent 10-scenario matrix
through the real resolver all correct.
2026-07-13 02:09:21 +00:00
nesquena-hermes 53a4b35555 harden #5979 round 3 (Codex): signature-based explicit-pick provenance
Codex found a verified lifecycle hole: the bare boolean model_explicitly_picked
was not cleared on /api/session/update (api/routes.py:14438-14460 changes+persists
the model/provider without touching it), so after an update+reload the flag stayed
True and a cold #433 stale leftover (openai/gpt-5.4) was wrongly preserved instead
of stripped to gpt-5.4.

Fix (Codex's prescription): replace the unscoped boolean with a SIGNATURE bound to
the routing context.
- api/models.py: new model_explicit_pick_signature(model, provider) ->
  '<model>\x1f<provider>'; Session.model_explicit_pick_signature persisted field
  (replaces the boolean; in METADATA_FIELDS, restored via kwargs, survives restart).
- api/routes.py /api/chat/start: on a fresh explicit_model_pick, stamp the signature
  of the resolved model+provider.
- api/streaming.py: treat the send as a deliberate pick ONLY when the persisted
  signature equals the signature recomputed from the session's CURRENT persisted
  model+provider. Any model/provider change (chat-start, session-update,
  normalization, provider repair) yields a different signature and auto-invalidates
  the stale pick — no per-mutation-site clearing needed.

So: deliberate pick on unchanged context -> preserve (b3nw); any switch away ->
signature mismatch -> unmarked -> legacy strip (#433 stays correct even after a
session-update+cold-restart). Adds signature match/invalidate/persist tests.
75 focused tests pass (resolver + sprint40 + the 2 streaming-mock suites).
2026-07-13 01:58:20 +00:00
nesquena-hermes 3559a867a9 test: fix resolve_model_provider mocks to accept explicitly_picked kwarg
Two streaming tests monkeypatch resolve_model_provider with a fixed-arg lambda
(_model only). The send path now calls it with explicitly_picked=, so the mocks
raised TypeError. Widened both lambdas to (_model, **_kw). Caught by the full
suite (not the focused resolver tests, which call the real function).
2026-07-13 01:45:07 +00: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 5e9116db95 Release exp: no false no-response after final tool (#5990, closes #5686). Thanks @jtstothard.
Final-answer detection understands structured content parts (text after tool-use
boundaries); genuine empty completions still show no_response; terminal provider
error still surfaces as apperror (never masked). Codex SAFE round2 (3 precedence
findings fixed), stream-gate GREEN all modes. Maintainer: updated the #5940 source-
assert test for the _captured_terminal_failure rename.
2026-07-13 01:28:48 +00:00
nesquena-hermes 1656f812f2 Merge branch 'rebase-5990' into stage/5990-false-noresponse 2026-07-13 01:27:48 +00:00
nesquena-hermes a9cf45f411 Release exp: cron unread scoped to active profile (#5975, closes #5960). Thanks @jbbottoms.
4-round convergence: source+profile marker tagging, clear-on-switch incl legacy/untagged,
stale-poll rejection, default<->renamed-root alias equivalence (no over/under-clear).
Codex SAFE round 4 (58 tests), focused suite 13/13.
2026-07-12 19:27:05 +00:00
nesquena-hermes 6abb6a4327 Merge branch 'rebase-5975' into stage/5975-cron-scope 2026-07-12 19:26:11 +00:00
nesquena-hermes 12a68ba27d test(#5979): stub grok-4.5 into x-ai catalog instead of asserting live presence
CI's hermes-agent catalog version may not contain grok-4.5, so asserting the
live _PROVIDER_MODELS['x-ai'] already lists it failed on CI (shard 4, all py
versions). Reproduce the data-driven first-party trigger deterministically by
stubbing grok-4.5 into the x-ai catalog for the test (restored in finally),
regardless of the agent version CI runs against. The other #5979 tests preserve
x-ai/grok-4.5 via the advertised-full / cold-unknown-prefix paths, which don't
depend on x-ai being a known first-party prefix.
2026-07-12 05:41:08 +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
nesquena-hermes 764a842785 Release exp: ROADMAP.md accuracy refresh (#5709) 2026-07-11 08:16:22 +00:00
nesquena-hermes 238d1b9aed Merge #5709: full accuracy refresh of ROADMAP.md 2026-07-11 08:15:35 +00:00
nesquena-hermes 43f15d963f Release exp: context-selection counter reset (#5929). Thanks @akay64. 2026-07-11 07:53:44 +00:00
nesquena-hermes 28d81ab40c Merge #5929: reset context-selection counter on clear/session-switch 2026-07-11 07:53:18 +00:00
nesquena-hermesandakay64 e0dea16940 test: cover context-selection counter reset on clear/session-switch (#5929)
Adds regression coverage for the akay64 fix: _selectionIdCounter must reset to 0
when the last chip is removed (_removeNamedContextBlock) and on
_clearPendingSelections, so context chips restart at 'Context 1' after a
clear-all or session switch instead of continuing at 'Context N+1'.

Co-authored-by: akay64 <akay64@users.noreply.github.com>
2026-07-11 07:47:25 +00:00
nesquena-hermes e10f2b1eea Release exp: redaction-awareness prompt guidance (#5927). Thanks @mvanhorn. 2026-07-11 07:24:10 +00:00
nesquena-hermes adbe272cc9 Merge #5927: teach ephemeral prompt that redacted credentials are intentional masking 2026-07-11 07:23:43 +00:00
nesquena-hermes 1ab1e8d3e1 Release exp: stale sidebar unread-dot on visit (#5917). Thanks @neaucode-bot. 2026-07-11 06:21:30 +00:00
nesquena-hermes 45fe6b4f10 Merge #5917: clear stale sidebar unread dot on session visit 2026-07-11 06:20:50 +00:00
nesquena-hermes 90e8f368fd test: widen fixed-window assertions after busy-reset reorder
The #5917 reorder (idle busy/stream reset moved before the visit-ack) shifted
code positions, so two pre-existing fixed-offset source-assertion tests
truncated: extend gateway_sse_reconnect_dedupe's loadSession _block end-anchor to
_mergePendingSessionMessage, and widen stale_stream_cleanup's 900->1400 char
window past the expanded comment. Behavior unchanged.
2026-07-11 06:06:04 +00:00
nesquena-hermes cb94cecc1a fix(sessions): reset busy/stream flags before the visit-ack repaint (gate)
Gate finding: the idle-session S.busy/S.activeStreamId reset ran AFTER
_acknowledgeSessionVisit(), so the ack's sidebar repaint could inherit the
PREVIOUS session's busy state and manufacture a phantom unread/streaming
indicator on the freshly-loaded idle session. Moved the activeStreamId calc +
idle reset BEFORE the acknowledge call. Co-authored-by: neaucode-bot.
2026-07-11 05:52:56 +00:00
nesquena-hermesandneaucode-bot 05f6e8c7f4 fix(sessions): guard viewed-count clear in message-load + derive stream state from target session (gate findings)
Co-authored-by: neaucode-bot <neaucode-bot@users.noreply.github.com>
2026-07-11 05:39:19 +00:00
nesquena-hermes 6622ab1676 Release exp: batch salvage-reliability #5909 + #5912 + #5915
Thanks @alexfoxtm, @harryazj, @brianmmaina.
2026-07-11 05:32:30 +00:00
nesquena-hermes b33c008de2 Merge salvage/5294-mobile-dictation 2026-07-11 05:27:11 +00:00
nesquena-hermes 42fcdb0090 Merge salvage/4750-composer-clear 2026-07-11 05:27:11 +00: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 63d3efb6c4 fix(sessions): guard post-load unread re-ack on active view (gate finding)
Gate found the unconditional post-load _acknowledgeSessionVisit() cleared a
hidden-tab completion's unread marker: a completion landing while
_ensureMessagesLoaded() is in flight AND the tab then goes hidden is correctly
marked unread, but the final re-ack wiped it. Gate the re-ack on
_isSessionActivelyViewedForList(sid) so only an actively-viewed session clears.
Regression test added. Co-authored-by: neaucode-bot.
2026-07-11 05:15:01 +00:00
nesquena-hermes 4d3b66976c Release exp: max reasoning level, availability-gated (#5910). Thanks @perejaslav. 2026-07-11 04:48:26 +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 5d2740cf56 fix(send): clearPending:false + preserve file objects (gate round 2)
Re-gate found 2 issues in the prior fix: (1) [CORE] uploadPendingFiles still
defaulted clearPending=true and re-cleared S.pendingFiles after the await,
wiping any attachment staged during the upload window — pass clearPending:false
since we already detached the snapshot. (2) [SILENT] converting File objects to
name strings broke draft-restore signature matching ({value:name} vs canonical
{name,path,size,type}) — preserve the file objects with [..._submittedFiles].
2026-07-11 04:13:53 +00:00
nesquena-hermes 1e5f86f190 Release exp: opt-in start-new-chat-on-workspace-switch (#5473). Thanks @ruizanthony. 2026-07-11 04:11:16 +00:00