phase2b: Auftraege ueberleben einen Neustart von MC2

- jobengine: jeder Auftrag laeuft als eigene systemd-Einheit mc2-job-<id> (systemd-run,
  RuntimeMaxSec als Zeitlimit); Akte, Protokoll und Exit-Code liegen unter
  <Datenordner>/mc2-jobs; MC2 nimmt laufende Auftraege beim Start wieder auf
- Geheimnisse (HF_TOKEN) ueber eine nur fuer den Nutzer lesbare Umgebungsdatei, die der
  Auftrag beim Start liest und loescht; nichts davon in Befehlszeile oder Einheit
- benannte Nacharbeiten (wartung:nach_update, modell:rolle) statt Closures, laufen auch
  nach einem Neustart
- ohne systemd (PC, Tests) weiter als Kindprozess
- deploy.sh wartet nur noch auf Update-Auftraege; Downloads laufen weiter
- 14 neue Tests; systemd-Weg auf der Box echt geprueft (Neustart, Abbruch, Zeitlimit,
  Geheimnis nicht sichtbar)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-24 17:41:26 +02:00
co-authored by Claude Opus 5.5
parent 75611be9a9
commit e1d7d499f8
7 changed files with 657 additions and 177 deletions
+19 -14
View File
@@ -55,6 +55,20 @@ def register(req: RegisterReq) -> dict:
return {"ok": True, "model_id": model_id}
@jobengine.nacharbeit("modell:rolle")
def _rolle_uebernehmen(model_id: str, role: str) -> None:
"""Nach dem Download die gewünschte Rolle setzen. hermes geht dabei durch den warm-bewussten
Hirn-Flow (brains-Gruppe, ttl 0, Hermes-Config, Gateway-Restart) — der rohe Alias-Move hatte
diesen Flow bisher umgangen."""
if role == "hermes":
from services.agent import set_agent_brain
res = set_agent_brain(model_id)
if not res.get("ok"):
raise RuntimeError(res.get("reason", "Hirn-Wechsel fehlgeschlagen"))
else:
llamaswap.set_role(model_id, role)
class InstallReq(BaseModel):
repo: str
role: str | None = None
@@ -109,20 +123,9 @@ def install(req: InstallReq) -> dict:
except PermissionError as exc:
raise HTTPException(500, str(exc))
# Rolle erst NACH erfolgreichem Download übernehmen. hermes geht dabei durch den
# warm-bewussten Hirn-Flow (brains-Gruppe, ttl 0, Hermes-Config, Gateway-Restart) —
# der rohe Alias-Move hatte diesen Flow bisher umgangen.
# Rolle erst NACH erfolgreichem Download übernehmen (_rolle_uebernehmen).
role = (req.role or "").strip().lower()
def _apply_role() -> None:
if role == "hermes":
from services.agent import set_agent_brain
res = set_agent_brain(model_id)
if not res.get("ok"):
raise RuntimeError(res.get("reason", "Hirn-Wechsel fehlgeschlagen"))
else:
llamaswap.set_role(model_id, role)
# Download-Job: alle GGUF-Teile (+ mmproj) per --include holen.
args = [hf.hf_bin(), "download", repo]
args.extend(info["files"])
@@ -133,8 +136,10 @@ def install(req: InstallReq) -> dict:
if token := geheimnisse.hf_token():
env["HF_TOKEN"] = token
# Gruppe „download“: so taucht der Download in der Oberfläche mit Fortschritt und Abbrechen auf.
job_id = jobengine.start_job(args, f"Download {repo}", env=env, group="download",
on_done=_apply_role if role else None, zeitlimit_s=6 * 3600)
# Der Download läuft als eigener Auftrag weiter, auch wenn MC2 zwischendurch neu startet.
job_id = jobengine.start_job(args, f"Download {repo}", env=env, group="download", zeitlimit_s=6 * 3600,
nacharbeit="modell:rolle" if role else None,
nacharbeit_daten={"model_id": model_id, "role": role} if role else None)
jobengine.attach_download_progress(job_id, str(target), info["total_bytes"])
return {"ok": True, "job_id": job_id, "model_id": model_id, "model_path": model_path,
"total_bytes": info["total_bytes"], "files": len(info["files"])}