Compare commits

...
Sign in to create a new pull request.

3 commits

Author SHA1 Message Date
Daniel Han
2534372537 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.
2026-06-11 18:39:42 +00:00
pre-commit-ci[bot]
963cec8268 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-06-11 17:50:35 +00:00
Daniel Han
98984c761b 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.
2026-06-11 17:31:34 +00:00
4 changed files with 116 additions and 12 deletions

View file

@ -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()

View file

@ -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 == {}

View file

@ -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))

View file

@ -179,6 +179,16 @@ 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`` 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"b(\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 +240,12 @@ 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.
update_available = (
installed_build is None or latest_build is None or installed_build < latest_build
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 = 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")