docs(changelog): stamp v0.52.106 stable section (promotion)

This commit is contained in:
nesquena-hermes committed 2026-07-29 20:50:41 +00:00
1 parent 0fa4b5f5ac
commit 16dfeeb3f0
1 file changed
+58 -52
+58 -52
View File
@@ -9,14 +9,10 @@
- **The repository is now pip-installable with standard packaging metadata.** `pyproject.toml` gains a `[build-system]` + `[project]` section (setuptools + setuptools-scm) so packagers and distros can `pip install .` and get a proper wheel (`api` + `static` bundled). The normal `bootstrap.py` / `start.sh` / `ctl.sh` source-checkout launch path is unchanged. setuptools-scm writes its version to a separate `api/_scm_version.py` (not the Docker-owned `api/_version.py`), and the runtime version detector reads it as an explicit fallback (normalizing the PEP 440 value to a `v…` form) so an installed wheel reports a real version instead of `unknown` — Docker and git-checkout version resolution are byte-identical to before. Thanks @rodboev. (#6337, #2695)
- **Transparent Stream can now hide its per-event timestamp chips.** A new opt-in setting (Settings → the Transparent Stream activity area) lets you suppress the small per-event timestamp chips inside Transparent Stream while keeping the assistant response footer time visible — narrowing the visual noise without removing timing information entirely. Default is unchanged (chips shown). Thanks @rodboev. (#6130, #6099)
- **Internal: removed a dead `rowIndex` parameter from the settled-scene row projector.** `attachLiveStream`'s `pushRow` helper carried an unused positional index left over from before final-segment eligibility moved to a row-identity `WeakSet`; dropping it is a pure no-op refactor (differential execution confirms byte-identical settled scenes — ordering, dedupe, final-prefix suppression, and sequence assignment all unchanged). Thanks @webtecnica. (#6258)
- **Internal: clarified the transcript-virtualization boot comment (opt-in default retained).** The comment above the boot-time `_virtualizeTranscript` apply now records that the #4346 Phase B footer-jitter fix resolved the original scroll-up flicker root cause, while virtualization stays opt-in (default OFF) until battle-tested further. Zero behavior change — the apply predicate, settings-load fallback, checkbox, and server default all remain opt-in/OFF and in agreement. Thanks @webtecnica. (#6318, #6155)
- **Docs: reconciled the assistant-reply lifecycle RFCs/contracts with shipped behavior.** The Stable Assistant Turn Anchors and Live-to-Final RFCs, `docs/CONTRACTS.md`, and the Phase 0 architecture inventory were updated from "proposed/scaffold" framing to "accepted/implemented" to match what actually ships (single assistant-turn owner projecting one `activity_scene_v1` into Compact Worklog / Transparent Stream / Final answer). Documentation-only — no runtime behavior or CI-gate change; the one code touch is a corrected comment header in `static/assistant_turn_anchors.js`. Remaining hardening stays tracked under #3400. Thanks @franksong2702. (#6144)
- **The busy-time send behavior is now called "Default message mode," and new installs default to Steer.** The Settings → Preferences control formerly labeled "Busy input mode" is renamed to "Default message mode," and a fresh install now defaults to **Steer** (inject a mid-turn correction without interrupting) instead of Queue. Your existing choice is preserved — if you ever saved settings, your current mode (Queue/Interrupt/Steer) is migrated as-is and unchanged; only never-configured installs pick up the new Steer default. The saved preference still survives a reload or a brief server outage (the localStorage mirror from the previous release is intact). Thanks @rodboev. (#5162, #5145)
### Added
@@ -31,8 +27,6 @@
- **The admin model-reload endpoint (`POST /api/admin/reload`) no longer crashes.** Reloading `api.models` rebound three names onto the routes module, but one of them — `_routes.compact = _models.compact` — referenced `compact` as if it were a module-level attribute when it is actually a method of the `Session` class, so the handler raised `AttributeError: module 'api.models' has no attribute 'compact'` and the reload failed with a 500. The invalid rebinding is removed (nothing reads `api.routes.compact`); the two genuinely module-level rebindings (`get_session`, `Session`) are unchanged, and the endpoint now returns `{"status": "ok"}`. Thanks @mspgle-dotcom. (#6477)
- **Abandoned embedded terminals are now reaped instead of leaking a running shell forever (#4633 backend half).** A WebUI terminal whose viewer dropped its output stream without an explicit close (tab close, crash, network drop) previously kept its shell process and pty fd alive indefinitely — there is no `PDEATHSIG`, so nothing tore it down until the terminal cap happened to evict it. A background reaper now closes any terminal that has been unwatched (zero attached output streams) for longer than a 15-minute grace, and sweeps dead-process terminals immediately; the grace deliberately spans a tab refresh or brief network drop so a genuine reconnect keeps the session. The output-stream attach is now atomic against the reaper (a single registry-locked lookup+subscribe), so a viewer can never end up subscribed to a terminal the reaper is tearing down, and a race resolves to exactly one winner — attach keeps the terminal, or the reaper wins and the stream cleanly reports "terminal not running." A related leak is closed too: if writing the SSE response headers fails after attaching (client already gone), the subscription is now always unsubscribed instead of pinning the terminal un-reapable. Thanks @ai-ag2026. (#5844, #4633)
- **Typing in the composer no longer causes a layout round-trip on every keystroke.** A single-row composer append now skips rewriting the textarea height when the height can't change, removing a per-keystroke reflow that could disturb caret stability and assistive-tech focus. An oversized composer, a text replacement, and session/draft restore still fully remeasure so the box shrinks back correctly, and the minimum-height check parses only genuine pixel values (a percentage/`auto`/`calc()` min-height fails closed to the full resize). Thanks @sheldon-im. (#6349)
- **Gateway approval cards are now routed by run ID, so approvals and Stop always act on the right run (#6008).** A gateway approval card that arrived without an `approval_id` couldn't be acted on, and with multiple concurrent runs an approval or a Stop could be applied to the wrong run. Approvals are now owned per-`(session, run)`: identity-less and identity-bearing approvals route through the same single-flight owner, concurrent duplicate submits can't double-relay, different runs don't block each other, and a failed relay releases ownership so a retry can proceed. The composer/sidebar Stop now waits boundedly for an acknowledged exact-run gateway stop and surfaces a persistent, actionable "Cancel failed." error toast (with Copy and Dismiss, localized) instead of silently settling the UI when the stop fails. When multiple approvals are queued the card shows a "1 of N pending" counter. Thanks @rodboev. (#6040, #6008)
@@ -77,12 +71,8 @@
- **Content-search and sidebar polling are faster: run-journal summaries are now cached.** Repeated reads of an unchanged run journal reuse a cached summary instead of re-parsing the whole file, keyed on a complete filesystem signature (device, inode, size, mtime, and ctime) so the cache is always dropped the instant the journal is appended to, replaced, truncated, or recreated — including a same-size rewrite that preserves mtime. Bounded LRU; a first-write race can never pin a stale empty result. Thanks @sjungwon03. (#6291)
- **Live Stream: a settled Worklog now shows the side effects a turn performed (memory/skill saves).** The `state_saved` SSE event (memory + skill saves) was classified as an Assistant-Turn-Anchor side effect but the renderer-neutral scene projection discarded it, so the browser only flashed a toast and a reloaded Worklog lost that record. Anchor `artifacts`/`side_effects` are now projected into the settled `activity_scene_v1`. Purely additive — turns with no side effects render byte-identical (side-effect-only turns still don't force a Worklog), and the change is compatible with the stable-run-identity plumbing. Thanks @franksong2702. (#6204, #3400)
- **Kanban board columns are now scrollable on mobile.** On a phone, a touch-drag inside a Kanban column was trapped in the column's nested scroller, so you couldn't scroll the board. The mobile layout now lets the column overscroll chain to the page/section, restoring natural scrolling; desktop and the pull-to-refresh behavior are unchanged. Thanks @jpalazz2. (#6306)
- **Live Stream: Assistant Turn Anchors now carry a stable run identity through the settled scene.** The activity-scene projection now threads a stable `run_id` (independent of the transport `stream_id`) through Anchor scenes and client hydration, so a settled/reloaded Worklog keeps a consistent identity and can't mis-own or duplicate rows if a producer ever emits `run_id != stream_id`. Behavior-preserving for current writers (which still emit `run_id == stream_id`) — verified byte-identical live/settle/reload render, with malformed/conflicting identities falling back to the transport identity. Thanks @franksong2702. (#6201, #3400)
- **Content search no longer evicts your open sessions from the cache.** A content search scans many sessions, and each scan used to promote/cold-load sessions into the in-memory LRU — evicting the sessions you actually have open. Search now reads through a scan-only accessor that keeps the same disk-freshness, journal-recovery, and state.db reconciliation (so recent content stays searchable) but doesn't disturb the working-set cache. Thanks @ai-ag2026. (#6084, #6083)
- **Watching the same terminal from two tabs no longer splits the output stream.** Terminal output is now broadcast to every subscribed viewer instead of draining from one shared queue, so a second tab (or a reconnect) sees the full stream rather than a random half. SSE responses carry event `id:`s and a reconnecting viewer replays only events after its `Last-Event-ID` cursor (fresh viewers still get the full bounded backlog). Thanks @ai-ag2026. (#5836)
@@ -121,40 +111,6 @@
- **OIDC allowlist values containing spaces (e.g. a group named `Hermes Users`) are no longer split into separate entries.** The allowlist parser split comma-separated values on all whitespace, so a multi-word group/user name became two spurious entries. Allowlist parsing now splits only on commas/newlines (multi-word values stay intact and match their exact signed claim — strictly tightening, no security loosening), while OAuth **scope** parsing keeps its space-delimited behavior per RFC 6749 §3.3. A blank allowlist entry no longer bricks an OIDC-only deployment. Thanks @webtecnica. (#6244)
- **`/sessions` and `/resume` typed in the composer now open the session browser instead of being sent as chat text.** Both commands appeared in the slash-command autocomplete (they're exposed by `/api/commands`) but the WebUI send-time dispatch didn't handle them, so they were posted to the agent as plain text. They now open the native WebUI session browser and clear the composer — on desktop and (via the mobile-aware opener) on phone-width layouts. Thanks @webtecnica. (#6245, #6224)
- **Z.AI GLM models now advertise the correct reasoning controls per version.** The WebUI offered the full `reasoning_effort` ladder (max/xhigh/high/medium/low/minimal) for every GLM model, but per Z.AI's API only GLM-5.2+ supports the effort ladder — GLM-4.5 through 5.1 support the binary `thinking` on/off toggle, and GLM-4.7 forces thinking on (not configurable). Reasoning controls are now gated correctly: GLM-5.2+ keeps the full ladder + None, GLM-4.5/4.6/5.0/5.1 get a two-way On/None thinking toggle (no effort levels), and GLM-4.7 shows forced thinking with no toggle. All three surfaces (config status, effort resolution, composer chip) agree across model tiers, provider aliases, and namespaces. Thanks @rh-id. (#6219)
- **Folder downloads now work when the WebUI is served under a subpath.** The file-tree "Download" context-menu action built an absolute `/api/folder/download?...` URL, which broke behind a reverse proxy that mounts the app under a base path (e.g. `/hermes/`). It now resolves the download URL against `document.baseURI` (matching the pattern the other subpath-aware download paths already use), so it works under any base path and is unchanged on root deployments. Thanks @steezypunk. (#6227)
- **Reloading an active turn now prefers the durable run-journal over a stale browser cache, and never truncates recovered tool arguments.** On a hard reload/recovery of a live turn, `loadSession()` could keep a stale local cache as the Worklog authority over the server's newer run-journal snapshot — or, worse, mistake a legacy cache entry with no stream identity for the current stream and suppress replay of a newer run's events. Recovery now selects by stream identity first, then run-journal sequence (`_selectLiveRecoveryInflight`), and carries a run-aware `after_event_id` cursor through the whole reconnect pipeline (server-snapshot hydration, in-memory INFLIGHT, localStorage compaction, session-switch persistence, SSE reconnect), resetting the cursor on stream-ownership change. The durable snapshot also preserves full tool-call arguments (previously truncated to 120 chars) so a recovered command/input is never silently shortened, while a shared content-aware bound (`bound_run_journal_snapshot_args`) caps pathological payloads so the snapshot can't balloon. Fixes #6196. Thanks @franksong2702. (#6197, #6196)
- **The "Compressing context" card no longer flashes on turns that aren't actually compressing.** The card was driven by a broad status match (anything containing `compressing`), so skip/cooldown/defer notices and post-compression summaries (`Compressed X → Y`) could paint a phantom compression card mid-turn. A dedicated `_is_agent_compression_start_status()` now recognizes only the five genuine start-of-compression markers the Hermes agent emits, and the transcript renderer no longer lets an unrecognized `terminal_status` row paint a compression anchor. Real compression turns still show the card unchanged; verified against every current agent compression emitter. Thanks @carlotestor. (#6184)
- **Transparent Stream no longer shows the start of the final answer twice after a multi-segment turn settles.** In a turn with interim assistant prose interleaved with tool calls, the beginning of the final answer could render twice after the live→settled transition — once as a stale live-token accumulator snapshot, once as the real assistant segment — persisting until reload. The earlier suppression gate (#5758) computed its "after the last tool row" boundary over the combined row list, where settled per-message tool rows re-list tools that ran earlier in the turn, pushing the boundary past the final segment's accumulator so the dedupe never fired. Final-segment eligibility is now judged against the live projection's own chronology (a live-prose row belongs to the final segment only when no *projected* tool row follows it), so the stale prefix is dropped while pre-tool narration and every tool/thinking row are preserved. Thanks @ai-ag2026. (#6189, #5749)
- **The frontend now reads the `msg_limit` ceiling from `/api/session` metadata instead of hand-mirroring the constant.** Following the ceiling clamp shipped in exp-v0.52.98, the backend advertises its `_MAX_MSG_LIMIT` as `_msg_limit_max` in every `/api/session` response, and the frontend reads it dynamically (`_msgLimitMax`, a module-scope value defaulting to the static fallback), so the two can no longer drift and the mirrored constant's drift-guard test is no longer needed. Older backends that omit the field fall back to the built-in default, so mixed-version deployments are unaffected. Thanks @webtecnica. (#6214, #6177)
- **`GET /api/session?msg_limit=` is now bounded by a server-side ceiling, and the frontend paginates around it without dropping rows.** A client could request `msg_limit=1000000` (or an outline-jump path asked for `9999`) and force the server to assemble and serialize an unbounded message payload. The backend now clamps `?msg_limit=` to `[1, 500]` via a dedicated `_parse_msg_limit()` helper and sets the existing `_messages_truncated` signal when it clamps; the bare no-`msg_limit` path (branch/undo/jump-to-start) still returns the full transcript. The frontend mirrors the ceiling: `_loadOlderMessages` grows its tail window below the ceiling and switches to bounded `msg_before` backward paging once the server would clamp, and the outline jump uses the bare full-transcript path instead of the old `msg_limit=9999` hack. Two silent row-loss regressions found during review were fixed before ship: a raw-row-heavy `msg_before` page that textually repeated the current tail could be misclassified as the cumulative tail and wholesale-replace it (losing older rows), and a same-session refresh above the ceiling could shrink an already-loaded >500-row transcript to the last 500 — both now route through the correct paging/full-transcript path. Thanks @rh-id. (#6152, #6154, #6177)
- **Background subagents no longer fail to persist when the parent's cached agent is reused mid-turn.** A parent session's `SessionDB` handle is shared by reference with the background subagents it spawns (`delegate_tool`), but the cached-agent reuse path unconditionally closed that handle when the parent got a new turn / server-side wakeup — so every still-running child then failed `append_message` with `'NoneType' object has no attribute 'execute'` (observed 600+ times under one parent). The reuse path now keeps a still-open handle (closing only the unused fresh one, preserving the #1421 FD-leak fix) and only replaces a dead/missing one. The credential self-heal path was hardened the same way: if rebuilding the handle fails it degrades cleanly to a lazy reinit instead of reusing a closed handle. Thanks @carlotestor. (#6143)
- **The CLI/cron session-list projection cache is now bounded.** `_CLI_SESSIONS_CACHE` was an unbounded plain dict of deep-copied session-list projections whose fingerprint-folded key advances on every streamed message, so orphaned heavy copies accumulated on a long-lived server until the next structural clear. It's now an `OrderedDict` capped at 8 entries with drop-oldest LRU eviction (recency refresh on hit), mirroring the existing `_CLAUDE_CODE_PARSE_CACHE` / `_SIDECAR_METADATA_CACHE` pattern; TTL stays the freshness control. Thanks @rh-id. (#6140)
- **Models selected from a list-shaped custom-provider allowlist now route to the right provider.** A `custom_providers[].models` allowlist can be written as either a mapping or a list, and the model picker accepted both — but runtime routing only read mapping keys, so a model chosen from a *list*-shaped allowlist could silently stay on the active/default provider instead of its own. The supported-model-ID extraction is now centralized in one helper used by both catalog construction and routing, so the picker and runtime can no longer drift. Existing mapping-shaped and list-of-strings configs are unaffected. Thanks @franksong2702. (#6127, #6121)
- **Stale sidebar approval attention badge now clears once its gateway approval has drained.** A parent session could keep showing the red approval "attention" dot in the sidebar after the underlying gateway approval was already resolved/gone — the live gateway queue drained but the sidebar summary kept the mirrored approval alive. `_session_attention_summary()` now reconciles the gateway pending-mirror before counting attention (the same self-healing repair the approval-pending route already does), so the badge clears correctly. Live approvals stay visible and actionable and the clarify fallback is unchanged. This is the WebUI read-path mitigation; the child-session context-propagation root cause stays tracked in #6100. Thanks @rodboev. (#6117, #6100)
- **The mobile composer model picker no longer opens off-screen.** On phones the model dropdown could open below the fold (looking like it "won't open", issue #6080) because it lived inside `.composer-footer`, whose `container-type` (and, under the Geist Contrast skin, `backdrop-filter`) made the footer the containing block for the dropdown's fixed positioning — so it anchored to the bottom-of-screen footer instead of the viewport. The picker now reparents to `<body>` while open (the same idiom the profile dropdown already uses), so its fixed positioning resolves against the visual viewport on every skin; it flips above/below the anchor by available space, clamps below the titlebar, and re-positions as the on-screen keyboard / URL bar move the visual viewport. Desktop behavior is unchanged. Thanks @webtecnica. (#6105, #6080)
- **Approval and clarify popups no longer collapse to tiny blank windows on very narrow screens.** On very narrow viewports the clarify card could shrink to an unreadable sliver; it now gets a `min-width` and `overflow:visible` matching the approval card. Thanks @webtecnica. (#6104, #6094)
- **Stream event timestamps no longer break the row layout on narrow foldable screens.** On screens under 360px the per-event timestamps could spill out of tool/thinking card headers; the headers now clip overflow so the timestamp layout holds. (Tool names stay visible — an earlier draft hid them at ≤360px, which lost tool identity; the header clip alone prevents the spill.) Thanks @webtecnica. (#6106, #6099)
- **Streaming sidebar polls no longer re-run the expensive CLI/cron projection every few seconds.** The per-stream cache TTL was pinned at 30s while the frontend's streaming sidebar poll cadence could approach it, so the state.db CLI/cron projection (and its ~200 sidecar reads) could be re-paid on nearly every poll during a live turn (the #4842/#4808 high-CPU `get_cli_sessions` case). The streaming TTL is now held strictly above the sidebar poll interval (45s) and paired with the stable streaming cache key, so repeated polls reuse the projection; a new test asserts the TTL stays above the poll cadence so this can't silently regress. Thanks @starship-s. (#6102, #4842)
- **Delegated subagent sessions in the sidebar now show a meaningful title instead of "Subagent Session".** Generic delegated child sessions previously all displayed the placeholder "Subagent Session" in the conversations list, making them indistinguishable. Their sidebar title is now derived (view-only) from the child's first user message during the existing batched sidebar-metadata pass — without mutating the stored session title, so the read-only boundary is preserved. Only generic "Subagent Session" placeholders are affected; custom titles are left untouched. Thanks @rodboev. (#6056, #6033)
- **Concurrent remote-gateway health checks no longer stampede the gateway.** When many requests needed a remote-gateway health probe at once, each fired its own probe (a thundering herd). Probes are now single-flighted: one "leader" thread probes while latecomers wait (bounded) and share the result, guarded by a single condition/lock with a `finally` that always releases waiters even if the probe errors. Thanks @ai-ag2026. (#5798, #5455)
- **Appending run-journal events no longer gets slower as a session's journal grows.** The next sequence number was recomputed by scanning existing entries on every append (O(n²) over a session's lifetime); it's now cached per journal path (guarded by a dedicated lock, evicted on journal delete), making each append O(1). Thanks @ai-ag2026. (#5799)
@@ -375,14 +331,6 @@
- **No more 57–70 s cold-startup stalls from the profile skills-stats thundering herd.** At container boot the frontend fires several profile-data requests at once; with `ThreadingHTTPServer` (one thread per request) they all missed the empty skills-stats cache simultaneously and each walked + parsed every profile's skill tree, stacking thousands of concurrent `stat()` calls under Docker's overlay2 filesystem. `_get_profile_skills_stats()` now serializes per-profile with double-checked locking (concurrent misses on one profile collapse to a single compute; independent profiles still compute in parallel), and `list_profiles_api()` single-flights the row build under `_LIST_PROFILES_CACHE_LOCK` so one thread builds while the rest wait for the cached result. The every-call cheap mtime probe (the #4783 out-of-band change-detection contract) is unchanged. (#5364)
### Added
- **Extension API: session-open interception + transcript rendering.** Two new extension-facing hooks let extensions react to session navigation and render message transcripts without DOM hacking. `window.registerHermesSessionOpenHandler(fn)` fires when a session opens — with `{preload:true}` a handler may return `{cancel:true}` to abort the navigation cleanly (no side-effect: no sidebar close, profile switch, stream open, transcript clear, or URL change on any entry path), and with `{loaded:true}` it runs after the session is fully populated. `window.renderTranscript(container, messages, opts)` renders an array of messages into any container, routing content through the same sanitized `renderMd`/`SAFE_TAGS` pipeline the chat uses (with a `textContent` fallback), so it carries no new XSS surface. Follows the established `registerHermesTtsEngine` registration pattern. Thanks @ChonSong. (#5508)
- **Pinch-to-zoom for Mermaid diagrams on touch devices.** The zoomable Mermaid viewer (which already had mouse-wheel zoom + single-finger pan) now supports two-finger pinch-to-zoom on phones/tablets, anchored on the pinch midpoint. Pointer/drag handlers are guarded so an active pinch never fights the pan gesture; desktop mouse-wheel zoom and single-finger pan are unchanged. Uses Touch Events for iOS Safari robustness. Thanks @silent-reader-cn. (#5913)
- **Collapsed `read_file` tool rows now show the requested line range.** When the agent reads a slice of a file (`read_file` with `offset`/`limit`), the collapsed tool label now appends the range — e.g. `Read hermes_constants.py · L321-480`, or `L42` for an offset-only read — so you can see which part of a large file was read without expanding the row. Full-file reads and any tool call without a trustworthy positive-integer range keep the plain filename label (invalid, non-integer, non-positive, and overflowing ranges all fall back safely). Only `read_file` is affected. Thanks @franksong2702. (#6062)
### Performance
- **Fork a scheduled-job (cron) conversation into your own editable chat.** Cron-run sessions are read-only (the scheduler owns them), so you couldn't continue one. You can now branch/fork a cron session into a new WebUI-owned conversation — the original read-only session is never modified, and the fork picks up its history so you can carry on. Only genuine cron sessions qualify (verified by the server-side source, not a guessable id), and every other read-only session stays unbranchable. Thanks @rodboev. (#5555, #5477)
@@ -435,6 +383,64 @@
- **Added regression coverage for messaging clear-watermark semantics.** New test locks the watermark behavior so a future change can't silently regress it. Thanks @rodboev. (#5589, #5572)
## [v0.52.106] — 2026-07-29
### Changed
- **Transparent Stream can now hide its per-event timestamp chips.** A new opt-in setting (Settings → the Transparent Stream activity area) lets you suppress the small per-event timestamp chips inside Transparent Stream while keeping the assistant response footer time visible — narrowing the visual noise without removing timing information entirely. Default is unchanged (chips shown). Thanks @rodboev. (#6130, #6099)
- **Docs: reconciled the assistant-reply lifecycle RFCs/contracts with shipped behavior.** The Stable Assistant Turn Anchors and Live-to-Final RFCs, `docs/CONTRACTS.md`, and the Phase 0 architecture inventory were updated from "proposed/scaffold" framing to "accepted/implemented" to match what actually ships (single assistant-turn owner projecting one `activity_scene_v1` into Compact Worklog / Transparent Stream / Final answer). Documentation-only — no runtime behavior or CI-gate change; the one code touch is a corrected comment header in `static/assistant_turn_anchors.js`. Remaining hardening stays tracked under #3400. Thanks @franksong2702. (#6144)
### Added
- **Extension API: session-open interception + transcript rendering.** Two new extension-facing hooks let extensions react to session navigation and render message transcripts without DOM hacking. `window.registerHermesSessionOpenHandler(fn)` fires when a session opens — with `{preload:true}` a handler may return `{cancel:true}` to abort the navigation cleanly (no side-effect: no sidebar close, profile switch, stream open, transcript clear, or URL change on any entry path), and with `{loaded:true}` it runs after the session is fully populated. `window.renderTranscript(container, messages, opts)` renders an array of messages into any container, routing content through the same sanitized `renderMd`/`SAFE_TAGS` pipeline the chat uses (with a `textContent` fallback), so it carries no new XSS surface. Follows the established `registerHermesTtsEngine` registration pattern. Thanks @ChonSong. (#5508)
- **Pinch-to-zoom for Mermaid diagrams on touch devices.** The zoomable Mermaid viewer (which already had mouse-wheel zoom + single-finger pan) now supports two-finger pinch-to-zoom on phones/tablets, anchored on the pinch midpoint. Pointer/drag handlers are guarded so an active pinch never fights the pan gesture; desktop mouse-wheel zoom and single-finger pan are unchanged. Uses Touch Events for iOS Safari robustness. Thanks @silent-reader-cn. (#5913)
- **Collapsed `read_file` tool rows now show the requested line range.** When the agent reads a slice of a file (`read_file` with `offset`/`limit`), the collapsed tool label now appends the range — e.g. `Read hermes_constants.py · L321-480`, or `L42` for an offset-only read — so you can see which part of a large file was read without expanding the row. Full-file reads and any tool call without a trustworthy positive-integer range keep the plain filename label (invalid, non-integer, non-positive, and overflowing ranges all fall back safely). Only `read_file` is affected. Thanks @franksong2702. (#6062)
### Fixed
- **Abandoned embedded terminals are now reaped instead of leaking a running shell forever (#4633 backend half).** A WebUI terminal whose viewer dropped its output stream without an explicit close (tab close, crash, network drop) previously kept its shell process and pty fd alive indefinitely — there is no `PDEATHSIG`, so nothing tore it down until the terminal cap happened to evict it. A background reaper now closes any terminal that has been unwatched (zero attached output streams) for longer than a 15-minute grace, and sweeps dead-process terminals immediately; the grace deliberately spans a tab refresh or brief network drop so a genuine reconnect keeps the session. The output-stream attach is now atomic against the reaper (a single registry-locked lookup+subscribe), so a viewer can never end up subscribed to a terminal the reaper is tearing down, and a race resolves to exactly one winner — attach keeps the terminal, or the reaper wins and the stream cleanly reports "terminal not running." A related leak is closed too: if writing the SSE response headers fails after attaching (client already gone), the subscription is now always unsubscribed instead of pinning the terminal un-reapable. Thanks @ai-ag2026. (#5844, #4633)
- **Live Stream: a settled Worklog now shows the side effects a turn performed (memory/skill saves).** The `state_saved` SSE event (memory + skill saves) was classified as an Assistant-Turn-Anchor side effect but the renderer-neutral scene projection discarded it, so the browser only flashed a toast and a reloaded Worklog lost that record. Anchor `artifacts`/`side_effects` are now projected into the settled `activity_scene_v1`. Purely additive — turns with no side effects render byte-identical (side-effect-only turns still don't force a Worklog), and the change is compatible with the stable-run-identity plumbing. Thanks @franksong2702. (#6204, #3400)
- **Live Stream: Assistant Turn Anchors now carry a stable run identity through the settled scene.** The activity-scene projection now threads a stable `run_id` (independent of the transport `stream_id`) through Anchor scenes and client hydration, so a settled/reloaded Worklog keeps a consistent identity and can't mis-own or duplicate rows if a producer ever emits `run_id != stream_id`. Behavior-preserving for current writers (which still emit `run_id == stream_id`) — verified byte-identical live/settle/reload render, with malformed/conflicting identities falling back to the transport identity. Thanks @franksong2702. (#6201, #3400)
- **`/sessions` and `/resume` typed in the composer now open the session browser instead of being sent as chat text.** Both commands appeared in the slash-command autocomplete (they're exposed by `/api/commands`) but the WebUI send-time dispatch didn't handle them, so they were posted to the agent as plain text. They now open the native WebUI session browser and clear the composer — on desktop and (via the mobile-aware opener) on phone-width layouts. Thanks @webtecnica. (#6245, #6224)
- **Z.AI GLM models now advertise the correct reasoning controls per version.** The WebUI offered the full `reasoning_effort` ladder (max/xhigh/high/medium/low/minimal) for every GLM model, but per Z.AI's API only GLM-5.2+ supports the effort ladder — GLM-4.5 through 5.1 support the binary `thinking` on/off toggle, and GLM-4.7 forces thinking on (not configurable). Reasoning controls are now gated correctly: GLM-5.2+ keeps the full ladder + None, GLM-4.5/4.6/5.0/5.1 get a two-way On/None thinking toggle (no effort levels), and GLM-4.7 shows forced thinking with no toggle. All three surfaces (config status, effort resolution, composer chip) agree across model tiers, provider aliases, and namespaces. Thanks @rh-id. (#6219)
- **Folder downloads now work when the WebUI is served under a subpath.** The file-tree "Download" context-menu action built an absolute `/api/folder/download?...` URL, which broke behind a reverse proxy that mounts the app under a base path (e.g. `/hermes/`). It now resolves the download URL against `document.baseURI` (matching the pattern the other subpath-aware download paths already use), so it works under any base path and is unchanged on root deployments. Thanks @steezypunk. (#6227)
- **Reloading an active turn now prefers the durable run-journal over a stale browser cache, and never truncates recovered tool arguments.** On a hard reload/recovery of a live turn, `loadSession()` could keep a stale local cache as the Worklog authority over the server's newer run-journal snapshot — or, worse, mistake a legacy cache entry with no stream identity for the current stream and suppress replay of a newer run's events. Recovery now selects by stream identity first, then run-journal sequence (`_selectLiveRecoveryInflight`), and carries a run-aware `after_event_id` cursor through the whole reconnect pipeline (server-snapshot hydration, in-memory INFLIGHT, localStorage compaction, session-switch persistence, SSE reconnect), resetting the cursor on stream-ownership change. The durable snapshot also preserves full tool-call arguments (previously truncated to 120 chars) so a recovered command/input is never silently shortened, while a shared content-aware bound (`bound_run_journal_snapshot_args`) caps pathological payloads so the snapshot can't balloon. Fixes #6196. Thanks @franksong2702. (#6197, #6196)
- **The "Compressing context" card no longer flashes on turns that aren't actually compressing.** The card was driven by a broad status match (anything containing `compressing`), so skip/cooldown/defer notices and post-compression summaries (`Compressed X → Y`) could paint a phantom compression card mid-turn. A dedicated `_is_agent_compression_start_status()` now recognizes only the five genuine start-of-compression markers the Hermes agent emits, and the transcript renderer no longer lets an unrecognized `terminal_status` row paint a compression anchor. Real compression turns still show the card unchanged; verified against every current agent compression emitter. Thanks @carlotestor. (#6184)
- **Transparent Stream no longer shows the start of the final answer twice after a multi-segment turn settles.** In a turn with interim assistant prose interleaved with tool calls, the beginning of the final answer could render twice after the live→settled transition — once as a stale live-token accumulator snapshot, once as the real assistant segment — persisting until reload. The earlier suppression gate (#5758) computed its "after the last tool row" boundary over the combined row list, where settled per-message tool rows re-list tools that ran earlier in the turn, pushing the boundary past the final segment's accumulator so the dedupe never fired. Final-segment eligibility is now judged against the live projection's own chronology (a live-prose row belongs to the final segment only when no *projected* tool row follows it), so the stale prefix is dropped while pre-tool narration and every tool/thinking row are preserved. Thanks @ai-ag2026. (#6189, #5749)
- **The frontend now reads the `msg_limit` ceiling from `/api/session` metadata instead of hand-mirroring the constant.** Following the ceiling clamp shipped in exp-v0.52.98, the backend advertises its `_MAX_MSG_LIMIT` as `_msg_limit_max` in every `/api/session` response, and the frontend reads it dynamically (`_msgLimitMax`, a module-scope value defaulting to the static fallback), so the two can no longer drift and the mirrored constant's drift-guard test is no longer needed. Older backends that omit the field fall back to the built-in default, so mixed-version deployments are unaffected. Thanks @webtecnica. (#6214, #6177)
- **`GET /api/session?msg_limit=` is now bounded by a server-side ceiling, and the frontend paginates around it without dropping rows.** A client could request `msg_limit=1000000` (or an outline-jump path asked for `9999`) and force the server to assemble and serialize an unbounded message payload. The backend now clamps `?msg_limit=` to `[1, 500]` via a dedicated `_parse_msg_limit()` helper and sets the existing `_messages_truncated` signal when it clamps; the bare no-`msg_limit` path (branch/undo/jump-to-start) still returns the full transcript. The frontend mirrors the ceiling: `_loadOlderMessages` grows its tail window below the ceiling and switches to bounded `msg_before` backward paging once the server would clamp, and the outline jump uses the bare full-transcript path instead of the old `msg_limit=9999` hack. Two silent row-loss regressions found during review were fixed before ship: a raw-row-heavy `msg_before` page that textually repeated the current tail could be misclassified as the cumulative tail and wholesale-replace it (losing older rows), and a same-session refresh above the ceiling could shrink an already-loaded >500-row transcript to the last 500 — both now route through the correct paging/full-transcript path. Thanks @rh-id. (#6152, #6154, #6177)
- **Background subagents no longer fail to persist when the parent's cached agent is reused mid-turn.** A parent session's `SessionDB` handle is shared by reference with the background subagents it spawns (`delegate_tool`), but the cached-agent reuse path unconditionally closed that handle when the parent got a new turn / server-side wakeup — so every still-running child then failed `append_message` with `'NoneType' object has no attribute 'execute'` (observed 600+ times under one parent). The reuse path now keeps a still-open handle (closing only the unused fresh one, preserving the #1421 FD-leak fix) and only replaces a dead/missing one. The credential self-heal path was hardened the same way: if rebuilding the handle fails it degrades cleanly to a lazy reinit instead of reusing a closed handle. Thanks @carlotestor. (#6143)
- **The CLI/cron session-list projection cache is now bounded.** `_CLI_SESSIONS_CACHE` was an unbounded plain dict of deep-copied session-list projections whose fingerprint-folded key advances on every streamed message, so orphaned heavy copies accumulated on a long-lived server until the next structural clear. It's now an `OrderedDict` capped at 8 entries with drop-oldest LRU eviction (recency refresh on hit), mirroring the existing `_CLAUDE_CODE_PARSE_CACHE` / `_SIDECAR_METADATA_CACHE` pattern; TTL stays the freshness control. Thanks @rh-id. (#6140)
- **Models selected from a list-shaped custom-provider allowlist now route to the right provider.** A `custom_providers[].models` allowlist can be written as either a mapping or a list, and the model picker accepted both — but runtime routing only read mapping keys, so a model chosen from a *list*-shaped allowlist could silently stay on the active/default provider instead of its own. The supported-model-ID extraction is now centralized in one helper used by both catalog construction and routing, so the picker and runtime can no longer drift. Existing mapping-shaped and list-of-strings configs are unaffected. Thanks @franksong2702. (#6127, #6121)
- **Stale sidebar approval attention badge now clears once its gateway approval has drained.** A parent session could keep showing the red approval "attention" dot in the sidebar after the underlying gateway approval was already resolved/gone — the live gateway queue drained but the sidebar summary kept the mirrored approval alive. `_session_attention_summary()` now reconciles the gateway pending-mirror before counting attention (the same self-healing repair the approval-pending route already does), so the badge clears correctly. Live approvals stay visible and actionable and the clarify fallback is unchanged. This is the WebUI read-path mitigation; the child-session context-propagation root cause stays tracked in #6100. Thanks @rodboev. (#6117, #6100)
- **The mobile composer model picker no longer opens off-screen.** On phones the model dropdown could open below the fold (looking like it "won't open", issue #6080) because it lived inside `.composer-footer`, whose `container-type` (and, under the Geist Contrast skin, `backdrop-filter`) made the footer the containing block for the dropdown's fixed positioning — so it anchored to the bottom-of-screen footer instead of the viewport. The picker now reparents to `<body>` while open (the same idiom the profile dropdown already uses), so its fixed positioning resolves against the visual viewport on every skin; it flips above/below the anchor by available space, clamps below the titlebar, and re-positions as the on-screen keyboard / URL bar move the visual viewport. Desktop behavior is unchanged. Thanks @webtecnica. (#6105, #6080)
- **Approval and clarify popups no longer collapse to tiny blank windows on very narrow screens.** On very narrow viewports the clarify card could shrink to an unreadable sliver; it now gets a `min-width` and `overflow:visible` matching the approval card. Thanks @webtecnica. (#6104, #6094)
- **Stream event timestamps no longer break the row layout on narrow foldable screens.** On screens under 360px the per-event timestamps could spill out of tool/thinking card headers; the headers now clip overflow so the timestamp layout holds. (Tool names stay visible — an earlier draft hid them at ≤360px, which lost tool identity; the header clip alone prevents the spill.) Thanks @webtecnica. (#6106, #6099)
- **Streaming sidebar polls no longer re-run the expensive CLI/cron projection every few seconds.** The per-stream cache TTL was pinned at 30s while the frontend's streaming sidebar poll cadence could approach it, so the state.db CLI/cron projection (and its ~200 sidecar reads) could be re-paid on nearly every poll during a live turn (the #4842/#4808 high-CPU `get_cli_sessions` case). The streaming TTL is now held strictly above the sidebar poll interval (45s) and paired with the stable streaming cache key, so repeated polls reuse the projection; a new test asserts the TTL stays above the poll cadence so this can't silently regress. Thanks @starship-s. (#6102, #4842)
- **Delegated subagent sessions in the sidebar now show a meaningful title instead of "Subagent Session".** Generic delegated child sessions previously all displayed the placeholder "Subagent Session" in the conversations list, making them indistinguishable. Their sidebar title is now derived (view-only) from the child's first user message during the existing batched sidebar-metadata pass — without mutating the stored session title, so the read-only boundary is preserved. Only generic "Subagent Session" placeholders are affected; custom titles are left untouched. Thanks @rodboev. (#6056, #6033)
## [v0.52.76] — 2026-07-18
### Fixed