Commit Graph
6502 Commits
Author SHA1 Message Date
allenliang2022 ad1843f31b fix(webui): recover virtual-scroll compensation when the anchor row is recycled
_compensateScrollForMeasurementDelta re-found its viewport anchor only by
rawIdx and bailed with a bare 'if(!row) return' when that row was recycled
out of the render window. On long sessions containing multi-thousand-px
turns, a large scroll delta recycles the anchor row, so the estimated->measured
topPad swap (a scrollHeight change of tens of thousands of px) hit scrollTop
uncompensated and threw the viewport to the top -- a mobile scroll jump-back
that did not reproduce on desktop (where the anchor row stays rendered).

Fall back when the rawIdx row is gone: (1) re-find the same row by its stable
data-session-msg-idx and compensate by its measured offset delta; (2) if that
row is also unrendered, compensate by the top-spacer (topPad) height delta
captured before the re-render. _captureMessageViewportAnchor now records
topPadBefore so the fallback has a reference.

Adds tests/test_issue4346_vscroll_recycled_anchor_jumpback.py (Node-harness
behavioral tests). All fail on the pre-fix bare-return and pass on the fix;
existing vscroll suites stay green.

Relates to #4346.
2026-07-05 19:53:47 +08:00
nesquena-hermes 8dc47b83c0 Merge pull request #5629 from nesquena/release/stage-5626
release #5626: ignore inactive compacted state-db rows in context replay
2026-07-05 02:18:28 -07:00
nesquena-hermes 8230785097 release #5626: exclude inactive compacted state-db rows from context replay + CHANGELOG 2026-07-05 09:14:40 +00:00
nesquena-hermes 1165bd9ca2 fix(#5626): ignore inactive compacted state-db rows in context replay
get_state_db_session_messages() returned archived (active=0) rows that in-place
compaction had marked inactive. WebUI reconciliation feeds this reader straight
into the next model-facing context (reconciled_state_db_messages_for_session,
prefer_context=True), so those archived rows got pulled back in — resurrecting
pre-compaction history and making every later turn re-trigger compression. Now,
WHEN the messages table exposes an `active` column, inactive rows are excluded by
default (`AND (active IS NULL OR active != 0)` — NULL/legacy rows preserved), with
an `include_inactive=True` escape hatch for explicit recovery/audit callers.
Schemas without the column are unaffected (guarded by `'active' in available`).

Contributor stage; ungated on arrival. Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-07-05 08:59:21 +00:00
nesquena-hermes 35464be634 Merge pull request #5627 from nesquena/fix/5588-auth-cache-isolation
fix(#5588): reset memoized password-hash cache around every test (isolation flake)
2026-07-05 01:53:12 -07:00
nesquena-hermes 3051dc4dda release #5588: reset memoized password-hash cache around every test (isolation flake) + CHANGELOG 2026-07-05 08:44:41 +00:00
nesquena-hermes 4c9690bf59 fix(#5588): reset memoized password-hash cache around every test (isolation flake)
tests/test_issue803.py::TestProfileCookieHelpers failed when run after
tests/test_session_static_assets.py (passed in isolation). Root cause:
api.auth.get_password_hash() memoizes the resolved hash process-wide
(_AUTH_HASH_CACHE / _AUTH_HASH_COMPUTED) and is NOT keyed on the
HERMES_WEBUI_PASSWORD env var. test_session_static_auth_exemption sets that env
var + populates the cache; monkeypatch pops the env on teardown but the cache
stays populated, so is_auth_enabled() reads stale True and build_profile_cookie
raises "requires a request handler when auth is enabled".

Fix: an autouse conftest fixture (_reset_password_hash_cache) that invalidates the
cache before AND after every test — matching the existing _isolate_hermes_config_path
/ _restore_profile_home_globals autouse-reset convention. Fixes the whole class
regardless of which test sets the password; no-op when auth is off. Adds a
direct regression test (cache a password, pop env, assert auth reads disabled +
build_profile_cookie doesn't raise). Paired-run repro now passes.
2026-07-05 08:35:14 +00:00
nesquena-hermes c922f179f7 Merge pull request #5625 from nesquena/release/stage-5582
release #5582: keep fork children out of compression stitch
2026-07-05 01:20:03 -07:00
nesquena-hermes 4f0b6576b5 release #5582: keep fork children out of compression stitch + CHANGELOG 2026-07-05 08:16:30 +00:00
nesquena-hermes dbc2345fea fix(#5571): keep fork children out of compression stitch
_webui_sidecar_lineage_messages_for_display stitches a session's pre-compression
snapshot parents for full-transcript display. A cleared fork child could resurrect
its ORIGINAL (non-fork) parent's transcript when that parent was later compressed.
Now, when the root session is itself a fork (session_source=='fork'), the stitch
walk allows fork-sourced snapshot parents but stops at the first non-fork
ancestor — so a compressed fork continuation still shows its own pre-compression
turns, while an ordinary fork child stays isolated from the original parent.
Additive guard (one extra break condition); non-fork lineage display unchanged.

Contributor stage; gate-pass. Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-05 08:07:38 +00:00
nesquena-hermes f1f4514799 Merge pull request #5623 from nesquena/release/stage-5459
release #5459: include attachments in steer submissions
2026-07-05 00:24:21 -07:00
nesquena-hermes 1e4d237cc2 release #5459: include attachments in steer submissions (+ retry-dedup + identity chip clear) + CHANGELOG 2026-07-05 07:20:24 +00:00
nesquena-hermes c78d5083ad fix(#5459): dedup steer upload on retry + remove only delivered files by identity (gate)
Codex gate found two SILENT defects, fixed:
1) Failed-steer RETRY re-uploaded the same File objects (uploadPendingFiles ran
   again with clearPending:false). Added _steerUploadCache keyed by ownerSid +
   file signature so a retry with the same staged set reuses the uploaded paths;
   invalidated when the staged set changes (signature miss) or on accepted steer.
2) Accepted steer cleared ALL S.pendingFiles, dropping files staged during the
   upload/API await. Now removes ONLY the delivered snapshot by object identity,
   preserving newly staged files.

(The owner-session draft is still cleared on accepted steer — clearing the
delivered session's draft is correct even after a mid-upload switch; the existing
test asserts this.) Tests updated for the identity-based removal + new
cache/dedup guard. 69 steer/composer tests pass, node --check clean.
2026-07-05 07:10:23 +00:00
nesquena-hermes ceea2438eb fix: include attachments in steer submissions
Steering (submitting mid-stream) dropped any pending composer attachments. Now the
pending files are uploaded before the /steer payload is sent, the uploaded file
paths are appended to the steer text so the active agent can inspect them with
tools, file chips stay staged if the steer fails (so nothing is lost), and
file-only busy submissions are handled.

Contributor stage; gate-pass. Co-authored-by: ruizanthony <ruizanthony@users.noreply.github.com>
2026-07-05 06:50:34 +00:00
nesquena-hermes abdf6c7ae3 Merge pull request #5620 from nesquena/fix/mobile-sidebar-header-icon-align
fix(ui): align mobile sidebar close/new-conversation header icons
2026-07-04 23:49:05 -07:00
nesquena-hermes 7ad9707c4b fix(ui): align mobile sidebar close (X) with the new-conversation (+) button
On the mobile sidebar header the close (X) button rendered an 18px glyph — the
only 18px .panel-head-btn icon — sitting ~6px lower than the adjacent 14px
new-conversation (+) button, an inconsistent pair. Match the X glyph to 14px and
vertically center it on the ~41px panel-head row so both icons share one size and
baseline (top:calc(safe-top - 2px) keeps the existing safe-area offset so the X
still clears the notch, with no extra drawer spacing). 44x44 tap target kept for
mobile touch; mobile @media(max-width:640px) only, and the X is display:none on
desktop, so desktop is unaffected. + CHANGELOG + source-guard tests.
2026-07-05 06:40:26 +00:00
nesquena-hermes 58e857ec95 Merge pull request #5617 from nesquena/release/stage-5602
release #5602: iOS PWA session-switch instant sidebar close + race guard
2026-07-04 22:46:39 -07:00
nesquena-hermes 9d85c23c5f release #5602: iOS PWA session-switch instant sidebar close + race guard + CHANGELOG 2026-07-05 05:43:45 +00:00
nesquena-hermes 4559ca85f0 fix(#5409): iOS PWA session-switch — close sidebar instantly + prevent _loadingSessionId race
Two iOS-PWA bugs when tapping an old/large session:
1) closeMobileSidebar() ran AFTER the slow `await _openSidebarSession()` (3-15s
   for large sessions on WKWebView), so the sidebar stayed open with only a tiny
   spinner. Moved closeMobileSidebar() to run synchronously BEFORE the await in
   the tap handler + lineage/child open paths for instant feedback.
2) _loadingSessionId race: an SSE-triggered idle reload
   (_scheduleActiveSessionIdleReload / refreshActiveSessionIfExternallyUpdated,
   fed by idle-reconcile/poll/visibility/focus) could overwrite _loadingSessionId
   and silently cancel an in-flight session switch. Both paths now skip while a
   different session's loadSession() is in flight.

Contributor stage; gate-pass. Co-authored-by: luperrypf <luperrypf@users.noreply.github.com>
2026-07-05 05:42:51 +00:00
nesquena-hermes b0340a7b3e Merge pull request #5616 from nesquena/release/stage-5611
release #5611: opencode-go uses curated static model list (no probe 404s)
2026-07-04 22:18:12 -07:00
nesquena-hermes f864c276ff release #5611: opencode-go uses curated static model list (no probe 404s) + CHANGELOG 2026-07-05 05:14:44 +00:00
nesquena-hermes 14ad45bd1f fix(#5311): skip live /v1/models probe for opencode-go — use curated static list
OpenCode Go's /v1/models endpoint returns models from the full public catalog that
are NOT enabled on the Go tier; selecting any such probe-only model 404s
('model not found') when the chat request is sent. get_available_models now skips
the live probe for opencode-go (bare branch) so the next `if not raw_models` falls
through to the curated static _PROVIDER_MODELS['opencode-go'] list — the actual
Go-tier models. Narrow, provider-scoped; no other provider affected.

Contributor stage; gate-pass. Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
2026-07-05 05:05:30 +00:00
nesquena-hermes 0bbf7c3d37 Merge pull request #5615 from nesquena/release/stage-5505
release #5505: improve session-list retry feedback
2026-07-04 22:01:28 -07:00
nesquena-hermes fbd3c8c26b release #5505: session-list retry feedback + a11y + stuck-retry guard + CHANGELOG 2026-07-05 04:58:04 +00:00
nesquena-hermesandrodboev f6cc48b834 fix(#5501): clear pending retry state on render invalidation (Codex gate)
Codex re-gate SILENT: a retry whose fetch is invalidated mid-flight (e.g. a
profile switch) left the button stuck as an inert 'Retrying…' with no request in
flight — the stale catch returns before _showSessionListLoadError and the
.finally() bails when the skeleton removed the old button. _invalidateSessionListRenders()
now clears the pending retry markers (retrying, _retryFailedFocus) so the next
repaint shows an actionable idle Retry. Regression test added (fails without the
fix); test harness extracts _invalidateSessionListRenders + declares its render-gen
globals.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-05 04:48:50 +00:00
nesquena-hermesandrodboev 9be4048c72 fix(#5501): fold in Fable a11y hardening — live region, aria-disabled, focus restore, ellipsis
Fable UX gate SHIP-WITH-UX-FIXES (non-blocking). Applied all: (1) role=status +
aria-live=polite on the error note so retry failure/restore is announced to
screen readers; (2) pending state uses aria-disabled (not the disabled property)
so the button retains keyboard focus, with click/keydown guards keeping it inert;
(3) on a failed retry, the fresh rebuilt Retry button reclaims keyboard focus
(_retryFailedFocus flag, rAF-guarded) so keyboard users aren't dropped to <body>;
(4) label uses a true ellipsis 'Retrying…' matching the app's other progress
string. CSS styles [aria-disabled=true] alongside [aria-busy]. Tests updated +
new a11y assertions; 8/8 pass, node --check clean.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-05 04:39:14 +00:00
nesquena-hermes 7349a9434f fix(#5501): improve session-list retry feedback
The sidebar's load-error Retry button behaved like a raw idle browser button while
the next slow fetch was pending, so it looked broken/unresponsive. Retry now paints
an immediate pending state (label 'Retrying...', disabled, aria-busy), keeps the
retry state local to the error block via _renderSessionListLoadErrorNote(), and
lets the existing success/failure paths replace the error object when the request
settles (restoring 'Retry' on failure). Also adds proper styling.

Contributor stage; gate-pass. Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-05 04:39:14 +00:00
nesquena-hermes bbcd6d19cd Merge pull request #5613 from nesquena/release/stage-5539
release #5539: increase approval card height cap on mobile
2026-07-04 21:25:57 -07:00
nesquena-hermes 2346f4b7f8 release #5539: raise mobile approval-card height cap + CHANGELOG 2026-07-05 04:18:09 +00:00
nesquena-hermes 39a4d67e5c fix(#5385): increase approval card height cap on mobile
The mobile command/tool approval popup rendered too small — options didn't all fit
and required scrolling within the cramped popup to select (Android WebUI wrapper).
Raises the mobile @supports(dvh) .approval-inner max-height cap from
min(52dvh,360px) to min(60dvh,420px) so the options fit without in-popup scrolling;
still overflow-y:auto so very tall content scrolls rather than clipping. Scoped to
the mobile dvh rule; desktop unaffected. Adds a source-level guard test.

Contributor stage; gate-pass. Co-authored-by: nankingjing <nankingjing@users.noreply.github.com>
2026-07-05 04:04:02 +00:00
nesquena-hermes 035e84ae00 Merge pull request #5612 from nesquena/release/stage-5534
release #5534: CORS preflight echoes only same-origin or allowlisted origins
2026-07-04 19:48:27 -07:00
nesquena-hermes c21a7629f4 release #5534: CORS preflight echoes only same-origin/allowlisted origins + CHANGELOG 2026-07-05 02:45:07 +00:00
nesquena-hermes 8da96a84d3 refactor(#5534): move CORS preflight header logic into api/routes (keep server.py thin)
The initial fix added ~16 lines to server.py, tripping test_server_py_under_750_lines
(server.py must stay a thin dispatcher; logic belongs in api/). Moved the whole
preflight-response into apply_cors_preflight_headers(handler) in api/routes.py;
server.py's do_OPTIONS is now a 4-line dispatcher. server.py 764 -> 746 lines.
Same security behavior (same-origin/allowlist echo, Vary: Origin, no wildcard,
header-less denial); test rewritten to the header-emitting API + added Vary and
no-header-on-denial assertions and a routes.py wildcard regression guard.
2026-07-05 02:35:10 +00:00
nesquena-hermes 4e8978f04c fix(security): CORS preflight echoes only same-origin or allowlisted origins
do_OPTIONS answered every CORS preflight with Access-Control-Allow-Origin: *,
advertising broader cross-origin access than a real request is granted — on a
password-less deployment that lets any site read authenticated responses. It now
echoes the request Origin only when _check_same_origin_browser_request approves
it (the exact same-origin / HERMES_WEBUI_ALLOWED_ORIGINS policy the CSRF gate
enforces for real requests), adds Vary: Origin, and never emits *. A disallowed
origin gets a 200 with no CORS headers (browser treats as preflight denial).
Default deployments (no allowlist) are unaffected.

Contributor stage; gate-pass. Co-authored-by: mo7al876any <mo7al876any@users.noreply.github.com>
2026-07-05 02:18:51 +00:00
nesquena-hermes 49a9560c45 Merge pull request #5610 from nesquena/release/stage-5584
release #5584: skip clear-sentinel backups during recovery
2026-07-04 19:15:06 -07:00
nesquena-hermes d1f18ac47c release #5584: skip clear-sentinel backups during recovery + CHANGELOG 2026-07-05 02:11:33 +00:00
nesquena-hermes bb818a56ba fix(#5570): also suppress restore when post-clear messages exist (gate fix)
Codex gate CORE: the exact-empty sentinel only suppressed restore for a bit-empty
cleared sidecar. If the user cleared, then sent ONE post-clear message, and the
stale pre-clear .json.bak still existed (unlink failed), recovery saw
bak_count>live_count and RESTORED the pre-clear transcript on top of the new
message. Added _live_supersedes_backup_by_clear_generation: suppress restore when
the live sidecar carries a clear_generation the backup lacks AND has a non-empty
transcript AND still shows the clear boundary reset (watermark==boundary==0.0).
Scoped to non-empty live so empty clear-shaped sidecars keep their existing
recovery semantics (active/pending/partial variants still restore). Fails open on
unreadable/partial reads. +2 regression tests (post-clear-message not restored;
post-clear-after-real-compaction still restores).
2026-07-05 02:10:30 +00:00
nesquena-hermes 8a6937978c fix(#5570): skip clear sentinel backups during recovery
Closes the crash-window resurrection corner in the #5532 clear-conversation
data-loss fix. /api/session/clear writes a .json.bak before truncating, and
because the cleared sidecar's truncation_watermark is 0.0, startup recovery saw
bak_count > live_count and could restore the backup — resurrecting the cleared
transcript after a crash/restart in the clear window.

Fix: stamp a unique per-clear `clear_generation` UUID on the sidecar when
clearing a session that had messages (persisted via Session model). Recovery's
_session_records_clear_sentinel() returns no_action when the live sidecar carries
a clear_generation the backup doesn't share AND the live sidecar matches the
exact cleared-empty shape (messages/context empty, watermark/boundary 0.0,
pending fields cleared). Same-generation backups stay recoverable; unreadable or
partial matches FAIL OPEN (normal recovery), so a real crash-loss is never
suppressed.

Contributor stage; gate-pass. Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-05 02:10:30 +00:00
nesquena-hermes 03b64ebeb1 Merge pull request #5609 from nesquena/release/stage-5573
release #5573: guard viewport anchor key against lone surrogates
2026-07-04 19:09:34 -07:00
nesquena-hermes 20ca99bc77 release #5573: guard viewport anchor key against lone surrogates + CHANGELOG 2026-07-05 02:06:09 +00:00
nesquena-hermes ef33ad44f2 fix(#5552): guard _messageViewportAnchorKeyForMessage against lone surrogates
A message whose text contained a lone UTF-16 surrogate (e.g. a truncated emoji
from a partial paste/stream) made encodeURIComponent throw URIError inside
_messageViewportAnchorKeyForMessage, crashing the viewport-anchor computation and
breaking scroll-anchor restore for the whole transcript. New _safeEncodeURIComponent
catches the URIError and rebuilds the string via a UTF-16 code-unit walk that keeps
valid high+low surrogate PAIRS intact (emoji survive) and drops only LONE
surrogates. Uses no regex lookbehind/lookahead so it parses on every browser
engine (older WebViews / Safari <16.4 lack lookbehind and would brick ui.js at
parse time).

Contributor stage; gate-pass. Co-authored-by: rumotoshino <rumotoshino@users.noreply.github.com>
2026-07-05 01:49:08 +00:00
nesquena-hermes daf5cc1bb0 Merge pull request #5607 from nesquena/release/stage-5593
release #5593: keep active CLI sessions visible before final output
2026-07-04 18:33:40 -07:00
nesquena-hermes b2446a54ec release #5593: keep active CLI sessions visible before final output + CHANGELOG 2026-07-05 01:26:23 +00:00
nesquena-hermes b9e8ade4b6 fix(#5587): keep active CLI sessions visible before final output
Self-built stage of contributor PR; gate-pass.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-05 01:25:50 +00:00
nesquena-hermes 73f77cb42a Merge pull request #5606 from nesquena/release/stage-5592
release #5592: preserve completed assistant replies during settle
2026-07-04 18:19:31 -07:00
nesquena-hermes 2694ba9e03 release #5592: preserve completed assistant replies during settle + CHANGELOG 2026-07-05 01:15:43 +00:00
nesquena-hermes 73d4b2b227 fix(#5575): preserve completed assistant replies during settle
The streaming settlement path could show a misleading "No response from provider"
error card AFTER a completed answer, because a stale terminal/partial result flag
still forced the generic no_response error path even when the merged current turn
already had a final assistant answer. Now the generic no_response terminal error
is suppressed ONLY when the already-merged current turn has a completed assistant
reply (scoped to soft `partial` results per the issue-thread recommendation),
letting normal `done` settlement continue. Real silent failures, partial
failures, auth/quota errors, cancellation, replayed rows, and terminal
tool/compression cases all keep their existing error behavior.

Self-built stage of contributor PR; gate-pass. Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-05 01:15:09 +00:00
nesquena-hermes 5eb081d30e Merge pull request #5603 from nesquena/release/stage-5565
release #5565: distinct Mermaid fullscreen toolbar icon
2026-07-04 18:10:58 -07:00
nesquena-hermes a646a23e5b release #5565: distinct Mermaid fullscreen toolbar icon + CHANGELOG 2026-07-05 01:07:35 +00:00
nesquena-hermes 7ee9f78a2e fix(#5525): distinct Mermaid fullscreen toolbar icon
The Mermaid toolbar's "fit" and "fullscreen" icons rendered byte-identical (the
fullscreen path was fit's corner-brackets drawn twice). Replaced the fullscreen
glyph with a distinct corner-frame + diagonal-expand-arrows icon so the two
actions are visually distinguishable at 14px. Still wired to openLightbox; no
dependency on the old path. (The other half of #5525 — mermaid height:0 on
mobile — does not reproduce on current master; verified live at 390x844.)

Self-built (nesquena-hermes); nesquena independent review APPROVED.
2026-07-05 00:53:42 +00:00