fix(update): refresh active memory provider deps after venv rebuild

Surgical reapply of #53505 by @LeonSGP43 onto current main (stale-base
cherry-pick conflicted in main.py):

- _refresh_active_memory_provider_dependencies() in hermes_cli/main.py,
  wired into BOTH the git-pull update path (after lazy refresh) and the
  ZIP update path — the provider's plugin.yaml bridge packages are not
  in extras or LAZY_DEPS, so the core reinstall could strip/downgrade
  them and the update flow never healed them (#53272 mem0ai).
- _install_dependencies(force=True) in memory_setup.py: hand every
  declared spec to the resolver on update so missing AND version-drifted
  packages are restored (no-op when satisfied).
- Skip guards: no provider / builtin store / memory.enabled=false.

Widened beyond the original PR for the #70636 half:
- _provider_pip_dependencies(): mode-aware expansion — Hindsight in
  local/local_embedded mode needs hindsight-all (daemon + embedder), not
  just the declared hindsight-client; setup installs it but plugin.yaml
  can't express it, so update-time healing previously missed
  hindsight-embed and the daemon stayed broken.
- Spec-aware import probing: version ranges in pip_dependencies
  (mem0ai>=2.0.10,<3) no longer break the pip-name -> import-name
  mapping.

Fixes #53272. Fixes the hindsight-embed half of #70636.
This commit is contained in:
LeonSGP43 2026-07-26 18:14:30 -07:00 committed by Teknium
parent ab2c9289ca
commit 5645169c8f
3 changed files with 192 additions and 5 deletions

View file

@ -8,6 +8,7 @@ the provider's config schema. Writes config to config.yaml + .env.
from __future__ import annotations
import os
import re
import sys
import shlex
from pathlib import Path
@ -18,6 +19,32 @@ from hermes_cli.secret_prompt import masked_secret_prompt
_CANCELLED = -1
def _provider_pip_dependencies(provider_name: str, declared: list) -> list:
"""Return the pip deps a provider actually needs on THIS install.
``plugin.yaml`` declares the provider's baseline bridge packages, but
some providers install mode-dependent extras at setup time that the
manifest can't express. Hindsight's ``local_embedded`` mode installs
``hindsight-all`` (daemon + embedder + client) during
``hermes memory setup`` if the update-time refresh only reinstalled
the declared ``hindsight-client``, the embedded daemon would stay
broken after a venv rebuild stripped ``hindsight-embed`` (#70636).
"""
deps = list(declared or [])
if provider_name == "hindsight":
try:
import json
cfg_path = get_hermes_home() / "hindsight" / "config.json"
cfg = json.loads(cfg_path.read_text(encoding="utf-8")) if cfg_path.exists() else {}
mode = cfg.get("mode", "")
# "local" is a legacy alias for "local_embedded"
if mode in {"local", "local_embedded"}:
deps.append("hindsight-all")
except Exception:
pass
return deps
# ---------------------------------------------------------------------------
# Curses-based interactive picker (same pattern as hermes tools)
# ---------------------------------------------------------------------------
@ -77,8 +104,16 @@ def _prompt(label: str, default: str | None = None, secret: bool = False) -> str
# Provider discovery
# ---------------------------------------------------------------------------
def _install_dependencies(provider_name: str) -> None:
"""Install pip dependencies declared in plugin.yaml."""
def _install_dependencies(provider_name: str, *, force: bool = False) -> None:
"""Install pip dependencies declared in ``plugin.yaml``.
When ``force`` is true, every declared dependency is handed to the
installer even if its import currently succeeds the resolver then
reinstalls anything missing or version-drifted and no-ops on satisfied
ranges. This is how ``hermes update`` heals the active memory provider
after a venv rebuild/sync removed or downgraded its bridge packages
(#53272, #70636).
"""
import subprocess
from plugins.memory import find_provider_dir
@ -96,7 +131,7 @@ def _install_dependencies(provider_name: str) -> None:
except Exception:
return
pip_deps = meta.get("pip_dependencies", [])
pip_deps = _provider_pip_dependencies(provider_name, meta.get("pip_dependencies", []))
if not pip_deps:
return
@ -108,10 +143,15 @@ def _install_dependencies(provider_name: str) -> None:
"hindsight-all": "hindsight",
}
# Check which packages are missing
# Check which packages need installation.
missing = []
for dep in pip_deps:
import_name = _IMPORT_NAMES.get(dep, dep.replace("-", "_").split("[")[0])
if force:
missing.append(dep)
continue
dep_name = re.match(r"^[A-Za-z0-9_][A-Za-z0-9_.\-]*", dep)
base = dep_name.group(0) if dep_name else dep
import_name = _IMPORT_NAMES.get(base, base.replace("-", "_").split("[")[0])
try:
__import__(import_name)
except ImportError: