Compare commits

...
2 Commits
8 changed files with 228 additions and 4 deletions

No files matched your search

+8
View File
@@ -1,3 +1,11 @@
# 2026-10-21 — Erfolgreiche Hermes-Prüfung ohne Versionswechsel
- Ein unveränderter Commit nach erfolgreichem `hermes update` gilt als Erfolg:
Der konfigurierte Stable-Kanal kann aktuell sein, obwohl `origin/main` weiter ist.
- Abschließende Meldungen erscheinen direkt im Journal des Update-Dienstes.
- Isolierte Regressionstests prüfen unveränderte/aktualisierte Commits,
Updater-Fehler und Gateway-Rollback ohne Netzwerk oder echte Dienstaufrufe.
# 2026-10-12 — VRAM-Reserve und portable CPU-Kompression # 2026-10-12 — VRAM-Reserve und portable CPU-Kompression
- `desktop-64k` mit 4096 MiB Fit-Zielmarge und 64K Kontext als Standard neuer - `desktop-64k` mit 4096 MiB Fit-Zielmarge und 64K Kontext als Standard neuer
+11 -3
View File
@@ -14,7 +14,7 @@ Reproduzierbares Benutzer-Deployment für Hermes Agent auf Arch Linux mit:
- optionalem CUDA-Build von llama-server, llama-cli und llama-bench - optionalem CUDA-Build von llama-server, llama-cli und llama-bench
- Qwen-/RTX-3080-Profilen mit prüfbarem Wechsel und Rückweg - Qwen-/RTX-3080-Profilen mit prüfbarem Wechsel und Rückweg
Stand: 2026-10-12. Für deine **bestehende Workstation** zuerst Stand: 2026-10-21. Für deine **bestehende Workstation** zuerst
[`docs/WORKSTATION.md`](docs/WORKSTATION.md) lesen. Der Schnellstart unten ist für [`docs/WORKSTATION.md`](docs/WORKSTATION.md) lesen. Der Schnellstart unten ist für
eine neue Maschine. Hardware-Leistungsgewinne sind noch nicht auf deiner GPU eine neue Maschine. Hardware-Leistungsgewinne sind noch nicht auf deiner GPU
gemessen; die kurze Funktions- und VRAM-Abnahme vom 12.10.2026 ist in gemessen; die kurze Funktions- und VRAM-Abnahme vom 12.10.2026 ist in
@@ -150,7 +150,7 @@ Update-Lock und löst bei Backend-HTTP-Timeouts keine Inferenz-Neustarts mehr au
1. exklusives Lock und Prüfung auf lokale Änderungen 1. exklusives Lock und Prüfung auf lokale Änderungen
2. `origin/main`-Fetch mit begrenzten Retries und letztem HTTP/1.1-Fallback 2. `origin/main`-Fetch mit begrenzten Retries und letztem HTTP/1.1-Fallback
3. schneller Abschluss ohne Backup, wenn kein Update vorliegt 3. schneller Abschluss ohne Backup, wenn Checkout und gefetchter Branch identisch sind
4. komprimiertes Backup vor jeder Änderung 4. komprimiertes Backup vor jeder Änderung
5. Stoppen des Gateways und Aufruf von `hermes update` 5. Stoppen des Gateways und Aufruf von `hermes update`
6. einmaliger Retry bei eindeutig transientem Fetch-Fehler 6. einmaliger Retry bei eindeutig transientem Fetch-Fehler
@@ -159,9 +159,17 @@ Update-Lock und löst bei Backend-HTTP-Timeouts keine Inferenz-Neustarts mehr au
9. separater llama.cpp-Healthcheck ohne unsinnigen Hermes-Rollback 9. separater llama.cpp-Healthcheck ohne unsinnigen Hermes-Rollback
10. Aufbewahrung der drei neuesten Hermes-Backups 10. Aufbewahrung der drei neuesten Hermes-Backups
Den Zielstand wählt `hermes update` anhand des konfigurierten Update-Kanals.
Bei Stable kann der Checkout bereits aktuell sein, obwohl `origin/main` weiter
ist. Ein unveränderter Commit bei Updater-Exitcode 0 und gesundem Gateway gilt
deshalb als Erfolg. Dabei können Backup und Gateway-Neustart trotzdem stattfinden.
Ein fehlgeschlagener Updater bei unverändertem Commit bleibt ein Fehler.
Ein Fehler von Telegram oder Signal kann den Updateprozess niemals fehlschlagen Ein Fehler von Telegram oder Signal kann den Updateprozess niemals fehlschlagen
lassen. Detaillierte Zwischenstände landen im Journal; nur zusammengefasste lassen. Detaillierte Zwischenstände landen im Journal; nur zusammengefasste
Ergebnisse werden versendet. Ergebnisse werden versendet. Abschlussmeldungen stehen auch direkt im Journal
von `hermes-stack-hermes-update.service`. Diagnose, gezielte Installation und
Prüfung sind in [HERMES_UPDATE.md](docs/HERMES_UPDATE.md) beschrieben.
## Bedienung ## Bedienung
+69
View File
@@ -0,0 +1,69 @@
# Hermes-Update betreiben und prüfen
## Bereits aktuelle Stable-Version
Der Updater prüft zunächst den konfigurierten Remote-Branch (standardmäßig
`origin/main`). Den tatsächlichen Zielstand wählt anschließend `hermes update`
anhand des Hermes-Update-Kanals. Bei Stable kann der installierte Release-Commit
aktuell sein, obwohl auf `main` neuere Entwicklungscommits liegen.
Bis zur Korrektur vom 21.10.2026 endete dieser Fall trotz `Already up to date!`
und Updater-Exitcode 0 mit einem fehlgeschlagenen Wartungsdienst. Das Skript
forderte fälschlich einen Commitwechsel. Jetzt endet ein erfolgreicher Updater
mit unverändertem Commit nach den Konfigurations- und Gateway-Prüfungen
erfolgreich. Der Kanal wird durch diese Korrektur nicht geändert.
Ein fehlgeschlagener Updater bei unverändertem Commit liefert weiterhin Exitcode
1. Ein ungesunder Gateway löst weiterhin den vorhandenen Rollback aus. Der
separate llama-Endpunkt wird unabhängig geprüft; dessen Ausfall führt nicht zu
einem Hermes-Rollback.
## Nur den korrigierten Wrapper installieren
Im Repository-Verzeichnis ausführen, wenn gerade kein Update läuft. Vorher
laufende Hermes-Aufgaben beenden lassen. Diese gezielte Installation ersetzt nur
den Wrapper; `./install.sh` installiert dagegen alle versionierten Wrapper/Units.
```bash
make test
backup_dir="$(mktemp -d "$HOME/.local/state/hermes-update-wrapper.XXXXXXXX")"
cp -p ~/.local/libexec/hermes-stack-hermes-update "$backup_dir/"
printf 'Wrapper-Sicherung: %s\n' "$backup_dir"
install -m 0755 libexec/hermes-stack-hermes-update \
~/.local/libexec/hermes-stack-hermes-update
cmp libexec/hermes-stack-hermes-update \
~/.local/libexec/hermes-stack-hermes-update
```
Die Unit bleibt unverändert; dafür ist kein `daemon-reload` erforderlich.
Die vorherige Wrapper-Version liegt im ausgegebenen bzw. in `$backup_dir`
gespeicherten Sicherungsverzeichnis. Bei Bedarf dieselbe Datei mit `install
-m 0755` zurückkopieren, während kein Update läuft.
## Manueller Lauf und Diagnose
Der Lauf kann den Gateway kurz stoppen und neu starten. Er erzeugt nach dem
Remote-Vergleich gegebenenfalls auch dann ein Backup, wenn Stable bereits aktuell
ist. Standardmäßig werden die drei neuesten Hermes-Backups behalten.
```bash
systemctl --user start hermes-stack-hermes-update.service
systemctl --user show hermes-stack-hermes-update.service \
-p ActiveState -p SubState -p Result -p ExecMainStatus
systemctl --user is-active hermes-gateway.service
journalctl --user -u hermes-stack-hermes-update.service -n 100 --no-pager
journalctl --user -t hermes-stack-maintenance -n 100 --no-pager
```
Erwartet: `Result=success`, `ExecMainStatus=0` und ein aktiver Gateway. Als
`Type=oneshot` ist der Update-Dienst nach erfolgreichem Abschluss selbst wieder
`inactive/dead`. Die Meldung `Hermes ist im konfigurierten Update-Kanal bereits
aktuell` bestätigt den erfolgreichen Lauf ohne Versionswechsel.
Updater-Ausgaben liegen zusätzlich unter
`~/.local/state/hermes-stack-maintenance/hermes-update-*.log`.
Wrapper-Abschlussmeldungen stehen im Dienstjournal; die Benachrichtigung schreibt
zusätzlich unter dem Tag `hermes-stack-maintenance`.
Die Abnahme auf Archwork und ihre Prüfgrenzen stehen in
[VALIDATION.md](VALIDATION.md).
+17
View File
@@ -1,3 +1,20 @@
# Validierung vom 2026-10-21
- `make test`: Syntax- und Secret-Musterprüfung sowie **31 Funktionstests
bestanden** (26 vorhandene Tests und fünf neue Updater-Regressionstests).
- Neue isolierte Tests: unveränderter Stable-Commit bei neuerem `main`,
fehlgeschlagener Updater ohne Commitwechsel, erfolgreicher Commitwechsel,
Commitwechsel mit Updater-Restartwarnung und Gateway-Rollback.
Externe Programme werden simuliert; kein echtes Netzwerk oder systemd darin.
- `git diff --check`: erfolgreich. ShellCheck ist lokal nicht installiert.
- Korrigierter Wrapper auf Archwork installiert und bytegleich geprüft.
- Echter Update-Dienstlauf am 21.10.2026, 16:26 MESZ: Stable v0.22.3 blieb auf
Commit `5b147ab036d6d19131bff8d20701b5001eab844a`; Abschluss nach rund 47 Sekunden
mit `Result=success` und `ExecMainStatus=0`. Gateway danach `active/running`.
Die neue Erfolgsmeldung erschien im Journal des Update-Dienstes.
- Ein echter Versionswechsel und ein echter Rollback wurden bei dieser Abnahme
nicht ausgelöst. Ein vollständiger Installer-Lauf wurde nicht durchgeführt.
# Validierung vom 2026-10-12 # Validierung vom 2026-10-12
- `make test`: Shell-Syntaxprüfung, Secret-Mustercheck und **26 isolierte - `make test`: Shell-Syntaxprüfung, Secret-Mustercheck und **26 isolierte
+13
View File
@@ -147,3 +147,16 @@ Repo-Dateien wurden danach aufgenommen und getestet, aber nicht erneut über
`install.sh` auf die Workstation ausgerollt. Der aktuelle Betrieb bleibt bei den `install.sh` auf die Workstation ausgerollt. Der aktuelle Betrieb bleibt bei den
bereits geprüften Einstellungen. Details zur Übertragung und zu bestehenden bereits geprüften Einstellungen. Details zur Übertragung und zu bestehenden
Units/Drop-ins stehen in [RTX3080.md](RTX3080.md). Units/Drop-ins stehen in [RTX3080.md](RTX3080.md).
## Hermes-Update-Korrektur vom 2026-10-21
Der korrigierte `hermes-stack-hermes-update` ist gezielt auf der Workstation
installiert und mit der Repository-Datei bytegleich geprüft. Der echte
Dienstlauf mit bereits aktuellem Stable v0.22.3 endete erfolgreich
(`Result=success`, `ExecMainStatus=0`); der Gateway war danach `active/running`.
Abschlussmeldungen erscheinen jetzt direkt im Dienstjournal.
Vorherige Wrapper-Version:
`~/.local/libexec/hermes-stack-hermes-update.before-noop-fix-20261021`.
Ein vollständiger Installer-Lauf war für diese Korrektur nicht erforderlich.
Betrieb und Fehlerdiagnose: [HERMES_UPDATE.md](HERMES_UPDATE.md).
+8 -1
View File
@@ -15,6 +15,8 @@ log() {
notify_user() { notify_user() {
local text="$1" local text="$1"
# Keep the outcome in this service's journal as well as the notifier's journal.
printf '%s\n' "$text"
if [[ -x "$NOTIFIER" ]]; then if [[ -x "$NOTIFIER" ]]; then
"$NOTIFIER" "$text" || true "$NOTIFIER" "$text" || true
else else
@@ -226,7 +228,12 @@ mapfile -t old_backups < <(
((${#old_backups[@]} == 0)) || rm -f -- "${old_backups[@]}" ((${#old_backups[@]} == 0)) || rm -f -- "${old_backups[@]}"
new_commit="$(git -C "$HERMES_REPO" rev-parse HEAD)" new_commit="$(git -C "$HERMES_REPO" rev-parse HEAD)"
if [[ "$new_commit" == "$old_commit" ]]; then # The updater selects the configured channel (e.g. stable), whereas the fetch
# above only checks remote connectivity. Stable may legitimately stay on the
# same commit even when origin/main has advanced.
if [[ "$new_commit" == "$old_commit" ]] && ((update_exit == 0)); then
notify_user "Hermes ist im konfigurierten Update-Kanal bereits aktuell: $new_commit. Konfiguration und frisch gestarteter Gateway sind gesund."
elif [[ "$new_commit" == "$old_commit" ]]; then
notify_user "Hermes-Update wurde nicht installiert (Updater-Exitcode $update_exit); der bisherige Commit $new_commit und der frisch gestartete Gateway sind gesund." notify_user "Hermes-Update wurde nicht installiert (Updater-Exitcode $update_exit); der bisherige Commit $new_commit und der frisch gestartete Gateway sind gesund."
exit 1 exit 1
elif ((update_exit != 0)); then elif ((update_exit != 0)); then
+1
View File
@@ -21,6 +21,7 @@ else
fi fi
python3 "$ROOT_DIR/tests/test_llama.py" || failures=$((failures + 1)) python3 "$ROOT_DIR/tests/test_llama.py" || failures=$((failures + 1))
python3 "$ROOT_DIR/tests/test_hermes_update.py" || failures=$((failures + 1))
if (cd -- "$ROOT_DIR" && rg -n --hidden --glob '!tests/smoke.sh' --glob '!tests/test_llama.py' --glob '!.git/**' \ if (cd -- "$ROOT_DIR" && rg -n --hidden --glob '!tests/smoke.sh' --glob '!tests/test_llama.py' --glob '!.git/**' \
'(TELEGRAM_BOT_TOKEN|SIGNAL_ACCOUNT|API_KEY)=("|'"'"')?[A-Za-z0-9+]' .); then '(TELEGRAM_BOT_TOKEN|SIGNAL_ACCOUNT|API_KEY)=("|'"'"')?[A-Za-z0-9+]' .); then
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""Run the actual maintenance script with isolated state and fake external tools."""
import os
from pathlib import Path
import shlex
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
RUNNER = ROOT / "libexec/hermes-stack-hermes-update"
class HermesUpdateTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="hermes-update-", dir=ROOT.parent)
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.bin = self.root / "bin"
self.bin.mkdir()
config = self.root / "config/hermes-stack-maintenance"
config.mkdir(parents=True)
(self.root / "hermes-agent/.git").mkdir(parents=True)
self.env = dict(os.environ, XDG_CONFIG_HOME=str(config.parent),
XDG_STATE_HOME=str(self.root / "state"),
XDG_RUNTIME_DIR=str(self.root), TEST_ROOT=str(self.root),
PATH=str(self.bin) + ":" + os.environ["PATH"],
TEST_UPDATE_EXIT="0", TEST_CHANGED="0", TEST_HEALTHY="1")
(config / "config.env").write_text(
f'HERMES_REPO={shlex.quote(str(self.root / "hermes-agent"))}\n'
f'NOTIFIER={shlex.quote(str(self.bin / "notify-test"))}\n'
'HERMES_REMOTE=origin\nHERMES_BRANCH=main\n'
'GATEWAY_SERVICE=test-gateway.service\n'
'GATEWAY_HEALTH_ATTEMPTS=1\nGATEWAY_HEALTH_DELAY=0\n'
'LLAMA_URL=http://unused.invalid\n')
self.program("git", '''case "$*" in
*"status --porcelain"*|*"fetch --no-tags"*) exit 0 ;;
*"rev-parse HEAD"*)
if [[ -f "$TEST_ROOT/changed" ]]; then printf 'c%.0s' {1..40};
else printf 'a%.0s' {1..40}; fi ;;
*"rev-parse refs/remotes/"*) printf 'b%.0s' {1..40} ;;
*) exit 99 ;;
esac
''')
self.program("hermes", '''case "$*" in
update)
[[ "$TEST_CHANGED" == 0 ]] || touch "$TEST_ROOT/changed"
echo "Simulated updater completion"
exit "$TEST_UPDATE_EXIT" ;;
"gateway status") [[ "$TEST_HEALTHY" == 1 ]] ;;
"config check"|"config migrate") exit 0 ;;
*) exit 99 ;;
esac
''')
self.program("systemctl", 'printf "%s\\n" "$*" >>"$TEST_ROOT/systemctl-calls"\n')
self.program("tar", 'printf "%s\\n" "$*" >>"$TEST_ROOT/tar-calls"\n')
self.program("curl", "echo '{\"status\":\"ok\"}'\n")
self.program("notify-test", 'printf "%s\\n" "$*" >>"$TEST_ROOT/notices"\n')
self.program("systemd-cat", "cat >/dev/null\n")
self.program("sleep", "exit 0\n")
def program(self, name, body):
path = self.bin / name
path.write_text("#!/usr/bin/env bash\nset -eu\n" + body)
path.chmod(0o755)
def run_update(self, **settings):
return subprocess.run(["bash", str(RUNNER)], env=dict(self.env, **settings),
capture_output=True, text=True, timeout=15)
def test_stable_unchanged_with_newer_main_succeeds(self):
result = self.run_update()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("bereits aktuell", result.stdout)
self.assertIn("restart test-gateway.service", (self.root / "systemctl-calls").read_text())
self.assertNotIn("zurückgerollt", (self.root / "notices").read_text())
def test_unchanged_failed_updater_still_fails(self):
result = self.run_update(TEST_UPDATE_EXIT="7")
self.assertEqual(result.returncode, 1, result.stderr)
self.assertIn("Updater-Exitcode 7", result.stdout)
def test_changed_commit_succeeds(self):
result = self.run_update(TEST_CHANGED="1")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("erfolgreich aktualisiert", result.stdout)
def test_changed_commit_with_restart_warning(self):
result = self.run_update(TEST_CHANGED="1", TEST_UPDATE_EXIT="7")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("Updater-Restartwarnung", result.stdout)
def test_unhealthy_gateway_still_rolls_back(self):
result = self.run_update(TEST_HEALTHY="0")
self.assertEqual(result.returncode, 1, result.stderr)
self.assertIn("zurückgerollt", result.stdout)
self.assertIn("-xf", (self.root / "tar-calls").read_text())
if __name__ == "__main__":
unittest.main()