mars: provision the Mnemosyne memory provider for Hermes #6

Merged
darman merged 8 commits from feat/mars-hermes-mnemosyne into master 2026-09-19 02:16:36 +02:00
2 changed files with 169 additions and 0 deletions
Showing only changes of commit ee0a2f39e9 - Show all commits
+123
View File
@@ -74,6 +74,17 @@ let
builtins.readFile ./gitea-pr-review-prompt.md
);
# Mnemosyne memory provider (local SQLite, third-party plugin — not bundled
# with Hermes). Requirements pins live in ./mnemosyne/requirements.txt; see
# the provisioning unit near the bottom of this file for the layout mapped
# into hermesHome. The symlink target below must be the SAME path the
# side venv was built with (hermesHome/mnemosyne-venv) — Hermes resolves
# plugin modules through it, so relative traversal after the bind mount
# still resolves inside the container identically.
mnemosyneReqs = pkgs.writeText "mnemosyne-requirements.txt" (
builtins.readFile ./mnemosyne/requirements.txt
);
# Wire event names (X-GitHub-Event) each route accepts — NOT the
# subscription names the gitea hooks in services/dev/gitea.nix use. The two
# namespaces collide; see the long comment on the route unit below.
@@ -418,4 +429,116 @@ in
mv -f "$tmp" "$conf"
'';
};
# ---- Mnemosyne memory provider ----------------------------------------
# Third-party plugin (PyPI: mnemosyne-hermes + mnemosyne-memory), not
# bundled with the official image. Two pieces must exist before the gateway
# starts for memory.provider = mnemosyne to activate:
#
# 1. ${hermesHome}/plugins/mnemosyne — a symlink to the plugin package
# inside the side venv. Hermes discovers providers by scanning
# $HERMES_HOME/plugins (see its plugins/memory discovery code), reads
# __init__.py/. For it to IMPORT cleanly the plugin's sibling
# `mnemosyne` core package must be importable too — which is exactly
# why the plugin code lives inside the side venv's site-packages
# rather than as a bare writable copy.
#
# 2. The side venv itself (${hermesHome}/mnemosyne-venv), built with the
# pinned pins in ./mnemosyne/requirements.txt. Inside hermesHome so
# it lands inside HERMES_WRITE_SAFE_ROOT=/opt/data (visible to the
# container at /opt/data/mnemosyne-venv) and survives image rebuilds.
#
# The venv's absolute paths embed ${hermesHome}: uv venv records the
# creation prefix, which is by construction identical inside and outside
# the container thanks to /opt/data being a bind mount of hermesHome.
#
# Idempotent: marked done by a stamp file keyed by the hash of the
# requirements text, so a changed pin re-provisions. Never deletes —
# removing memory.provider from config is what retires it.
#
# Ordering: before podman-hermes-agent (the gateway needs the plugin at
# import time), after network (uv may fetch wheels on first provision),
# with a bounded timeout so a broken proxy cannot hang boot.
systemd.services.hermes-agent-mnemosyne-provision = {
description = "Provision Mnemosyne memory provider (side venv + plugin symlink)";
before = [ "podman-hermes-agent.service" ];
wantedBy = [ "podman-hermes-agent.service" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
unitConfig.RequiresMountsFor = [ "/mnt/jupiter" ];
path = [ pkgs.python3 pkgs.uv pkgs.coreutils ];
serviceConfig = {
Type = "oneshot";
TimeoutStartSec = 600;
};
environment = {
UV_PYTHON_INSTALL_DIR = "${hermesHome}/mnemosyne-uv/python";
UV_CACHE_DIR = "${hermesHome}/mnemosyne-uv/cache";
UV_COMPILE_BYTECODE = "1";
};
script = ''
set -euo pipefail
venv=${hermesHome}/mnemosyne-venv
pluginDir=${hermesHome}/plugins/mnemosyne
stampFile=${hermesHome}/mnemosyne-provision.stamp
reqHash=$(sha256sum ${mnemosyneReqs} | cut -d" " -f1)
if [ -x "$venv/bin/python" ] && [ -f "$stampFile" ] \
&& [ "$(cat "$stampFile")" = "$reqHash" ] \
&& [ -e "$pluginDir" ] \
&& [ -x "$venv/bin/mnemosyne-hermes" ]; then
exit 0
fi
mkdir -p ${hermesHome}/plugins ${hermesHome}/mnemosyne
uv venv "$venv" --python ${pkgs.python313}/bin/python3 --quiet
UV_VENV="$venv" uv pip install \
--python "$venv/bin/python" \
--requirement ${mnemosyneReqs} --quiet
# The plugin wrapper lands as hermes_memory_provider inside site-packages;
# uv installs the exact entry point scripts shown below. Symlink the
# discovered package dir (never a fixed guess find it by marker).
siteDir=$("$venv/bin/python" -c 'import site; print(site.getsitepackages()[0])')
target="$siteDir/hermes_memory_provider"
[ -d "$target" ] || { echo "mnemosyne plugin package not found in venv" >&2; exit 1; }
install -d -m 0755 -o ${hermesUid} -g ${hermesGid} \
"$(dirname "$pluginDir")"
rm -f "$pluginDir"
ln -s "$target" "$pluginDir"
printf '%s' "$reqHash" > "$stampFile"
chown -R ${hermesUid}:${hermesGid} \
"$venv" "$(dirname "$pluginDir")" "$stampFile" \
${hermesHome}/mnemosyne-uv
# Mirror the "active provider" cue into config.yaml equivalent to
# `hermes config set memory.provider mnemosyne`, but idempotent and
# non-interactive. Only touches the one key, never rewrites the file.
cfg=${hermesHome}/config.yaml
if [ -f "$cfg" ]; then
if ! grep -q '^ provider: mnemosyne' "$cfg"; then
if grep -q '^memory:' "$cfg"; then
sed -i 's/^memory:$/memory:\n provider: mnemosyne/' "$cfg"
else
printf '\nmemory:\n provider: mnemosyne\n' >> "$cfg"
fi
chown ${hermesUid}:${hermesGid} "$cfg"
fi
else
printf 'memory:\n provider: mnemosyne\n' > "$cfg"
chown ${hermesUid}:${hermesGid} "$cfg"
fi
'';
};
# Also assert mnemosyne as the active provider so the container's own
# config.yaml says the same thing statelessly — mirrored from the docs'
# `hermes config set memory.provider mnemosyne`. Done here (not a separate
# unit) so venv and config-cue stay in lockstep; never edits anything else
# in the file. Runs at the tail of the provisioning oneshot, after a
# successful venv, so a half-provision never flips the provider on.
# (system.activationScripts is NOT used — the file must exist first, and
# activation would run before hermesHome's own cont-init has created it.)
}
+46
View File
@@ -0,0 +1,46 @@
# Pinned requirements for a Mnemosyne side-venv on mars.
#
# Hermes vendors its own Python (the official image's venv) and deliberately
# stays minimal: no pip module inside it, PEP 668 external-management on top.
# Installing provider packages straight into that interpreter would fight the
# image on every rebuild, so Mnemosyne (and its plugin wrapper) live in their
# own venv instead — see the provisioning unit in hosts/mars/hermes-agent.nix.
#
# Freeze captured 2026-09-19 from a verified container-side install of
# `mnemosyne-memory[embeddings]` + `mnemosyne-hermes` — side venv at
# $HERMES_HOME/mnemosyne-venv, activated via $HERMES_HOME/plugins/mnemosyne.
# Versions pinned exactly; transitive deps frozen for reproducibility
# (onnxruntime/numpy drift under a long-lived SQLite state dir is what a
# freeze is here to prevent).
#
anyio==4.15.0
certifi==2026.7.22
charset-normalizer==3.5.1
click==8.5.0
fastembed==0.8.0
filelock==3.32.5
flatbuffers==25.12.19
fsspec==2026.7.0
h11==0.16.0
hf-xet==1.6.0
httpcore==1.0.9
httpx==0.28.1
huggingface-hub==1.32.0
idna==3.19
loguru==0.7.3
mmh3==5.3.0
mnemosyne-hermes==0.5.0
mnemosyne-memory==3.15.1
numpy==2.5.3
onnxruntime==1.30.0
packaging==26.3
pillow==12.3.0
protobuf==7.36.1
py-rust-stemmers==0.1.8
pyyaml==6.0.3
requests==2.34.2
sqlite-vec==0.1.9
tokenizers==0.23.2
tqdm==4.70.0
typing-extensions==4.16.0
urllib3==2.7.0