Skip to content

fix(lazy_deps): only mark a backend active when its signature package is present - #54178

Closed
spiky02plateau wants to merge 2 commits into
NousResearch:mainfrom
spiky02plateau:fix/lazy-deps-signature-active
Closed

fix(lazy_deps): only mark a backend active when its signature package is present#54178
spiky02plateau wants to merge 2 commits into
NousResearch:mainfrom
spiky02plateau:fix/lazy-deps-signature-active

Conversation

@spiky02plateau

Copy link
Copy Markdown
Contributor

Problem

tools/lazy_deps.py::active_features() flags a backend as "active" if any of its declared specs is installed:

if any(_is_present(s) for s in specs):
    active.append(feature)

Several LAZY_DEPS entries list a defining package followed by generic shared libraries, e.g.:

"platform.matrix": ("mautrix[encryption]==0.21.0", "aiosqlite==0.22.1",
                    "asyncpg==0.31.0", "aiohttp-socks==0.11.0"),

When one of those shared libs (asyncpg, aiosqlite, aiohttp, …) happens to be installed transitively for an unrelated reason, the backend is falsely flagged active. hermes update_refresh_active_lazy_features() then tries to refresh it and fails building mautrix[encryption]python-olm (bundled libolm make static):

⚠ platform.matrix failed to refresh: pip install failed: ...

This surfaces on every hermes update for users who never enabled Matrix but have asyncpg/aiosqlite present — a common situation, so the noisy "failed to refresh" warning is widespread.

Root cause

The implementation contradicts the function's own docstring, which states active means "backends the user has previously activated." Keying on any spec means an incidental shared transitive dep is enough to mark a backend active.

Fix

Gate on the signature package — the first spec in each feature tuple. Every LAZY_DEPS entry already leads with its defining package and lists shared libs afterwards, so this matches both the data convention and the documented intent:

if specs and _is_present(specs[0]):
    active.append(feature)

Real users of a backend still have its signature package installed, so genuine refreshes (the reason this function exists — picking up moved version pins) are unaffected. Only false positives are removed.

Tests

Adds TestActiveFeaturesSignatureGating in tests/tools/test_lazy_deps.py:

  • shared dep present + signature absent ⇒ not active (the regression)
  • signature present ⇒ active
  • empty specs ⇒ never active

Full tests/tools/test_lazy_deps.py suite passes locally.

Alternative considered

An explicit shared-dep denylist instead of positional specs[0]. The positional approach is smaller and self-maintaining (no list to keep in sync as backends are added), but happy to switch to a denylist if you prefer that shape.

@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint dependencies Pull requests that update a dependency file P3 Low — cosmetic, nice to have labels Jun 28, 2026

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary\n\nVerdict: LGTM\n\nFixes false-positive active_features() detection by gating on the signature (first) spec only. Previously, shared transitive deps like asyncpg would trigger platform.matrix refresh attempts. Clean regression tests.\n\n---\nReviewed by Hermes Agent

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Focused fix (54 additions). Gates active_features() on the signature (first) spec only, preventing false positives from shared transitive dependencies. Clear documentation of the regression being fixed. Includes 3 test cases.

Reviewed by Hermes Agent

… is present

active_features() flagged a feature as active when *any* of its declared
packages was installed. Trailing specs are often generic shared libraries
(asyncpg, aiosqlite, aiohttp, qrcode, numpy) pulled in transitively by
unrelated features, so a host that merely had asyncpg installed for some
other reason was treated as having platform.matrix active. `hermes update`
then tried to refresh it and failed building mautrix[encryption] ->
python-olm (bundled libolm `make static`) on every run, emitting a
"failed to refresh" warning to users who never enabled Matrix.

Gate on the signature package (the first spec in each LAZY_DEPS tuple)
instead, matching the function's documented intent of detecting backends
the user actually enabled. Every LAZY_DEPS entry already leads with its
defining package and lists shared libs afterwards. Real users of a backend
still have its signature package installed, so genuine refreshes are
unaffected. Adds regression tests.
Satisfies the contributor-check CI gate. Appended at the end of the dict
to avoid the top-of-map churn that conflicts across concurrent PRs.
@spiky02plateau
spiky02plateau force-pushed the fix/lazy-deps-signature-active branch from 8c92324 to 07dddc9 Compare June 28, 2026 20:51
@spiky02plateau

