* 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:
1 parent
803b081506
commit
9c1e77de48
3 files changed
+190
-4
No files matched your search
@@ -11,6 +11,8 @@
|
||||
|
||||
### 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)
|
||||
|
||||
- **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
@@ -716,8 +716,17 @@ def channel_version_badge(channel=None) -> str:
|
||||
if channel is None:
|
||||
channel = _read_update_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(
|
||||
['describe', '--tags', '--always', '--match', _channel_tag_glob(channel)],
|
||||
['describe', '--tags', '--match', _channel_tag_glob(channel)],
|
||||
REPO_ROOT,
|
||||
)
|
||||
if ok and out:
|
||||
@@ -758,6 +767,26 @@ def _release_gap(tags, current, latest):
|
||||
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):
|
||||
"""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)
|
||||
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
|
||||
# keeps committing to master between tagged releases), the release check
|
||||
# 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,
|
||||
'behind': behind,
|
||||
# GitHub compare URLs accept tag names, and tag-to-tag links are the
|
||||
# clearest "what changed in this release?" view for operators.
|
||||
'current_sha': current_tag,
|
||||
# clearest "what changed in this release?" view for operators. Use a
|
||||
# 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,
|
||||
'branch': latest_tag,
|
||||
'repo_url': remote_url,
|
||||
'release_based': True,
|
||||
'current_version': current_tag,
|
||||
'current_version': current_version_display,
|
||||
'latest_version': latest_tag,
|
||||
'channel': channel,
|
||||
}
|
||||
|
||||
@@ -285,3 +285,117 @@ def test_clear_lock_retry_preserves_experimental_channel(tmp_path, monkeypatch):
|
||||
)
|
||||
assert result['ok'] is True
|
||||
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 '')
|
||||
Reference in new issue
Block a user