From 98984c761b71f89372e747d02a8395ccf3a4c6e6 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 11 Jun 2026 17:31:34 +0000 Subject: [PATCH 1/3] Studio: never offer an older llama.cpp prebuilt as an update The in-app update banner offered an OLDER build than installed (e.g. b9585 -> b9518) and reappeared even after clicking Update. Two causes: - get_update_status gated update_available on installed_tag != latest_tag, which fires when latest is older than installed (a stale 24h release cache, or the host resolver walking back to an older partial release). - After a successful apply, reset_caches left the on-disk release cache and the markerless resolver memo intact, so the next poll re-derived the same stale older latest and re-showed the banner. Gate update_available on a strict build-number compare (latest must be strictly newer than installed) in both the marker and source-build paths, and after a successful apply drop the disk release cache and resolver memo. --- .../backend/tests/test_llama_cpp_freshness.py | 14 +++++ studio/backend/tests/test_llama_cpp_update.py | 63 +++++++++++++++++++ studio/backend/utils/llama_cpp_freshness.py | 12 +++- studio/backend/utils/llama_cpp_update.py | 37 ++++++++--- 4 files changed, 115 insertions(+), 11 deletions(-) diff --git a/studio/backend/tests/test_llama_cpp_freshness.py b/studio/backend/tests/test_llama_cpp_freshness.py index cb17e0d5e7..14e4c9b904 100644 --- a/studio/backend/tests/test_llama_cpp_freshness.py +++ b/studio/backend/tests/test_llama_cpp_freshness.py @@ -303,3 +303,17 @@ def test_format_stale_warning_singular_day(): msg = fr.format_stale_warning({"installed_tag": "b9190", "latest_tag": "b9300", "age_days": 1}) assert "1 day" in msg assert "1 days" not in msg + + +def test_reset_caches_drop_disk_removes_cache_file(monkeypatch, tmp_path): + # Default reset keeps the on-disk cache; drop_disk=True removes it so the next + # read refetches the latest tag (used after an in-app update). + cache_dir = tmp_path / ".freshness" + cache_dir.mkdir() + cache_file = cache_dir / "unslothai__llama.cpp.json" + cache_file.write_text("{}") + monkeypatch.setattr(fr, "_cache_dir", lambda: cache_dir) + fr.reset_caches() + assert cache_file.exists() + fr.reset_caches(drop_disk = True) + assert not cache_file.exists() diff --git a/studio/backend/tests/test_llama_cpp_update.py b/studio/backend/tests/test_llama_cpp_update.py index d21d653b76..b8c6d3aae5 100644 --- a/studio/backend/tests/test_llama_cpp_update.py +++ b/studio/backend/tests/test_llama_cpp_update.py @@ -795,3 +795,66 @@ def test_start_update_source_build_refuses_when_newer(monkeypatch, tmp_path): res = upd.start_update() assert res["started"] is False assert res["reason"] == "up_to_date" + + +def test_status_marker_no_downgrade_on_stale_cache(monkeypatch, tmp_path): + # Regression: marker is freshly newer (b9585) while the cached GitHub "latest" + # still holds the previous release (b9518). Must never offer a downgrade. + binary = _write_install(tmp_path, "b9585") + monkeypatch.setattr(upd, "_find_binary", lambda: binary) + monkeypatch.setattr(freshness, "_fetch_latest_release_tag", lambda repo, timeout = 5.0: "b9518") + st = upd.get_update_status(force_refresh = True) + assert st["installed_tag"] == "b9585" + assert st["latest_tag"] == "b9518" + assert st["update_available"] is False + + +def test_status_source_build_no_downgrade(monkeypatch, tmp_path): + # Markerless install whose --version is a known newer build (9585); the resolver + # walked back to an older partial release (b9518). Must never downgrade. + binary = tmp_path / "llama.cpp" / "build" / "bin" / "llama-server" + binary.parent.mkdir(parents = True) + binary.write_text("stub") + monkeypatch.setattr(upd, "_find_binary", lambda: str(binary)) + _prebuilt(monkeypatch, release_tag = "b9518") + monkeypatch.setattr(upd, "_installed_build_number", lambda b: 9585) + st = upd.get_update_status() + assert st["update_available"] is False + + +def test_status_source_build_unparseable_latest_fails_open(monkeypatch, tmp_path): + # Resolver returned a tag with no build number; with an unknown installed version + # this must fail open to "no update" rather than offer blindly. + binary = tmp_path / "llama.cpp" / "build" / "bin" / "llama-server" + binary.parent.mkdir(parents = True) + binary.write_text("stub") + monkeypatch.setattr(upd, "_find_binary", lambda: str(binary)) + _prebuilt(monkeypatch, release_tag = "master", llama_tag = "master") + monkeypatch.setattr(upd, "_installed_build_number", lambda b: None) + st = upd.get_update_status() + assert st["update_available"] is False + + +def test_apply_clears_resolve_memo(monkeypatch, tmp_path): + # After a successful update the markerless resolver memo must be cleared so a + # stale older "latest" cannot make the banner reappear. + install_dir = tmp_path / "llama.cpp" + binary = _write_install(install_dir, "b9493") + monkeypatch.setattr(upd, "_find_binary", lambda: binary) + monkeypatch.setattr(upd, "_installer_script", lambda: tmp_path / "install_llama_prebuilt.py") + monkeypatch.setattr(freshness, "_fetch_latest_release_tag", lambda repo, timeout = 5.0: "b9518") + upd._resolve_memo.update(at = time.time(), value = {"prebuilt_available": True}) + _patch_installer_popen( + monkeypatch, + lines = ["installed\n"], + on_start = lambda cmd: _write_install(install_dir, "b9518"), + ) + assert upd.start_update()["started"] is True + deadline = time.time() + 10 + while time.time() < deadline: + job = upd.get_update_status()["job"] + if job["state"] in ("success", "error"): + break + time.sleep(0.05) + assert job["state"] == "success", job + assert upd._resolve_memo == {} diff --git a/studio/backend/utils/llama_cpp_freshness.py b/studio/backend/utils/llama_cpp_freshness.py index 3e5066ca2d..53b565e68c 100644 --- a/studio/backend/utils/llama_cpp_freshness.py +++ b/studio/backend/utils/llama_cpp_freshness.py @@ -232,7 +232,15 @@ def format_stale_warning(info: dict) -> str: ) -def reset_caches() -> None: - """Test-only: drop all in-memory caches.""" +def reset_caches(*, drop_disk: bool = False) -> None: + """Drop in-memory caches. drop_disk=True also removes the on-disk release cache + so the next read refetches the latest tag (used after an in-app update so the + banner cannot reappear off a stale 'latest').""" _marker_cache.clear() _release_memo.clear() + if drop_disk: + try: + for path in _cache_dir().glob("*.json"): + path.unlink(missing_ok = True) + except OSError as exc: + logger.debug("freshness cache clear failed", error = str(exc)) diff --git a/studio/backend/utils/llama_cpp_update.py b/studio/backend/utils/llama_cpp_update.py index 55bf4b3d84..18dec59743 100644 --- a/studio/backend/utils/llama_cpp_update.py +++ b/studio/backend/utils/llama_cpp_update.py @@ -179,6 +179,15 @@ def _installed_build_number(binary: Optional[str]) -> Optional[int]: return n if n > 1 else None +def _tag_build_number(tag: Optional[str]) -> Optional[int]: + """Integer build from a ``bNNNN`` release tag ('b9585' -> 9585). None when the + tag is missing or has no number, so callers fail open (offer nothing).""" + if not tag: + return None + m = re.search(r"(\d+)", tag) + return int(m.group(1)) if m else None + + def _is_under(path: Path, root: Path) -> bool: try: p, r = path.resolve(), root.resolve() @@ -230,12 +239,13 @@ def _source_build_status(binary: str, *, force_refresh: bool) -> Optional[dict]: if _llama_install_root(binary) is None: return None installed_build = _installed_build_number(binary) - m = re.search(r"(\d+)", latest) - latest_build = int(m.group(1)) if m else None - # Suppress only when the source build is reliably newer/equal; unknown - # version (the involuntary source-build case) is treated as behind. + latest_build = _tag_build_number(latest) + # Offer only a STRICTLY newer build. Unknown installed version (version: 1 or + # unparseable) stays "behind" so the involuntary source build still gets the + # prebuilt, but a known build is never downgraded to an older walked-back release. update_available = ( - installed_build is None or latest_build is None or installed_build < latest_build + latest_build is not None + and (installed_build is None or installed_build < latest_build) ) with _job_lock: job = dict(_job) @@ -286,8 +296,15 @@ def get_update_status(*, force_refresh: bool = False) -> dict: freshness = check_prebuilt_freshness(binary) installed = freshness.get("installed_tag") latest = freshness.get("latest_tag") + installed_build = _tag_build_number(installed) + latest_build = _tag_build_number(latest) + # Offer only a STRICTLY newer build. A stale release cache (latest behind the + # freshly installed marker) or an older partial release must never downgrade. update_available = bool( - freshness.get("has_marker") and installed and latest and installed != latest + freshness.get("has_marker") + and installed_build is not None + and latest_build is not None + and latest_build > installed_build ) with _job_lock: @@ -405,9 +422,11 @@ def _run_update(install_dir: Path, repo: str, asset: Optional[str], script: Path tail = "".join(tail_lines).strip()[-1500:] raise RuntimeError(f"installer exited {returncode}: {tail or 'no output'}") - # New UNSLOTH_PREBUILT_INFO.json is on disk; drop caches so the next - # status read reflects the freshly installed tag. - reset_caches() + # New UNSLOTH_PREBUILT_INFO.json is on disk; drop caches (incl. the disk + # release cache and the markerless resolver memo) so the next status read + # reflects the freshly installed tag and cannot reappear off stale data. + reset_caches(drop_disk = True) + _resolve_memo.clear() new_marker = read_install_marker(_find_binary()) new_tag = (new_marker or {}).get("tag") or (new_marker or {}).get("release_tag") From 963cec8268d34ab0b1af821ccfa621e52a62ed98 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 17:50:31 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- studio/backend/utils/llama_cpp_update.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/studio/backend/utils/llama_cpp_update.py b/studio/backend/utils/llama_cpp_update.py index 18dec59743..0a38a04b36 100644 --- a/studio/backend/utils/llama_cpp_update.py +++ b/studio/backend/utils/llama_cpp_update.py @@ -243,9 +243,8 @@ def _source_build_status(binary: str, *, force_refresh: bool) -> Optional[dict]: # Offer only a STRICTLY newer build. Unknown installed version (version: 1 or # unparseable) stays "behind" so the involuntary source build still gets the # prebuilt, but a known build is never downgraded to an older walked-back release. - update_available = ( - latest_build is not None - and (installed_build is None or installed_build < latest_build) + update_available = latest_build is not None and ( + installed_build is None or installed_build < latest_build ) with _job_lock: job = dict(_job) From 2534372537e913c15fc835ac78053c8af6212797 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 11 Jun 2026 18:38:48 +0000 Subject: [PATCH 3/3] Studio: anchor the build-number parse on the bNNNN prefix Match b(\d+) instead of the first digit run so wrapper/date tags and master-style asset names (e.g. llama-prebuilt-master-ac76808) fail open to no-update rather than comparing a stray number. --- studio/backend/utils/llama_cpp_update.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/studio/backend/utils/llama_cpp_update.py b/studio/backend/utils/llama_cpp_update.py index 0a38a04b36..d48620b994 100644 --- a/studio/backend/utils/llama_cpp_update.py +++ b/studio/backend/utils/llama_cpp_update.py @@ -180,11 +180,12 @@ def _installed_build_number(binary: Optional[str]) -> Optional[int]: def _tag_build_number(tag: Optional[str]) -> Optional[int]: - """Integer build from a ``bNNNN`` release tag ('b9585' -> 9585). None when the - tag is missing or has no number, so callers fail open (offer nothing).""" + """Integer build from a ``bNNNN`` llama.cpp release tag ('b9585' -> 9585). + Anchored on the ``b`` prefix so wrapper/date tags do not match a stray number; + None when absent, so callers fail open (offer nothing).""" if not tag: return None - m = re.search(r"(\d+)", tag) + m = re.search(r"b(\d+)", tag) return int(m.group(1)) if m else None