Copy link
Copy Markdown
Contributor Author

Rebased onto latest main and added the contributor AUTHOR_MAP mapping — the PR is now mergeable and check-attribution is green. Quick note on CI for reviewers:

The only remaining red is Build&Test Docker image / build-arm64, and it isn't related to this change. It fails at the registry push with:

ERROR: failed to solve: error writing layer blob: denied: installation not allowed to Write organization package

That's the standard fork-PR limitation — a PR from an outside fork can't write to the org's GHCR package. The build-amd64 job (same Dockerfile, same code) builds successfully, so this is purely the cross-fork registry permission, not a build break. It should pass when run from an internal branch / with the maintainer-side token.

For context, an earlier run also showed Python tests / Run tests slice 8/8 red, but that was a transient (tests/test_install_sh_browser_install.py, unrelated to this diff) and it's green on the current commit.

The change itself is isolated to tools/lazy_deps.py::active_features() (gate on the signature package instead of "any spec present") plus a regression test — no production behavior change for real users of any backend. Happy to switch from the positional specs[0] approach to an explicit shared-dep denylist if you'd prefer that shape.

@spiky02plateau

Copy link
Copy Markdown
Contributor Author

@teknium1 — flagging this for review whenever you have a moment (no rush); you touched tools/lazy_deps.py most recently, so it seemed the right call to ping you.

Quick state of the PR:

  • Mergeable, no conflicts, and all required checks are green (tests 8/8, ruff/ty, check-attribution, build-amd64, supply-chain).
  • The only red is Build&Test Docker image / build-arm64, which is the standard fork-PR limitation — a PR from an outside fork can't write to the org GHCR package (denied: installation not allowed to Write organization package). build-amd64 builds the same Dockerfile/code successfully, so it should pass from an internal branch / with the maintainer-side token. It's not a required check, so it isn't blocking the merge.

The change itself is isolated to active_features(): gate on the signature package (first spec) instead of "any spec present", which stops shared transitive deps (e.g. asyncpg) from false-flagging platform.matrix as active and triggering a doomed refresh on every hermes update. Every LAZY_DEPS entry already leads with its defining package, so this matches the existing convention. Three regression tests included.

Happy to reshape specs[0] into an explicit shared-dep denylist if you'd prefer that form. Thanks!

rolldav commented Jul 15, 2026

Copy link
Copy Markdown

Confirmed this fix against current main on macOS arm64.

Environment:

  • Hermes Agent v0.18.2 (2026.7.7.2)
  • checkout 0c1adb487
  • Python 3.11.15
  • CMake 4.4.0
  • Apple Clang 21

Before the patch, hermes update reported 25 active lazy backends and attempted to refresh platform.matrix even though neither mautrix nor python-olm was installed. The only matching Matrix spec present was the shared aiohttp==3.14.1 pin. The resulting python-olm==3.2.16 source build fails first on CMake's removed <3.5 compatibility and, after applying CMAKE_POLICY_VERSION_MINIMUM=3.5, on libolm's const-qualified pointer error under Apple Clang 21.

Applying the signature-package gate from this PR produced:

65 passed
active_count=24
matrix_active=False
→ Refreshing 24 active lazy backend(s)...
  ✓ 24 already current

git diff --check also passed. This confirms the PR addresses the same false-positive refresh path on modern macOS, not only Windows.

@teknium1

Copy link
Copy Markdown
Contributor

Fixed on main by #72361 — anchor-gated (first-spec) activation, functionally identical to your signature-package approach. #27878 by @paralegalia was the earliest submission of this fix so it got the salvage; your matrix/python-olm repro was the clearest statement of the impact. Thanks @spiky02plateau — and your no-downgrade analysis from #68008 informed the lockstep invariant that shipped alongside it.

@teknium1 teknium1 closed this Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint dependencies Pull requests that update a dependency file P3 Low — cosmetic, nice to have sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants