fix(tools,cli): never downgrade lazy deps at runtime; truthful memory status - #80517
fix(tools,cli): never downgrade lazy deps at runtime; truthful memory status#80517Crqptx wants to merge 4 commits into
Conversation
ensure() used strict spec satisfaction, so a package installed NEWER than its pinned floor was reported missing and pip was asked to install the pin — an unattended downgrade on every retain (NousResearch#80390). The embedded hindsight stack migrates its Postgres (pg0) data directory forward per release; once 0.8.x has run, the DB cannot go back to 0.6.1. - feature_missing(feature, *, runtime=False): strict by default — unchanged for every existing caller; runtime=True applies floor semantics via _is_satisfied_runtime (strict first, then installed >= floor; no parseable floor -> installed means satisfied, don't churn). - ensure(): runtime semantics for pre-check and post-install verify, so a newer install is never touched, a below-pin install still upgrades, and an absent package still installs (install failure still raises FeatureUnavailable). - refresh_active_features(): untouched — the strict pre-check still flags off-pin installs so 'hermes update' keeps propagating pin bumps, but the only installer no-ops on newer, so a downgrade is impossible anywhere. - is_available(): runtime-readiness predicate (not feature_missing(f, runtime=True)) so the wake_word readiness path never disables a working newer install in a sealed env. - _describe_missing(): prompt distinguishes absent from installed-at-wrong-version. Tests: TestRuntimeNeverDowngrade (12 cases) + managed-suite fixture compat for the new kwarg. Fixes NousResearch#80390
'hermes memory status' reported 'available ✓' whenever the provider's static config was healthy, even when its lazy deps were pinned below the floor — so status and the retain path (ensure("memory.<provider>")) could disagree and the CLI green-lit a provider whose deps would fail at runtime (NousResearch#80388).
- _runtime_lazy_missing(provider): feature_missing("memory.<provider>", runtime=True) — the exact predicate the retain path gates on; fails open for unknown providers and import errors.
- cmd_status: healthy iff plugin_ok and not runtime_missing; otherwise 'not available ✗' + 'Missing runtime deps:' with the specs and the canonical install hint from feature_install_command(). Env-var checklist only on static failure. Healthy line byte-identical.
Tests: TestStatusUsesRuntimePredicate (6 cases); _run_cmd_status gained a providers= param.
Fixes NousResearch#80388
Issue NousResearch#80390 suggested a floor pin (>=0.6.1) for the hindsight backend, which ships outside the image lockfile on a moving PyPI train and whose data dir ratchets forward with each release; the reporter validated the relaxation on 0.8.6. The old ==0.6.1 pin made ensure() install the stale 0.6.1 train for below-pin users, and the status hint coached fresh users toward the exact state the issue calls broken. Every other lazy feature stays exactly pinned (==) and the pyproject 'hindsight' extra (CI) is untouched. Also restore exclusive (>) floor tracking so >1.0 with 1.0.0 installed is a genuine runtime mismatch (an upgrade, never a downgrade).
|
Status update for maintainers: @webtecnica closed #80496 in favor of this PR ("the better implementation: strict-by-default contract preserved, runtime floor semantics opted-in explicitly"). Two later PRs (#80559, #80555) were filed against #80390/#80388 after this one and remain open; this PR already discloses both in its description. Added one commit since the original status update: |
|
Thanks @Crqptx for the kind words and for documenting the comparison in the PR body — the credit flows both ways, and I'm glad the exclusive/range cases in |
…ver 'refreshed' for a no-op (NousResearch#80390)
What does this PR do?
Fixes two related defects in the lazy-deps / memory-status path — #80390 is the root cause, #80388 its visible symptom:
#80390 — auto-downgrade on every retain.
tools/lazy_deps.pypinnedhindsight-client==0.6.1and the strict_is_satisfied()treated any off-pin install as missing. An operator-installed, working, NEWER hindsight stack was reported missing, andensure()attempted an unattended downgrade to the pin on every retain. This is destructive: the embedded hindsight stack migrates its Postgres (pg0) data directory forward with each release — once 0.8.x has run, the DB cannot go back to 0.6.1. The==pin was also the wrong shape for this backend (reporter suggestion #4, validated by the reporter on 0.8.6): hindsight ships outside the image's lockfile on a moving PyPI train, so the pin is now a floor (hindsight-client>=0.6.1) — absent/below-pin installs stay flagged, butensure()installs the current train instead of stale 0.6.1, and the status hint no longer coaches toward the broken state.#80388 — dishonest status.
hermes memory statuschecked only the provider's static config, so it printedavailable ✓while every retain was dropped on a version/install check the runtime enforces.Approach — explicit runtime semantics, strict preserved. Rather than rewriting the strict predicate globally, this PR opts specific runtime paths into never-downgrade behavior:
feature_missing(feature, *, runtime=False)— strict by default: byte-identical behavior for every caller that doesn't opt in (matrix adapter, doctor).runtime=Trueapplies floor semantics via_is_satisfied_runtime: strict check first (same cost on the hot path), then installed ≥ floor (exclusive>bounds are honored —>1.0with 1.0.0 installed is a genuine mismatch, an upgrade never a downgrade); a spec with no parseable floor means "installed ⇒ satisfied" (don't churn).ensure()usesruntime=Truefor the pre-check AND post-install verify — newer installs are never touched, below-pin installs still upgrade (pin propagation intact), absent packages still install, install failure still raisesFeatureUnavailable.refresh_active_features()pre-checks with the same runtime semantics asensure()(feature_missing(feature, runtime=True)): a newer-than-pin install is reported"current"— never"refreshed", which would claim a reinstall that never ran — while a genuinely below-pin install is still flagged and upgraded, so pin-bump propagation is preserved. A downgrade is impossible anywhere.is_available()becomes a runtime-readiness predicate — the wake_word readiness path (tools/wake_word.py:851/877/901) never disables a working newer install in a sealed env._describe_missing()— prompts distinguish "absent" from "installed at a different version".hermes memory statusnow uses the exact runtime predicate (_runtime_lazy_missing→feature_missing(f"memory.{provider}", runtime=True)), so status and runtime cannot disagree by construction. Healthy line byte-identical; env-var checklist only on static failure. Missing deps are listed with the canonical install hint fromfeature_install_command().Note on competing PR #80496: @webtecnica's
fix(memory): never auto-downgrade installed hindsight; honest status predicatefixed the same two issues with a different approach — rewriting_is_satisfied()globally so newer-than-pin counts as satisfied, plus changing the hindsight pin to>=0.6.1. That fix was valid and its tests passed. I tested both trees head-to-head (identical suite + behavioral probes, same venv) before filing: this PR preserves the strict/update-path contract for all existing callers (their version silently makesfeature_missing("platform.matrix")and thehermes updaterefresh pre-check newer-tolerant at the check level), renders an actionable install command in status, and adds tests for the refresh-never-downgrades contract their PR doesn't cover. The final commit adopts their hindsight pin shape (>=0.6.1) — it implements the reporter's own suggestion #4 and was validated by the reporter on 0.8.6 — while every other lazy feature stays exactly pinned (==) and the CIhindsightextra is untouched. The difference that remains is architectural: ours keeps strict pin semantics for every existing caller by default, with runtime paths opting in. @webtecnica closed #80496 in favor of this PR after filing.Related Issue
Fixes #80390, Fixes #80388
Type of Change
Changes Made
tools/lazy_deps.py—_spec_floor_version(floor + exclusive>bounds),_is_satisfied_runtime,feature_missing(runtime=),ensure()runtime semantics,is_available()runtime,_describe_missing, hindsight floor pin (+140)tests/tools/test_lazy_deps.py—TestRuntimeNeverDowngrade, 14 tests (+162)tests/tools/test_lazy_deps_managed.py— fixture compat for the new kwarg (+7)hermes_cli/memory_setup.py—_runtime_lazy_missing+ truthful status block (+66)tests/hermes_cli/test_memory_status.py—TestStatusUsesRuntimePredicate, 6 tests (+93)How to Test
>=0.6.1), the old code treated it as missing andensure()attempted to install the pin (a downgrade). Now:feature_missing("memory.hindsight", runtime=True)→(),ensure()no-ops,is_available()→ True.hermes memory statusnow printsnot available ✗+Missing runtime deps:+ the specs + the canonicaluv pip install 'hindsight-client>=0.6.1'hint (current train, not stale 0.6.1) — instead ofavailable ✓.python scripts/run_tests_parallel.py -j 8 tests/tools/test_lazy_deps.py tests/tools/test_lazy_deps_managed.py tests/plugins/memory/test_memory_lazy_install.py tests/hermes_cli/test_memory_status.py tests/hermes_cli/test_memory_setup.py tests/hermes_cli/test_memory_setup_provider_arg.py tests/hermes_cli/test_lazy_refresh_venv_repair.py tests/tools/test_wake_word.py→ 135 passed, 0 failed (1 skip:
importorskip("numpy")— numpy absent from the dev venv).Checklist
fix(tools): …,fix(cli): …Documentation & Housekeeping
cli-config.yaml.example— N/A (no config keys added)CONTRIBUTING.md/AGENTS.md— N/A (no architecture or workflow changes)scripts/check-windows-footguns.pyclean on the diff (5 files); no new POSIX-only calls; Linux/macOS not tested locally (CI covers Ubuntu/Python 3.11)Full-suite caveat: on this Windows 11 box the full suite has pre-existing environmental failures unrelated to this change — temp-dir symlink fixtures fail with
WinError 1314(Windows symlink privilege, the documented AGENTS.md §3.4 footgun), optional-SDK tests fail on packages CI installs via extras (e.g.hindsight_client_apivia--extra hindsight), andtest_npm_enginetrips warnings-as-errors. The targeted suite above plus CI (Linux) are the authoritative gates.Security impact
uv pip installpath runs specs fromLAZY_DEPSconstants (maintainer-controlled) via subprocess list-form; no user input reaches a shell._describe_missingrenders spec strings into a prompt message only.Screenshots / Logs
Behavior probe output and repro scripts available on request.