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>
This commit is contained in:
nesquena-hermesandt authored and GitHub committed 2026-07-09 20:18:03 -07:00
1 parent 803b081506
commit 9c1e77de48
3 files changed
+190 -4

No files matched your search

+2
View File
@@ -11,6 +11,8 @@
### Fixed ### Fixed
- **The Experimental update channel now shows the real installed version instead of "unknown" / a bare SHA on a stable-tagged install.** When an install pinned to a stable `v*` tag opts into the Experimental channel, no `exp-v*` tag sits behind HEAD, so the channel-scoped version lookup returned nothing and the UI rendered "unknown" (and a bogus ahead-count of 1). The check now recovers the real installed version and the true number of Experimental releases ahead. The installed-version fallback is scoped to the WebUI repo (never injected into the Agent repo's payload), and the compare-link ref is always a git-verified tag (or omitted) rather than a possibly-dirty version string. Thanks @nesquena-hermes. (#5864, #5862)
- **The activity worklog no longer freezes the browser on reasoning-heavy history (#4633 frontend half).** A settled turn's activity worklog (the reasoning + tool steps behind the "Processed Xm Ys" chip) can carry 80+ rows; in compact-worklog mode (the default) every collapsed group still eagerly built all its row DOM, so a long history ballooned the DOM and a later synchronous layout could tip the tab into a multi-GB freeze. Collapsed settled worklogs now defer their row DOM until first expand. Expanded rows still get full post-processing (syntax highlighting, copy buttons, Mermaid, KaTeX, structured trees) and their open/scroll state is preserved across transcript rebuilds — matching (and, for nested-card scroll, improving on) the eager path. Thanks @nesquena-hermes. (#5860, #5839, #4633) - **The activity worklog no longer freezes the browser on reasoning-heavy history (#4633 frontend half).** A settled turn's activity worklog (the reasoning + tool steps behind the "Processed Xm Ys" chip) can carry 80+ rows; in compact-worklog mode (the default) every collapsed group still eagerly built all its row DOM, so a long history ballooned the DOM and a later synchronous layout could tip the tab into a multi-GB freeze. Collapsed settled worklogs now defer their row DOM until first expand. Expanded rows still get full post-processing (syntax highlighting, copy buttons, Mermaid, KaTeX, structured trees) and their open/scroll state is preserved across transcript rebuilds — matching (and, for nested-card scroll, improving on) the eager path. Thanks @nesquena-hermes. (#5860, #5839, #4633)
- **Server memory growth on reasoning-heavy sessions is fixed (#4633 backend root cause).** Reasoning-heavy sessions accumulate 250–480 KB of `anchor_activity_scenes` that were serialized *before* the `messages` array in the session sidecar, so the cheap 64 KB metadata-prefix read always overflowed and fell back to a full parse on every read — driving memory growth. The scene data is now serialized *after* `messages`, so the metadata prefix stays small (≈2 KB) and modern reads need no full parse. Existing session files in the old layout still load correctly (one authoritative parse, then cached), and round-trips preserve scene content exactly. Thanks @nesquena-hermes. (#5858, #5854, #4633) - **Server memory growth on reasoning-heavy sessions is fixed (#4633 backend root cause).** Reasoning-heavy sessions accumulate 250–480 KB of `anchor_activity_scenes` that were serialized *before* the `messages` array in the session sidecar, so the cheap 64 KB metadata-prefix read always overflowed and fell back to a full parse on every read — driving memory growth. The scene data is now serialized *after* `messages`, so the metadata prefix stays small (≈2 KB) and modern reads need no full parse. Existing session files in the old layout still load correctly (one authoritative parse, then cached), and round-trips preserve scene content exactly. Thanks @nesquena-hermes. (#5858, #5854, #4633)
+74 -4
View File
@@ -716,8 +716,17 @@ def channel_version_badge(channel=None) -> str:
if channel is None: if channel is None:
channel = _read_update_channel() channel = _read_update_channel()
channel = _normalize_channel(channel) channel = _normalize_channel(channel)
# NOTE: no ``--always`` here (deliberately different from _detect_webui_version).
# The current version is channel-INDEPENDENT — it's just what's installed. The
# channel only picks which tag family we compare AGAINST for updates. On a
# stable-tagged install (e.g. HEAD == v0.52.0) that opts into Experimental, no
# ``exp-v*`` tag is reachable BEHIND HEAD (the exp tags sit ahead on master), so
# ``--always`` would fall through to a bare SHA and render "WebUI: d4e80b45 ·
# Experimental" instead of the real installed version. Falling back to the
# channel-neutral WEBUI_VERSION keeps the badge showing "v0.52.0 · Experimental".
# (#5862)
out, ok = _run_git( out, ok = _run_git(
['describe', '--tags', '--always', '--match', _channel_tag_glob(channel)], ['describe', '--tags', '--match', _channel_tag_glob(channel)],
REPO_ROOT, REPO_ROOT,
) )
if ok and out: if ok and out:
@@ -758,6 +767,26 @@ def _release_gap(tags, current, latest):
return 1 return 1
def _count_channel_tags_ahead(path, channel=DEFAULT_UPDATE_CHANNEL):
"""Count channel release tags strictly ahead of HEAD (fast-forwardable).
Used only when NO channel tag is reachable behind HEAD — the channel-scoped
``describe`` returned None — e.g. a stable ``v0.52.0`` install opting into
Experimental (all ``exp-v*`` tags sit ahead on master). ``_release_gap`` can't
position HEAD in the tag list then and returns a bogus 1. ``git tag --contains
HEAD`` lists tags whose history includes HEAD, i.e. tags that are ahead of (or
on) HEAD; since HEAD carries no channel tag in this path, that count is exactly
the number of channel releases the install can fast-forward to. (#5862)
"""
out, ok = _run_git(
['tag', '--list', _channel_tag_glob(channel), '--contains', 'HEAD'],
path,
)
if not (ok and out):
return 0
return sum(1 for line in out.splitlines() if line.strip())
def _head_is_past_latest_tag(path, current_tag, channel=DEFAULT_UPDATE_CHANNEL): def _head_is_past_latest_tag(path, current_tag, channel=DEFAULT_UPDATE_CHANNEL):
"""Return True when HEAD has moved past the latest reachable channel tag. """Return True when HEAD has moved past the latest reachable channel tag.
@@ -901,6 +930,42 @@ def _check_repo_release(path, name, channel=DEFAULT_UPDATE_CHANNEL):
current_tag = _current_release_tag(path, channel) current_tag = _current_release_tag(path, channel)
behind = _release_gap(tags, current_tag, latest_tag) behind = _release_gap(tags, current_tag, latest_tag)
# When NO channel tag is reachable behind HEAD, _current_release_tag returns
# None (channel-scoped `describe --abbrev=0` fatals with "No tags can describe").
# This is the normal state of a stable-tagged install (HEAD == v0.52.0) opting
# into Experimental: every exp-v* tag sits AHEAD on master. _release_gap can't
# position None in the tag list and returns a bogus 1, and the display fields
# would carry current_version=None (rendered as "unknown"). Recover the real
# ahead-count and show the channel-neutral installed version as the current
# version — the channel only chooses the comparison tag family, not what's
# installed. (#5862)
current_version_display = current_tag
# A git-verified ref for the compare link (defaults to the resolved channel
# tag; may be refined below in the no-channel-tag-behind-HEAD fallback).
current_sha_ref = current_tag
if current_tag is None:
ahead = _count_channel_tags_ahead(path, channel)
if ahead > 0:
behind = ahead
# Scope the installed-version fallback to the WebUI repo only.
# _check_repo_release() is shared with the Agent repo, and WEBUI_VERSION
# (e.g. v0.52.0) is not a valid ref/tag in the Agent repository — injecting
# it there would display the WebUI version as the Agent's installed version
# and produce a broken Agent compare link. (#5864)
if name == "webui":
current_version_display = WEBUI_VERSION
# For the compare link, derive a git-VERIFIED installed tag rather than
# reusing WEBUI_VERSION (which can be `vX.Y.Z-dirty-<hash>`, `-N-g<sha>`,
# a bare SHA, or `unknown` — none guaranteed refs). Prefer the exact tag
# on HEAD across ALL release families (channel-neutral), so a stable-
# pinned Experimental install still gets a resolvable /compare/<tag>...
# link; fall back to None (no link) when HEAD is not exactly on a tag. (#5864)
exact_tag, ok = _run_git(
['describe', '--tags', '--exact-match', 'HEAD'], path
)
exact_tag = (exact_tag or '').strip()
current_sha_ref = exact_tag if ok and exact_tag else None
# If behind == 0 but HEAD has moved past the tag (e.g. the agent repo # If behind == 0 but HEAD has moved past the tag (e.g. the agent repo
# keeps committing to master between tagged releases), the release check # keeps committing to master between tagged releases), the release check
# would report "Up to date" even though hundreds of commits are missing. # would report "Up to date" even though hundreds of commits are missing.
@@ -944,13 +1009,18 @@ def _check_repo_release(path, name, channel=DEFAULT_UPDATE_CHANNEL):
'name': name, 'name': name,
'behind': behind, 'behind': behind,
# GitHub compare URLs accept tag names, and tag-to-tag links are the # GitHub compare URLs accept tag names, and tag-to-tag links are the
# clearest "what changed in this release?" view for operators. # clearest "what changed in this release?" view for operators. Use a
'current_sha': current_tag, # git-VERIFIED ref for the compare link: the resolved channel tag when
# one is reachable behind HEAD, else None. WEBUI_VERSION is NOT safe here
# — it can be `v0.52.0-dirty-<hash>`, `v0.52.0-N-g<sha>`, a bare SHA, or
# `unknown`, none of which are guaranteed refs, so reusing it would emit
# a broken /compare link (ui.js) and lose update-summary commit subjects. (#5864)
'current_sha': current_sha_ref,
'latest_sha': latest_tag, 'latest_sha': latest_tag,
'branch': latest_tag, 'branch': latest_tag,
'repo_url': remote_url, 'repo_url': remote_url,
'release_based': True, 'release_based': True,
'current_version': current_tag, 'current_version': current_version_display,
'latest_version': latest_tag, 'latest_version': latest_tag,
'channel': channel, 'channel': channel,
} }
+114
View File
@@ -285,3 +285,117 @@ def test_clear_lock_retry_preserves_experimental_channel(tmp_path, monkeypatch):
) )
assert result['ok'] is True assert result['ok'] is True
assert result['lock_recovery']['action'] == 'no-lock-found' assert result['lock_recovery']['action'] == 'no-lock-found'
# ── Stable-tagged install opting into Experimental (#5862) ───────────────────
#
# b3nw's report: a plain stable install pinned on v0.52.0 that flips to the
# Experimental channel. Unlike `channel_repo` (which tags stable and exp on the
# SAME commits), here the stable tag PREDATES every exp-v* tag — so from HEAD ==
# v0.52.0 there is NO exp-v* tag reachable behind HEAD. Both channel-scoped git
# lookups then degrade: the chip `describe --always` leaks a bare SHA, and the
# banner `describe --abbrev=0` fatals -> current_version=None -> "unknown".
@pytest.fixture
def stable_pinned_repo(tmp_path):
"""v0.52.0 tagged first (stable only), then 3 exp-v* batches AHEAD on master.
HEAD is checked out ON v0.52.0, so no exp-v* tag is reachable behind HEAD.
"""
repo = tmp_path / 'stable_pinned'
repo.mkdir()
_git(repo, 'init', '-q')
_git(repo, 'config', 'user.email', 't@t.co')
_git(repo, 'config', 'user.name', 'Test')
_git(repo, 'remote', 'add', 'origin', 'https://github.com/nesquena/hermes-webui.git')
_git(repo, 'commit', '-q', '--allow-empty', '-m', 'v0.52.0 release')
_git(repo, 'tag', 'v0.52.0') # stable tag, NO exp tag on this commit
for i in range(1, 4):
_git(repo, 'commit', '-q', '--allow-empty', '-m', f'exp batch {i}')
_git(repo, 'tag', f'exp-v0.52.{i}')
_git(repo, 'checkout', '-q', 'v0.52.0') # pin HEAD on the stable release
return repo
def test_stable_pinned_current_release_tag_is_none_on_experimental(stable_pinned_repo):
"""Precondition: the channel-scoped current-tag lookup genuinely returns None
(no exp-v* reachable behind a v0.52.0 HEAD). This is the trigger for #5862."""
assert updates._current_release_tag(stable_pinned_repo, 'experimental') is None
# Stable still resolves the real tag.
assert updates._current_release_tag(stable_pinned_repo, 'stable') == 'v0.52.0'
def test_stable_pinned_experimental_reports_installed_version_not_unknown(stable_pinned_repo, monkeypatch):
"""#5862: current_version must be the neutral installed tag (v0.52.0), NOT
None (which the UI renders as 'unknown'), and the count must be the real
number of exp releases ahead (3), NOT the bogus _release_gap fallback of 1."""
monkeypatch.setattr(updates, 'WEBUI_VERSION', 'v0.52.0')
info = updates._check_repo_release(stable_pinned_repo, 'webui', 'experimental')
assert info is not None
assert info['current_version'] == 'v0.52.0', 'must not be None/"unknown"'
assert info['current_sha'] == 'v0.52.0'
assert info['latest_version'] == 'exp-v0.52.3'
assert info['behind'] == 3, 'all 3 exp releases are ahead, not a bogus 1'
def test_stable_pinned_stable_channel_still_up_to_date(stable_pinned_repo, monkeypatch):
"""Sanity: the SAME install on the stable channel is up-to-date on v0.52.0
(the exp-v* tags ahead must not be offered as stable updates)."""
monkeypatch.setattr(updates, 'WEBUI_VERSION', 'v0.52.0')
info = updates._check_repo_release(stable_pinned_repo, 'webui', 'stable')
assert info is not None
assert info['behind'] == 0
assert info['current_version'] == 'v0.52.0'
def test_channel_version_badge_no_bare_sha_on_experimental(stable_pinned_repo, monkeypatch):
"""#5862 chip: channel_version_badge must fall back to the neutral installed
version, never a bare git SHA, when no channel tag is reachable."""
monkeypatch.setattr(updates, 'REPO_ROOT', stable_pinned_repo)
monkeypatch.setattr(updates, 'WEBUI_VERSION', 'v0.52.0')
badge = updates.channel_version_badge('experimental')
assert badge == 'v0.52.0', f'expected installed version, got bare SHA-ish {badge!r}'
# Stable channel resolves its own reachable tag directly.
assert updates.channel_version_badge('stable') == 'v0.52.0'
def test_count_channel_tags_ahead(stable_pinned_repo):
"""The ahead-count helper: 3 exp-v* tags sit ahead of a v0.52.0 HEAD.
This helper is only ever CALLED when no channel tag is reachable on/behind
HEAD (current_tag is None) — exactly the experimental case here, where no
exp-v* tag sits on the v0.52.0 commit, so `--contains HEAD` counts precisely
the 3 tags strictly ahead.
"""
_git(stable_pinned_repo, 'checkout', '-q', 'v0.52.0')
assert updates._count_channel_tags_ahead(stable_pinned_repo, 'experimental') == 3
def test_stable_pinned_experimental_agent_repo_does_not_inject_webui_version(stable_pinned_repo, monkeypatch):
"""#5864: the installed-version fallback is WebUI-only. _check_repo_release is
shared with the Agent repo, where WEBUI_VERSION (v0.52.0) is not a valid ref —
it must NOT be injected as the Agent's current_version/current_sha (that would
show the WebUI version as the Agent's and emit a broken Agent compare link)."""
monkeypatch.setattr(updates, 'WEBUI_VERSION', 'v0.52.0')
info = updates._check_repo_release(stable_pinned_repo, 'agent', 'experimental')
assert info is not None
# Agent must NOT carry the WebUI version.
assert info['current_version'] != 'v0.52.0'
assert info['current_sha'] != 'v0.52.0'
def test_stable_pinned_experimental_current_sha_is_verified_ref_only(stable_pinned_repo, monkeypatch):
"""#5864: current_sha must be a git-VERIFIED ref, never a raw WEBUI_VERSION
that can be dirty/`-N-g<sha>`/bare-SHA/`unknown`. When HEAD is exactly on a
tag, current_sha resolves to that tag; when WEBUI_VERSION is a non-ref dirty
string, the compare ref falls back to None (no broken /compare link) while the
displayed version still shows the real installed string."""
# HEAD is exactly on v0.52.0 → verified tag resolves for the compare link.
monkeypatch.setattr(updates, 'WEBUI_VERSION', 'v0.52.0-dirty-deadbeef')
info = updates._check_repo_release(stable_pinned_repo, 'webui', 'experimental')
assert info is not None
# current_version shows the real (dirty) installed string...
assert info['current_version'] == 'v0.52.0-dirty-deadbeef'
# ...but current_sha is the git-verified exact tag, NOT the dirty display string.
assert info['current_sha'] == 'v0.52.0'
assert 'dirty' not in (info['current_sha'] or '')