Commit Graph
7305 Commits
Author SHA1 Message Date
Ruby Hartono f445c3ae39 fix(routes): gate state.db row backstop on absence of truncation_boundary prefix
Reviewer regression (reproduced): _STATE_DB_DISPLAY_ROW_BACKSTOP (newest-50k SQL
cap) was applied UNCONDITIONALLY to the GET /api/session display path's state.db
read. But merge_session_messages_append_only needs the rows at/around the
session's truncation_boundary (the preserved prefix) to reconcile correctly. A
newest-N-only cap drops those boundary rows for a >N-row session and corrupts
the merge (silently losing the preserved prefix), and also prevents older-page
(msg_before) requests from ever reaching them. So a pathological-but-real >50k-
row compressed session got a corrupted merge / unreachable older history rather
than a clean bound.

Fix: gate the backstop on the SAME conditions
_state_db_since_timestamp_for_limited_display uses to decide a read is
boundary-free — not msg_before paging, and no truncation_watermark /
truncation_boundary. Those cases stay on the full (uncapped) read; the backstop
is now a defensive net for the common uncompressed initial-tail path only.
Extracted into _state_db_backstop_limit_for_display for direct test coverage.

The get_state_db_session_messages(limit=) signature + newest-rows subquery are
unchanged — only the display-path APPLICATION of the cap is gated.

Test: tests/test_state_db_backstop_boundary_gate.py — backstop applies with no
boundary prefix; SKIPPED when truncation_boundary / truncation_watermark set or
msg_before paging; empty-string markers treated as unset (matching the existing
helper). Existing backstop + reconciliation suites (53 tests) unchanged.

Model Used: builtin:zai-coding-plan/GLM-5.2 (ZCode agent).
2026-07-17 07:19:47 +07:00
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
Ruby Hartono dec041997f fix(models): add defensive row backstop to display-path state.db message read
Memory: get_state_db_session_messages had no SQL LIMIT — it full-scanned the
state.db messages table and fetchall()'d into a Python list. The display path
(GET /api/session) already has a careful since_timestamp optimization that
bounds the common uncompressed tail load, but it deliberately bails (returns
None) on compressed sessions (truncation_watermark/boundary) and msg_before
paging — in those cases the read used to full-scan with no cap. A
pathological/huge state.db could then materialize unbounded rows into memory.

Add an opt-in `limit` parameter to get_state_db_session_messages (applied as a
SQL LIMIT that keeps the NEWEST rows via a DESC subquery re-sorted ASC). It is
a BACKSTOP, not a semantic window: the display window counts visible rows
post-reconciliation, so a true window LIMIT here would corrupt the
sidecar/state.db merge (see _state_db_since_timestamp_for_limited_display,
which deliberately does NOT SQL-LIMIT raw rows for exactly that reason).

Thread a generous _STATE_DB_DISPLAY_ROW_BACKSTOP (50000) from the GET /api/session
display caller only. The full-history model-context callers (reconciliation at
models.py:3437, context reconstruction at :8576, new-turn context at
streaming.py:8443) do NOT pass a limit — their semantics require the full
history, unchanged.

Test: tests/test_state_db_read_backstop.py — no-limit returns full history
oldest-first (model-context contract unchanged); limit=N keeps the newest N
re-sorted oldest-first; limit > row count returns all; limit<=0 clamps to 1
(newest); invalid limit falls back to unbounded; limit composes with
since_timestamp. Existing state.db active-filter + reconciliation suites (54
tests) unchanged.

