Initial commit
This commit is contained in:
commit
8641464a6c
44 files changed
+2192
No files matched your search
@@ -0,0 +1,17 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
smoke:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: archlinux:latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install test dependencies
|
||||||
|
run: pacman -Syu --noconfirm bash shellcheck ripgrep python jq
|
||||||
|
- name: Run smoke tests
|
||||||
|
run: ./tests/smoke.sh
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
# Local configuration and secrets
|
||||||
|
config/config.env
|
||||||
|
config/hermes.env
|
||||||
|
config/llama.env
|
||||||
|
config/signal-cli.env
|
||||||
|
*.local
|
||||||
|
*.secret
|
||||||
|
*.key
|
||||||
|
|
||||||
|
# Runtime and generated files
|
||||||
|
dist/
|
||||||
|
*.tar.zst
|
||||||
|
*.log
|
||||||
|
*.rej
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
/hermes-test-*/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# 2026-09-05 — RTX-3080-/Qwen-Deployment
|
||||||
|
|
||||||
|
- CUDA-Quellbuild mit expliziter Revision, dokumentierter SHA, drei Tools und
|
||||||
|
getrennten Build-Verzeichnissen. Bestehende Binary-Auswahl bleibt erhalten.
|
||||||
|
- Modellwahl per HF oder lokaler GGUF, optional lokaler Vision-Projektor,
|
||||||
|
stabiler Hermes-Modellalias.
|
||||||
|
- Vier Profile mit 64K/48K/32K Kontext und 1536/1280 MiB Fit-Zielmarge;
|
||||||
|
ursprüngliche Parameter bleiben Standard und Rückweg.
|
||||||
|
- Vorprüfung bekannter Optionsnamen ohne Modellstart. Keine automatische
|
||||||
|
Hardware-/Treiberänderung oder Behauptung einer garantierten VRAM-Reserve.
|
||||||
|
- Profilwahl mit Backup ohne Neustart; separater Neustart mit ausdrücklicher
|
||||||
|
Bestätigung, dass Jobs fertig sind. Geteiltes Maintenance-Lock.
|
||||||
|
- Healthcheck startet den laufenden llama-server bei HTTP-Timeouts nicht neu.
|
||||||
|
Automatischer Start inaktiver llama-Dienste standardmäßig deaktiviert.
|
||||||
|
- Installationssicherung vorhandener Wrapper/Units; bestehende Konfigurationen
|
||||||
|
und Gateway-Unit bleiben erhalten. CUDA-Installation ausdrücklich optional.
|
||||||
|
- Anleitungen für neue Rechner, bestehende Workstation, Messung und Rücknahme.
|
||||||
|
- Isolierte Tests für Profilwerte, Legacy-Konfiguration, Pfade mit Leerzeichen,
|
||||||
|
Modellquellen, Optionsprüfung, Locks, Neustartschutz, HTTP-Fehler, CUDA-Build-
|
||||||
|
Veröffentlichung/Fehler und Signal-JSON-RPC-Payloads ohne echte Nachrichten.
|
||||||
|
|
||||||
|
Nicht hier getestet: echte CUDA-Kompilierung, Qwen-Modell-/Projektor-Download,
|
||||||
|
GPU-Offload, Hyprland-Spitzenlast, Inferenzqualität, tatsächlicher Hermes-Update-
|
||||||
|
Rollback oder externe Signal-/Telegram-Zustellung. Das Repository ist ein
|
||||||
|
Deployment-Paket, kein vollständiges Image deiner privaten Installation.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Johannes
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
.PHONY: test install doctor archive
|
||||||
|
|
||||||
|
test:
|
||||||
|
./tests/smoke.sh
|
||||||
|
|
||||||
|
install:
|
||||||
|
./install.sh
|
||||||
|
|
||||||
|
doctor:
|
||||||
|
~/.local/libexec/hermes-stack-doctor
|
||||||
|
|
||||||
|
archive:
|
||||||
|
mkdir -p dist
|
||||||
|
git archive --format=tar.gz --prefix=hermes-arch-stack/ -o dist/hermes-arch-stack.tar.gz HEAD
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
# Hermes Arch Stack
|
||||||
|
|
||||||
|
Reproduzierbares Benutzer-Deployment für Hermes Agent auf Arch Linux mit:
|
||||||
|
|
||||||
|
- offizieller Hermes-Git-Installation unter `~/.hermes/hermes-agent`
|
||||||
|
- von Hermes erzeugtem systemd-User-Service für den Messaging Gateway
|
||||||
|
- optionalem lokalen llama.cpp/OpenAI-kompatiblen Modellendpunkt
|
||||||
|
- optionalem signal-cli HTTP-Daemon
|
||||||
|
- robustem Hermes-Update mit Vorprüfung, Retry, Backup und Rollback
|
||||||
|
- regelmäßigen Healthchecks und Selbstheilungsversuchen
|
||||||
|
- Statusmeldungen an Telegram und Signal
|
||||||
|
- systemd-Timern für Updates und Zustandsprüfungen
|
||||||
|
- optionalem CUDA-Build von llama-server, llama-cli und llama-bench
|
||||||
|
- Qwen-/RTX-3080-Profilen mit prüfbarem Wechsel und Rückweg
|
||||||
|
|
||||||
|
Stand: 2026-09-05. Für deine **bestehende Workstation** zuerst
|
||||||
|
[`docs/WORKSTATION.md`](docs/WORKSTATION.md) lesen. Der Schnellstart unten ist für
|
||||||
|
eine neue Maschine. Hardware-Leistungsgewinne sind noch nicht auf deiner GPU
|
||||||
|
gemessen. Es wird kein Gitea-Server angelegt und nichts automatisch gepusht.
|
||||||
|
|
||||||
|
Das Repository enthält **keine Zugangsdaten, Signal-Sitzung oder
|
||||||
|
Modellgewichte**. Diese Daten werden lokal angelegt und sind durch `.gitignore`
|
||||||
|
ausgeschlossen.
|
||||||
|
|
||||||
|
## Schnellstart auf einer neuen Arch-Maschine
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone ssh://git@GITEA/OWNER/hermes-arch-stack.git
|
||||||
|
cd hermes-arch-stack
|
||||||
|
|
||||||
|
./install.sh --install-packages --install-hermes
|
||||||
|
```
|
||||||
|
|
||||||
|
Für den lokalen GPU-Endpunkt anschließend den
|
||||||
|
[`CUDA-Installer`](docs/LLAMA_INSTALL.md) ausführen. Ein vorhandener Server kann
|
||||||
|
stattdessen über `LLAMA_SERVER_BIN` verwendet werden. Der Installer benötigt
|
||||||
|
funktionierende NVIDIA-Treiber; diese werden nicht von diesem Repo eingerichtet.
|
||||||
|
`--install-packages` führt ein vollständiges `pacman -Syu` aus: bei Kernel-/
|
||||||
|
Treiberänderungen einen Neustart einplanen.
|
||||||
|
|
||||||
|
Danach die beim ersten Lauf erzeugten lokalen Konfigurationen bearbeiten:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$EDITOR ~/.config/hermes-stack-maintenance/config.env
|
||||||
|
$EDITOR ~/.config/hermes-stack-maintenance/llama.env
|
||||||
|
$EDITOR ~/.config/hermes-stack-maintenance/signal-cli.env
|
||||||
|
$EDITOR ~/.hermes/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Hermes und die Messaging-Plattformen konfigurieren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hermes setup
|
||||||
|
hermes gateway setup
|
||||||
|
hermes gateway install --force
|
||||||
|
```
|
||||||
|
|
||||||
|
In Hermes als lokalen OpenAI-kompatiblen Endpunkt `http://127.0.0.1:8080/v1`
|
||||||
|
und als Modell-ID `qwen38-27b-local` wählen. Bestehende Hermes-Konfigurationen,
|
||||||
|
Profile, Skills und Memories werden nicht durch generische Vorgaben ersetzt.
|
||||||
|
Für Neuinstallationen diese Einstellungen interaktiv vornehmen oder die eigenen
|
||||||
|
Daten gemäß Migration sicher übernehmen. Der Alias allein setzt kein Kontextlimit.
|
||||||
|
|
||||||
|
Signal benötigt zusätzlich eine installierte/verknüpfte signal-cli-Instanz.
|
||||||
|
Nach Konfiguration kann `./scripts/install-signal-cli.sh` den vorhandenen
|
||||||
|
Signal-Updater aufrufen; siehe dessen Konfiguration und `docs/MIGRATION.md`.
|
||||||
|
|
||||||
|
Optionale Dienste erst nach ihrer Konfiguration aktivieren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user enable --now signal-cli.service
|
||||||
|
systemctl --user enable --now llama-qwen.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Maintenance aktivieren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user enable --now \
|
||||||
|
hermes-stack-hermes-update.timer \
|
||||||
|
hermes-stack-health.timer \
|
||||||
|
hermes-stack-signal-health.timer \
|
||||||
|
hermes-stack-signal-update-check.timer
|
||||||
|
```
|
||||||
|
|
||||||
|
Für Start ohne interaktive Anmeldung:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo loginctl enable-linger "$USER"
|
||||||
|
```
|
||||||
|
|
||||||
|
Abschließend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.local/libexec/hermes-stack-doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installationsoptionen
|
||||||
|
|
||||||
|
```text
|
||||||
|
./install.sh [Optionen]
|
||||||
|
|
||||||
|
--install-packages Arch-Pakete mit pacman installieren
|
||||||
|
--install-hermes offiziellen Hermes-Installer ausführen
|
||||||
|
--install-llama CUDA-Build installieren (benötigt funktionierenden Treiber)
|
||||||
|
--llama-ref REF expliziter llama.cpp-Tag/Commit oder bewusst master
|
||||||
|
--enable-timers Maintenance-Timer sofort aktivieren
|
||||||
|
--skip-browser Hermes ohne Browser-Automation installieren (Standard)
|
||||||
|
--with-browser Browser-Automation nicht überspringen
|
||||||
|
```
|
||||||
|
|
||||||
|
Ohne Optionen installiert das Skript nur die versionierten Dateien und legt
|
||||||
|
fehlende Beispielkonfigurationen an. Bestehende lokale Konfigurationen werden
|
||||||
|
nicht überschrieben.
|
||||||
|
Ersetzte Wrapper und Units werden vorher gesichert. Eine vorhandene
|
||||||
|
hermes-gateway.service wird nicht neu erzeugt. Ein Modell wird erst beim echten
|
||||||
|
Start des llama-Dienstes geladen, nicht bei der Konfigurationsprüfung.
|
||||||
|
|
||||||
|
## RTX 3080 und Qwen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.local/libexec/hermes-stack-llama-run --check
|
||||||
|
~/.local/libexec/hermes-stack-llama-profile list
|
||||||
|
~/.local/libexec/hermes-stack-llama-report
|
||||||
|
```
|
||||||
|
|
||||||
|
`baseline` behält deine bisherigen 64K Kontext / 1536 MiB Fit-Zielmarge.
|
||||||
|
`balanced` reduziert nur den Kontext auf 48K; `vram-plus` danach optional die
|
||||||
|
Marge auf 1280 MiB. `compact` bietet 32K bei 1536 MiB. Das sind Vergleichsprofile,
|
||||||
|
keine Garantie gegen Desktop-Speicherdruck. Details, Messprotokoll und Rücknahme:
|
||||||
|
[`docs/RTX3080.md`](docs/RTX3080.md).
|
||||||
|
|
||||||
|
Profilwechsel ändern zunächst nur eine separate Auswahl-Datei. Ein Neustart
|
||||||
|
benötigt `hermes-stack-llama-restart --jobs-finished`. Der Healthcheck teilt das
|
||||||
|
Update-Lock und löst bei Backend-HTTP-Timeouts keine Inferenz-Neustarts mehr aus.
|
||||||
|
|
||||||
|
## Update-Verhalten
|
||||||
|
|
||||||
|
`hermes-stack-hermes-update` führt folgende Schritte aus:
|
||||||
|
|
||||||
|
1. exklusives Lock und Prüfung auf lokale Änderungen
|
||||||
|
2. `origin/main`-Fetch mit begrenzten Retries und letztem HTTP/1.1-Fallback
|
||||||
|
3. schneller Abschluss ohne Backup, wenn kein Update vorliegt
|
||||||
|
4. komprimiertes Backup vor jeder Änderung
|
||||||
|
5. Stoppen des Gateways und Aufruf von `hermes update`
|
||||||
|
6. einmaliger Retry bei eindeutig transientem Fetch-Fehler
|
||||||
|
7. Konfigurationsmigration und frischer Gateway-Neustart
|
||||||
|
8. Gateway-Healthcheck; bei Fehler Wiederherstellung des gesicherten Checkouts und der erfassten Konfigurationsdateien
|
||||||
|
9. separater llama.cpp-Healthcheck ohne unsinnigen Hermes-Rollback
|
||||||
|
10. Aufbewahrung der drei neuesten Hermes-Backups
|
||||||
|
|
||||||
|
Ein Fehler von Telegram oder Signal kann den Updateprozess niemals fehlschlagen
|
||||||
|
lassen. Detaillierte Zwischenstände landen im Journal; nur zusammengefasste
|
||||||
|
Ergebnisse werden versendet.
|
||||||
|
|
||||||
|
## Bedienung
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update manuell
|
||||||
|
systemctl --user start hermes-stack-hermes-update.service
|
||||||
|
|
||||||
|
# Status und Logs
|
||||||
|
systemctl --user status hermes-stack-hermes-update.service
|
||||||
|
journalctl --user -u hermes-stack-hermes-update.service -n 200
|
||||||
|
journalctl --user -t hermes-stack-maintenance -n 100
|
||||||
|
|
||||||
|
# Benachrichtigung testen
|
||||||
|
~/.local/libexec/hermes-stack-notify \
|
||||||
|
"Hermes-Stack: Benachrichtigungstest"
|
||||||
|
|
||||||
|
# signal-cli nach einer Update-Meldung aktualisieren
|
||||||
|
~/.local/libexec/hermes-stack-signal-update
|
||||||
|
|
||||||
|
# Gesamtprüfung
|
||||||
|
~/.local/libexec/hermes-stack-doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
Deployment-Code und Benutzerdaten werden bewusst getrennt behandelt. Siehe
|
||||||
|
[`docs/MIGRATION.md`](docs/MIGRATION.md) für die Übernahme von Hermes-Daten,
|
||||||
|
Signal-Sitzung und Modellgewichten.
|
||||||
|
|
||||||
|
## Gitea
|
||||||
|
|
||||||
|
Nach dem Entpacken oder Erzeugen des Repositories:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git init
|
||||||
|
git add .
|
||||||
|
git commit -m "Initial Hermes Arch deployment"
|
||||||
|
git branch -M main
|
||||||
|
git remote add origin ssh://git@GITEA/OWNER/hermes-arch-stack.git
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
Die mitgelieferte Gitea-Actions-Workflowdatei führt Syntax- und Smoke-Tests aus.
|
||||||
|
`make test` führt zusätzlich isolierte Funktionstests mit simulierten Programmen
|
||||||
|
aus. Ein echter NVIDIA-/Arch-/Signal-/Hermes-End-to-End-Test bleibt auf der
|
||||||
|
Zielmaschine erforderlich. `make archive` verwendet den letzten Commit (also
|
||||||
|
zuerst eigene Änderungen prüfen und committen).
|
||||||
|
|
||||||
|
## Grenzen der Reproduktion
|
||||||
|
|
||||||
|
Das Repo reproduziert die Deployment-Logik, nicht deine privaten Daten oder ein
|
||||||
|
eingefrorenes Arch-Abbild. Für denselben Modell-/Backendstand Quell-SHA sowie
|
||||||
|
Modell-/Projektor-Prüfsummen notieren. Der offizielle Hermes-Installer und Arch
|
||||||
|
liefern beim Neuaufsetzen sonst den dann verfügbaren Stand. Technische Quellen
|
||||||
|
stehen in `docs/LLAMA_INSTALL.md` und `docs/RTX3080.md`.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Wird als Bash-Konfiguration eingelesen. Keine Leerzeichen um '=' verwenden.
|
||||||
|
HERMES_REPO="$HOME/.hermes/hermes-agent"
|
||||||
|
HERMES_ENV="$HOME/.hermes/.env"
|
||||||
|
HERMES_REMOTE="origin"
|
||||||
|
HERMES_BRANCH="main"
|
||||||
|
|
||||||
|
GATEWAY_SERVICE="hermes-gateway.service"
|
||||||
|
LLAMA_SERVICE="llama-qwen.service"
|
||||||
|
LLAMA_URL="http://127.0.0.1:8080"
|
||||||
|
# 1: inaktiven Dienst wieder starten. HTTP-Timeouts allein starten nie neu.
|
||||||
|
# Standard 0 verhindert unerwünschte Modellstarts während Wartung/Tuning.
|
||||||
|
LLAMA_HEALTH_AUTORESTART=0
|
||||||
|
SIGNAL_SERVICE="signal-cli.service"
|
||||||
|
|
||||||
|
# Zusammengefasste Maintenance-Meldungen. Credentials/Ziele stehen in HERMES_ENV.
|
||||||
|
TELEGRAM_NOTIFY=1
|
||||||
|
SIGNAL_NOTIFY=1
|
||||||
|
|
||||||
|
# Backup- und Prüfverhalten
|
||||||
|
BACKUP_KEEP=3
|
||||||
|
GATEWAY_HEALTH_ATTEMPTS=12
|
||||||
|
GATEWAY_HEALTH_DELAY=5
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Diese Datei dient nur als Feldübersicht. Auf dem Zielsystem verwendet Hermes
|
||||||
|
# standardmäßig ~/.hermes/.env. Niemals die ausgefüllte Datei committen.
|
||||||
|
|
||||||
|
# Telegram
|
||||||
|
TELEGRAM_BOT_TOKEN=
|
||||||
|
TELEGRAM_HOME_CHANNEL=
|
||||||
|
TELEGRAM_ALLOWED_USERS=
|
||||||
|
|
||||||
|
# Signal
|
||||||
|
SIGNAL_HTTP_URL=http://127.0.0.1:8090
|
||||||
|
SIGNAL_ACCOUNT=
|
||||||
|
SIGNAL_HOME_CHANNEL=
|
||||||
|
SIGNAL_ALLOWED_USERS=
|
||||||
|
|
||||||
|
# Modellprovider bzw. lokaler OpenAI-kompatibler Endpunkt werden bevorzugt mit
|
||||||
|
# `hermes setup` bzw. `hermes model` konfiguriert.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Bash-Konfiguration für hermes-stack-llama-run
|
||||||
|
# Leer: zuerst den verwalteten CUDA-Build, dann llama-server aus PATH verwenden.
|
||||||
|
# Auf deiner bestehenden Workstation explizit /usr/bin/llama-server eintragen.
|
||||||
|
LLAMA_SERVER_BIN=""
|
||||||
|
LLAMA_MODEL=""
|
||||||
|
LLAMA_HF_REPO="ggml-org/Qwen3.8-27B-GGUF:Q4_K_M"
|
||||||
|
LLAMA_ALIAS="qwen38-27b-local"
|
||||||
|
LLAMA_MMPROJ=""
|
||||||
|
LLAMA_HOST="127.0.0.1"
|
||||||
|
LLAMA_PORT="8080"
|
||||||
|
|
||||||
|
# Erster Start unverändert mit 64K / 1536 MiB Fit-Zielmarge.
|
||||||
|
# Anschließend erst balanced, danach ggf. vram-plus vergleichen.
|
||||||
|
LLAMA_PROFILE="baseline"
|
||||||
|
LLAMA_ARGS=()
|
||||||
|
# HF lädt beim ersten echten Start herunter. Für offline/reproduzierbar:
|
||||||
|
# LLAMA_MODEL und optional LLAMA_MMPROJ setzen; LLAMA_HF_REPO leeren.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Bash-Konfiguration für hermes-stack-signal-run
|
||||||
|
SIGNAL_CLI_BIN="$HOME/.local/bin/signal-cli"
|
||||||
|
SIGNAL_ACCOUNT=""
|
||||||
|
SIGNAL_HTTP_BIND="127.0.0.1:8090"
|
||||||
|
SIGNAL_CONFIG_DIR="$HOME/.local/share/signal-cli"
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# llama.cpp samt Server, CLI und Benchmark installieren
|
||||||
|
|
||||||
|
Der Installer baut die drei Programme `llama-server`, `llama-cli`, `llama-bench`
|
||||||
|
aus dem offiziellen Repository mit CUDA. Er überschreibt weder `/usr/bin` noch
|
||||||
|
einen vorhandenen Build und startet keinen Dienst. Kein AUR-Helper erforderlich.
|
||||||
|
|
||||||
|
## Neue Arch-Maschine
|
||||||
|
|
||||||
|
Zuerst einen zur GPU passenden NVIDIA-Treiber einrichten. `nvidia-smi` muss
|
||||||
|
funktionieren. Treiberwahl und Kernelupdates werden nicht automatisiert.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo pacman -Syu --needed base-devel git cmake ninja cuda openssl
|
||||||
|
```
|
||||||
|
|
||||||
|
Dies ist eine vollständige Arch-Systemaktualisierung. Bei Kernel-/Treiberupdate
|
||||||
|
Jobs beenden und neu starten, bevor du die GPU-Installation testest. Keine
|
||||||
|
partielle Aktualisierung durch Ausschließen einzelner Systempakete erzwingen.
|
||||||
|
|
||||||
|
Dann aus dem Deployment-Repo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# master bewusst ausgewählt, keine als stabil behauptete Release-Version:
|
||||||
|
./scripts/install-llama-cpp.sh --ref master --jobs 4 --cuda-arch 86
|
||||||
|
```
|
||||||
|
|
||||||
|
`86` ist für RTX 3080 gewählt. Für einen anderen Rechner dessen GPU-Architektur
|
||||||
|
angeben. Mit `--ref <vollständiger-Commit>` lässt sich derselbe Quellstand erneut
|
||||||
|
bauen. Tags/Branches werden aufgelöst und im jeweiligen `BUILD-INFO.txt` zusammen
|
||||||
|
mit der SHA gespeichert. CUDA/Compiler/Arch-Pakete bleiben rollende Abhängigkeiten;
|
||||||
|
das ist kein bitidentisch eingefrorenes Betriebssystem-Image.
|
||||||
|
`--prefix /absoluter/pfad` erlaubt einen anderen Build-Speicherort. Dann den
|
||||||
|
ausgegebenen Binary-Pfad ausdrücklich in `LLAMA_SERVER_BIN` konfigurieren.
|
||||||
|
|
||||||
|
Falls CUDA den installierten Hostcompiler ablehnt: kompatiblen Compiler nach
|
||||||
|
CUDA-/Arch-Dokumentation wählen und CMake z.B. über `CC`, `CXX`, `CUDAHOSTCXX`
|
||||||
|
konfigurieren. Kein automatisches `--allow-unsupported-compiler` und keine
|
||||||
|
Treiber-/Systemdateipatches. Ein fehlgeschlagener Build lässt aktive Versionen
|
||||||
|
unverändert und bleibt zur Diagnose erhalten.
|
||||||
|
|
||||||
|
Der Installer kopiert das Build-bin-Verzeichnis einschließlich dortiger
|
||||||
|
Begleitdateien. Die Projektbibliotheken werden statisch gebaut; CUDA- und
|
||||||
|
Systembibliotheken bleiben Laufzeitabhängigkeiten. Auf neuer Hardware neu bauen,
|
||||||
|
nicht nur die Binary übertragen.
|
||||||
|
|
||||||
|
## Auswahl
|
||||||
|
|
||||||
|
- Erster verwalteter Build: Link `~/.local/opt/llama.cpp/current` wird angelegt.
|
||||||
|
- Spätere Builds: der Link bleibt stehen, der neue Pfad wird ausgegeben.
|
||||||
|
- Expliziter `LLAMA_SERVER_BIN` in `llama.env` hat Vorrang vor dem Link.
|
||||||
|
- Ohne expliziten Pfad: verwalteter Link, sonst `llama-server` aus PATH.
|
||||||
|
|
||||||
|
Auf der bisherigen Workstation **zunächst `/usr/bin/llama-server` beibehalten**.
|
||||||
|
Einen Backend-Versionswechsel und VRAM-Tuning nicht im selben Vergleich vermischen.
|
||||||
|
Für einen späteren Build dessen ausgegebenen Pfad in `LLAMA_SERVER_BIN` eintragen,
|
||||||
|
prüfen und erst nach Jobende mit dem Restart-Helfer aktivieren. Den alten Pfad
|
||||||
|
notieren; Rücknahme bedeutet Pfad zurücksetzen und erneut kontrolliert starten.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.local/libexec/hermes-stack-llama-run --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Prüfung lädt kein Modell. Das erste echte Starten mit `LLAMA_HF_REPO`
|
||||||
|
benötigt Netzwerk, genug Plattenplatz und ein vorhandenes HF-Repository. Bei
|
||||||
|
Download-/Modellproblemen die vom Nutzer angegebene Modell-ID prüfen; sie wurde
|
||||||
|
hier nicht heruntergeladen. Für offline und feste Modellversionen lokale
|
||||||
|
GGUF-Dateien und ggf. passenden Vision-Projektor verwenden, SHA256 notieren.
|
||||||
|
Modell und Projektor werden separat gesichert, nicht im Git-Repo abgelegt.
|
||||||
|
|
||||||
|
## Offizielle Grundlagen
|
||||||
|
|
||||||
|
- [llama.cpp bauen, CUDA und GPU-Architekturen](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md)
|
||||||
|
- [Arch CUDA-Paket](https://archlinux.org/packages/extra/x86_64/cuda/)
|
||||||
|
- [NVIDIA Compute Capability](https://developer.nvidia.com/cuda-gpus)
|
||||||
|
|
||||||
|
Ein echter CUDA-Build und Modellbetrieb müssen auf der Zielmaschine geprüft
|
||||||
|
werden. Die mitgelieferten Tests benutzen keine GPU und laden keine Gewichte.
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# Migration auf eine neue Arch-Linux-Maschine
|
||||||
|
|
||||||
|
Das Gitea-Repository reproduziert Programme, Units und Wartungslogik. Persönliche
|
||||||
|
Hermes-Daten, Secrets, Signal-Geräteidentität und Modellgewichte werden separat
|
||||||
|
übertragen.
|
||||||
|
|
||||||
|
## 1. Deployment installieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone ssh://git@GITEA/OWNER/hermes-arch-stack.git
|
||||||
|
cd hermes-arch-stack
|
||||||
|
./install.sh --install-packages --install-hermes
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Hermes-Benutzerdaten übertragen
|
||||||
|
|
||||||
|
Auf der alten Maschine Hermes und Gateway stoppen und ein verschlüsseltes
|
||||||
|
Backup erstellen. Mindestens relevant sind:
|
||||||
|
|
||||||
|
- `~/.hermes/config.yaml`
|
||||||
|
- `~/.hermes/.env`
|
||||||
|
- `~/.hermes/auth.json`, sofern vorhanden
|
||||||
|
- Profile, Sessions, Memories, Skills und Cron-Daten unter `~/.hermes/`
|
||||||
|
|
||||||
|
Der Upstream-Checkout `~/.hermes/hermes-agent/` muss nicht übertragen werden;
|
||||||
|
er wird vom offiziellen Installer neu erzeugt. Ein unverschlüsseltes Archiv mit
|
||||||
|
Secrets darf weder ins Repository noch auf einen ungeschützten Fileserver.
|
||||||
|
|
||||||
|
Alternativ den jeweils aktuellen Hermes-Backup-/Import-Befehl verwenden und
|
||||||
|
vorher dessen Hilfe prüfen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hermes backup --help
|
||||||
|
hermes import --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Signal
|
||||||
|
|
||||||
|
Entweder signal-cli auf dem neuen Rechner als neues verknüpftes Gerät anmelden
|
||||||
|
oder das vollständige signal-cli-Datenverzeichnis verschlüsselt übertragen:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.local/share/signal-cli/
|
||||||
|
```
|
||||||
|
|
||||||
|
Dieses Verzeichnis enthält Zugangsdaten und ist wie ein Passwort zu behandeln.
|
||||||
|
Es darf niemals committed werden. Anschließend Account und Port in
|
||||||
|
`~/.config/hermes-stack-maintenance/signal-cli.env` sowie in `~/.hermes/.env`
|
||||||
|
abgleichen.
|
||||||
|
Eine kopierte Signal-Geräteidentität nicht auf beiden Maschinen gleichzeitig
|
||||||
|
betreiben. Für Parallelbetrieb die neue Maschine als separates Gerät verknüpfen.
|
||||||
|
|
||||||
|
## 4. llama.cpp und Modell
|
||||||
|
|
||||||
|
`llama-server` mit [LLAMA_INSTALL.md](LLAMA_INSTALL.md) installieren (CUDA-Build
|
||||||
|
inklusive CLI und Benchmark) oder vorhandene Binary verwenden. Die GGUF-Datei
|
||||||
|
und einen ggf. benötigten Vision-Projektor übertragen oder beim ersten Start
|
||||||
|
über HF beziehen. Danach
|
||||||
|
`~/.config/hermes-stack-maintenance/llama.env` anpassen. Modellgewichte gehören
|
||||||
|
wegen Größe und Lizenz nicht in dieses Repository.
|
||||||
|
|
||||||
|
Die RTX-3080-Profile sind für 10 GB VRAM und die bisherige CPU ausgelegt; auf
|
||||||
|
anderer Hardware neu messen. `LLAMA_SERVER_BIN`, Modellpfad, Port und Alias
|
||||||
|
zwischen Hermes und llama.cpp abgleichen. Der neue Rechner braucht seine eigenen
|
||||||
|
NVIDIA-/Kernelpakete. Keine Binärkopie des alten CUDA-Builds erzwingen.
|
||||||
|
|
||||||
|
## 5. Aktivieren und prüfen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hermes gateway install --force
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now signal-cli.service llama-qwen.service
|
||||||
|
systemctl --user enable --now \
|
||||||
|
hermes-stack-hermes-update.timer \
|
||||||
|
hermes-stack-health.timer \
|
||||||
|
hermes-stack-signal-health.timer \
|
||||||
|
hermes-stack-signal-update-check.timer
|
||||||
|
|
||||||
|
~/.local/libexec/hermes-stack-doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
Erst wenn der Doctor erfolgreich ist, die alte Maschine bzw. deren Signal-Gerät
|
||||||
|
außer Betrieb nehmen.
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# Qwen auf RTX 3080 10 GB / Hyprland
|
||||||
|
|
||||||
|
Zielhardware: RTX 3080 mit 10 GiB, i9-13900KF, 64 GB RAM. Der Desktop und
|
||||||
|
llama.cpp nutzen dieselbe GPU. Das 27B-Modell in Q4_K_M passt nicht vollständig
|
||||||
|
in 10 GiB: CPU/RAM-Offload bleibt erforderlich. Mehr VRAM für Gewichte kann
|
||||||
|
helfen; weder Voll-Offload noch eine bestimmte Beschleunigung werden versprochen.
|
||||||
|
|
||||||
|
## Profile
|
||||||
|
|
||||||
|
| Profil | Kontextlimit | Fit-Zielmarge | Einsatz |
|
||||||
|
| --- | ---: | ---: | --- |
|
||||||
|
| `baseline` | 65536 | 1536 MiB | bisherige Startparameter, Referenz und Rückweg |
|
||||||
|
| `balanced` | 49152 | 1536 MiB | erster Vergleich, nur Kontext reduzieren |
|
||||||
|
| `vram-plus` | 49152 | 1280 MiB | zweiter Vergleich, kleinere Zielmarge |
|
||||||
|
| `compact` | 32768 | 1536 MiB | Alternative für kürzere Sitzungen |
|
||||||
|
|
||||||
|
Alle Profile: ein Slot, K/V-Cache q8_0, Flash Attention auto, GPU-Layer auto,
|
||||||
|
Fit an, 6 Generierungs-/16 Batch-Threads, Jinja, Reasoning auto/512/preserve,
|
||||||
|
Image-Minimum 1024 Tokens und Idle-Sleep nach 300 Sekunden. Modellalias bleibt
|
||||||
|
`qwen38-27b-local`. Es werden keine Sampling-Werte oder Hermes-Memories geändert.
|
||||||
|
|
||||||
|
Die Definitionen stehen zentral in `libexec/hermes-stack-llama-run`.
|
||||||
|
Die Konfiguration und Optionsnamen werden anhand der installierten Binary
|
||||||
|
geprüft. Nicht unterstützte Optionen führen zum Abbruch, nicht zum stillen
|
||||||
|
Weglassen. Diese Prüfung kann GPU-Laufzeitfehler oder ungültige Optionswerte
|
||||||
|
nicht ausschließen. Auf anderer Hardware die Profile nicht ungeprüft übernehmen.
|
||||||
|
|
||||||
|
**Fit-Zielmarge ist keine geschützte VRAM-Reservierung.** Sie beeinflusst die
|
||||||
|
Speicheranpassung beim Laden; später wachsende Browser-/Desktop-/Bildlast kann
|
||||||
|
trotzdem zu Speicherdruck führen. Verfügbare Layer und Puffer hängen auch von
|
||||||
|
Modellarchitektur, Kontext, Projektor und llama.cpp-Version ab. Weniger Kontext
|
||||||
|
bedeutet nicht proportional weniger Gesamtspeicher. 1024 MiB und weniger sind
|
||||||
|
hier bewusst nicht als fertiges Profil vorgesehen.
|
||||||
|
|
||||||
|
## Messung statt Auslastungsziel
|
||||||
|
|
||||||
|
1. Zuerst `baseline` mit deiner normalen Browser-/Hyprland-Nutzung messen.
|
||||||
|
2. Jobs beenden. `balanced` wählen und kontrolliert neu starten.
|
||||||
|
3. Dieselben Aufgaben erneut ausführen: kurze Textantwort, längerer Agent-Task
|
||||||
|
mit Tools und eine repräsentative Bildaufgabe. Qualität mitbewerten.
|
||||||
|
4. Kalten Start nach Idle-Sleep getrennt von warmem Prompt-Cache messen.
|
||||||
|
5. Erst bei genügend Spielraum `vram-plus` probieren. Bei Rucklern, OOM,
|
||||||
|
Grafikfehlern oder deutlich schlechterer Latenz zurück auf `baseline`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.local/libexec/hermes-stack-llama-profile list
|
||||||
|
~/.local/libexec/hermes-stack-llama-report
|
||||||
|
|
||||||
|
# Nur speichern, unterbricht nichts:
|
||||||
|
~/.local/libexec/hermes-stack-llama-profile balanced
|
||||||
|
|
||||||
|
# Ausschließlich nach Abschluss aller Backend-Jobs:
|
||||||
|
~/.local/libexec/hermes-stack-llama-restart --jobs-finished
|
||||||
|
journalctl --user -u llama-qwen.service -f
|
||||||
|
```
|
||||||
|
|
||||||
|
Laufende Inferenz überlebt diesen **llama.cpp-Neustart nicht**. Ein Hermes-Update
|
||||||
|
ist davon getrennt. Die neue Healthcheck-Version startet einen laufenden
|
||||||
|
llama-server nicht allein wegen eines HTTP-Timeouts neu. Ein inaktiver Dienst
|
||||||
|
wird nur mit `LLAMA_HEALTH_AUTORESTART=1` automatisch wieder gestartet.
|
||||||
|
|
||||||
|
GPU während aller Vergleichsläufe beobachten, nicht nur nach Modellstart:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nvidia-smi --query-gpu=timestamp,memory.used,memory.free,utilization.gpu \
|
||||||
|
--format=csv --loop=1
|
||||||
|
```
|
||||||
|
|
||||||
|
Pro Profil notieren: freie VRAM-Minima, tatsächlich ausgelagerte GPU-Layer,
|
||||||
|
Prompt-tok/s, Generierungs-tok/s, Zeit bis erster Antwort, Job-Gesamtdauer,
|
||||||
|
Desktop-Verhalten und Ergebnisqualität. Nicht die höchste VRAM-Anzeige gewinnt,
|
||||||
|
sondern die beste zuverlässig nutzbare Leistung.
|
||||||
|
|
||||||
|
Bei kleinerem Kontext auch Hermes' Kontextbudget passend konfigurieren: erst
|
||||||
|
die in deiner Hermes-Version verfügbaren Modell-/Konfigurationsoptionen ansehen.
|
||||||
|
Der Alias bleibt gleich, daher erkennt Hermes eine geänderte Grenze nicht
|
||||||
|
zwingend automatisch. Lange alte Sitzungen nicht ungeprüft fortsetzen; eine
|
||||||
|
frische Sitzung für den Vergleich verwenden. Das Deployment überschreibt deine
|
||||||
|
`config.yaml` bewusst nicht.
|
||||||
|
|
||||||
|
## Rückkehr
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.local/libexec/hermes-stack-llama-profile baseline
|
||||||
|
~/.local/libexec/hermes-stack-llama-restart --jobs-finished
|
||||||
|
```
|
||||||
|
|
||||||
|
Eine Profiländerung stellt keine anderen Konfigurationen zurück. Für alte
|
||||||
|
vollständige Startparameter siehe `WORKSTATION.md`. Browserbeschleunigung,
|
||||||
|
NVIDIA-Treiber, Hyprland und Kernelparameter werden nicht verändert.
|
||||||
|
|
||||||
|
## Quellen
|
||||||
|
|
||||||
|
Offizielle [llama.cpp-Argumentdefinitionen](https://github.com/ggml-org/llama.cpp/blob/master/common/arg.cpp)
|
||||||
|
für Fit-Marge, Kontext, Cache und Idle-Sleep; geprüft am 2026-09-05. Maßgeblich
|
||||||
|
für deinen Rechner bleibt `llama-server --help` seiner tatsächlich installierten
|
||||||
|
Version. Die Profile übernehmen die von dir gezeigten Modellparameter; sie
|
||||||
|
sind noch kein auf deiner GPU gemessener Optimalwert.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Sicherheit
|
||||||
|
|
||||||
|
- Keine ausgefüllten `.env`-Dateien, Signal-Daten oder Modellzugangsdaten committen.
|
||||||
|
- Lokale Konfigurationen werden mit Modus `0600` angelegt.
|
||||||
|
- Signal- und Telegram-Ziele werden ausschließlich aus `~/.hermes/.env` gelesen.
|
||||||
|
- Signal-Zugriff per `SIGNAL_ALLOWED_USERS` oder Hermes-DM-Pairing beschränken.
|
||||||
|
- `GATEWAY_ALLOW_ALL_USERS` nicht für einen Agenten mit Terminalzugriff verwenden.
|
||||||
|
- Das Update löscht beim Rollback nur einen geprüften Pfad innerhalb von `$HOME`,
|
||||||
|
dessen letzter Bestandteil `hermes-agent` ist.
|
||||||
|
- Backups enthalten Secrets und müssen wie Zugangsdaten geschützt werden.
|
||||||
|
- Gitea-Repository privat halten, auch wenn keine Secrets eingecheckt werden.
|
||||||
|
- Vor produktivem Einsatz `git diff --cached` und Secret-Scanning ausführen.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Validierung vom 2026-09-05
|
||||||
|
|
||||||
|
Ausgeführt in einer isolierten Arbeitsumgebung, nicht auf der Workstation:
|
||||||
|
|
||||||
|
- `make test`: erfolgreich.
|
||||||
|
- `bash -n`: Installer, Uninstaller, alle libexec-/Installationsskripte und
|
||||||
|
alle vier Beispielkonfigurationen erfolgreich.
|
||||||
|
- Python-Vertragstests: **25 Tests erfolgreich**. Fake-Binaries ersetzen
|
||||||
|
CUDA-Build, Git-Netzwerk, systemd, HTTP und Signal-Zustellung.
|
||||||
|
- `git diff --check`: erfolgreich.
|
||||||
|
- Einfacher Secret-Mustercheck des Smoke-Skripts: kein Treffer. Kein Ersatz
|
||||||
|
für einen vollständigen Secret-Scanner oder die Prüfung vor einem Push.
|
||||||
|
- ShellCheck war hier nicht installiert und wurde nicht ausgeführt; die
|
||||||
|
mitgelieferte Gitea-CI installiert und verwendet ihn.
|
||||||
|
|
||||||
|
Nicht durchgeführt: Arch-Paketinstallation, echter CUDA-Build, Modell-Download,
|
||||||
|
GPU-/Hyprland-Benchmark, Hermes-Installation, End-to-End-Update/Rollback oder
|
||||||
|
echte Signal-/Telegram-Nachrichten. Die 25 Tests sind kein Nachweis dafür.
|
||||||
|
|
||||||
|
Vor Produktivbetrieb: `WORKSTATION.md` bzw. `MIGRATION.md` abarbeiten, danach
|
||||||
|
`RTX3080.md` für kontrollierte Vergleiche. Kein Dienst auf der Workstation wurde
|
||||||
|
durch die Erstellung dieses Pakets verändert.
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# Bestehende Workstation behutsam übernehmen
|
||||||
|
|
||||||
|
Nicht blind den Schnellstart für eine neue Maschine ausführen. Vorhandene
|
||||||
|
Hermes-/Signal-Konfiguration bleibt privat und wird nicht durch das Repo ersetzt.
|
||||||
|
Das aktuelle Deployment enthält deine gezeigten Anpassungen; weitere lokale,
|
||||||
|
hier nicht übermittelte Änderungen können nicht automatisch rekonstruiert werden.
|
||||||
|
|
||||||
|
## 1. Ausgangszustand sichern
|
||||||
|
|
||||||
|
Jobs zu Ende laufen lassen, sicherstellen dass gerade kein Hermes-Update läuft.
|
||||||
|
Vorhandene Startparameter, Versionen, Units und Drop-ins lokal dokumentieren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user cat llama-qwen.service
|
||||||
|
systemctl --user cat hermes-gateway.service
|
||||||
|
/usr/bin/llama-server --version
|
||||||
|
systemctl --user list-timers --all --no-pager
|
||||||
|
```
|
||||||
|
|
||||||
|
Bestehende Maintenance-Timer während der Umstellung anhalten; zuvor aktive
|
||||||
|
Timer notieren und nur diese später wieder aktivieren. Laufende Service-Jobs
|
||||||
|
werden durch Stoppen ihrer Timer nicht beendet.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user stop hermes-stack-health.timer hermes-stack-hermes-update.timer \
|
||||||
|
hermes-stack-signal-health.timer hermes-stack-signal-update-check.timer
|
||||||
|
```
|
||||||
|
|
||||||
|
Das folgende `install.sh` sichert ersetzte Dateien unter
|
||||||
|
`~/.local/state/hermes-stack-deployment.*`, behält vorhandene env-Dateien und
|
||||||
|
Gateway-Unit bei und startet ohne Optionen keine Timer und keinen Backend-Dienst.
|
||||||
|
Eigene systemd-Drop-ins bleiben bestehen; sie können den neuen ExecStart übersteuern!
|
||||||
|
Vergleiche nachher `systemctl --user cat llama-qwen.service`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Nicht `--install-hermes`, `--install-llama` oder `--install-packages` verwenden,
|
||||||
|
wenn zunächst nur das bestehende funktionierende Setup übernommen werden soll.
|
||||||
|
|
||||||
|
## 2. llama.env bewusst migrieren
|
||||||
|
|
||||||
|
Der Installer überschreibt `llama.env` nicht. Vergleiche sie mit
|
||||||
|
`config/llama.env.example`. Vor manueller Bearbeitung eine datierte Kopie anlegen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp -a ~/.config/hermes-stack-maintenance/llama.env \
|
||||||
|
~/.config/hermes-stack-maintenance/llama.env.before-rtx-tuning
|
||||||
|
```
|
||||||
|
|
||||||
|
Den Backup-Namen nur einmal verwenden oder ein neues Datum anhängen. Folgende
|
||||||
|
Werte aus der Vorlage übernehmen, vorhandene individuelle Modellpfade prüfen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LLAMA_SERVER_BIN="/usr/bin/llama-server"
|
||||||
|
LLAMA_MODEL=""
|
||||||
|
LLAMA_HF_REPO="ggml-org/Qwen3.8-27B-GGUF:Q4_K_M"
|
||||||
|
LLAMA_ALIAS="qwen38-27b-local"
|
||||||
|
LLAMA_MMPROJ=""
|
||||||
|
LLAMA_HOST="127.0.0.1"
|
||||||
|
LLAMA_PORT="8080"
|
||||||
|
LLAMA_PROFILE="baseline"
|
||||||
|
LLAMA_ARGS=()
|
||||||
|
```
|
||||||
|
|
||||||
|
Eine alte Konfiguration mit `LLAMA_MODEL` und vollständigem `LLAMA_ARGS` läuft
|
||||||
|
weiter, solange `LLAMA_PROFILE` nicht gesetzt ist. Der Profilschalter verweigert
|
||||||
|
unmigrierte Konfigurationen. Lokale GGUF statt HF: Modellpfad setzen und HF leeren.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.local/libexec/hermes-stack-llama-run --print
|
||||||
|
~/.local/libexec/hermes-stack-llama-run --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Kein Modellstart, kein Neustart. Erst nach erfolgreicher Prüfung und Jobende:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.local/libexec/hermes-stack-llama-restart --jobs-finished
|
||||||
|
curl -fsS http://127.0.0.1:8080/health
|
||||||
|
~/.local/libexec/hermes-stack-llama-report
|
||||||
|
```
|
||||||
|
|
||||||
|
Ein kurzzeitiges 503 während des Ladens ist noch kein Beweis für einen Defekt.
|
||||||
|
Logs kontrollieren und auf Bereitschaft warten. Danach eine echte Text-/Bild-
|
||||||
|
und Tool-Aufgabe in Hermes prüfen. Erst dann vorher aktive Timer wieder starten.
|
||||||
|
`LLAMA_HEALTH_AUTORESTART=0` ist auch bei älterer config.env ohne diesen Schlüssel
|
||||||
|
der neue Standard: HTTP-Probleme werden gemeldet, nicht durch Neustarts behandelt.
|
||||||
|
|
||||||
|
## 3. Optimieren und zurücksetzen
|
||||||
|
|
||||||
|
Nun gemäß [RTX3080.md](RTX3080.md) jeweils nur ein Profil vergleichen. Zurück:
|
||||||
|
Profil `baseline` wählen und kontrolliert neustarten. Für vollständige Rückkehr
|
||||||
|
alte llama.env und ggf. alte Unit/Wrapper aus der Deployment-Sicherung einzeln
|
||||||
|
wiederherstellen, `daemon-reload` und Neustart nach Jobende. Nicht pauschal ein
|
||||||
|
Archiv über dein gesamtes Home extrahieren. Die Datei `llama-profile.env` hat nur
|
||||||
|
bei aktivem Profilbetrieb Vorrang; ihre vorherigen Stände liegen daneben.
|
||||||
Executable
+135
@@ -0,0 +1,135 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||||
|
INSTALL_PACKAGES=0
|
||||||
|
INSTALL_HERMES=0
|
||||||
|
INSTALL_LLAMA=0
|
||||||
|
LLAMA_REF=""
|
||||||
|
ENABLE_TIMERS=0
|
||||||
|
SKIP_BROWSER=1
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
sed -n '/^## Installationsoptionen/,/^## /p' "$ROOT_DIR/README.md" |
|
||||||
|
sed '$d'
|
||||||
|
}
|
||||||
|
|
||||||
|
while (($# > 0)); do
|
||||||
|
case "$1" in
|
||||||
|
--install-packages) INSTALL_PACKAGES=1 ;;
|
||||||
|
--install-hermes) INSTALL_HERMES=1 ;;
|
||||||
|
--install-llama) INSTALL_LLAMA=1 ;;
|
||||||
|
--llama-ref) [[ $# -ge 2 ]] || exit 2; LLAMA_REF="$2"; shift ;;
|
||||||
|
--enable-timers) ENABLE_TIMERS=1 ;;
|
||||||
|
--skip-browser) SKIP_BROWSER=1 ;;
|
||||||
|
--with-browser) SKIP_BROWSER=0 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) printf 'Unbekannte Option: %s\n' "$1" >&2; usage >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
if ((INSTALL_LLAMA)) && [[ -z "$LLAMA_REF" ]]; then
|
||||||
|
printf -- '--install-llama benötigt --llama-ref TAG_ODER_COMMIT (oder bewusst master).\n' >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -e /etc/arch-release ]]; then
|
||||||
|
printf 'Warnung: Dieses Deployment ist für Arch Linux ausgelegt.\n' >&2
|
||||||
|
fi
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
|
||||||
|
if ((INSTALL_PACKAGES)); then
|
||||||
|
# Vollständige Aktualisierung statt partieller Arch-Aktualisierung.
|
||||||
|
packages=(
|
||||||
|
bash git curl xz jq zstd tar ripgrep ffmpeg coreutils util-linux \
|
||||||
|
jre-openjdk-headless
|
||||||
|
)
|
||||||
|
if ((INSTALL_LLAMA)); then
|
||||||
|
packages+=(base-devel cmake ninja cuda openssl)
|
||||||
|
fi
|
||||||
|
sudo pacman -Syu --needed "${packages[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ((INSTALL_LLAMA)); then
|
||||||
|
"$ROOT_DIR/scripts/install-llama-cpp.sh" --ref "$LLAMA_REF"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ((INSTALL_HERMES)) && ! command -v hermes >/dev/null 2>&1; then
|
||||||
|
installer_args=()
|
||||||
|
((SKIP_BROWSER)) && installer_args+=(--skip-browser)
|
||||||
|
curl -fsSL https://hermes-agent.nousresearch.com/install.sh |
|
||||||
|
bash -s -- "${installer_args[@]}"
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
|
||||||
|
unit_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||||
|
libexec_dir="$HOME/.local/libexec"
|
||||||
|
|
||||||
|
mkdir -p "$config_dir" "$unit_dir" "$libexec_dir" "${XDG_STATE_HOME:-$HOME/.local/state}"
|
||||||
|
|
||||||
|
# Bestehende Deployment-Dateien vor Ersetzen sichern. Keine Secrets ausgeben.
|
||||||
|
deploy_backup="$(mktemp -d "${XDG_STATE_HOME:-$HOME/.local/state}/hermes-stack-deployment.XXXXXXXX")"
|
||||||
|
mkdir -p "$deploy_backup/libexec" "$deploy_backup/units"
|
||||||
|
for file in "$ROOT_DIR"/libexec/*; do
|
||||||
|
target="$libexec_dir/$(basename "$file")"
|
||||||
|
[[ ! -e "$target" ]] || cp -a -- "$target" "$deploy_backup/libexec/"
|
||||||
|
done
|
||||||
|
for file in "$ROOT_DIR"/systemd/user/*; do
|
||||||
|
target="$unit_dir/$(basename "$file")"
|
||||||
|
[[ ! -e "$target" ]] || cp -a -- "$target" "$deploy_backup/units/"
|
||||||
|
done
|
||||||
|
printf 'Vorherige Deployment-Dateien: %s\n' "$deploy_backup"
|
||||||
|
|
||||||
|
for file in "$ROOT_DIR"/libexec/*; do
|
||||||
|
install -m 0755 "$file" "$libexec_dir/$(basename "$file")"
|
||||||
|
done
|
||||||
|
|
||||||
|
for file in "$ROOT_DIR"/systemd/user/*; do
|
||||||
|
install -m 0644 "$file" "$unit_dir/$(basename "$file")"
|
||||||
|
done
|
||||||
|
|
||||||
|
install_config_if_missing() {
|
||||||
|
local example="$1" target="$2"
|
||||||
|
if [[ ! -e "$target" ]]; then
|
||||||
|
install -m 0600 "$example" "$target"
|
||||||
|
printf 'Konfiguration angelegt: %s\n' "$target"
|
||||||
|
else
|
||||||
|
printf 'Konfiguration beibehalten: %s\n' "$target"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_config_if_missing \
|
||||||
|
"$ROOT_DIR/config/config.env.example" "$config_dir/config.env"
|
||||||
|
install_config_if_missing \
|
||||||
|
"$ROOT_DIR/config/llama.env.example" "$config_dir/llama.env"
|
||||||
|
install_config_if_missing \
|
||||||
|
"$ROOT_DIR/config/signal-cli.env.example" "$config_dir/signal-cli.env"
|
||||||
|
|
||||||
|
if [[ ! -e "$HOME/.hermes/.env" ]]; then
|
||||||
|
mkdir -p "$HOME/.hermes"
|
||||||
|
install -m 0600 "$ROOT_DIR/config/hermes.env.example" "$HOME/.hermes/.env"
|
||||||
|
printf 'Hermes-Umgebung angelegt: %s\n' "$HOME/.hermes/.env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
|
||||||
|
if command -v hermes >/dev/null 2>&1 && [[ ! -e "$unit_dir/hermes-gateway.service" ]]; then
|
||||||
|
if [[ -d "$HOME/.hermes/hermes-agent/.git" ]]; then
|
||||||
|
hermes gateway install ||
|
||||||
|
printf 'Gateway-Unit konnte noch nicht erzeugt werden; nach `hermes setup` erneut ausführen.\n' >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ((ENABLE_TIMERS)); then
|
||||||
|
systemctl --user enable --now \
|
||||||
|
hermes-stack-hermes-update.timer \
|
||||||
|
hermes-stack-health.timer \
|
||||||
|
hermes-stack-signal-health.timer \
|
||||||
|
hermes-stack-signal-update-check.timer
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\nDeployment installiert. Nächster Schritt:\n %s\n' \
|
||||||
|
"$libexec_dir/hermes-stack-doctor"
|
||||||
Executable
+58
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Euo pipefail
|
||||||
|
|
||||||
|
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/config.env"
|
||||||
|
failures=0
|
||||||
|
|
||||||
|
ok() { printf '✓ %s\n' "$*"; }
|
||||||
|
bad() { printf '✗ %s\n' "$*"; failures=$((failures + 1)); }
|
||||||
|
|
||||||
|
[[ -r "$CONFIG_FILE" ]] || {
|
||||||
|
bad "$CONFIG_FILE fehlt"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$CONFIG_FILE"
|
||||||
|
|
||||||
|
for command in git curl jq tar zstd systemctl hermes; do
|
||||||
|
command -v "$command" >/dev/null 2>&1 && ok "$command vorhanden" || bad "$command fehlt"
|
||||||
|
done
|
||||||
|
|
||||||
|
for script in "$HOME/.local/libexec"/hermes-stack-*; do
|
||||||
|
[[ -f "$script" ]] || continue
|
||||||
|
bash -n "$script" && ok "Syntax: ${script##*/}" || bad "Syntaxfehler: ${script##*/}"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -d "$HERMES_REPO/.git" ]]; then
|
||||||
|
ok "Hermes-Git-Checkout vorhanden"
|
||||||
|
dirty="$(git -C "$HERMES_REPO" status --porcelain --untracked-files=all |
|
||||||
|
grep -vxF '?? .install_method' || true)"
|
||||||
|
[[ -z "$dirty" ]] && ok "Hermes-Checkout sauber" || bad "Hermes-Checkout enthält lokale Änderungen"
|
||||||
|
else
|
||||||
|
bad "Hermes-Git-Checkout fehlt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl --user is-active --quiet "$GATEWAY_SERVICE" && ok "Gateway aktiv" || bad "Gateway inaktiv"
|
||||||
|
hermes gateway status >/dev/null 2>&1 && ok "Gateway gesund" || bad "Gateway-Status fehlgeschlagen"
|
||||||
|
|
||||||
|
if curl --fail --silent --max-time 5 "$LLAMA_URL/health" |
|
||||||
|
jq -e '.status == "ok"' >/dev/null 2>&1; then
|
||||||
|
ok "llama.cpp-Endpunkt gesund"
|
||||||
|
else
|
||||||
|
bad "llama.cpp-Endpunkt nicht gesund"
|
||||||
|
fi
|
||||||
|
|
||||||
|
set +u
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$HERMES_ENV" 2>/dev/null || true
|
||||||
|
set +a
|
||||||
|
set -u
|
||||||
|
if [[ -n "${SIGNAL_HTTP_URL:-}" ]]; then
|
||||||
|
status="$(curl --silent --output /dev/null --max-time 5 --write-out '%{http_code}' \
|
||||||
|
"${SIGNAL_HTTP_URL%/}/api/v1/check" || true)"
|
||||||
|
[[ "$status" =~ ^2[0-9][0-9]$ ]] && ok "signal-cli HTTP erreichbar" || bad "signal-cli HTTP nicht erreichbar"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\nFehler: %d\n' "$failures"
|
||||||
|
((failures == 0))
|
||||||
Executable
+55
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Euo pipefail
|
||||||
|
|
||||||
|
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/config.env"
|
||||||
|
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/hermes-stack-maintenance"
|
||||||
|
NOTIFIER="$HOME/.local/libexec/hermes-stack-notify"
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$CONFIG_FILE"
|
||||||
|
|
||||||
|
# Niemals den absichtlich gestoppten Gateway mitten im Update neu starten.
|
||||||
|
exec 9>"${XDG_RUNTIME_DIR:-/tmp}/hermes-stack-hermes-update.lock"
|
||||||
|
flock -n 9 || exit 0
|
||||||
|
|
||||||
|
state_file="$STATE_DIR/health-state"
|
||||||
|
problems=()
|
||||||
|
|
||||||
|
if ! systemctl --user is-active --quiet "$GATEWAY_SERVICE"; then
|
||||||
|
problems+=("$GATEWAY_SERVICE war inaktiv; Neustart wurde versucht")
|
||||||
|
systemctl --user restart "$GATEWAY_SERVICE" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! hermes gateway status >/dev/null 2>&1; then
|
||||||
|
problems+=("Hermes-Gateway antwortete nicht gesund; Neustart wurde versucht")
|
||||||
|
systemctl --user restart "$GATEWAY_SERVICE" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${LLAMA_HEALTH_AUTORESTART:-0}" == 1 ]] &&
|
||||||
|
! systemctl --user is-active --quiet "$LLAMA_SERVICE"; then
|
||||||
|
problems+=("$LLAMA_SERVICE war inaktiv; Neustart wurde versucht")
|
||||||
|
systemctl --user restart "$LLAMA_SERVICE" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! curl --fail --silent --max-time 10 "$LLAMA_URL/health" |
|
||||||
|
jq -e '.status == "ok"' >/dev/null 2>&1; then
|
||||||
|
problems+=("llama-server antwortete nicht gesund; kein HTTP-bedingter Neustart laufender Inferenz")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ((${#problems[@]} == 0)); then
|
||||||
|
current="healthy"
|
||||||
|
previous="$(cat "$state_file" 2>/dev/null || true)"
|
||||||
|
if [[ "$previous" == unhealthy ]]; then
|
||||||
|
"$NOTIFIER" "Hermes-Stack: Dienste sind wieder gesund." || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
current="unhealthy"
|
||||||
|
previous="$(cat "$state_file" 2>/dev/null || true)"
|
||||||
|
if [[ "$previous" != unhealthy ]]; then
|
||||||
|
message="$(IFS='; '; printf '%s' "${problems[*]}")"
|
||||||
|
"$NOTIFIER" "Hermes-Stack: Gesundheitsprüfung fehlgeschlagen. $message. Details: journalctl --user -u $LLAMA_SERVICE -u $GATEWAY_SERVICE" || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$current" >"$state_file"
|
||||||
|
[[ "$current" == healthy ]]
|
||||||
Executable
+236
@@ -0,0 +1,236 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/config.env"
|
||||||
|
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/hermes-stack-maintenance"
|
||||||
|
NOTIFIER="$HOME/.local/libexec/hermes-stack-notify"
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$CONFIG_FILE"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '%s\n' "$*" | systemd-cat -t hermes-stack-maintenance
|
||||||
|
}
|
||||||
|
|
||||||
|
notify_user() {
|
||||||
|
local text="$1"
|
||||||
|
if [[ -x "$NOTIFIER" ]]; then
|
||||||
|
"$NOTIFIER" "$text" || true
|
||||||
|
else
|
||||||
|
log "$text"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_origin_main_with_retry() {
|
||||||
|
local -a retry_delays=(0 5 20)
|
||||||
|
local -a git_options=()
|
||||||
|
local attempt delay
|
||||||
|
|
||||||
|
for ((attempt = 1; attempt <= ${#retry_delays[@]}; attempt++)); do
|
||||||
|
delay="${retry_delays[attempt - 1]}"
|
||||||
|
if ((delay > 0)); then
|
||||||
|
log "Hermes-Git-Fetch: neuer Versuch $attempt/${#retry_delays[@]} in ${delay}s."
|
||||||
|
sleep "$delay"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git_options=()
|
||||||
|
if ((attempt == ${#retry_delays[@]})); then
|
||||||
|
git_options=(-c http.version=HTTP/1.1)
|
||||||
|
log "Hermes-Git-Fetch: letzter Versuch mit HTTP/1.1."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if GIT_TERMINAL_PROMPT=0 timeout --foreground 90s \
|
||||||
|
git "${git_options[@]}" -C "$HERMES_REPO" fetch --no-tags \
|
||||||
|
"$HERMES_REMOTE" \
|
||||||
|
"+refs/heads/$HERMES_BRANCH:refs/remotes/$HERMES_REMOTE/$HERMES_BRANCH"; then
|
||||||
|
((attempt == 1)) ||
|
||||||
|
log "Hermes-Git-Fetch war im Versuch $attempt/${#retry_delays[@]} erfolgreich."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
log "Hermes-Git-Fetch fehlgeschlagen (Versuch $attempt/${#retry_delays[@]})."
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
run_hermes_update_attempt() {
|
||||||
|
local attempt="$1"
|
||||||
|
local -a pipe_status
|
||||||
|
|
||||||
|
printf '\n===== Hermes update: Versuch %s/2 (%s) =====\n' \
|
||||||
|
"$attempt" "$(date --iso-8601=seconds)" >>"$update_log"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
hermes update 2>&1 | tee -a "$update_log"
|
||||||
|
pipe_status=("${PIPESTATUS[@]}")
|
||||||
|
set -e
|
||||||
|
|
||||||
|
((pipe_status[1] == 0)) || return "${pipe_status[1]}"
|
||||||
|
return "${pipe_status[0]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_retryable_update_fetch_failure() {
|
||||||
|
local current_commit dirty_after
|
||||||
|
current_commit="$(git -C "$HERMES_REPO" rev-parse HEAD)" || return 1
|
||||||
|
[[ "$current_commit" == "$old_commit" ]] || return 1
|
||||||
|
|
||||||
|
dirty_after="$(
|
||||||
|
git -C "$HERMES_REPO" status --porcelain --untracked-files=all |
|
||||||
|
grep -vxF '?? .install_method' || true
|
||||||
|
)"
|
||||||
|
[[ -z "$dirty_after" ]] || return 1
|
||||||
|
grep -Fq 'Fetching updates' "$update_log" || return 1
|
||||||
|
grep -Fq 'Pulling updates' "$update_log" && return 1
|
||||||
|
grep -Eqi \
|
||||||
|
'HTTP (429|500|502|503|504)|returned error: (429|500|502|503|504)|rate.?limit|could not read Username|Authentication failed|Could not resolve host|Failed to connect|Connection (timed out|reset)|TLS|early EOF|Failed to fetch updates|Network error' \
|
||||||
|
"$update_log"
|
||||||
|
}
|
||||||
|
|
||||||
|
for command in git tar zstd jq curl systemctl timeout flock hermes; do
|
||||||
|
command -v "$command" >/dev/null 2>&1 || {
|
||||||
|
notify_user "Hermes-Update abgebrochen: benötigtes Kommando fehlt: $command"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
LOCK_FILE="${XDG_RUNTIME_DIR:-/tmp}/hermes-stack-hermes-update.lock"
|
||||||
|
exec 9>"$LOCK_FILE"
|
||||||
|
flock -n 9 || exit 0
|
||||||
|
|
||||||
|
if [[ ! -d "$HERMES_REPO/.git" ]]; then
|
||||||
|
notify_user "Hermes-Update abgebrochen: Git-Installation nicht gefunden."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$HERMES_REPO" in
|
||||||
|
"$HOME"/*/hermes-agent) ;;
|
||||||
|
*)
|
||||||
|
notify_user "Hermes-Update abgebrochen: unsicherer Repository-Pfad: $HERMES_REPO"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
dirty="$(
|
||||||
|
git -C "$HERMES_REPO" status --porcelain --untracked-files=all |
|
||||||
|
grep -vxF '?? .install_method' || true
|
||||||
|
)"
|
||||||
|
if [[ -n "$dirty" ]]; then
|
||||||
|
notify_user "Hermes-Update abgebrochen: lokale Änderungen im Hermes-Repository:
|
||||||
|
$dirty"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
old_commit="$(git -C "$HERMES_REPO" rev-parse HEAD)"
|
||||||
|
|
||||||
|
if ! fetch_origin_main_with_retry; then
|
||||||
|
notify_user "Hermes-Updateprüfung fehlgeschlagen: $HERMES_REMOTE/$HERMES_BRANCH ist nicht erreichbar. Gateway und Installation bleiben unverändert."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
remote_commit="$(git -C "$HERMES_REPO" rev-parse "refs/remotes/$HERMES_REMOTE/$HERMES_BRANCH")"
|
||||||
|
if [[ ! "$remote_commit" =~ ^[0-9a-f]{40}$ ]]; then
|
||||||
|
notify_user "Hermes-Updateprüfung fehlgeschlagen: Remote lieferte keinen gültigen Commit."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$remote_commit" == "$old_commit" ]]; then
|
||||||
|
notify_user "Hermes ist bereits aktuell: $old_commit"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
backup_dir="$STATE_DIR/backups"
|
||||||
|
mkdir -p "$backup_dir"
|
||||||
|
timestamp="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
backup="$backup_dir/hermes-$timestamp.tar.zst"
|
||||||
|
update_log="$STATE_DIR/hermes-update-$timestamp.log"
|
||||||
|
|
||||||
|
paths=("${HERMES_REPO#"$HOME/"}")
|
||||||
|
for path in \
|
||||||
|
"$HOME/.hermes/config.yaml" "$HOME/.hermes/.env" "$HOME/.hermes/auth.json" \
|
||||||
|
"${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/$GATEWAY_SERVICE"; do
|
||||||
|
[[ -e "$path" && "$path" == "$HOME/"* ]] && paths+=("${path#"$HOME/"}")
|
||||||
|
done
|
||||||
|
|
||||||
|
tar --zstd -cf "$backup" -C "$HOME" "${paths[@]}"
|
||||||
|
|
||||||
|
rollback() {
|
||||||
|
trap - ERR
|
||||||
|
set +e
|
||||||
|
systemctl --user stop "$GATEWAY_SERVICE"
|
||||||
|
if [[ "$HERMES_REPO" == "$HOME/"*/hermes-agent ]]; then
|
||||||
|
rm -rf -- "$HERMES_REPO"
|
||||||
|
tar --zstd -xf "$backup" -C "$HOME"
|
||||||
|
fi
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user restart "$GATEWAY_SERVICE"
|
||||||
|
notify_user "Hermes-Update zurückgerollt auf $old_commit. Backup: $backup"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
trap rollback ERR
|
||||||
|
|
||||||
|
systemctl --user stop "$GATEWAY_SERVICE"
|
||||||
|
|
||||||
|
if run_hermes_update_attempt 1; then
|
||||||
|
update_exit=0
|
||||||
|
else
|
||||||
|
update_exit=$?
|
||||||
|
log "Hermes-Updater meldete Exitcode $update_exit."
|
||||||
|
if is_retryable_update_fetch_failure; then
|
||||||
|
log "Transienter Fetch-Fehler erkannt; Hermes-Update wird einmal wiederholt."
|
||||||
|
sleep 20
|
||||||
|
if run_hermes_update_attempt 2; then
|
||||||
|
update_exit=0
|
||||||
|
else
|
||||||
|
update_exit=$?
|
||||||
|
log "Hermes-Updater-Retry meldete Exitcode $update_exit."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! hermes config check >/dev/null 2>&1; then
|
||||||
|
hermes config migrate
|
||||||
|
hermes config check
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user reset-failed "$GATEWAY_SERVICE" || true
|
||||||
|
systemctl --user restart "$GATEWAY_SERVICE"
|
||||||
|
|
||||||
|
gateway_ready=0
|
||||||
|
for ((attempt = 1; attempt <= ${GATEWAY_HEALTH_ATTEMPTS:-12}; attempt++)); do
|
||||||
|
if systemctl --user is-active --quiet "$GATEWAY_SERVICE" &&
|
||||||
|
hermes gateway status >/dev/null 2>&1; then
|
||||||
|
gateway_ready=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep "${GATEWAY_HEALTH_DELAY:-5}"
|
||||||
|
done
|
||||||
|
|
||||||
|
if ((gateway_ready == 0)); then
|
||||||
|
notify_user "Hermes-Gateway wurde nach dem Update nicht gesund. Rollback wird ausgeführt."
|
||||||
|
false
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! curl --fail --silent --max-time 10 "$LLAMA_URL/health" |
|
||||||
|
jq -e '.status == "ok"' >/dev/null; then
|
||||||
|
notify_user "Hermes und Gateway sind aktualisiert, aber der lokale Modell-Endpunkt ist nicht gesund: $LLAMA_URL. Kein Hermes-Rollback, da llama-server unabhängig ist."
|
||||||
|
fi
|
||||||
|
|
||||||
|
trap - ERR
|
||||||
|
|
||||||
|
mapfile -t old_backups < <(
|
||||||
|
find "$backup_dir" -maxdepth 1 -type f -name 'hermes-*.tar.zst' \
|
||||||
|
-printf '%T@ %p\n' | sort -nr | tail -n "+$((${BACKUP_KEEP:-3} + 1))" |
|
||||||
|
cut -d' ' -f2-
|
||||||
|
)
|
||||||
|
((${#old_backups[@]} == 0)) || rm -f -- "${old_backups[@]}"
|
||||||
|
|
||||||
|
new_commit="$(git -C "$HERMES_REPO" rev-parse HEAD)"
|
||||||
|
if [[ "$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."
|
||||||
|
exit 1
|
||||||
|
elif ((update_exit != 0)); then
|
||||||
|
notify_user "Hermes erfolgreich aktualisiert: $old_commit -> $new_commit. Eine Updater-Restartwarnung wurde durch den frischen Gateway-Neustart behoben."
|
||||||
|
else
|
||||||
|
notify_user "Hermes erfolgreich aktualisiert: $old_commit -> $new_commit"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
umask 077
|
||||||
|
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
|
||||||
|
runner="$(dirname -- "${BASH_SOURCE[0]}")/hermes-stack-llama-run"
|
||||||
|
|
||||||
|
case "${1:-list}" in
|
||||||
|
list)
|
||||||
|
printf '%-12s %8s %10s\n' Profil Kontext Fit-MiB \
|
||||||
|
baseline 65536 1536 balanced 49152 1536 vram-plus 49152 1280 compact 32768 1536
|
||||||
|
exit 0 ;;
|
||||||
|
baseline|balanced|vram-plus|compact) selected="$1" ;;
|
||||||
|
*) printf 'Aufruf: %s [list|baseline|balanced|vram-plus|compact]\n' "$0" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
[[ $# -eq 1 ]] || exit 2
|
||||||
|
# Profilwechsel nur für bewusst migrierte Konfigurationen erlauben.
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$config_dir/llama.env"
|
||||||
|
[[ -n "${LLAMA_PROFILE:-}" ]] || {
|
||||||
|
printf 'Zuerst llama.env nach docs/WORKSTATION.md migrieren. Keine Änderung.\n' >&2; exit 1;
|
||||||
|
}
|
||||||
|
"$runner" --profile "$selected" --check
|
||||||
|
exec 9>"${XDG_RUNTIME_DIR:-/tmp}/hermes-stack-hermes-update.lock"
|
||||||
|
flock -n 9 || { printf 'Maintenance läuft. Später erneut versuchen.\n' >&2; exit 1; }
|
||||||
|
target="$config_dir/llama-profile.env"
|
||||||
|
if [[ -e "$target" ]]; then
|
||||||
|
backup="$(mktemp "$config_dir/llama-profile.backup.XXXXXX")"
|
||||||
|
cp -p -- "$target" "$backup"
|
||||||
|
printf 'Vorherige Profilwahl: %s\n' "$backup"
|
||||||
|
fi
|
||||||
|
candidate="$(mktemp "$config_dir/llama-profile.env.XXXXXX")"
|
||||||
|
printf 'LLAMA_PROFILE=%q\n' "$selected" >"$candidate"
|
||||||
|
mv -T -- "$candidate" "$target"
|
||||||
|
printf 'Profil %s gespeichert. Laufende Jobs bleiben unverändert.\n' "$selected"
|
||||||
|
printf 'Erst nach Jobende: hermes-stack-llama-restart (siehe docs/WORKSTATION.md).\n'
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Nur lesen: kein Modellstart, keine Inferenz, kein Neustart.
|
||||||
|
set -Euo pipefail
|
||||||
|
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
|
||||||
|
if [[ -r "$config_dir/config.env" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$config_dir/config.env"
|
||||||
|
fi
|
||||||
|
printf '\nGPU (Gesamtspeicher, belegt, frei in MiB):\n'
|
||||||
|
nvidia-smi --query-gpu=name,memory.total,memory.used,memory.free,utilization.gpu,power.draw,temperature.gpu \
|
||||||
|
--format=csv || true
|
||||||
|
printf '\nDienstzustand:\n'
|
||||||
|
systemctl --user show "${LLAMA_SERVICE:-llama-qwen.service}" \
|
||||||
|
-p ActiveState -p SubState -p MainPID -p ExecMainStartTimestamp --no-pager || true
|
||||||
|
printf '\nLetzte Speicher-/Leistungsdaten (keine vollständigen Prompts):\n'
|
||||||
|
journalctl --user -u "${LLAMA_SERVICE:-llama-qwen.service}" -n 1000 --no-pager |
|
||||||
|
rg 'offload|buffer size|n_ctx|prompt eval time|eval time|sleeping|waking' |
|
||||||
|
tail -n 35 || true
|
||||||
|
printf '\nDas ist eine Momentaufnahme, kein Spitzenlast-Benchmark.\n'
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
if [[ "${1:-}" != --jobs-finished || $# -ne 1 ]]; then
|
||||||
|
printf 'Stoppt laufende Inferenz! Erst nach Jobende ausführen:\n %s --jobs-finished\n' "$0" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$config_dir/config.env"
|
||||||
|
exec 9>"${XDG_RUNTIME_DIR:-/tmp}/hermes-stack-hermes-update.lock"
|
||||||
|
flock -n 9 || { printf 'Maintenance läuft; kein Neustart.\n' >&2; exit 1; }
|
||||||
|
"$(dirname -- "${BASH_SOURCE[0]}")/hermes-stack-llama-run" --check
|
||||||
|
systemctl --user restart "$LLAMA_SERVICE"
|
||||||
|
printf 'Neustart ausgelöst. Modell kann noch laden; Logs und Inferenz prüfen.\n'
|
||||||
Executable
+107
@@ -0,0 +1,107 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
|
||||||
|
CONFIG_FILE="${HERMES_LLAMA_CONFIG:-$config_dir/llama.env}"
|
||||||
|
LLAMA_ARGS=()
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$CONFIG_FILE"
|
||||||
|
|
||||||
|
mode=run
|
||||||
|
profile_override=""
|
||||||
|
while (($#)); do
|
||||||
|
case "$1" in
|
||||||
|
--check) mode=check ;;
|
||||||
|
--print) mode=print ;;
|
||||||
|
--profile) [[ $# -ge 2 ]] || exit 2; profile_override="$2"; shift ;;
|
||||||
|
*) printf 'Aufruf: %s [--check|--print] [--profile NAME]\n' "$0" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
if [[ -n "${LLAMA_PROFILE:-}" && -r "$config_dir/llama-profile.env" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$config_dir/llama-profile.env"
|
||||||
|
fi
|
||||||
|
[[ -z "$profile_override" ]] || LLAMA_PROFILE="$profile_override"
|
||||||
|
|
||||||
|
if [[ -z "${LLAMA_SERVER_BIN:-}" ]]; then
|
||||||
|
LLAMA_SERVER_BIN="$HOME/.local/opt/llama.cpp/current/bin/llama-server"
|
||||||
|
if [[ ! -x "$LLAMA_SERVER_BIN" ]]; then
|
||||||
|
LLAMA_SERVER_BIN="$(command -v llama-server || true)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
[[ -x "$LLAMA_SERVER_BIN" ]] || {
|
||||||
|
printf 'llama-server nicht ausführbar: %s\n' "$LLAMA_SERVER_BIN" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
model_args=()
|
||||||
|
if [[ -n "${LLAMA_MODEL:-}" && -n "${LLAMA_HF_REPO:-}" ]]; then
|
||||||
|
printf 'Nur LLAMA_MODEL ODER LLAMA_HF_REPO setzen.\n' >&2
|
||||||
|
exit 1
|
||||||
|
elif [[ -n "${LLAMA_MODEL:-}" ]]; then
|
||||||
|
[[ -r "$LLAMA_MODEL" ]] || { printf 'Modell nicht lesbar.\n' >&2; exit 1; }
|
||||||
|
model_args=(--model "$LLAMA_MODEL")
|
||||||
|
elif [[ -n "${LLAMA_HF_REPO:-}" ]]; then
|
||||||
|
model_args=(--hf-repo "$LLAMA_HF_REPO")
|
||||||
|
else
|
||||||
|
printf 'LLAMA_MODEL oder LLAMA_HF_REPO fehlt.\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ -n "${LLAMA_MMPROJ:-}" ]]; then
|
||||||
|
[[ -r "$LLAMA_MMPROJ" ]] || { printf 'Projektor nicht lesbar.\n' >&2; exit 1; }
|
||||||
|
model_args+=(--mmproj "$LLAMA_MMPROJ")
|
||||||
|
fi
|
||||||
|
[[ -z "${LLAMA_ALIAS:-}" ]] || model_args+=(--alias "$LLAMA_ALIAS")
|
||||||
|
|
||||||
|
profile_args=()
|
||||||
|
if [[ -n "${LLAMA_PROFILE:-}" ]]; then
|
||||||
|
case "$LLAMA_PROFILE" in
|
||||||
|
baseline) ctx=65536; margin=1536 ;;
|
||||||
|
balanced) ctx=49152; margin=1536 ;;
|
||||||
|
vram-plus) ctx=49152; margin=1280 ;;
|
||||||
|
compact) ctx=32768; margin=1536 ;;
|
||||||
|
*) printf 'Unbekanntes Profil: %s\n' "$LLAMA_PROFILE" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
profile_args=(
|
||||||
|
--ctx-size "$ctx" --parallel 1 --jinja
|
||||||
|
--reasoning auto --reasoning-budget 512 --reasoning-preserve
|
||||||
|
--flash-attn auto --threads 6 --threads-batch 16
|
||||||
|
--n-gpu-layers auto --fit on --fit-target "$margin"
|
||||||
|
--cache-type-k q8_0 --cache-type-v q8_0
|
||||||
|
--sleep-idle-seconds 300 --image-min-tokens 1024
|
||||||
|
--cors-origins localhost --no-ui
|
||||||
|
)
|
||||||
|
# Alte vollständige LLAMA_ARGS nicht still mit einem Profil vermischen.
|
||||||
|
if ((${#LLAMA_ARGS[@]})); then
|
||||||
|
printf 'Bei Profilbetrieb LLAMA_ARGS leeren; eigene vollständige Args nur ohne LLAMA_PROFILE.\n' >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
args=("${model_args[@]}" --host "${LLAMA_HOST:-127.0.0.1}" --port "${LLAMA_PORT:-8080}"
|
||||||
|
"${profile_args[@]}" "${LLAMA_ARGS[@]}")
|
||||||
|
if [[ "$mode" == print ]]; then
|
||||||
|
printf '%q ' "$LLAMA_SERVER_BIN" "${args[@]}"
|
||||||
|
printf '\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Kein Download/Modellstart: nur prüfen, ob die installierte Version die Namen kennt.
|
||||||
|
help_text="$(timeout 30 "$LLAMA_SERVER_BIN" --help 2>&1)" || {
|
||||||
|
printf 'llama-server --help fehlgeschlagen.\n' >&2; exit 1;
|
||||||
|
}
|
||||||
|
for arg in "${args[@]}"; do
|
||||||
|
[[ "$arg" == --* ]] || continue
|
||||||
|
option="${arg%%=*}"
|
||||||
|
if ! grep -Eq -- "(^|[[:space:],])${option}([[:space:],=]|$)" <<<"$help_text"; then
|
||||||
|
printf 'Die installierte llama.cpp-Version kennt %s nicht. Kein automatisches Weglassen.\n' "$option" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ "$mode" == check ]]; then
|
||||||
|
printf 'Konfiguration und Optionsnamen OK; GPU/Modell/Optionswerte noch nicht laufzeitgetestet.\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$LLAMA_SERVER_BIN" "${args[@]}"
|
||||||
Executable
+83
@@ -0,0 +1,83 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Euo pipefail
|
||||||
|
|
||||||
|
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/config.env"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '%s\n' "$*" | systemd-cat -t hermes-stack-maintenance 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
text="${*:-}"
|
||||||
|
if [[ -z "$text" ]]; then
|
||||||
|
printf 'Verwendung: %s TEXT\n' "${0##*/}" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "$text"
|
||||||
|
|
||||||
|
if [[ ! -r "$CONFIG_FILE" ]]; then
|
||||||
|
log "Benachrichtigung übersprungen: $CONFIG_FILE ist nicht lesbar."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
set +u
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$CONFIG_FILE" || {
|
||||||
|
set -u
|
||||||
|
log "Benachrichtigung übersprungen: Maintenance-Konfiguration ist ungültig."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
telegram_notify="${TELEGRAM_NOTIFY:-0}"
|
||||||
|
signal_notify="${SIGNAL_NOTIFY:-0}"
|
||||||
|
|
||||||
|
if [[ -r "${HERMES_ENV:-}" ]] &&
|
||||||
|
[[ "$telegram_notify" == 1 || "$signal_notify" == 1 ]]; then
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$HERMES_ENV" || true
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
set -u
|
||||||
|
|
||||||
|
if [[ "$telegram_notify" == 1 &&
|
||||||
|
-n "${TELEGRAM_BOT_TOKEN:-}" &&
|
||||||
|
-n "${TELEGRAM_HOME_CHANNEL:-}" ]]; then
|
||||||
|
if ! curl --fail --silent --show-error --max-time 15 \
|
||||||
|
--request POST \
|
||||||
|
"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||||||
|
--data-urlencode "chat_id=${TELEGRAM_HOME_CHANNEL}" \
|
||||||
|
--data-urlencode "text=${text}" >/dev/null; then
|
||||||
|
log "Telegram-Benachrichtigung konnte nicht zugestellt werden."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$signal_notify" == 1 &&
|
||||||
|
-n "${SIGNAL_HTTP_URL:-}" &&
|
||||||
|
-n "${SIGNAL_ACCOUNT:-}" &&
|
||||||
|
-n "${SIGNAL_HOME_CHANNEL:-}" ]]; then
|
||||||
|
if [[ "$SIGNAL_HOME_CHANNEL" == group:* ]]; then
|
||||||
|
signal_payload="$(
|
||||||
|
jq -nc --arg account "$SIGNAL_ACCOUNT" --arg message "$text" \
|
||||||
|
--arg group_id "${SIGNAL_HOME_CHANNEL#group:}" \
|
||||||
|
'{jsonrpc:"2.0",method:"send",params:{account:$account,message:$message,groupId:$group_id},id:"hermes-maintenance"}'
|
||||||
|
)" || signal_payload=""
|
||||||
|
else
|
||||||
|
signal_payload="$(
|
||||||
|
jq -nc --arg account "$SIGNAL_ACCOUNT" --arg message "$text" \
|
||||||
|
--arg recipient "$SIGNAL_HOME_CHANNEL" \
|
||||||
|
'{jsonrpc:"2.0",method:"send",params:{account:$account,message:$message,recipient:[$recipient]},id:"hermes-maintenance"}'
|
||||||
|
)" || signal_payload=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$signal_payload" ]] ||
|
||||||
|
! curl --fail --silent --show-error --max-time 30 \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data-binary "$signal_payload" \
|
||||||
|
"${SIGNAL_HTTP_URL%/}/api/v1/rpc" |
|
||||||
|
jq -e '((.error // null) == null) and (.result != null)' >/dev/null; then
|
||||||
|
log "Signal-Benachrichtigung konnte nicht zugestellt werden."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
Executable
+45
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Euo pipefail
|
||||||
|
|
||||||
|
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/config.env"
|
||||||
|
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/hermes-stack-maintenance"
|
||||||
|
NOTIFIER="$HOME/.local/libexec/hermes-stack-notify"
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$CONFIG_FILE"
|
||||||
|
|
||||||
|
set +u
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$HERMES_ENV" || true
|
||||||
|
set +a
|
||||||
|
set -u
|
||||||
|
|
||||||
|
state_file="$STATE_DIR/signal-health-state"
|
||||||
|
signal_url="${SIGNAL_HTTP_URL:-}"
|
||||||
|
[[ -n "$signal_url" ]] || exit 0
|
||||||
|
|
||||||
|
check_signal() {
|
||||||
|
local status
|
||||||
|
status="$(curl --silent --output /dev/null --max-time 5 \
|
||||||
|
--write-out '%{http_code}' "${signal_url%/}/api/v1/check" || true)"
|
||||||
|
[[ "$status" =~ ^2[0-9][0-9]$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
if check_signal; then
|
||||||
|
current=healthy
|
||||||
|
else
|
||||||
|
systemctl --user restart "$SIGNAL_SERVICE" || true
|
||||||
|
sleep 5
|
||||||
|
check_signal && current=healthy || current=unhealthy
|
||||||
|
fi
|
||||||
|
|
||||||
|
previous="$(cat "$state_file" 2>/dev/null || true)"
|
||||||
|
if [[ "$current" == unhealthy && "$previous" != unhealthy ]]; then
|
||||||
|
"$NOTIFIER" "Hermes-Stack: signal-cli ist nicht erreichbar; Neustart wurde versucht." || true
|
||||||
|
elif [[ "$current" == healthy && "$previous" == unhealthy ]]; then
|
||||||
|
"$NOTIFIER" "Hermes-Stack: signal-cli ist wieder erreichbar." || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$current" >"$state_file"
|
||||||
|
[[ "$current" == healthy ]]
|
||||||
Executable
+24
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/signal-cli.env"
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$CONFIG_FILE"
|
||||||
|
|
||||||
|
[[ -x "$SIGNAL_CLI_BIN" ]] || {
|
||||||
|
printf 'signal-cli nicht ausführbar: %s\n' "$SIGNAL_CLI_BIN" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ -n "$SIGNAL_ACCOUNT" ]] || {
|
||||||
|
printf 'SIGNAL_ACCOUNT ist nicht konfiguriert.\n' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
args=(--account "$SIGNAL_ACCOUNT")
|
||||||
|
if [[ -n "${SIGNAL_CONFIG_DIR:-}" ]]; then
|
||||||
|
mkdir -p "$SIGNAL_CONFIG_DIR"
|
||||||
|
args=(--config "$SIGNAL_CONFIG_DIR" "${args[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$SIGNAL_CLI_BIN" "${args[@]}" daemon \
|
||||||
|
--http "${SIGNAL_HTTP_BIND:-127.0.0.1:8090}"
|
||||||
Executable
+108
@@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/config.env"
|
||||||
|
SIGNAL_CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/signal-cli.env"
|
||||||
|
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/hermes-stack-maintenance"
|
||||||
|
NOTIFIER="$HOME/.local/libexec/hermes-stack-notify"
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$CONFIG_FILE"
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$SIGNAL_CONFIG_FILE"
|
||||||
|
|
||||||
|
version="${1:-}"
|
||||||
|
if [[ -z "$version" ]]; then
|
||||||
|
latest_url="$(curl -LsS --max-time 30 -o /dev/null -w '%{url_effective}' \
|
||||||
|
https://github.com/AsamK/signal-cli/releases/latest)"
|
||||||
|
version="${latest_url##*/v}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "$version" =~ ^[0-9]+(\.[0-9]+)+$ ]] || {
|
||||||
|
printf 'Ungültige signal-cli-Version: %s\n' "$version" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
old_version="unbekannt"
|
||||||
|
if [[ -x "$SIGNAL_CLI_BIN" ]]; then
|
||||||
|
old_version="$("$SIGNAL_CLI_BIN" --version 2>/dev/null |
|
||||||
|
grep -Eo '[0-9]+(\.[0-9]+)+' | head -n1 || true)"
|
||||||
|
fi
|
||||||
|
[[ -n "$old_version" ]] || old_version="unbekannt"
|
||||||
|
|
||||||
|
tmp_dir="$(mktemp -d)"
|
||||||
|
trap 'rm -rf -- "$tmp_dir"' EXIT
|
||||||
|
archive="$tmp_dir/signal-cli.tar.gz"
|
||||||
|
url="https://github.com/AsamK/signal-cli/releases/download/v${version}/signal-cli-${version}.tar.gz"
|
||||||
|
|
||||||
|
curl --fail --location --show-error "$url" -o "$archive"
|
||||||
|
tar -xzf "$archive" -C "$tmp_dir"
|
||||||
|
binary="$(find "$tmp_dir" -type f -path '*/bin/signal-cli' -print -quit)"
|
||||||
|
[[ -n "$binary" ]] || {
|
||||||
|
printf 'signal-cli-Binary wurde im Archiv nicht gefunden.\n' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
source_root="$(dirname "$(dirname "$binary")")"
|
||||||
|
|
||||||
|
timestamp="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
backup_dir="$STATE_DIR/backups/signal-cli/$timestamp"
|
||||||
|
mkdir -p "$backup_dir"
|
||||||
|
|
||||||
|
old_target=""
|
||||||
|
if [[ -e "$SIGNAL_CLI_BIN" || -L "$SIGNAL_CLI_BIN" ]]; then
|
||||||
|
old_target="$(readlink -f "$SIGNAL_CLI_BIN" 2>/dev/null || true)"
|
||||||
|
if [[ -n "$old_target" ]]; then
|
||||||
|
old_root="$(dirname "$(dirname "$old_target")")"
|
||||||
|
[[ -d "$old_root" ]] && cp -a "$old_root" "$backup_dir/program"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "${SIGNAL_CONFIG_DIR:-}" ]]; then
|
||||||
|
tar --zstd -cf "$backup_dir/signal-data.tar.zst" \
|
||||||
|
-C "$(dirname "$SIGNAL_CONFIG_DIR")" "$(basename "$SIGNAL_CONFIG_DIR")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
was_active=0
|
||||||
|
systemctl --user is-active --quiet "$SIGNAL_SERVICE" && was_active=1
|
||||||
|
((was_active == 0)) || systemctl --user stop "$SIGNAL_SERVICE"
|
||||||
|
|
||||||
|
target="$HOME/.local/opt/signal-cli-$version"
|
||||||
|
mkdir -p "$target" "$(dirname "$SIGNAL_CLI_BIN")"
|
||||||
|
cp -a "$source_root/." "$target/"
|
||||||
|
ln -sfn "$target/bin/signal-cli" "$SIGNAL_CLI_BIN"
|
||||||
|
|
||||||
|
if ((was_active)); then
|
||||||
|
systemctl --user restart "$SIGNAL_SERVICE"
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
set +u
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$HERMES_ENV" || true
|
||||||
|
set +a
|
||||||
|
set -u
|
||||||
|
|
||||||
|
signal_http_url="${SIGNAL_HTTP_URL:-}"
|
||||||
|
status=""
|
||||||
|
if [[ -n "$signal_http_url" ]]; then
|
||||||
|
status="$(curl --silent --output /dev/null --max-time 10 \
|
||||||
|
--write-out '%{http_code}' "${signal_http_url%/}/api/v1/check" || true)"
|
||||||
|
fi
|
||||||
|
if [[ ! "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
systemctl --user stop "$SIGNAL_SERVICE" || true
|
||||||
|
if [[ -n "$old_target" ]]; then
|
||||||
|
ln -sfn "$old_target" "$SIGNAL_CLI_BIN"
|
||||||
|
systemctl --user restart "$SIGNAL_SERVICE" || true
|
||||||
|
fi
|
||||||
|
"$NOTIFIER" "signal-cli-Update auf $version fehlgeschlagen; Binary wurde auf $old_version zurückgestellt. Backup: $backup_dir" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
mapfile -t old_backups < <(
|
||||||
|
find "$STATE_DIR/backups/signal-cli" -mindepth 1 -maxdepth 1 -type d \
|
||||||
|
-printf '%T@ %p\n' | sort -nr | tail -n +4 | cut -d' ' -f2-
|
||||||
|
)
|
||||||
|
((${#old_backups[@]} == 0)) || rm -rf -- "${old_backups[@]}"
|
||||||
|
|
||||||
|
"$NOTIFIER" "signal-cli wurde erfolgreich aktualisiert: $old_version -> $version. Daten- und Binary-Backup: $backup_dir" || true
|
||||||
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Euo pipefail
|
||||||
|
|
||||||
|
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/hermes-stack-maintenance"
|
||||||
|
NOTIFIER="$HOME/.local/libexec/hermes-stack-notify"
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
|
command -v signal-cli >/dev/null 2>&1 || exit 0
|
||||||
|
installed="$(signal-cli --version 2>/dev/null | grep -Eo '[0-9]+(\.[0-9]+)+' | head -n1)"
|
||||||
|
latest_url="$(curl -LsS --max-time 30 -o /dev/null -w '%{url_effective}' \
|
||||||
|
https://github.com/AsamK/signal-cli/releases/latest || true)"
|
||||||
|
latest="${latest_url##*/v}"
|
||||||
|
|
||||||
|
[[ "$installed" =~ ^[0-9]+(\.[0-9]+)+$ ]] || exit 1
|
||||||
|
[[ "$latest" =~ ^[0-9]+(\.[0-9]+)+$ ]] || exit 1
|
||||||
|
|
||||||
|
if [[ "$(printf '%s\n%s\n' "$installed" "$latest" | sort -V | tail -n1)" == "$latest" &&
|
||||||
|
"$installed" != "$latest" ]]; then
|
||||||
|
"$NOTIFIER" "signal-cli-Update verfügbar: $installed -> $latest. Manuell ausführen: ~/.local/libexec/hermes-stack-signal-update $latest" || true
|
||||||
|
fi
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Baut in einem neuen Verzeichnis. Kein Ersetzen von /usr/bin oder laufenden Builds.
|
||||||
|
set -Eeuo pipefail
|
||||||
|
umask 077
|
||||||
|
revision=""
|
||||||
|
jobs=4
|
||||||
|
cuda_arch=86
|
||||||
|
base_dir="$HOME/.local/opt/llama.cpp"
|
||||||
|
usage() {
|
||||||
|
printf 'Aufruf: %s --ref TAG_ODER_COMMIT [--jobs N] [--cuda-arch 86] [--prefix ABSOLUTER_PFAD]\n' "$0"
|
||||||
|
printf 'Explizit --ref master für aktuellen Stand; aufgelöster Commit wird gespeichert.\n'
|
||||||
|
}
|
||||||
|
while (($#)); do
|
||||||
|
case "$1" in
|
||||||
|
--ref) [[ $# -ge 2 ]] || exit 2; revision="$2"; shift ;;
|
||||||
|
--jobs) [[ $# -ge 2 ]] || exit 2; jobs="$2"; shift ;;
|
||||||
|
--cuda-arch) [[ $# -ge 2 ]] || exit 2; cuda_arch="$2"; shift ;;
|
||||||
|
--prefix) [[ $# -ge 2 ]] || exit 2; base_dir="$2"; shift ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) usage >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
[[ "$revision" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ ]] || { usage >&2; exit 2; }
|
||||||
|
[[ "$jobs" =~ ^[1-9][0-9]*$ && "$cuda_arch" =~ ^[1-9][0-9]*$ ]] || exit 2
|
||||||
|
[[ "$base_dir" == /* && "$base_dir" != / && "$base_dir" != "$HOME" ]] || exit 2
|
||||||
|
for dependency in git cmake ninja c++ nvidia-smi; do
|
||||||
|
command -v "$dependency" >/dev/null || {
|
||||||
|
printf '%s fehlt. Siehe docs/LLAMA_INSTALL.md.\n' "$dependency" >&2; exit 1;
|
||||||
|
}
|
||||||
|
done
|
||||||
|
nvidia-smi >/dev/null
|
||||||
|
nvcc_path="${CUDACXX:-/opt/cuda/bin/nvcc}"
|
||||||
|
[[ -x "$nvcc_path" ]] || { printf 'CUDA nvcc fehlt: %s\n' "$nvcc_path" >&2; exit 1; }
|
||||||
|
mkdir -p "$base_dir"
|
||||||
|
exec 9>"$base_dir/build.lock"
|
||||||
|
flock -n 9 || { printf 'Ein CUDA-Build läuft bereits.\n' >&2; exit 1; }
|
||||||
|
build_dir="$(mktemp -d "$base_dir/build.XXXXXXXX")"
|
||||||
|
printf 'Neuer Build unter %s (bleibt auch bei Fehler zur Diagnose erhalten).\n' "$build_dir"
|
||||||
|
git init -q "$build_dir/source"
|
||||||
|
git -C "$build_dir/source" remote add origin https://github.com/ggml-org/llama.cpp.git
|
||||||
|
GIT_TERMINAL_PROMPT=0 timeout 300 git -C "$build_dir/source" fetch --depth 1 origin "$revision"
|
||||||
|
git -C "$build_dir/source" checkout --detach FETCH_HEAD
|
||||||
|
commit="$(git -C "$build_dir/source" rev-parse HEAD)"
|
||||||
|
cmake -S "$build_dir/source" -B "$build_dir/build" -G Ninja \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release -DGGML_CUDA=ON -DBUILD_SHARED_LIBS=OFF \
|
||||||
|
-DCMAKE_CUDA_COMPILER="$nvcc_path" -DCMAKE_CUDA_ARCHITECTURES="$cuda_arch"
|
||||||
|
cmake --build "$build_dir/build" --parallel "$jobs" --target llama-server llama-cli llama-bench
|
||||||
|
cp -a -- "$build_dir/build/bin" "$build_dir/bin"
|
||||||
|
"$build_dir/bin/llama-server" --version
|
||||||
|
"$build_dir/bin/llama-cli" --version
|
||||||
|
printf 'upstream=https://github.com/ggml-org/llama.cpp.git\nref=%s\ncommit=%s\ncuda_arch=%s\nbuilt=%s\n' \
|
||||||
|
"$revision" "$commit" "$cuda_arch" "$(date --iso-8601=seconds)" >"$build_dir/BUILD-INFO.txt"
|
||||||
|
if [[ ! -e "$base_dir/current" && ! -L "$base_dir/current" ]]; then
|
||||||
|
ln -s -- "$build_dir" "$base_dir/current"
|
||||||
|
printf 'Erster verwalteter Build ausgewählt; noch kein Dienst gestartet.\n'
|
||||||
|
else
|
||||||
|
printf 'Bestehender current-Build bleibt unverändert.\n'
|
||||||
|
fi
|
||||||
|
printf 'Neue Binary: %s/bin/llama-server\nCommit: %s\n' "$build_dir" "$commit"
|
||||||
|
printf 'Zur bewussten Auswahl LLAMA_SERVER_BIN in llama.env setzen. Keine automatische Aktualisierung.\n'
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||||
|
exec "$ROOT_DIR/libexec/hermes-stack-signal-update" "$@"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Check and recover Hermes stack
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=%h/.local/libexec/hermes-stack-health
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Periodic Hermes stack health check
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=2min
|
||||||
|
OnUnitActiveSec=10min
|
||||||
|
AccuracySec=30s
|
||||||
|
Unit=hermes-stack-health.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Update Hermes Agent with backup and rollback
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=%h/.local/libexec/hermes-stack-hermes-update
|
||||||
|
TimeoutStartSec=30min
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Weekly Hermes Agent update
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=weekly
|
||||||
|
Persistent=true
|
||||||
|
RandomizedDelaySec=2h
|
||||||
|
Unit=hermes-stack-hermes-update.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Check and recover signal-cli for Hermes
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=%h/.local/libexec/hermes-stack-signal-health
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Periodic signal-cli health check
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=3min
|
||||||
|
OnUnitActiveSec=10min
|
||||||
|
AccuracySec=30s
|
||||||
|
Unit=hermes-stack-signal-health.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Check for a new signal-cli release
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=%h/.local/libexec/hermes-stack-signal-update-check
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Daily signal-cli update check
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=daily
|
||||||
|
Persistent=true
|
||||||
|
RandomizedDelaySec=1h
|
||||||
|
Unit=hermes-stack-signal-update-check.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Local llama.cpp Qwen model server
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=%h/.local/libexec/hermes-stack-llama-run
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
TimeoutStopSec=60
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=signal-cli daemon for Hermes
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=%h/.local/libexec/hermes-stack-signal-run
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
TimeoutStopSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||||
|
failures=0
|
||||||
|
|
||||||
|
for script in "$ROOT_DIR/install.sh" "$ROOT_DIR/uninstall.sh" \
|
||||||
|
"$ROOT_DIR"/libexec/* "$ROOT_DIR"/scripts/*.sh "$ROOT_DIR"/config/*.example; do
|
||||||
|
if bash -n "$script"; then
|
||||||
|
printf 'OK bash -n %s\n' "${script#"$ROOT_DIR/"}"
|
||||||
|
else
|
||||||
|
failures=$((failures + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if command -v shellcheck >/dev/null 2>&1; then
|
||||||
|
shellcheck -x "$ROOT_DIR/install.sh" "$ROOT_DIR/uninstall.sh" \
|
||||||
|
"$ROOT_DIR"/libexec/* "$ROOT_DIR"/scripts/*.sh || failures=$((failures + 1))
|
||||||
|
else
|
||||||
|
printf 'HINWEIS: shellcheck fehlt; Lint wird nur in entsprechend ausgestatteter CI ausgeführt.\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 "$ROOT_DIR/tests/test_llama.py" || failures=$((failures + 1))
|
||||||
|
|
||||||
|
if 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+]' "$ROOT_DIR"; then
|
||||||
|
printf 'Möglicher eingecheckter Secret-Wert gefunden.\n' >&2
|
||||||
|
failures=$((failures + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
for unit in "$ROOT_DIR"/systemd/user/*.{service,timer}; do
|
||||||
|
[[ -s "$unit" ]] || {
|
||||||
|
printf 'Leere Unit: %s\n' "$unit" >&2
|
||||||
|
failures=$((failures + 1))
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
((failures == 0))
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Isolierte Vertragstests. Kein HOME-Override, GPU, Netzwerk oder echter systemd-Aufruf."""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
RUNNER = ROOT / "libexec/hermes-stack-llama-run"
|
||||||
|
PROFILE = ROOT / "libexec/hermes-stack-llama-profile"
|
||||||
|
RESTART = ROOT / "libexec/hermes-stack-llama-restart"
|
||||||
|
OPTIONS = """--model --hf-repo --mmproj --alias --host --port --ctx-size --parallel
|
||||||
|
--jinja --reasoning --reasoning-budget --reasoning-preserve --flash-attn --threads
|
||||||
|
--threads-batch --n-gpu-layers --fit --fit-target --cache-type-k --cache-type-v
|
||||||
|
--sleep-idle-seconds --image-min-tokens --cors-origins --no-ui"""
|
||||||
|
|
||||||
|
|
||||||
|
class LlamaTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.temp = tempfile.TemporaryDirectory(prefix="hermes-test-", dir=ROOT.parent)
|
||||||
|
self.addCleanup(self.temp.cleanup)
|
||||||
|
self.root = Path(self.temp.name)
|
||||||
|
self.config = self.root / "config/hermes-stack-maintenance"
|
||||||
|
self.config.mkdir(parents=True)
|
||||||
|
self.bin = self.root / "bin"
|
||||||
|
self.bin.mkdir()
|
||||||
|
self.record = self.root / "calls"
|
||||||
|
self.env = dict(os.environ, XDG_CONFIG_HOME=str(self.config.parent),
|
||||||
|
XDG_STATE_HOME=str(self.root / "state"),
|
||||||
|
XDG_RUNTIME_DIR=str(self.root),
|
||||||
|
PATH=str(self.bin) + ":" + os.environ["PATH"],
|
||||||
|
TEST_RECORD=str(self.record))
|
||||||
|
self.server = self.bin / "llama-server"
|
||||||
|
self.write_server()
|
||||||
|
self.write_config()
|
||||||
|
self.program("systemctl", 'printf "%s\\n" "$*" >>"$TEST_RECORD"\nexit 0\n')
|
||||||
|
self.program("notify-test", "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)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def write_server(self, options=OPTIONS):
|
||||||
|
self.program("llama-server", 'if [[ "${1:-}" == --help ]]; then\n'
|
||||||
|
f' printf "%s\\n" {shlex.quote(options)}\n exit 0\nfi\n'
|
||||||
|
'printf "%s\\n" "$@" >"$TEST_RECORD"\n')
|
||||||
|
|
||||||
|
def write_config(self, extra="", profile="baseline", model="", hf="test/model:Q4_K_M"):
|
||||||
|
content = (f'LLAMA_SERVER_BIN={shlex.quote(str(self.server))}\n'
|
||||||
|
f'LLAMA_MODEL={shlex.quote(model)}\nLLAMA_HF_REPO={shlex.quote(hf)}\n'
|
||||||
|
f'LLAMA_PROFILE={shlex.quote(profile)}\n'
|
||||||
|
'LLAMA_ALIAS=qwen38-27b-local\nLLAMA_ARGS=()\n')
|
||||||
|
(self.config / "llama.env").write_text(content + extra)
|
||||||
|
(self.config / "config.env").write_text(
|
||||||
|
"LLAMA_SERVICE=llama-qwen.service\nGATEWAY_SERVICE=hermes-gateway.service\n"
|
||||||
|
"LLAMA_URL=http://127.0.0.1:8080\n"
|
||||||
|
f'NOTIFIER={shlex.quote(str(self.bin / "notify-test"))}\n')
|
||||||
|
|
||||||
|
def run_tool(self, script, *args, success=True):
|
||||||
|
result = subprocess.run(["bash", str(script), *args], env=self.env,
|
||||||
|
text=True, capture_output=True, timeout=8)
|
||||||
|
if success:
|
||||||
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||||
|
else:
|
||||||
|
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def test_profile_values(self):
|
||||||
|
for name, ctx, margin in [("baseline", "65536", "1536"),
|
||||||
|
("balanced", "49152", "1536"),
|
||||||
|
("vram-plus", "49152", "1280"),
|
||||||
|
("compact", "32768", "1536")]:
|
||||||
|
with self.subTest(name=name):
|
||||||
|
result = self.run_tool(RUNNER, "--profile", name, "--print")
|
||||||
|
args = shlex.split(result.stdout)
|
||||||
|
self.assertEqual(args[args.index("--ctx-size") + 1], ctx)
|
||||||
|
self.assertEqual(args[args.index("--fit-target") + 1], margin)
|
||||||
|
self.assertEqual(args.count("--ctx-size"), 1)
|
||||||
|
self.assertIn("qwen38-27b-local", args)
|
||||||
|
|
||||||
|
def test_check_does_not_run_inference(self):
|
||||||
|
self.run_tool(RUNNER, "--check")
|
||||||
|
self.assertFalse(self.record.exists())
|
||||||
|
|
||||||
|
def test_unsupported_option_rejected(self):
|
||||||
|
self.write_server(OPTIONS.replace("--fit-target", ""))
|
||||||
|
result = self.run_tool(RUNNER, "--check", success=False)
|
||||||
|
self.assertIn("--fit-target", result.stderr)
|
||||||
|
self.assertFalse(self.record.exists())
|
||||||
|
|
||||||
|
def test_execution_uses_array(self):
|
||||||
|
self.run_tool(RUNNER)
|
||||||
|
args = self.record.read_text().splitlines()
|
||||||
|
self.assertIn("--hf-repo", args)
|
||||||
|
self.assertIn("test/model:Q4_K_M", args)
|
||||||
|
|
||||||
|
def test_local_legacy_config_and_path_spaces(self):
|
||||||
|
model = self.root / "a model.gguf"
|
||||||
|
model.touch()
|
||||||
|
self.write_config(profile="", model=str(model), hf="",
|
||||||
|
extra="LLAMA_ARGS=(--ctx-size 8192 --parallel 1)\n")
|
||||||
|
self.run_tool(RUNNER)
|
||||||
|
args = self.record.read_text().splitlines()
|
||||||
|
self.assertIn(str(model), args)
|
||||||
|
self.assertNotIn("--fit", args)
|
||||||
|
|
||||||
|
def test_model_source_conflict(self):
|
||||||
|
self.write_config(model="/some/model.gguf")
|
||||||
|
self.run_tool(RUNNER, "--check", success=False)
|
||||||
|
|
||||||
|
def test_missing_model(self):
|
||||||
|
self.write_config(model="/does/not/exist.gguf", hf="")
|
||||||
|
self.run_tool(RUNNER, "--check", success=False)
|
||||||
|
|
||||||
|
def test_missing_model_source(self):
|
||||||
|
self.write_config(hf="")
|
||||||
|
self.run_tool(RUNNER, "--check", success=False)
|
||||||
|
|
||||||
|
def test_unknown_profile(self):
|
||||||
|
self.run_tool(RUNNER, "--profile", "../unknown", "--check", success=False)
|
||||||
|
|
||||||
|
def test_profile_rejects_legacy_args(self):
|
||||||
|
self.write_config(extra="LLAMA_ARGS=(--ctx-size 64)\n")
|
||||||
|
self.run_tool(RUNNER, "--check", success=False)
|
||||||
|
|
||||||
|
def test_profile_selection_preserves_config(self):
|
||||||
|
before = (self.config / "llama.env").read_bytes()
|
||||||
|
self.run_tool(PROFILE, "balanced")
|
||||||
|
self.assertEqual((self.config / "llama.env").read_bytes(), before)
|
||||||
|
self.assertFalse(self.record.exists())
|
||||||
|
result = self.run_tool(RUNNER, "--print")
|
||||||
|
self.assertIn("49152", result.stdout)
|
||||||
|
self.run_tool(PROFILE, "baseline")
|
||||||
|
self.assertEqual(len(list(self.config.glob("llama-profile.backup.*"))), 1)
|
||||||
|
self.assertEqual((self.config / "llama-profile.env").stat().st_mode & 0o777, 0o600)
|
||||||
|
|
||||||
|
def test_profile_does_not_migrate_implicitly(self):
|
||||||
|
self.write_config(profile="")
|
||||||
|
self.run_tool(PROFILE, "balanced", success=False)
|
||||||
|
self.assertFalse((self.config / "llama-profile.env").exists())
|
||||||
|
|
||||||
|
def test_restart_requires_confirmation(self):
|
||||||
|
self.run_tool(RESTART, success=False)
|
||||||
|
self.assertFalse(self.record.exists())
|
||||||
|
|
||||||
|
def test_restart_after_check(self):
|
||||||
|
self.run_tool(RESTART, "--jobs-finished")
|
||||||
|
self.assertEqual(self.record.read_text(), "--user restart llama-qwen.service\n")
|
||||||
|
|
||||||
|
def test_failed_check_does_not_restart(self):
|
||||||
|
self.write_server("")
|
||||||
|
self.run_tool(RESTART, "--jobs-finished", success=False)
|
||||||
|
self.assertFalse(self.record.exists())
|
||||||
|
|
||||||
|
def test_lock_blocks_restart_and_profile_change(self):
|
||||||
|
import fcntl
|
||||||
|
with (self.root / "hermes-stack-hermes-update.lock").open("w") as lock:
|
||||||
|
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
self.run_tool(RESTART, "--jobs-finished", success=False)
|
||||||
|
self.run_tool(PROFILE, "balanced", success=False)
|
||||||
|
self.assertFalse(self.record.exists())
|
||||||
|
self.assertFalse((self.config / "llama-profile.env").exists())
|
||||||
|
|
||||||
|
def test_failed_http_does_not_restart_backend(self):
|
||||||
|
self.program("hermes", "exit 0\n")
|
||||||
|
self.program("curl", "exit 28\n")
|
||||||
|
self.run_tool(ROOT / "libexec/hermes-stack-health", success=False)
|
||||||
|
self.assertNotIn("restart llama", self.record.read_text())
|
||||||
|
|
||||||
|
def test_installer_requires_explicit_ref(self):
|
||||||
|
self.run_tool(ROOT / "scripts/install-llama-cpp.sh", success=False)
|
||||||
|
self.run_tool(ROOT / "install.sh", "--install-llama", success=False)
|
||||||
|
|
||||||
|
def test_installer_help(self):
|
||||||
|
self.run_tool(ROOT / "scripts/install-llama-cpp.sh", "--help")
|
||||||
|
self.run_tool(ROOT / "install.sh", "--help")
|
||||||
|
|
||||||
|
def prepare_build_mocks(self):
|
||||||
|
self.env["CUDACXX"] = str(self.program("nvcc", "exit 0\n"))
|
||||||
|
self.program("nvidia-smi", "exit 0\n")
|
||||||
|
self.program("ninja", "exit 0\n")
|
||||||
|
self.program("c++", "exit 0\n")
|
||||||
|
self.program("git", 'if [[ "$*" == *rev-parse* ]]; then\n'
|
||||||
|
'printf "%s\\n" 0123456789012345678901234567890123456789\nfi\n')
|
||||||
|
self.program("cmake", 'if [[ "${1:-}" == --build ]]; then\n'
|
||||||
|
' [[ "${TEST_BUILD_FAIL:-0}" != 1 ]] || exit 1\n'
|
||||||
|
' mkdir -p "$2/bin"\n'
|
||||||
|
' for name in llama-server llama-cli llama-bench; do\n'
|
||||||
|
' cp "$CUDACXX" "$2/bin/$name"\n'
|
||||||
|
' done\nfi\n')
|
||||||
|
|
||||||
|
def test_build_publishes_all_three_tools(self):
|
||||||
|
self.prepare_build_mocks()
|
||||||
|
prefix = self.root / "managed"
|
||||||
|
self.run_tool(ROOT / "scripts/install-llama-cpp.sh", "--ref", "master",
|
||||||
|
"--prefix", str(prefix))
|
||||||
|
for name in ("llama-server", "llama-cli", "llama-bench"):
|
||||||
|
self.assertTrue((prefix / "current/bin" / name).is_file())
|
||||||
|
manifest = (prefix / "current/BUILD-INFO.txt").read_text()
|
||||||
|
self.assertIn("0123456789012345678901234567890123456789", manifest)
|
||||||
|
self.assertIn("cuda_arch=86", manifest)
|
||||||
|
|
||||||
|
def test_build_preserves_existing_selection(self):
|
||||||
|
self.prepare_build_mocks()
|
||||||
|
prefix = self.root / "managed"
|
||||||
|
prefix.mkdir()
|
||||||
|
old = prefix / "old"
|
||||||
|
old.mkdir()
|
||||||
|
(prefix / "current").symlink_to(old)
|
||||||
|
self.run_tool(ROOT / "scripts/install-llama-cpp.sh", "--ref", "master",
|
||||||
|
"--prefix", str(prefix))
|
||||||
|
self.assertEqual((prefix / "current").resolve(), old)
|
||||||
|
|
||||||
|
def test_failed_build_does_not_activate(self):
|
||||||
|
self.prepare_build_mocks()
|
||||||
|
self.env["TEST_BUILD_FAIL"] = "1"
|
||||||
|
prefix = self.root / "managed"
|
||||||
|
self.run_tool(ROOT / "scripts/install-llama-cpp.sh", "--ref", "master",
|
||||||
|
"--prefix", str(prefix), success=False)
|
||||||
|
self.assertFalse((prefix / "current").exists())
|
||||||
|
|
||||||
|
def signal_fixture(self, target, response):
|
||||||
|
env_file = self.root / "hermes.env"
|
||||||
|
env_file.write_text('SIGNAL_HTTP_URL=http://127.0.0.1:8090\n'
|
||||||
|
'SIGNAL_ACCOUNT=test-account\n'
|
||||||
|
f'SIGNAL_HOME_CHANNEL={shlex.quote(target)}\n')
|
||||||
|
with (self.config / "config.env").open("a") as config:
|
||||||
|
config.write(f'HERMES_ENV={shlex.quote(str(env_file))}\n'
|
||||||
|
'SIGNAL_NOTIFY=1\nTELEGRAM_NOTIFY=0\n')
|
||||||
|
self.program("systemd-cat", "exit 0\n")
|
||||||
|
self.program("curl", 'while (($#)); do\n'
|
||||||
|
' if [[ "$1" == --data-binary ]]; then\n'
|
||||||
|
' printf "%s" "$2" >"$TEST_RECORD"\n fi\n shift\ndone\n'
|
||||||
|
f'printf "%s\\n" {shlex.quote(response)}\n')
|
||||||
|
|
||||||
|
def test_signal_direct_payload(self):
|
||||||
|
self.signal_fixture("test-recipient", '{"result":{"timestamp":1}}')
|
||||||
|
message = 'Test "Anführungszeichen"\nzweite Zeile'
|
||||||
|
self.run_tool(ROOT / "libexec/hermes-stack-notify", message)
|
||||||
|
payload = json.loads(self.record.read_text())
|
||||||
|
self.assertEqual(payload["params"]["recipient"], ["test-recipient"])
|
||||||
|
self.assertEqual(payload["params"]["message"], message)
|
||||||
|
|
||||||
|
def test_signal_group_payload(self):
|
||||||
|
self.signal_fixture("group:test-group", '{"result":{"timestamp":1}}')
|
||||||
|
self.run_tool(ROOT / "libexec/hermes-stack-notify", "Test")
|
||||||
|
payload = json.loads(self.record.read_text())
|
||||||
|
self.assertEqual(payload["params"]["groupId"], "test-group")
|
||||||
|
self.assertNotIn("recipient", payload["params"])
|
||||||
|
|
||||||
|
def test_signal_rpc_failure_is_nonfatal(self):
|
||||||
|
self.signal_fixture("test-recipient", '{"error":{"code":-1}}')
|
||||||
|
self.run_tool(ROOT / "libexec/hermes-stack-notify", "Test")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# Entfernt ausschließlich die von diesem Repository verwalteten Dateien.
|
||||||
|
# Hermes-Daten, Modelle, Signal-Sitzung, Konfigurationen und Backups bleiben erhalten.
|
||||||
|
units=(
|
||||||
|
hermes-stack-hermes-update.service hermes-stack-hermes-update.timer
|
||||||
|
hermes-stack-health.service hermes-stack-health.timer
|
||||||
|
hermes-stack-signal-health.service hermes-stack-signal-health.timer
|
||||||
|
hermes-stack-signal-update-check.service hermes-stack-signal-update-check.timer
|
||||||
|
llama-qwen.service signal-cli.service
|
||||||
|
)
|
||||||
|
|
||||||
|
systemctl --user disable --now \
|
||||||
|
hermes-stack-hermes-update.timer \
|
||||||
|
hermes-stack-health.timer \
|
||||||
|
hermes-stack-signal-health.timer \
|
||||||
|
hermes-stack-signal-update-check.timer 2>/dev/null || true
|
||||||
|
|
||||||
|
for unit in "${units[@]}"; do
|
||||||
|
rm -f -- "${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/$unit"
|
||||||
|
done
|
||||||
|
|
||||||
|
for file in \
|
||||||
|
hermes-stack-notify hermes-stack-hermes-update hermes-stack-health \
|
||||||
|
hermes-stack-signal-health hermes-stack-signal-update-check hermes-stack-signal-update \
|
||||||
|
hermes-stack-llama-run hermes-stack-llama-profile hermes-stack-llama-restart \
|
||||||
|
hermes-stack-llama-report \
|
||||||
|
hermes-stack-signal-run hermes-stack-doctor; do
|
||||||
|
rm -f -- "$HOME/.local/libexec/$file"
|
||||||
|
done
|
||||||
|
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
printf 'Deployment-Dateien entfernt; Benutzerdaten und Konfigurationen wurden beibehalten.\n'
|
||||||
Reference in new issue
Block a user