Commit Graph
100 Commits
Author SHA1 Message Date
0037061cfd Release exp-v0.52.45: errored-turn response visible (#5941) + recovery honors model pick (#5924) (#5964)
* fix(streaming): keep errored-turn assistant response visible (#5941)

An errored turn that produced assistant content (tool calls + reasoning)
folded that content into a collapsed worklog above the error card, so the
user read a lone error bubble as "nothing came back". The settled-scene
renderer collapsed the worklog unconditionally, never consulting the
turn terminal_state. Now an errored/failure terminal_state keeps the
worklog expanded by default (unless the user explicitly collapsed it),
while completed turns and genuinely-empty errored turns are unchanged.

Reported by @b3nw.

* fix(review): update #4970 brittle collapsed-expr assertion for #5941

test_issue4970_stream_done_shrink_regression asserted the literal
`collapsed:!keepSettledWorklogOpen` in _renderSettledAnchorSceneForMessage.
#5941 OR'd an errored-turn keep-open term into that expression
(`collapsed:!(keepSettledWorklogOpen||erroredWorklogKeepOpen)`), so the
substring no longer matched and the test failed in the full suite (the PR
updated its own touched tests but missed this pre-existing one).

Re-anchor to `collapsed:!(keepSettledWorklogOpen` — still guards #4970's
invariant (keepSettledWorklogOpen negates into the settled-render collapsed
flag) without pinning the exact term list. #4970 + #5941 suites green.

* fix(chat): honor explicit model pick on post-failure recovery send (#5924)

The onchange explicit-pick marker is single-shot: send() consumes it once.
submitEdit() and cmdRetry() truncated and called send() directly without
re-arming it, so the recovery send went out with explicit_model_pick=false and
the server's compatible-model resolution re-reverted a freshly-picked
cross-family model back to the failed/stale value (Facet 1). Facet 4 is the
same loop keeping the persisted model_provider pinned across fork/refresh.

Re-arm the pending explicit-pick marker from the current selector state
immediately before await send() in both recovery paths. Survives a second
consecutive recovery send. Normal send path and the #3737/#5731 server repair
guard are unchanged.

Reported by @b3nw.

* fix(#5924): gate recovery re-arm on genuine pick + session-race guards

Codex gate found 4 defects on the recovery send path; fixed all:
- /retry + edit-resubmit re-armed the explicit-pick marker UNCONDITIONALLY, forcing
  explicit_model_pick even with no fresh pick (suppressed server compatible-model
  resolution). Now gated on _recoveryPick (selector model differs from session's own
  stored model), captured pre-await.
- session-switch races: added active-session guards after the retry GET await and the
  edit truncate await so session A's recovery intent can't apply to session B.
- 3 new regression tests (gated re-arm, pre-await capture, post-await re-guard).

* fix(#5924) round-2: derive recovery pick from non-default session model, not inference

Codex round-2 CORE: the state-comparison predicate false-negatived an already-applied
pick (consumed marker → looks unchanged) and false-positived on provider inference
(@removed:mistral-large + null stored provider → inferred 'removed' → fake change).
Replace with _deliberateSessionModelPick(sid): reports {model,provider} only when the
session's OWN model is genuinely non-default vs window._defaultModel/_activeProvider —
inference-free + survives marker consumption. Both recovery paths + tests updated.

* fix(#5924) round-3: require known-default+owned-provider evidence + fire-time re-arm guard

Codex round-3: (1) _deliberateSessionModelPick still false-positived when the
profile default was unknown or the provider was only inferred — now requires a
session-OWNED provider AND a known window._defaultModel/_activeProvider, else
fails closed. (2) new same-session race: a model change DURING the recovery awaits
made the pre-await pick stale and clobbered a newer marker — new _reArmRecoveryPick
helper re-arms only if the current session model/provider still equals the captured
pick AND no different pending marker exists. Both recovery paths route through it.

* Release exp-v0.52.45: errored-turn response stays visible (#5950/#5941) + recovery honors model pick (#5949/#5924)

* fix: keep absoluteKeepCount capture before _recoveryPick in submitEdit

The #5924 _recoveryPick comment contained the word 'await', tripping the
pre-existing test_issue_edit_regenerate_absolute_keep_count regex (first \bawait\b
must come after the absoluteKeepCount capture). Reorder both synchronous pre-network
captures (absoluteKeepCount first) + reword the comment. No behavior change.

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-07-11 18:07:28 -07:00
3a8f980e2b Release exp-v0.52.44: Docker/no-git image update notices (#5733, #2729) (#5958)
* fix(#2729): show Docker image update notices

* Release exp-v0.52.44: show Docker/no-git image update notices (#5733, #2729)

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 16:46:13 -07:00
b7b6b3a4c8 Release exp-v0.52.43: trusted-header auth / reverse-proxy SSO (#5568, #3351) (#5956)
* feat(#3351): add trusted-header auth core

* fix(#3351): reconcile trusted sessions per request

* fix(#3351): preserve valid trusted group mappings

* fix(#3351): defer trusted logout reconciliation until after csrf

* fix(auth): clear _pending_set_cookies in reset_trusted_auth_request_state (keep-alive cookie cross-request leak)

Codex gate CORE finding: a Set-Cookie queued in request N but not flushed
survived onto the reused HTTP/1.1 handler and could be emitted by request N+1
— after trusted-identity rotation on logout this could overwrite a valid login
cookie and 401 the user. Reset the queue at the per-request boundary + add a
keep-alive regression test.

* docs(.env.example): document trusted-header auth / reverse-proxy SSO env vars

HERMES_WEBUI_TRUSTED_AUTH_HEADER + TRUSTED_PROXY_CIDRS (with exact-proxy-IP
security warning) + optional groups-header/JSON group-profile-map/logout-url.

* Release exp-v0.52.43: trusted-header auth / reverse-proxy SSO (#5568, #3351) + keep-alive cookie fix + docs

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 16:34:14 -07:00
18793a1ba4 Release exp-v0.52.42: ?profile= deep links (#5730) + Russian locale refresh (#5778) (#5953)
* Refresh Russian localization bundle

* feat(#5682): honor profile query during boot

* fix(profile): refresh deep-link reasoning state (#5682)

* test(ci): initialize reasoning transition harness state

* test(ci): accept reasoning fetch context override

* fix(#5682): preserve retryable profile query intent

* test(#5682): accept profile switch result contract

* fix(#5682): bind reasoning fetches to profile transitions

* Release exp-v0.52.42: ?profile= deep links (#5730) + Russian locale refresh (#5778)

---------

Co-authored-by: DrMaks22 <DrMaks22@users.noreply.github.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 15:58:47 -07:00
9a1acbff2a Release exp-v0.52.41: dedupe pending prompts across compaction boundary (#5920) (#5952)
* fix: dedupe pending prompt across compaction

* Release exp-v0.52.41: dedupe pending prompts across compaction boundary (#5920, starship-s)

---------

Co-authored-by: Hermes Agent <hermes-agent@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 15:26:05 -07:00
20d258d552 Release exp-v0.52.40: ACP sessions in sidebar (#5939) + date-stamped Claude 3.0 reasoning fix (#5934) (#5951)
* fix(reasoning): stop date-stamped Claude 3.0 ids from reading reasoning-capable

_candidate_supports_reasoning captured a trailing date stamp as the minor
version so bare date-stamped Claude 3.0 ids like claude-3-opus-20240229
satisfied the major==3 and minor>=7 gate and wrongly surfaced reasoning-effort
controls. Cap the minor group to 1-2 digits with a (?!\d) guard so the date
stamp is not read as a minor version -- the same date-stamp defense
_is_pre_adaptive_anthropic already uses. Claude 3.0/3.5 now correctly report
no reasoning support; 3.7+ and 4.x (incl. date-stamped builds) are unaffected.

* test(reasoning): cover date-stamped Claude 3.0 not reasoning-capable

Locks the heuristic so claude-3-opus-20240229 (and sibling date-stamped 3.0
ids) report no reasoning support while 3.7+/4.x stay capable.

* fix(session): classify ACP adapter sessions into the CLI sidebar family

Gateway ACP adapter sessions (source='acp' in state.db — Zed, external
device bridges like a Rabbit R1) normalised to session_source 'other',
which fell through BOTH sidebar buckets: sidebar_source=webui skips the
state.db projection entirely (and ACP rows have no WebUI sidecar), while
sidebar_source=cli keeps only CLI-classified rows. Result: ACP
conversations were invisible and could never be clicked/imported.

Classify 'acp' as a CLI-family source, mirroring 'tui' (a local
interactive agent client): normalize_agent_session_source maps it to
session_source 'cli' with label 'ACP', is_cli_session_row recognises the
raw source, is_cli_session_row_visible keeps ended/untitled ACP rows
visible (they are always user-driven), and frontend _isCliSession
accepts raw === 'acp'. Zero-message ACP connect/reconnect stubs stay
hidden via the existing message-count guard. Claim policy is untouched:
ACP stays claimable like CLI/TUI per the documented 'future local agent
sources' allowance in _is_claimable_cli_source.

Rollback: revert this commit (pre-change file copies also in
~/.hermes/backups/acp-sidebar-fix-20260711/), then restart WebUI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(session): require a user turn before surfacing ACP rows

An ended ACP connection can record only assistant/tool/system messages
(replayed or aborted turn), which the blanket TUI-style visibility
exception would surface despite not being user-driven. Gate the ACP
branch on _count_user_turns(row) > 0, per Greptile review on #5939.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Release exp-v0.52.40: ACP sessions in sidebar (#5939) + date-stamped Claude 3.0 reasoning-control fix (#5934)

---------

Co-authored-by: 黄云龙 <76432572+nankingjing@users.noreply.github.com>
Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 15:20:41 -07:00
nesquena-hermesandnesquena-hermes 29f67b4289 Release exp-v0.52.39: transparent-stream mode-precedence fix (#5942/#5943) + hermetic OpenRouter test (#1426) (#5948)
* fix(transparent-stream): stop compact worklog from hijacking transparent live render (#5942, #5943)

Transparent Stream turns with tool calls alternated between the grouped compact
worklog summary ("Processed Ns") and the individual per-event row list (#5942), and
rebuilt every live row element each tick causing flicker (#5943, regression of #5367).

Root cause (bisected to the #5739 merge as a MERGE INTERACTION; #5700/#5739 timestamp
code is NOT causal): #5746 (final-answer-only mode) reworked renderLiveAnchorActivityScene
so a caller-supplied `requestedMode` (opts.mode) OVERRODE the user's active display mode.
Several live callers (appendLiveToolCard, ensureLiveWorklogShell, appendLiveCompressionCard,
appendLiveThinking, session restore in sessions.js) hardcode {mode:'compact_worklog'}.
Before #5746 the dispatch checked isTransparentStream() FIRST and ignored that hint; after,
those callers forced the compact grouped render onto a transparent turn. The compact pass
deletes the keyed transparent rows, so the next transparent tick's preserveByKey reuse
(the #5367 element-reuse fix) misses -> it rebuilds a grouped frame then tears it down and
recreates every row = alternating + flicker.

Fix (two layers, defence in depth):
1. renderLiveAnchorActivityScene: the USER's active chatActivityMode() is authoritative;
   opts.mode is only a fallback when there is no usable active mode. A compact_worklog hint
   can no longer override an active transparent_stream (restores the good-build precedence).
2. Removed the now-dead hardcoded {mode:'compact_worklog'} argument from all 8 current-view
   live/restore call sites (ui.js x5, sessions.js x3) so intent is explicit and the helpers
   resolve the active mode via chatActivityMode().

Compact_worklog folding, hide_all_activity suppression, the #5700/#5739 timestamp feature,
and #5860 lazy settled-worklog materialization are all unaffected.

Verified with the crown-jewel stream regression gate (private workspace): transparent_stream
goes rowElementSwaps 1105->0 and groupedFramesInTransparent 42->0; hide_all_activity clean.
Independently root-caused + fix-shape-confirmed by Codex adversarial review.

* fix(transparent-stream): also make projection mode active-authoritative in _renderLiveAnchorActivitySceneForStream (Fable advisory — kill latent requestedMode-wins drift)

* test(#5942/#5943): update brittle source-string assertions for removed {mode:'compact_worklog'} arg

test_live_to_final_anchor_visible_order.py asserted the literal presence of the now-removed
hardcoded compact-mode arg (in ensureLiveWorklogShell + the sessions.js restore path). The fix
correctly drops that arg so the helpers resolve the active display mode. Updated both to assert
the mode-free call / active-mode behavior instead of the removed literal. 63/63 in-file pass.

* revert wrapper projection-mode change (preserve #3820 explicit-mode contract)

The follow-up 8f654f0c0 made _renderLiveAnchorActivitySceneForStream projection-mode
active-authoritative, but that broke the #3820 contract (test_issue3820: explicit hide_all
honored when active=compact; hide_all-active always wins). Fable flagged this only as an
advisory/latent cleanup, NOT required — the real fix is the central renderLiveAnchorActivityScene
dispatch + call-site arg removal, both proven by the gate. Reverting the wrapper restores #3820
(109/109 targeted tests pass) with the bug fix fully intact.

* test(#1426): make OpenRouter live-fetch test hermetic by mocking curated fetch

test_openrouter_group_uses_live_fetch_when_available failed deterministically
in any environment where the hermes_cli agent package is installed AND the
network is reachable, e.g.:

    pytest "tests/test_issue1426_openrouter_free_tier_live_fetch.py::\
test_openrouter_group_uses_live_fetch_when_available" -p no:xdist

    assert 'minimax/minimax-m2.5:free' in
        ['anthropic/claude-sonnet-4.6', 'anthropic/claude-fable-5', ...]

Root cause — a gap in the test's mock, not a production regression:

get_available_models() populates the OpenRouter group from TWO live fetches:

  (1) hermes_cli.models.fetch_openrouter_models() — the curated catalog.
      This routes through hermes_cli's open_credentialed_url(), which builds
      its own opener and calls .open(); it does NOT go through the
      module-level urllib.request.urlopen symbol the test monkeypatches.
  (2) a direct urllib.request.urlopen(".../v1/models") free-tier augment —
      this one IS intercepted by the monkeypatch.

The test only patched urllib.request.urlopen, so fetch (1) hit the real
network and returned ~36 live curated models. Those filled the visible picker
cap (15), pushing the mocked free-tier entries (minimax/minimax-m2.5:free,
openrouter/elephant-alpha) into the group's extra_models bucket. The
assertion only inspected the visible `models` bucket, so it failed. In CI
(hermes_cli absent) fetch (1) raised ImportError, the curated base was empty,
the 2-3 mocked entries fit the visible cap, and the test passed — hence the
environment-dependent failure.

Production is correct: free-tier models DO surface in the picker (verified
they land in extra_models). The fix keeps the coverage intent intact:

  - Mock fetch (1) too (as sibling tests already do) so the test is genuinely
    hermetic — matching this module's docstring promise of "without depending
    on real network access" — with a small curated base so live data, not the
    fallback list, is exercised.
  - Assert the free-tier IDs surface across BOTH picker surfaces (`models` +
    `extra_models`), matching the sibling fail-closed and cap tests, so a
    large curated base overflowing the visible cap no longer flips the result.

No production code changed.

* Release exp-v0.52.39: transparent-stream mode-precedence fix (#5946/#5942/#5943) + hermetic OpenRouter live-fetch test (#5947/#1426)

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-07-11 14:40:59 -07:00
nesquena-hermes 02227d07ca Merge pull request #5931 from nesquena/release/stage-5709
Release exp-v0.52.38: ROADMAP.md accuracy refresh (#5709)
2026-07-11 01:24:59 -07:00
nesquena-hermes cc38b57369 Merge pull request #5930 from nesquena/release/stage-5929
Release exp-v0.52.37: context-selection counter reset (#5929)
2026-07-11 01:08:51 -07:00
nesquena-hermes fcf313d843 Merge pull request #5928 from nesquena/release/stage-5927
Release exp-v0.52.36: redaction-awareness prompt guidance (#5927)
2026-07-11 00:35:55 -07:00
nesquena-hermes 751fbc69d0 Merge pull request #5925 from nesquena/release/stage-5917
Release exp-v0.52.35: stale sidebar unread-dot on visit (#5917)
2026-07-10 23:36:24 -07:00
nesquena-hermes 701e5a2959 Merge pull request #5923 from nesquena/release/stage-batch-reliability
Release exp-v0.52.34: salvage-reliability batch (#5909, #5912, #5915)
2026-07-10 22:41:58 -07:00
nesquena-hermes 6abb447b89 Merge pull request #5922 from nesquena/release/stage-5910
Release exp-v0.52.33: max reasoning-effort level, availability-gated (#5910)
2026-07-10 22:01:59 -07:00
nesquena-hermes 17e7fb7811 Merge pull request #5921 from nesquena/release/stage-5473
Release exp-v0.52.32: opt-in start-new-chat-on-workspace-switch (#5473)
2026-07-10 21:27:42 -07:00
nesquena-hermes 101421ff67 Merge pull request #5914 from nesquena/release/stage-5337
Release exp-v0.52.31: public read-only conversation sharing (#5337)
2026-07-10 19:50:34 -07:00
nesquena-hermes 91804b30fc Merge pull request #5908 from nesquena/release/stage-5739
Release exp-v0.52.30: worklog event timestamps (#5739)
2026-07-10 17:56:34 -07:00
nesquena-hermes bc3bc31172 Merge pull request #5906 from nesquena/release/stage-5746
Release exp-v0.52.29: final answer only activity display mode (#5746)
2026-07-10 17:07:51 -07:00
nesquena-hermes 62a185e453 Merge pull request #5904 from nesquena/release/stage-5792
Release: CSV media download affordance (#5792)
2026-07-10 14:52:25 -07:00
nesquena-hermes 63fe02e209 Merge pull request #5901 from nesquena/release/stage-5677
Release: resumable session SSE stream (#5677, #4812)
2026-07-10 13:56:20 -07:00
nesquena-hermes 157714a1b7 Merge pull request #5898 from nesquena/release/stage-5732
Release: pause credential-exhausted process wakeups (#5732, #3929)
2026-07-10 12:37:06 -07:00
nesquena-hermes b6ebb72960 Merge pull request #5897 from nesquena/release/stage-5887
Release: focus composer after URL prefill (#5887)
2026-07-10 11:50:21 -07:00
nesquena-hermes e9caa7ba95 Merge pull request #5893 from nesquena/release/stage-5224
Release: stale-session-load pane isolation (#5889) + fail-closed OpenRouter pricing (#5882)
2026-07-10 11:02:47 -07:00
d7c3c2b734 Release: repair stale session provider binding at chat start (#5873, #5731) (#5881)
* fix(#5731): repair poisoned session provider at chat start

* fix(#5731): fail safe on incomplete catalog ownership

* changelog: session provider-repair on stale binding (#5873, #5731)

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: t <a@b>
2026-07-10 06:11:34 -07:00
de0e6eb452 Release: centralize auxiliary-model task catalog (#5865) (#5870)
Auxiliary-model task definitions were maintained in BOTH Python and JS and could
drift (the retired session_search task lingered in one). Centralize to a single
backend catalog that panels.js renders+saves from exclusively; purge the retired
slot on reset while preserving other custom bindings; old configs still load.

Gate-pass: Codex SAFE (only session_search removed, 11 other tasks + defaults
unchanged, 22 keys match across 15 locales, legacy migrate preserves custom
bindings, executable DOM harness rendered all rows). 53 own + 887 regression green.

Co-authored-by: t <a@b>
Co-authored-by: starship-s <starship-s@users.noreply.github.com>
2026-07-09 20:31:54 -07:00
nesquena-hermesandt 9c1e77de48 Release: fix Experimental channel version display on stable-tagged installs (#5864, #5862) (#5868)
* fix(updates): scope installed-version fallback to webui + verified current_sha (#5864 gate fixes)

Gate (Codex) found 2 CORE in the #5862/#5864 experimental-version-display fix:
1. api/updates.py:947 — the installed-version fallback ran in shared _check_repo_release
   for BOTH repos, injecting WEBUI_VERSION (v0.52.0, not an Agent ref) into Agent payloads.
   Now scoped to name=='webui'.
2. api/updates.py:996 — current_sha reused current_version_display, but WEBUI_VERSION can be
   vX.Y.Z-dirty-<hash>/-N-g<sha>/bare-SHA/unknown (not git refs) -> broken /compare link +
   lost update-summary subjects. current_sha now derives from a git-VERIFIED exact-match tag
   (channel-neutral) or None; current_version keeps the real display string.

+2 regression tests (agent-repo-no-inject, dirty-HEAD-verified-sha). 21/21 update-channel green.

* changelog: #5864 experimental-channel version display fix (#5862)

---------

Co-authored-by: t <a@b>
2026-07-09 20:18:03 -07:00
nesquena-hermesandt 803b081506 Release: lazy-render collapsed worklog rows w/ post-process + disclosure preservation (#5860, #5839) (#5866)
* fix(#5839): post-process + restore disclosure on lazily-materialized worklog rows

Gate (live Chromium) found the deferred-render path skipped two things the eager
path does: (1) materialized Markdown rows bypassed postProcessRenderedMessages ->
lost syntax highlight / copy button / mermaid / katex / trees; (2) detail-disclosure
(open tool/thinking card + scroll) was lost across rebuilds because restore ran
before deferred rows existed. _materializeDeferredWorklogRows now rAF-schedules
_postProcessWithAnchorSuppression + re-applies stashed disclosure state; the rebuild
restore pass stashes the captured state on still-deferred groups. +2 regression tests.

* changelog: #5860 lazy-worklog frontend fix (#5839, #4633)

---------

Co-authored-by: t <a@b>
2026-07-09 19:25:11 -07:00
e4342a65cf Release: split anchor_activity_scenes after messages to fix #4633 server memory growth (#5858, #5854) (#5863)
Reasoning-heavy sessions accumulate 250-480KB of anchor_activity_scenes serialized
BEFORE the messages array, so the 64KB metadata-prefix read always overflowed ->
full-parse fallback on every read -> memory growth. Move scene data AFTER messages;
metadata prefix now ~2KB, modern reads skip the full parse. Old-layout files still
load (one authoritative parse then cached); round-trip preserves scenes exactly.

Independent review: nesquena APPROVED (end-to-end, on head). Gate-pass: Codex SAFE
(933KB legacy sidecar loads clean + round-trip verified), 17 own + 2945 regression green.

Co-authored-by: t <a@b>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-07-09 19:05:54 -07:00
e0afdcb546 Release: write Office upload sidecars (#5734, #540) (#5859)
On .docx/.xlsx/.pptx upload, write a best-effort <name>.md sidecar with extracted
text (reuses api/office_documents.py — no new dep). Path-contained (O_NOFOLLOW
openat-walk within workspace), collision-safe (O_CREAT|O_EXCL never overwrites an
existing .md), best-effort (sidecar failure leaves the Office file intact, partial
cleanup on failure). Nathan concept-approved.

Gate-pass: Codex SAFE (containment + exclusivity + best-effort all verified), 27 own
+ 1443 upload/workspace regression tests green.

Co-authored-by: t <a@b>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-09 15:57:21 -07:00
34342b9f4d fix(security): trusted-proxy-verified local-origin gate (fixes #5764 terminal-gate bypass) (#5857)
* fix(security): trusted-proxy-verified local-origin gate (maintainer fix of #5764)

Rebuild _onboarding_request_is_local with a single, symmetric trust model that
closes the embedded-terminal local-gate bypass AND fixes the direct-LAN lockout
regression from the contributor's #5764 attempt (bounced with these findings):

1. SPOOF CLOSED: a forwarded client IP is honored ONLY when the un-spoofable raw
   socket peer is a trusted proxy (loopback, or HERMES_WEBUI_TRUSTED_PROXY_CIDRS).
   A public direct client sending X-Forwarded-For: 127.0.0.1 is judged on its raw
   public peer -> DENY. New _raw_peer_is_trusted_proxy + _forwarded_client_ip_from
   _trusted_proxy consume the FULL XFF chain (get_all, repeated headers), walk it
   right-to-left skipping trusted hops, and fail closed on empty/blank/garbage.
2. NO LAN LOCKOUT: a direct loopback/LAN client with no proxy header stays local
   (onboarding, first-password/passkey, passwordless terminal keep working).
3. COMPAT: TRUST_FORWARDED_FOR=1 stays the opt-in that consults the chain; the new
   TRUSTED_PROXY_CIDRS only WIDENS trust to a non-loopback proxy (loopback always
   trusted). Shipped private-peer-with-forwarded-header deny nuance preserved.

Updated the one existing test that encoded the vulnerable behavior; added a
21-case adversarial truth-table regression (tests/test_cvd3_terminal_local_origin_gate.py).
66 gate tests + 720 auth/terminal regression pass.

Co-authored-by: Hinotoi-agent <Hinotoi-agent@users.noreply.github.com>

* fix(security): family-aware trusted-proxy membership (IPv4-mapped-IPv6)

Codex re-gate CORE: a mapped-IPv6 address (::ffff:10.9.9.9) never matched an
IPv4 CIDR allowlist via 'addr in net' -> (1) legit proxy treated as untrusted =
lockout; (2) mapped trusted HOP in the XFF chain mis-returned as client -> a
preceding public client admitted to the terminal gate. New _ip_in_networks
helper checks addr AND addr.ipv4_mapped; both the peer check and the chain-walk
_is_trusted_hop route through it. +3 mapped-IPv6 regression cases (incl the
public-client-hidden-behind-mapped-trusted-hop security case).

---------

Co-authored-by: t <a@b>
Co-authored-by: Hinotoi-agent <Hinotoi-agent@users.noreply.github.com>
2026-07-09 15:33:16 -07:00
1a64d7d313 Release: fix workspace image previews + downloads under subpath mounts (#5830) (#5856)
_workspaceRouteForPath now resolves app-relative /api/... routes against document.baseURI
so the direct consumers (previewImg.src, media/pdf/html frame src, download href,
window.open) work under a subpath mount instead of 404ing at the server root. Root-mount
deployment unaffected (identical final URLs); api() callers not double-prefixed.

Gate-pass: Codex SAFE (no root-mount regression, all 6 consumers verified), 825 workspace
regression tests + browser-smoke green. Nathan-approved.

Co-authored-by: t <a@b>
Co-authored-by: tomtong2015 <tomtong2015@users.noreply.github.com>
2026-07-09 14:38:53 -07:00
a0067b0b24 Release: classify external_agent imports as CLI to match client render (#5846, #5831) (#5855)
Fixes the #5831 brick: "WebUI sessions (N)" chip showed a count but an empty list.
Server is_cli_session_row classified read-only external_agent imports (Claude Code,
Codex) with a real title as non-CLI (counted under webui_session_count), while the
client _isCliSession filed them in the CLI bucket -> mismatch. Server now classifies
external_agent/external-agent as CLI, matching the client. WebUI/messaging/subagent
unaffected (their exclusions precede the new branch); external_agent imports stay read-only.

Self-built (nesquena-hermes); independent nesquena review APPROVED on this head (17:28);
gate-pass (Codex SAFE, no over-capture, caller audit clean), 14 own + 180 regression green.

Co-authored-by: t <a@b>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-07-09 14:20:20 -07:00
f449ff21c6 Release: hard-prune post-compression tool context (#5667) (#5852)
WebUI hard token-cap on protected-tail tool-result payloads in the MODEL CONTEXT
only, applied once after a confirmed compression event, to prevent post-compression
context blowup. Deep-copy content-replace only (no message removal/reorder → no
orphaned tool_result); visible transcript (s.messages) untouched.

Deep-review re-gate: Codex SAFE (pairing-safety + context-only + idempotent verified
by reproduction), 65 own + 992 regression tests green. Crown-jewel streaming path.

Co-authored-by: t <a@b>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-07-09 13:41:31 -07:00
46720a39dd Release: Copy conversation link copies an openable URL (#5478) (#5851)
Was copying the internal session:// reference (not browser-openable); now copies
an absolute origin + /session/<id> URL (verified single-origin, no doubling —
_sessionUrlForSid returns a relative path, the caller prepends origin once),
tracking params stripped. Product/UX parity with major chat apps (Anchor C).

Nathan-approved. 17 copy-link regression tests green.

Co-authored-by: t <a@b>
Co-authored-by: djsavvy <djsavvy@users.noreply.github.com>
2026-07-09 13:24:45 -07:00
8cc2d6f881 Release: model-picker Selected badge + Transparent Stream dupe-prefix suppression (#5757, #5758) (#5850)
Two visible render PRs, gate-passed (deep-review round), Nathan-approved to ship.

- #5757 (rodboev): mark active model with a Selected badge + open its provider group (#5748).
  Screenshot-verified (badge clear + correctly placed on active row).
- #5758 (rodboev): suppress the live-token prose row that is a strict prefix of the final
  answer (#5749). Provenance-gated (live-prose:* token rows after last tool row only) so it
  can't eat legitimate narration; reproduction test proves the dupe is gone.

Codex SAFE + full regression green (1576 render tests). 18 own tests pass together.

Co-authored-by: t <a@b>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-09 12:51:18 -07:00
2b457f1646 Release: cap StreamChannel offline replay buffer (#5788) (#5849)
Bounded deque(maxlen) drop-oldest so a disconnect-without-cancel can't grow the
per-turn offline replay buffer unbounded (per-abandoned-turn OOM risk, #4633 family).
Dropped frames recovered via run-journal last_event_id replay path.

Deep-review gate: Codex SAFE (journal-recovery verified end-to-end), 20 own + 1106
regression tests green. Crown-jewel streaming path.

Co-authored-by: t <a@b>
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-07-09 12:26:05 -07:00
d6e5dc302d Release Batch D: terminal resource-leak fix + fsync atomic writers (#5835, #5843) (#5848)
Phase 2 backend reliability/data-integrity batch. Each byte-verified vs PR head,
Codex SAFE, 20 own + 355 regression tests pass, disjoint files.

- #5835 (ai-ag2026): stop leaking fd + reader thread + shell on client/shell exit (#4633 family)
- #5843 (ai-ag2026): fsync atomic JSON writers (oauth/passkeys/discoverability) + thread-safe temp

Co-authored-by: t <a@b>
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-07-09 10:54:06 -07:00
7a1a122e6e Release Batch C: hidden-tab dashboard poll + SSE write-deadline + rate-limit map bound (#5840, #5841, #5842) (#5847)
Phase 2 low-risk backend/perf batch (ai-ag2026 reliability cluster). Each byte-verified
vs PR head, Codex SAFE, 33 own + 720 regression tests pass, disjoint routes.py regions.

- #5840: skip dashboard status poll while tab hidden (perf; forced polls unaffected)
- #5841: arm write deadline on session-events invalidation SSE (stuck-client teardown)
- #5842: bound client rate-limit maps by sweeping stale keys (long-uptime memory)

Co-authored-by: t <a@b>
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-07-09 10:38:47 -07:00
1e275d04ed Release: queue gateway-owned steer fallback (#5823) (#5845)
Steering a live gateway-owned run had no process-local agent in SESSION_AGENT_CACHE,
so it errored with "No agent cached" and dropped the typed text. Now detects the live
gateway ACTIVE_RUNS stream and queues the steer as the next turn (model/provider/profile
snapshotted first), updates the queue badge, clears the draft, shows a toast — without
cancelling the stream. Gateway sibling of the shipped #5597 fallback.

Codex SAFE + Fable SHIP-UX; byte-identical to PR head; 66 steer/busy-input tests pass;
Nathan visual-approved (desktop + mobile screenshots).

Co-authored-by: t <a@b>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-09 10:04:21 -07:00
91606a15e0 Release Batch B: OPTIONS preflight framing + session-index bytes-parse (#5828, #5834) (#5838)
Phase 1 low-risk backend batch (on top of Batch A). Both byte-verified vs PR head,
Codex SAFE, 64 own + 749 regression tests pass, coexists with Batch A's #5833.

- #5828 (ai-ag2026): frame CORS OPTIONS preflight with Content-Length: 0 (keep-alive hang fix)
- #5834 (ai-ag2026): parse session _index.json from bytes not decoded str (all 10 sites)

Co-authored-by: t <a@b>
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-07-09 09:19:14 -07:00
0d3ee7de9f Release Batch A: worktree cli-shadow + atomic settings.json + state.db read-only (#5832, #5829, #5833) (#5837)
Phase 1 low-risk backend reliability batch. Three independent contributor fixes
on distinct files, each byte-identical to its PR head, Codex SAFE, 691+16 tests pass.

- #5832 (silent-reader-cn): importlib-load agent cli.py by absolute path so a
  shadowing cli/ namespace package can't brick worktree creation
- #5829 (ai-ag2026): atomic settings.json writes (temp+fsync+os.replace, mode-preserving)
- #5833 (ai-ag2026): open the remaining 8 state.db read projections read-only

Co-authored-by: t <a@b>
Co-authored-by: silent-reader-cn <silent-reader-cn@users.noreply.github.com>
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-07-09 09:04:59 -07:00
nesquena-hermes 819949fd8c Merge pull request #5825 from nesquena/release/stage-5789
Release: gateway SSE terminal idle-timeout, honor Stop (#5789)
2026-07-09 01:10:12 -07:00
nesquena-hermes 531525269e Merge pull request #5824 from nesquena/release/stage-5557
Release: centralize WebUI static asset resolution (#5557)
2026-07-08 23:37:35 -07:00
nesquena-hermes 66d3b6476d Merge pull request #5822 from nesquena/release/stage-5803
Release: session-load latency perf (#5803)
2026-07-08 23:10:54 -07:00
nesquena-hermes 216eb77ade Merge pull request #5821 from nesquena/release/stage-5800
Release: document lock-free GIL contract for STREAM_* buffers (#5800)
2026-07-08 22:55:01 -07:00
nesquena-hermes 0c49ed832f Merge pull request #5820 from nesquena/release/stage-5768
Release: localize process wakeup label (#5768)
2026-07-08 21:40:10 -07:00
nesquena-hermes d4e80b4549 Merge pull request #5819 from nesquena/release/stage-5798
Release: single-flight remote gateway health probe (#5798)
2026-07-08 20:58:55 -07:00
nesquena-hermes b3496c3cde Merge pull request #5818 from nesquena/release/stage-5799
Release: cache run-journal next-seq per path (#5799)
2026-07-08 20:53:25 -07:00
nesquena-hermes 4781f0f594 Merge pull request #5817 from nesquena/release/stage-5801
Release: clearInterval (not clearTimeout) for login connectivity retry (#5801)
2026-07-08 20:48:32 -07:00
nesquena-hermes a61d7bd3a6 Merge pull request #5815 from nesquena/release/stage-5766
Release: render background wakeups as status rows (#5766)
2026-07-08 20:26:12 -07:00
nesquena-hermes 432f596cfb Merge pull request #5814 from nesquena/release/stage-5762
Release: derive service tier from agent fast-mode metadata (#5762)
2026-07-08 20:21:11 -07:00
nesquena-hermes 4111e097c8 Merge pull request #5813 from nesquena/release/stage-5773
Release: keep live reasoning on one renderer path (#5773)
2026-07-08 20:16:18 -07:00
nesquena-hermes 45575f588a Merge pull request #5812 from nesquena/release/stage-5770
Release: contain streaming code block layout (#5770)
2026-07-08 20:12:05 -07:00
nesquena-hermes 862a121b24 Merge pull request #5811 from nesquena/release/stage-5721
Release: workspace switcher screen reader accessibility (#5721)
2026-07-08 19:46:24 -07:00
nesquena-hermes d6e6e91de4 Merge pull request #5810 from nesquena/release/stage-5729
Release: keep child streams from moving parent sidebar bucket (#5729)
2026-07-08 19:40:04 -07:00
nesquena-hermes 522e90b5b8 Merge pull request #5809 from nesquena/release/stage-5786
Release: handle terminal output EventSource transport errors (#5786)
2026-07-08 19:35:21 -07:00
nesquena-hermes 9765f0e45b Merge pull request #5808 from nesquena/release/stage-5761
Release: refresh OpenCode Go static catalog (#5761)
2026-07-08 18:28:43 -07:00
nesquena-hermes f8f60d5fdd Merge pull request #5807 from nesquena/release/stage-5782
Release: back off bg drain loop instead of silent tight-spin (#5782)
2026-07-08 18:22:36 -07:00
nesquena-hermes 8e00277ce5 Merge pull request #5805 from nesquena/release/stage-5785
Release: frame passkey-login 200 with Content-Length (#5785)
2026-07-08 18:10:07 -07:00
nesquena-hermes 9dacbe60de Merge pull request #5804 from nesquena/release/stage-5787
Release: pair metering begin_session with end_session teardown (#5787)
2026-07-08 17:55:24 -07:00
nesquena-hermes 4fc637d8d0 Merge pull request #5790 from nesquena/feat/update-channels
Release channels: stable / experimental self-updater
2026-07-08 17:12:50 -07:00
nesquena-hermes 3730a4a551 Merge pull request #5796 from nesquena/release/stage-5784
Release: evict run-journal writer-lock cache on journal delete (#5784)
2026-07-08 14:23:09 -07:00
nesquena-hermes 6ab95bffa4 Merge pull request #5794 from nesquena/release/stage-5783
Release: serialize daemon-thread start under a lifecycle lock (#5783)
2026-07-08 14:03:05 -07:00
nesquena-hermes 460f3eb47d Merge pull request #5793 from nesquena/release/stage-5781
Release: sweep completion-dedup map by delivery lifecycle (#5781)
2026-07-08 13:47:35 -07:00
nesquena-hermes c008e4443a Merge pull request #5791 from nesquena/release/stage-5780
Release: open state.db read-only for lineage reads + watcher (#5780)
2026-07-08 13:22:57 -07:00
nesquena-hermes 16272dd646 Merge pull request #5777 from nesquena/release/stage-5772
Release: scope MCP list config reads to active profile (#5772)
2026-07-08 09:23:22 -07:00
nesquena-hermes 07b9708cde Merge pull request #5756 from nesquena/release/stage-5751
Release: non-virtualized transcript scroll jump-back (#5751)
2026-07-07 14:27:32 -07:00
nesquena-hermes 923cadcb80 Merge pull request #5754 from nesquena/release/stage-5742
Release: desktop stale-snapshot scroll compensation (#5742)
2026-07-07 13:19:28 -07:00
nesquena-hermes 57aa5be2ed Merge pull request #5753 from nesquena/release/stage-5463-v2
Release: mirror WebUI usage counters to state.db (#5463)
2026-07-07 13:12:06 -07:00
nesquena-hermes 1b58e4df47 Merge pull request #5750 from nesquena/release/stage-5744
Release: clear dead steer busy state (#5723)
2026-07-07 09:45:35 -07:00
nesquena-hermes c3ff9f41c7 Merge pull request #5741 from nesquena/release/stage-5738
Release: keep the composer above iPad keyboards (#5701)
2026-07-06 22:20:06 -07:00
nesquena-hermes 021bc1280f Merge pull request #5740 from nesquena/release/stage-5737
Release: drop empty tool_calls arrays before API send (#5737)
2026-07-06 21:56:50 -07:00
nesquena-hermes ad7d0abf44 Merge pull request #5736 from nesquena/release/stage-tlsflake
test(tls): drain full stdout for the TLS-fallback assertion (flake fix)
2026-07-06 21:14:27 -07:00
nesquena-hermes b0c79fd4d3 Merge pull request #5735 from nesquena/release/stage-5567-resolver
Release: #5567 model-picker provider resolution fix
2026-07-06 20:56:50 -07:00
nesquena-hermes 1d8208026c Merge pull request #5728 from nesquena/release/stage-5419
Release: auto-switch profiles for cross-profile session links (#5419)
2026-07-06 19:39:22 -07:00
nesquena-hermes 243c0f698d Merge pull request #5725 from nesquena/release/stage-5692
Release: Windows git subprocess console-window fix (#5692)
2026-07-06 19:25:03 -07:00
nesquena-hermes 8fee7da3ee Merge pull request #5724 from nesquena/release/stage-5717
Release: iteration-limit graceful summary (#5717)
2026-07-06 18:48:36 -07:00
nesquena-hermes c4a3d4f816 Merge pull request #5716 from nesquena/release/stage-5664
Release: preserve workspace file-tree scroll position (#5664)
2026-07-06 16:29:05 -07:00
nesquena-hermes 5ecd248e92 Merge pull request #5708 from nesquena/release/stage-5688b
Release: self-update .git/index.lock recovery (#5688)
2026-07-06 14:52:25 -07:00
nesquena-hermes 8d5d28903c Merge pull request #5546: add Czech (cs) locale support
Complete Czech locale (1642/1642 key parity, all function values, login screen, test guard). Rebased + translation-quality fixes by maintainer; original author @ostravajih. Gate: full suite + Codex SAFE TO SHIP.
2026-07-06 14:42:12 -07:00
nesquena-hermes bb0439dcfa Merge pull request #5707 from nesquena/fix/last-two-flakes
Fix the last 2 chronic local full-suite test-isolation flakes (test-only)
2026-07-06 14:24:13 -07:00
nesquena-hermes ade6de5294 Merge pull request #5704 from nesquena/fix/5696-profiles-ttl
fix(#5696): revert profiles cache TTL to 4s (stale profile rows)
2026-07-06 13:44:17 -07:00
nesquena-hermes 116d81d3c8 Merge pull request #5703 from nesquena/release/stage-5696
Release: session-load latency fix (#5696)
2026-07-06 13:36:48 -07:00
nesquena-hermes e785264e4e Merge pull request #5702 from nesquena/release/stage-flakefix
Fix chronic full-suite test-isolation flakes (hermes_cli __path__ poison, 8 tests)
2026-07-06 12:57:23 -07:00
nesquena-hermes 40ac0c6be2 Merge pull request #5697 from nesquena/release/stage-5674
Release: stabilize sidebar lineage grouping across live refreshes (#5674)
2026-07-06 11:25:36 -07:00
nesquena-hermes 93b7ed90bf Merge pull request #5695 from nesquena/release/stage-5672
Release: reserve real off-screen height for user rows (#5672)
2026-07-06 11:13:56 -07:00
nesquena-hermes c93672abee Merge pull request #5694 from nesquena/release/stage-5666
Release: refuse stale-anchor scroll restore during streaming, Android (#5666)
2026-07-06 10:56:52 -07:00
nesquena-hermes cfc0ffe102 Merge pull request #5693 from nesquena/release/stage-5681
Release: restore unpinned reader after mid-stream scroll clamp (#5681)
2026-07-06 10:45:24 -07:00
nesquena-hermes 501d7184ff Merge pull request #5691 from nesquena/release/stage-5685
Release: stop pinned tail-follower mid-stream scroll jitter (#5685)
2026-07-06 10:33:39 -07:00
nesquena-hermes 426f12ce51 Merge pull request #5690 from nesquena/release/stage-5673
Release: proxy-auth PWA caveat docs (#5673)
2026-07-06 10:13:02 -07:00
nesquena-hermes 26a829c952 docs: weekly contributor tally refresh (20260706) (#5689) 2026-07-06 09:29:07 -07:00
nesquena-hermes f80e641a9a Merge pull request #5670 from nesquena/release/stage-5668
Release: shared layout-assertion test helpers (#5668)
2026-07-05 23:19:11 -07:00
nesquena-hermes 79d3b5996d Merge pull request #5669 from nesquena/release/stage-visible-2026-07-06
Release: visible-UI batch — mobile nav (#5605), mermaid mobile (#5560), steer upload scope (#5630), header font-scale (#5633), three-panel floor (#5594)
2026-07-05 22:53:31 -07:00
nesquena-hermes 7721e530fb fix(#5657): preserve workspace file-tree scroll position across re-render
renderFileTree() clears its scroll container with box.innerHTML='' and rebuilds
every row. Detaching the rows collapses scrollHeight, so the browser clamps
scrollTop to 0 — every folder expand/collapse, breadcrumb nav, refresh, and
hidden-files toggle that re-runs the renderer teleported the reader to the top of
a long tree, losing their place (the dominant interaction in the panel).

Fix: capture box.scrollTop before the innerHTML='' wipe and restore it after the
normal render tail (after _renderTreeItems repaints and the container is tall
again). A plain scrollTop restore is sufficient and correct here — expand/collapse
insert or remove rows BELOW the clicked disclosure, so the clicked row keeps its
offset from the top; no getBoundingClientRect anchor delta is needed (that's only
for the prepend-above case in the chat messages list). The reporter's data-path
anchor sketch is deliberately NOT used: _renderTreeItems rows carry no data-path,
so that selector would match nothing.

The two early-return paths (no-workspace hides the box; empty-dir has nothing to
scroll) are left untouched — they legitimately reset. The restore guards only the
normal render tail. loadDir's two-render sequence (initial + post-expanded-dirs
prefetch) is handled correctly: each renderFileTree() call independently captures
then restores its own pre-wipe scrollTop across the await.

Test: tests/test_issue5657_filetree_scroll_preserve.py — static guards that the
capture precedes the wipe and the restore follows _renderTreeItems (per the
maintainer test plan; drops the getBoundingClientRect anchor assertion since the
fix deliberately doesn't use it). Fail-without-fix verified; 355 workspace-tree
regression tests green.

Closes #5657.
2026-07-06 04:36:13 +00:00
nesquena-hermes 3403870ef9 Merge pull request #5663 from nesquena/release/stage-phase1-2026-07-06
Release: Phase-1 batch — table whitespace (#5644), empty error-hint guard (#5653), Playwright CI (#5661)
2026-07-05 21:17:40 -07:00
nesquena-hermes 90102892e0 Merge pull request #5662 from nesquena/review-5655
Release: compression-recovery card on reloaded sessions (#5655)
2026-07-05 20:08:56 -07:00
nesquena-hermesandnesquena-hermes 9738ebb614 fix(tests): sync on subscriber count in multitab SSE test (#5628) (#5660)
test_same_stream_in_two_tabs_receives_identical_token_sequence started both
SSE-handler threads then immediately put_nowait'd the tokens + stream_end
without waiting for both threads to subscribe. StreamChannel.put_nowait()
buffers events only while there are zero subscribers and clears that buffer
the moment the first subscriber attaches and broadcasts live — so producing
events in the window after tab A subscribes but before tab B does orphans
tab B (subscribes to a cleared buffer, never sees stream_end, hangs ->
thread.join timeout -> 'every tab should finish' fails). ~20% timing flake
on slow CI runners (3.11 shard-4).

Wait until len(stream._subscribers) == len(handlers) (bounded 5s) before
producing events, so delivery is deterministic regardless of thread-start
timing. Test-only; no production change (buffer-then-broadcast is correct for
the real reconnect case).

Verified: original flakes 2/30 under CPU contention; fixed 30/30.

Fixes #5628

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-07-05 19:52:16 -07:00
nesquena-hermes 7990b7412a Merge pull request #5659 from nesquena/review-5645
Release: reload config cache on profile switch (#5645, fixes #5619)
2026-07-05 19:37:58 -07:00
nesquena-hermes d5bd28b49a Merge pull request #5656 from nesquena/review-5555
Release: fork read-only cron sessions into a new writable chat (#5555)
2026-07-05 19:07:04 -07:00
nesquena-hermes 89b6ab5c34 Merge pull request #5654 from nesquena/review-5554
Release: redesign cron preset schedule builder — sentence UI (#5554)
2026-07-05 18:40:07 -07:00
nesquena-hermes 8cdb4ea27b Merge pull request #5652 from nesquena/review-5506
Release: restore prose-level fade for Transparent Stream (#5506)
2026-07-05 17:35:47 -07:00