Contract Routing
- Task type: memory-bound fix (unbounded state.db row materialization on the
  display path's bail cases).
- Touched areas: state.db message reader (api/models.py) + GET /api/session
  display caller (api/routes.py). NOT the model-context callers.
- Relevant public docs: AGENTS.md, CONTRIBUTING.md, docs/CONTRACTS.md,
  docs/rfcs/webui-run-state-consistency-contract.md (Model context layer).
- State layer mutated: none (read-only). The model-context read path is
  UNCHANGED (no limit passed) — Invariant 1 (visible current turns enter model
  context) and the reconciliation contract are preserved. Only the display path
  gets the defensive cap, which does not affect what the agent receives.
- Scope boundaries: this is narrower than a naive "add LIMIT everywhere" —
  _state_db_since_timestamp_for_limited_display's docstring explains why a
  semantic window LIMIT would corrupt the merge; this cap is purely a
  pathological-input safety net above any legitimate session size.

Release note: GET /api/session's state.db read now has a defensive row backstop,
capping memory when the bounded since_timestamp optimization bails on
compressed/paged sessions.

Model Used: builtin:zai-coding-plan/GLM-5.2 (ZCode agent).
2026-07-16 23:40:26 +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
c39e8edaf1 fix(#6100): clear stale gateway approval attention badge (#6117) (#6125)
Reconcile the gateway pending-mirror in _session_attention_summary()
before counting sidebar approval attention, so a parent session's red
approval dot clears once the underlying gateway approval has drained.
Live approvals stay visible; clarify fallback unchanged. WebUI read-path
mitigation; child-session context-propagation root cause stays in #6100.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-15 22:34:19 -07:00
nesquena-hermes 6e60ae5e98 Merge pull request #6119 from nesquena/stage/6013
Release exp: cache GET /api/providers per-profile with short TTL (#6013)
2026-07-15 19:50:58 -07: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 1845e63931 Merge pull request #6116 from nesquena/fix/6105-reparent
Release exp: reparent mobile model picker to body so it never opens off-screen (#6105/#6080)
2026-07-15 18:31:31 -07: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 7a862c9f6d Merge pull request #6115 from nesquena/stage/css2-batch
Release exp: narrow-screen CSS fixes — popup min-width (#6104) + timestamp foldable layout (#6106)
2026-07-15 17:23:14 -07: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 a155a616c7 Merge pull request #6111 from nesquena/stage/5913-pinchzoom
Release exp: pinch-to-zoom for Mermaid diagrams on touch devices (#5913)
2026-07-15 16:06:58 -07: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
silent-reader-cn 0403f00789 fix: address Greptile review and fix CI test mock for touch handlers
Three fixes in one commit:

1. **CI test mock** — Added addEventListener/removeEventListener to the
   Node.js makeElement mock so viewport.addEventListener() calls in the
   touch handler registration don't crash with 'not a function'.

2. **Two-finger pan during pinch** — Replaced _setScale() call with
   direct transform math. The new formula computes scale and position
   simultaneously:
     ratio = boundedScale / pinchStartScale
     x = currMidX - (startMidX - startPosX) * ratio
   This keeps the diagram under both fingers regardless of midpoint
   drift, enabling simultaneous zoom + pan.

3. **Post-pinch click suppression** — _onTouchEnd now sets dragged=true
   when exiting pinch mode so any synthesized click event is caught
   by the existing dragged guard in _openViewerOnClick instead of
   opening the lightbox.
2026-07-15 23:02:13 +00:00
silent-reader-cn 7419b1bb51 feat: add pinch-to-zoom support for Mermaid diagrams on touch devices
Adds touch event handlers (touchstart/touchmove/touchend/touchcancel) to
the Mermaid viewer viewport for two-finger pinch zoom, using the existing
_setScale() function with midpoint anchoring.

- Guards pointer handlers (down/move/click) during active pinch to prevent
  interference between touch and pointer event systems
- Uses {passive: false} on touchstart/touchmove to allow preventDefault()
- Falls back gracefully on non-touch devices — touch listeners are inert
  without touch hardware

Fixes: Mermaid diagrams in Hermes WebUI were not zoomable via multi-touch
pinch on mobile/tablet devices, frustrating users with complex diagrams.
2026-07-15 23:02:13 +00:00
nesquena-hermes a486c2f583 Merge pull request #6110 from nesquena/stage/6102-cachettl
Release exp: hold streaming CLI-cache TTL above sidebar poll cadence (#6102)
2026-07-15 15:55:24 -07: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
webtecnica 9ac68feecb fix: stream event timestamps layout on narrow foldable screens (#6099) 2026-07-15 18:31:51 -03:00
webtecnica 2143563109 fix: ensure approval/clarify popups have min-width on narrow screens (#6094) 2026-07-15 18:30:54 -03:00
nesquena-hermes 0f0f733b83 Merge pull request #6103 from nesquena/stage/6056-delegtitles
Release exp: delegated subagent sidebar titles (#6056) + sidebar title-redaction hardening
2026-07-15 14:20:17 -07: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
starship-s f5ef3688c6 fix(cache): keep streaming TTLs above poll cadence 2026-07-15 20:59:59 +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 93a9bdcc3f Merge pull request #6101 from nesquena/stage/6062-readrange
Release exp: show requested line range in collapsed read_file tool labels (#6062)
2026-07-15 13:41:13 -07: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
Frank Song 59c8f988ef fix(ui): reject invalid read range limits 2026-07-15 20:37:02 +00:00
Frank Song 5b46c54c92 feat(ui): show read ranges in collapsed tool labels 2026-07-15 20:37:02 +00:00
nesquena-hermes 3c9304aef5 Merge pull request #6098 from nesquena/stage/6088-manualupd
Release exp: allow manual update check when auto-update is disabled (#6088)
2026-07-15 13:20:26 -07: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 ad88366baa Merge pull request #6097 from nesquena/stage/6081-noresp
Release exp: suppress false no-response after tool-limit turn with a final answer (#6081)
2026-07-15 12:43:22 -07: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
chsong 139c76535b fix tool-limit final answer classification 2026-07-15 19:38:25 +00:00
nesquena-hermes 57e8aafb16 Merge pull request #6096 from nesquena/stage/6083-brick
Release exp: fix new-conversations-unstartable brick + eviction leak bound (#6083)
2026-07-15 12:21:19 -07: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
Sean 2a191007f1 Merge branch 'master' into fix/6010-cache-providers 2026-07-16 00:54:24 +08:00
webtecnica 40d5e17006 fix: allow manual force-check even when auto-update is disabled (#6082) 2026-07-15 12:18:04 -03:00