v0.50.254: Opus follow-up + CHANGELOG

- popstate handler now refuses to switch sessions mid-stream (S.busy guard)
  Mirrors the same guard the cross-tab storage handler had. PR #1392 added
  the popstate listener but missed this. Without it, browser Back during
  a live stream silently yanks the user out of their turn.
  (Opus pre-release advisor finding)

- CHANGELOG entry for v0.50.254 (4 PRs + 1 Opus follow-up)

1 regression test in test_v050254_opus_followups.py.
This commit is contained in:
nesquena-hermes 2026-05-01 16:25:04 +00:00
parent db548fc872
commit e3a2b0b3d2
3 changed files with 63 additions and 1 deletions

View file

@ -2,6 +2,25 @@
## [Unreleased]
## [v0.50.254] — 2026-05-01
### Fixed
- **API 500 regression on /api/sessions, /api/memory: `_combined_redact` TypeError** (#1394, closes #1394) — PR #1387 follow-up `fc88981` started passing `force=True` to `redact_sensitive_text()`, but older hermes-agent builds don't accept the `force` kwarg. Every redaction call on the hot path crashed with `TypeError`, degrading the entire API to 500 errors. `_combined_redact` now wraps the call in `try/except TypeError` and falls back to the no-kwarg call. The local fallback (ghp_/sk-/hf_/AKIA) still runs unconditionally, so coverage doesn't regress. (`api/helpers.py`) @bergeouss — PR #1400, fixes #1394
- **Code block tree-view: newlines stripped from data-raw, jsyaml retry loop missing** (#1397, closes #1397) — Two bugs in the JSON/YAML tree-view renderer. (1) Browsers normalize newlines to spaces inside HTML attribute values (HTML spec); the `data-raw` attribute on `.code-tree-wrap` lost every newline, so multi-line YAML/JSON came out as single-line tree views. Fixed by encoding `\n` as `
` before writing the attribute. (2) When jsyaml hadn't loaded yet, `initTreeViews()` set `data-tree-init=1` immediately and bailed — the lazy-load callback never re-invoked init, leaving the block in raw view forever. Fixed by removing `data-tree-init` and calling `_loadJsyamlThen(initTreeViews)` to retry after load. (`static/ui.js`) @bergeouss — PR #1400, fixes #1397
- **Credential permission fixer respects HERMES_HOME_MODE and HERMES_SKIP_CHMOD** (#1389, closes #1389) — `fix_credential_permissions()` was unconditionally forcing 0600 on every credential file in `HERMES_HOME` at startup. Docker setups that intentionally use group bits (e.g. `HERMES_HOME_MODE=0640` for shared volumes) had their declared mode silently overridden. Now `HERMES_SKIP_CHMOD=1` bypasses the fixer entirely; when `HERMES_HOME_MODE` is set, the fixer only strips world bits (0o007) and preserves operator-declared group access. (`api/startup.py`) @bergeouss — PR #1400, fixes #1389
- **Sidebar session click is now instant on mouse, drag-aware on touch** (#1398) — clicking a chat in the sidebar previously had a 300ms delay on every device to disambiguate single-tap from double-tap-rename. Mouse users perceived this as lag. Now the delay is 0 for `pointerType==='mouse'` and stays 300ms for touch (where it's needed for tap-vs-drag disambiguation). Adds pointermove drag detection: movement >5px from pointerdown marks the gesture as a drag, cancels the pending tap timer, suppresses hover highlighting via a `.dragging` class, and clears 50ms after release so the row doesn't flash hover mid-scroll. (`static/sessions.js`, `static/style.css`) @JKJameson — PR #1398
- **Per-tab session URL anchors via `/session/<id>`** (#1392) — replaces the cross-tab `localStorage['hermes-webui-session']` active-session bus with per-tab URL ownership. Each tab anchors its active conversation in the path (`/session/<id>`), so two tabs viewing different sessions can no longer yank each other around when localStorage changes. The `<base href>` script in `static/index.html` stops at the `/session/` marker so subpath mounts (`/myapp/session/<id>`) still resolve assets correctly; all `new URL('api/...', location.href)` calls migrated to `document.baseURI||location.href` for the same reason. New helpers `_sessionIdFromLocation()`, `_sessionUrlForSid()`, `_setActiveSessionUrl()` in `sessions.js`. Lineage-aware active highlighting (`_sessionLineageContainsSession`) keeps a forked session highlighted even when collapsed inside a parent lineage row. The `popstate` handler navigates between sessions via browser back/forward but refuses to switch mid-stream (`S.busy` guard, mirroring the cross-tab storage handler). The cross-tab storage handler was deliberately defanged so it only re-renders the sidebar — it no longer force-loads the new sid into the current tab. (`api/routes.py`, `static/boot.js`, `static/commands.js`, `static/index.html`, `static/messages.js`, `static/sessions.js`, `static/terminal.js`, `static/ui.js`, `static/workspace.js`, `tests/test_session_cross_tab_sync.py`, `tests/test_session_lineage_collapse.py`) @dso2ng — PR #1392
### Changed
- **Settings toggle: "Show CLI sessions" → "Show non-WebUI sessions"** (#1407) — the old label was misleading: the feature surfaces conversations from CLI, Telegram, Discord, Slack, WeChat, and other non-WebUI channels — not just CLI. The new label captures the actual scope. Pure rename across all 8 locales (en, zh, zh-Hant, ru, es, de, pt, ko); underlying logic untouched. Reordered channel examples by global adoption (Telegram, Discord, Slack first; WeChat de-emphasized). (`static/i18n.js`, `static/index.html`, `tests/test_korean_locale.py`) @franksong2702 — PR #1407
- **`popstate` handler refuses to switch sessions mid-stream** — Opus pre-release follow-up. Mirrors the same `S.busy` guard the cross-tab storage handler had. A user mid-stream who absent-mindedly hits browser Back used to lose their active turn (PR #1392 introduced the popstate listener without the guard). Now shows a toast and stays on the current session. 1 regression test in `test_v050254_opus_followups.py`. (`static/sessions.js`)
## [v0.50.253] — 2026-05-01
### Added

View file

@ -1733,7 +1733,15 @@ if(typeof window!=='undefined'){
window.addEventListener('storage', (e) => { void _handleActiveSessionStorageEvent(e); });
window.addEventListener('popstate', () => {
const sid=(typeof _sessionIdFromLocation==='function')?_sessionIdFromLocation():null;
if(sid && (!S.session || S.session.session_id!==sid)) void loadSession(sid);
if(!sid || (S.session && S.session.session_id===sid)) return;
// Refuse to switch sessions mid-stream — same UX guard the storage-event
// handler had. A user mid-turn who hits browser Back should NOT lose the
// active stream. They can hit Back again once the turn ends.
if(S.busy){
if(typeof showToast==='function') showToast('Finish the current turn before switching sessions.',3000);
return;
}
void loadSession(sid);
});
}

View file

@ -0,0 +1,35 @@
"""Regression tests for v0.50.254 Opus pre-release follow-ups.
Apr 2026 v0.50.254 batch added per-tab session URL anchors (#1392). Opus advisor
flagged that the new popstate handler was missing the same `S.busy` guard the
storage-event handler had — a user mid-stream who hits browser Back would lose
their active turn the same way cross-tab churn used to do. Adds the guard.
"""
from __future__ import annotations
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
def test_popstate_handler_guards_busy_state():
"""Browser back/forward must not switch sessions while a stream is live.
The new `popstate` handler in `static/sessions.js` (added by #1392) has to
mirror the `S.busy` guard that the cross-tab storage handler had. Otherwise
a user mid-stream who absent-mindedly hits Back will get yanked out of their
active turn — exactly the regression the storage-event guard was added to
prevent.
"""
src = (REPO / "static" / "sessions.js").read_text(encoding="utf-8")
popstate_idx = src.find("addEventListener('popstate'")
assert popstate_idx != -1, "popstate handler missing from sessions.js"
# Look at the next ~600 chars of the handler body.
body = src[popstate_idx : popstate_idx + 600]
assert "S.busy" in body, (
"popstate handler must check S.busy before calling loadSession() — "
"otherwise mid-stream users lose their turn when they hit browser Back. "
"Mirror the same guard the cross-tab storage handler had."
)
assert "loadSession" in body, "popstate handler must call loadSession when allowed"