From 21695a10bfca888fe45e841f8dd3cae6da077b72 Mon Sep 17 00:00:00 2001 From: falkoro <39274208+falkoro@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:56:06 +0200 Subject: [PATCH 1/2] fix(lazy_deps): unpin huggingface-hub to a range so refresh stops breaking Hindsight tool.trace_upload pinned huggingface-hub==1.2.3, but huggingface-hub is a shared dependency: transformers (via sentence-transformers, the Hindsight local-embeddings provider) requires huggingface-hub>=1.5.0,<2.0. active_features() flags a feature as active from mere package presence, so having sentence-transformers installed marks tool.trace_upload active even for users who never ran a trace upload. On the next hermes update, _refresh_active_lazy_features() sees the ==1.2.3 pin unsatisfied and downgrades the shared package, breaking Hindsight startup with ImportError: huggingface-hub>=1.5.0,<2.0 is required. Widen the pin to huggingface-hub>=1.2.3,<2.0 (ranges are the norm in LAZY_DEPS; the == pin was the outlier): every transformers-compatible version now satisfies the spec, so the refresh treats it as current instead of downgrading, and a fresh lazy install resolves to a current 1.x. The HfApi surface trace upload uses (whoami / create_repo / upload_file) is stable across the whole 1.x line. Tests pin the invariant: the trace_upload spec must admit every version transformers accepts (loud failure if someone re-pins it into conflict), and feature_missing() must report a newer in-range hub as satisfied. Fixes #60783 --- tests/tools/test_lazy_deps.py | 47 +++++++++++++++++++++++++++++++++++ tools/lazy_deps.py | 10 +++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/tests/tools/test_lazy_deps.py b/tests/tools/test_lazy_deps.py index 2b319ae049d8..b067c0ff4db8 100644 --- a/tests/tools/test_lazy_deps.py +++ b/tests/tools/test_lazy_deps.py @@ -294,6 +294,53 @@ def test_extras_block_mismatch_returns_false(self, monkeypatch): assert ld._is_satisfied("mautrix[encryption]==0.21.0") is False +class TestSharedDependencyPins: + """Pins on packages other Hermes features install transitively must be + ranges, not == pins: active_features() flags a feature "active" from mere + package presence, so a hard pin makes the post-update lazy refresh + downgrade the shared package underneath its other consumers (#60783).""" + + # transformers (via sentence-transformers, the Hindsight local-embedding + # provider) requires this range of huggingface-hub. + _TRANSFORMERS_HF_HUB_REQ = ">=1.5.0,<2.0" + + def _hub_spec_tail(self): + (spec,) = ld.LAZY_DEPS["tool.trace_upload"] + assert ld._pkg_name_from_spec(spec) == "huggingface-hub" + return ld._specifier_from_spec(spec) + + def test_trace_upload_hub_pin_admits_transformers_range(self): + """A version satisfying transformers' floor must satisfy our spec too, + so the refresh pass never downgrades it out from under transformers.""" + from packaging.specifiers import SpecifierSet + from packaging.version import Version + + ours = SpecifierSet(self._hub_spec_tail()) + # Representative versions across transformers' accepted range. + for v in ("1.5.0", "1.22.0", "1.99.0"): + assert Version(v) in SpecifierSet(self._TRANSFORMERS_HF_HUB_REQ) + assert Version(v) in ours, ( + f"tool.trace_upload huggingface-hub spec {ours!r} rejects " + f"{v}, which transformers accepts — the lazy refresh would " + f"downgrade the shared package and break Hindsight (#60783)" + ) + + def test_transformers_compatible_hub_version_is_satisfied(self, monkeypatch): + """hermes update must treat an already-compatible newer hub version + as current instead of reinstalling the old pin.""" + from importlib.metadata import PackageNotFoundError + + def _version(pkg): + if pkg == "huggingface-hub": + return "1.22.0" + raise PackageNotFoundError(pkg) + + import importlib.metadata as _md + monkeypatch.setattr(_md, "version", _version) + + assert ld.feature_missing("tool.trace_upload") == () + + # --------------------------------------------------------------------------- # active_features + refresh_active_features (Piece A — hermes update wiring) # --------------------------------------------------------------------------- diff --git a/tools/lazy_deps.py b/tools/lazy_deps.py index ec5692ecd550..03735d077276 100644 --- a/tools/lazy_deps.py +++ b/tools/lazy_deps.py @@ -239,7 +239,15 @@ "starlette==1.0.1", # CVE-2026-48710 — keep in sync with pyproject [computer-use] ), # HF Agent Trace Viewer upload (hermes trace upload / /upload-trace). - "tool.trace_upload": ("huggingface-hub==1.2.3",), + # RANGE, not an == pin: huggingface-hub is shared with transformers / + # sentence-transformers (Hindsight local embeddings), which require + # huggingface-hub>=1.5.0,<2.0. A hard pin makes the post-update lazy + # refresh downgrade the shared package underneath them and break their + # import (#60783) — active_features() flags this feature "active" from + # mere package presence, so the downgrade fires even for users who never + # ran a trace upload. The HfApi surface used here (whoami / create_repo / + # upload_file) is stable across the whole 1.x line. + "tool.trace_upload": ("huggingface-hub>=1.2.3,<2.0",), } From 3ab819dc87d372179e2e42d917b15a3a8d7f3619 Mon Sep 17 00:00:00 2001 From: spiky02plateau <155588579+spiky02plateau@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:10:59 +0200 Subject: [PATCH 2/2] fix(lazy-deps): never downgrade a shared dependency; track a range for huggingface-hub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An exact ==1.2.3 pin on huggingface-hub (feature tool.trace_upload) force-downgraded the shared venv on every lazy refresh whenever the core embedding stack (transformers/sentence-transformers, used by local/local_embedded Hindsight) had installed a newer version — transformers 5.x requires huggingface-hub>=1.5,<2.0, so the downgrade made sentence_transformers unimportable and the embedded Hindsight daemon abort at startup (silent memory loss until noticed). Two layers: - Track the compatibility range the trace-upload client actually needs (>=1.5,<2.0) instead of an exact pin, so an already-healthy shared version satisfies the spec and is left alone. - Add a general no-downgrade guard in _is_satisfied: a lazy, opt-in backend must never move an already-installed package backwards; treat 'installed newer than the pin allows' as satisfied and warn to widen the pin. Legitimate upgrades (installed below the spec) are unaffected. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01MN8RMDLwxCfFxwtADoEJJf --- tools/lazy_deps.py | 48 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/tools/lazy_deps.py b/tools/lazy_deps.py index 03735d077276..434adb7a1632 100644 --- a/tools/lazy_deps.py +++ b/tools/lazy_deps.py @@ -246,8 +246,10 @@ # import (#60783) — active_features() flags this feature "active" from # mere package presence, so the downgrade fires even for users who never # ran a trace upload. The HfApi surface used here (whoami / create_repo / - # upload_file) is stable across the whole 1.x line. - "tool.trace_upload": ("huggingface-hub>=1.2.3,<2.0",), + # upload_file) is stable across the whole 1.x line. (See also the + # no-downgrade guard in _is_satisfied, which backstops this for any + # shared-dep pin.) + "tool.trace_upload": ("huggingface-hub>=1.5.0,<2.0",), } @@ -552,12 +554,52 @@ def _is_satisfied(spec: str) -> bool: return True try: - return Version(installed) in SpecifierSet(spec_tail) + iv = Version(installed) + ss = SpecifierSet(spec_tail) + if iv in ss: + return True + # No-downgrade guard. The installed version is outside the spec — but if + # installing this spec would move the package BACKWARDS, refuse. A lazy, + # opt-in backend must never downgrade a package that the core (or another + # backend) already installed at a higher version: that is exactly how an + # exact "==" pin on a SHARED transitive dependency (e.g. huggingface-hub, + # pulled by transformers) silently bricks an unrelated core feature on + # `hermes update`. Treat "already newer than this pin allows" as + # satisfied, leave the higher version in place, and warn the maintainer + # to widen the pin instead of churning a shared dependency. + if _would_downgrade(iv, ss): + logger.warning( + "Lazy spec %r would DOWNGRADE already-installed %s==%s; leaving the " + "installed version in place. Widen this pin to a range that includes " + "the installed version if the downgrade is not intended.", + spec, pkg, installed, + ) + return True + return False except (InvalidSpecifier, InvalidVersion, Exception): # Malformed spec or installed version we can't parse — don't churn. return True +def _would_downgrade(installed, spec_set) -> bool: + """True if ``spec_set`` permits no version >= ``installed``. + + ``installed`` is already known to fall outside ``spec_set``. If every + upper-bounding operator (``==``, ``<``, ``<=``, ``~=``) in the spec sits + below the installed version, the only way to satisfy the spec is to install + something older — a downgrade. Purely local version arithmetic; no network. + """ + try: + from packaging.version import Version + for s in spec_set: + if s.operator in ("==", "<", "<=", "~="): + if installed > Version(s.version): + return True + return False + except Exception: + return False + + def _is_present(spec: str) -> bool: """Cheap presence-only check (package name installed at any version).