Commit Graph

6469 Commits

Author SHA1 Message Date
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
v0.51.875
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
v0.51.874
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
v0.51.873
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
v0.51.872
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
v0.51.871
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
nesquena-hermes 1bbecb9f2c Merge pull request #5600 from nesquena/release/stage-5559
release #5559: classify credential-pool exhaustion with an actionable hint
v0.51.870
2026-07-04 17:39:14 -07:00
nesquena-hermes 7f869b8e39 release #5559: classify credential-pool exhaustion with an actionable hint + CHANGELOG 2026-07-05 00:35:59 +00:00
nesquena-hermes 93f447c7af fix(#3929 partial): classify credential-pool exhaustion with an actionable hint
'All 0 credential(s) exhausted for <provider>' did not match _is_quota_error_text
so it fell through to a generic 'Error' with no hint (the exception path's else
branch discarded the hint entirely). Adds a distinct credential_pool_empty
classification (ordered before the quota check) + an explicit exception-path
branch + an actionable pool-specific hint, so the user sees why the turn failed
and what to do. Does not over-match (doesn't swallow real quota errors) and
doesn't block partial-harvest; downstream tolerates the new type.

NOTE: this is the CLASSIFICATION half of #3929 only — the data-loss half
(reasoning/partial-work restore on reload-reconcile) is a scoped follow-up that
needs the reporter's server logs.

Self-built (nesquena-hermes); nesquena independent review APPROVED.
2026-07-05 00:35:26 +00:00
nesquena-hermes 94e5778feb Merge pull request #5599 from nesquena/release/stage-5579-r2
release #5578: stop login next= param nesting / URL explosion
v0.51.869
2026-07-04 17:27:27 -07:00
nesquena-hermes 60c748511f release #5578: stop login next= param nesting/URL-explosion + CHANGELOG 2026-07-05 00:23:06 +00:00
nesquena-hermes e09e819f08 fix(#5578): stop login next= param from nesting into itself and exploding the URL
An expired-auth 401 on the login page fed the login redirect its own address:
each of the three redirect guards (_safeNextPath in login.js,
_safe_login_redirect_path in routes.py, the api() 401 redirect in workspace.js,
_safe_login_inner_next in auth.py) validated against open-redirect but none
rejected a `next` pointing back at the login page, so every auth bounce wrapped +
re-encoded the previous login URL one level deeper — growing the URL exponentially
until the browser's length limit broke the tab. All guards now collapse/reject a
`next` whose decoded leading PATH resolves to the login route (bounded
percent-decoding up to 8 levels, fail-closed at the cap), while preserving the
existing open-redirect protections AND legitimate non-login paths that merely
carry their own `next=` query key. Length cap as belt-and-suspenders.

Self-built (nesquena-hermes); nesquena independent review APPROVED. Gate (Codex)
found + fixed an over-broad nested-next reject (regressed next-carrying paths) and
a deep-encode decode off-by-one; both root-fixed, login guards verified clean.
2026-07-05 00:12:19 +00:00
nesquena-hermes 1f68c8cfa5 Merge pull request #5596 from nesquena/release/stage-5577
release #5567: context-local HERMES_HOME override for background workers
v0.51.868
2026-07-04 16:55:18 -07:00
nesquena-hermes 3d11a6ad54 release #5567: context-local HERMES_HOME override for background workers + CHANGELOG 2026-07-04 23:50:54 +00:00
nesquena-hermes 019a4814b7 fix(#5567): pin worker profile via hermes-agent v0.18.0 context-local HERMES_HOME override
profile_env_for_background_worker mirrored the profile home into process-global
os.environ["HERMES_HOME"] and yielded the worker body OUTSIDE the setup lock, so a
concurrent cross-profile worker (e.g. background title generation on another
profile) could clobber os.environ mid-body — the agent config reader then resolved
the WRONG profile's config (intermittent turn-init failures citing an unused
provider; b3nw's v0.51.849 repro). Now installs hermes-agent v0.18.0's
context-local home override (set_hermes_home_override, a ContextVar that
get_hermes_home() consults before os.environ) for the resolved non-default profile
home — set before yield, reset in finally — so a config read in the worker body
resolves THIS profile's home from task-local state, immune to a concurrent
os.environ clobber. No worker serialization, no os.environ mutation, no detection
heuristic (both earlier WebUI-only mitigations were rejected as regression-unsafe).

Graceful degradation: the override module is resolved lazily+optionally
(_resolve_hermes_home_override, mirroring _resolve_secret_scope_module). On agents
< v0.18.0 the symbol is absent -> returns None -> pre-existing os.environ-mirror
behavior unchanged (no regression, no new failure mode).

Self-built (nesquena-hermes); nesquena independent review APPROVED.
2026-07-04 23:40:10 +00:00
nesquena-hermes 73643f21b6 Merge pull request #5595 from nesquena/release/stage-5564
release #5564: mint stable per-message ids for large-session fork alignment
v0.51.867
2026-07-04 16:28:22 -07:00
nesquena-hermes 9b293f660c release #5564: mint stable per-message ids for large-session fork alignment + CHANGELOG 2026-07-04 23:24:58 +00:00
nesquena-hermes 8e954a423d fix(#5564): mint stable ids BEFORE dedupe at all commit sites (stale-boundary path)
Codex round-3 gate finding: _dedupe_replayed_context_messages deep-copies the
stale-user repaired boundary row (streaming.py:4501) into the context array. When
the mint ran AFTER dedupe, that deep-copied context row was no longer the shared
result dict, so it stayed id-less while the display copy got the minted id —
re-opening the cross-array id divergence for the stale-repair path. Fix: move
_assign_stable_message_ids() to run immediately after _restore_reasoning_metadata
and BEFORE _dedupe_replayed_context_messages at all four commit sites (streaming
main / in-band self-heal / except-path self-heal + routes runs/MoA), so the shared
result rows are stamped before any deep-copy — both arrays inherit the id.
2026-07-04 23:15:41 +00:00
nesquena-hermes a8568749a1 fix(#5564): carry minted id onto eager-save checkpoint row in display merge
Codex/Opus gate finding: in eager session-save mode the current user turn is
checkpointed into s.messages before the agent runs (no id yet); after
_assign_stable_message_ids stamps the returned result row, the display merge
keeps the durable eager checkpoint and skips the stamped result — leaving the
display row id-less while its context twin carries the minted id, silently
defeating id-based fork/truncate alignment for eager-mode users. The merge now
copies the minted id onto the kept checkpoint when it lacks one (guarded so it
can't overwrite or duplicate). Opus verified cross-array id pairing holds across
the real 3-turn transform pipeline.
2026-07-04 23:06:21 +00:00
nesquena-hermes ee93425b26 fix(#5564): mint stable per-message ids so large-session forks align
WebUI keeps two parallel arrays per session: messages (display transcript) and
context_messages (what's sent to the model). In large/compacted sessions they
diverge. Forking / in-place truncation copies a prefix of both and relies on
truncate_context_for_display_keep() to translate a display keep-index into the
matching context index — an aligner that prefers a stable per-row `id`. But the
model-context rows carried neither id nor timestamp (a 989-session live scan
found 0 with any id), so alignment fell back to fragile content-signature
matching that goes ambiguous on repeated tool calls / empty assistant turns.

This closes the DATA gap (the alignment logic itself already prefers `id` and
handles the compacted case via the matcher, shipped in #5563): mint a monotonic,
session-unique integer `id` on the per-turn result rows AFTER the context restore
and BEFORE both arrays are built, so the display and model-context copies of a
logical row share the same id.

- api/streaming.py: new _assign_stable_message_ids(); _restore_reasoning_metadata
  carries `id` forward across turns (as it already does timestamp); wired into all
  three streaming commit sites (main turn, retry, self-heal).
- api/routes.py: same mint on the runs/MoA _handle_chat_sync commit path.
- api/gateway_chat.py: mints ids on the two new rows of a gateway turn.
- The `id` is stripped before the provider API call (not in _API_SAFE_MSG_KEYS),
  same treatment as timestamp — nothing new reaches the provider.

session_ops.py alignment was already delivered by #5563 (id-preferring matcher +
compacted-case fall-through), so no change is needed there now; tests updated to
assert the post-#5563 behavior (id-bearing = exact cut; id-less = errs toward
under-keeping, never the old raw-index mis-cut).

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-07-04 22:46:56 +00:00
nesquena-hermes b504fe3057 Merge pull request #5591 from nesquena/release/stage-5562
release #5562: refresh sidebar recency after missed session events
v0.51.866
2026-07-04 15:38:58 -07:00
nesquena-hermes 7ce5c83d69 release #5562: refresh sidebar recency after missed session events + CHANGELOG 2026-07-04 22:34:54 +00:00
nesquena-hermes b36bd789ce fix(#5551): refresh sidebar recency after missed session events
A reactivated older conversation wasn't bumped to the top / into Today until a
manual refresh when the viewing tab was backgrounded/unfocused: refreshSessionList
early-returns on document.hidden unless force=true, and the sidebar SSE closes on
blur — but the in-flight/pending coalescing only preserved the refresh REASON
string, dropping the opts (force/refreshActive). So a coalesced 'sessions_changed'
refresh lost its force flag and got skipped on a hidden tab. Now the pending
request preserves merged opts (force/refreshActive OR-merged), and a dedicated
resume-refresh path forces a catch-up when the sidebar regains focus, so
cross-device recency re-sorts land without a manual refresh.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-04 22:34:28 +00:00
nesquena-hermes 2cc2eb2b3c Merge pull request #5590 from nesquena/release/stage-5558
release #5558: keep Transparent Stream final replies visible after settle
v0.51.865
2026-07-04 15:23:21 -07:00
nesquena-hermes a7527bef13 release #5558: keep Transparent Stream final replies visible after settle + CHANGELOG 2026-07-04 22:19:25 +00:00
nesquena-hermes ae43091993 fix(#5550): keep Transparent Stream final replies visible after settle
The live-anchor scene renderer hardcoded mode:'compact_worklog' in
_renderAnchorLiveScene / _projectLiveAnchorActivityScene / the settle projection,
regardless of the user's actual active display mode. In Transparent Stream mode
the final reply was therefore projected with compact-worklog display hints, so on
turn-settle it folded into the collapsed worklog disclosure group (a refresh fixed
it because the stored transcript was correct). Now the renderer reads the real
active mode via _anchorSceneActiveMode() (window.chatActivityMode() ->
_chatActivityDisplayMode -> _transparentStream, default compact_worklog) and
applies per-mode display hints via _anchorSceneRowDisplayHintForMode(), so the
final answer stays a visible chronological reply in Transparent Stream.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-04 22:09:50 +00:00
nesquena-hermes f09d9bd446 Merge pull request #5586 from nesquena/release/stage-5541-r3
release #5541: iOS PWA streaming freeze + SSE reconnect reliability
v0.51.864
2026-07-04 15:06:02 -07:00
nesquena-hermes cebfdf41f9 fix(#5541): iOS PWA streaming freeze + SSE reconnect reliability
Two mobile reliability fixes on the crown-jewel chat streaming path:
(1) content-visibility:auto on off-screen .msg-row under @media(pointer:coarse)
so WKWebView skips layout/paint for off-screen rows during streaming (kills the
long-chat freeze). The LIVE turn is kept content-visibility:visible via the
STABLE #liveAssistantTurn id (covers ALL render modes — Compact Worklog,
Transparent Stream, restored-live — not just the Transparent-Stream path that
stamps data-live-assistant-turn), so a normal live turn on touch never blanks
mid-stream and its height keeps growing so the new-message cue still fires.
contain-intrinsic-size:auto 1px preserves flick-scroll momentum. Scoped to
touch — desktop find-in-page untouched. Deliberately does NOT set
overflow-anchor:none (inert on iOS WebKit; re-opens #4856/#5338).
(2) SSE reconnect ladder extended 4->6 steps + a last-ditch
_restoreSettledSession full-session poll (8s watchdog) after retries exhaust, so
a response completed during an iOS Tailscale/VPN reconnect is recovered without
an error banner.

Co-authored-by: luperrypf <luperrypf@users.noreply.github.com>
2026-07-04 21:51:38 +00:00
nesquena-hermes 56a133cc72 Merge pull request #5581 from nesquena/release/stage-5543-r3
release #5543: fire-and-forget memory commit + drain-safe managed stop
v0.51.863
2026-07-04 14:44:22 -07:00
nesquena-hermes e974f03829 release #5543: fire-and-forget memory commit + drain-safe managed stop + CHANGELOG 2026-07-04 21:41:05 +00:00
nesquena-hermes 19d00e3586 fix(#5543): fire-and-forget commit_session_memory + drain-safe managed stop
Move the 1-5s commit_session_memory() extraction off the POST /api/session/new
request thread (was blocking + New Chat) into a fire-and-forget daemon thread,
guarded by the existing per-session in-flight serialization.

Data-loss prevention on managed stop:
- server.py installs a SIGTERM handler that requests an orderly stop so
  serve_forever()'s finally block runs drain_all_on_shutdown() (the default
  SIGTERM disposition terminated without unwinding, losing in-flight commits).
- session_lifecycle: bounded self-unregistering background-commit registry;
  a _draining flag so no NEW worker starts once the drain begins (the inline
  generation-drain commits any late arrival instead); a 30s overall drain
  deadline so a stuck worker cannot hang the stop path forever.

Co-authored-by: luperrypf <luperrypf@users.noreply.github.com>
2026-07-04 21:40:21 +00:00
nesquena-hermes 04717ad305 Merge pull request #5580 from nesquena/release/stage-5544-r3
release #5544: re-pin auto-scroll at true bottom after scroll-to-bottom
v0.51.862
2026-07-04 14:39:02 -07:00
nesquena-hermes 3b541b745f release #5544: re-pin auto-scroll at true bottom + CHANGELOG 2026-07-04 21:24:39 +00:00
nesquena-hermes 4dc87e9069 fix(#5544): re-pin auto-scroll only at true bottom after scroll-to-bottom
A single scroll-up during streaming set _messageUserUnpinned=true and
scrollIfPinned() then permanently stopped auto-follow (only scrollToBottom()
cleared it) — a permanent auto-follow lockout even after the user returned to
the bottom. scrollIfPinned() now re-pins, but ONLY when the reader has genuinely
reached the true-bottom tail (<=80px) AND shows no active scroll intent
(wheel/key/touch/non-message), reusing the listener's _nearBottomCount debounce.
Proximity alone (the ~250px nearBottom band) must never re-pin — that is the
#4295 invariant. Restores the listener's <=80px true-bottom gate too.

Co-authored-by: luperrypf <luperrypf@users.noreply.github.com>
2026-07-04 21:24:17 +00:00
nesquena-hermes 627c6a173c Merge pull request #5576 from nesquena/release/stage-5563-r2
release #5563: align forked context to display for large/compacted sessions
v0.51.861
2026-07-04 14:15:49 -07:00
nesquena-hermes e588901f2f release #5563: align forked context to display for large/compacted sessions + CHANGELOG 2026-07-04 21:07:49 +00:00
nesquena-hermes 322c9fdb62 fix(#5563): align forked context to display for large (compacted) sessions
Fork ("Fork from here") on a large/compacted session could cut context off a
real turn boundary — slicing mid-turn or leaving a dangling tool call, feeding
a malformed context to the model. Align the fork cut to a resolved turn boundary
via a signature matcher handling both compacted and non-compacted display
coordinate spaces; errs toward under-keeping with send-time sanitization backstop.

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-07-04 20:55:06 +00:00
nesquena-hermes 4dfeb78bd0 Merge pull request #5574 from nesquena/release/stage-5532b
Release — clear-conversation P0 data-loss (#5532, #5556, #5553)
v0.51.860
2026-07-04 13:18:48 -07:00
nesquena-hermes 3f599ead04 fix(test): drop the one genuinely-unused _seed_session_dir binding (lint)
CI lint (diff-scoped ruff F) flagged 'models =' unused at line 92 (that test
uses only the seed side-effect). Removed the assignment there; kept it at line
320 where test_clear_survives_startup_recovery legitimately reads models.SESSION_DIR.
Ruff clean, 11 #5532 tests pass.
2026-07-04 20:15:20 +00:00