phase2e: Modelltausch schreibt die llama-swap-Config einmal statt bis zu siebenmal
- llamaswap.sammeln(): Aenderungen lesen dieselbe Config aus dem Speicher, geschrieben wird einmal am Ende, bei einem Fehler gar nicht - Radar-Tausch und Hirn-Umstellung nutzen es; Hermes wird erst nach dem Schreiben umgestellt - Test-Attrappe fuer update_brain_model: der Radar-Test faesst auf der Box nie die echte Hermes-Config an - Doku: Ziel-Modell, strukturierter Verlauf, Sammel-Schreiben Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
589c14d5e9
commit
a47489fa85
@@ -107,12 +107,14 @@ def agent_status() -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def set_agent_brain(model_id: str) -> dict:
|
def set_agent_brain(model_id: str, hermes_umstellen: bool = True) -> dict:
|
||||||
"""Setzt ein (bereits installiertes) Modell als Agent-Hirn — WARM-bewusst:
|
"""Setzt ein (bereits installiertes) Modell als Agent-Hirn — WARM-bewusst:
|
||||||
1) vergibt den 'hermes'-Alias (das Agent-Hirn-Slot),
|
1) vergibt den 'hermes'-Alias (das Agent-Hirn-Slot),
|
||||||
2) tauscht es in die residente brains-Gruppe (altes Hirn raus, fast/vision bleiben),
|
2) tauscht es in die residente brains-Gruppe (altes Hirn raus, fast/vision bleiben),
|
||||||
3) zeigt die Hermes-Config auf den 'hermes'-Alias + Gateway-Restart.
|
3) zeigt die Hermes-Config auf den 'hermes'-Alias + Gateway-Restart.
|
||||||
So bleibt das neue Hirn warm und der Agent nutzt es sofort."""
|
So bleibt das neue Hirn warm und der Agent nutzt es sofort.
|
||||||
|
hermes_umstellen=False: Schritt 3 macht der Aufrufer selbst — etwa das Radar, das erst nach dem
|
||||||
|
einen gesammelten Schreibvorgang der llama-swap-Config Hermes neu startet."""
|
||||||
from services import llamaswap
|
from services import llamaswap
|
||||||
models = {m["name"]: m for m in llamaswap.list_models()}
|
models = {m["name"]: m for m in llamaswap.list_models()}
|
||||||
if model_id not in models:
|
if model_id not in models:
|
||||||
@@ -122,6 +124,7 @@ def set_agent_brain(model_id: str) -> dict:
|
|||||||
# Idempotent härten: auch wenn schon Hirn, warm (brains) + ttl 0 sicherstellen.
|
# Idempotent härten: auch wenn schon Hirn, warm (brains) + ttl 0 sicherstellen.
|
||||||
try:
|
try:
|
||||||
from services.llamaswap import set_ttl
|
from services.llamaswap import set_ttl
|
||||||
|
with llamaswap.sammeln():
|
||||||
brains = (llamaswap.list_groups().get("brains") or {}).get("members") or []
|
brains = (llamaswap.list_groups().get("brains") or {}).get("members") or []
|
||||||
if model_id not in brains:
|
if model_id not in brains:
|
||||||
llamaswap.set_group("brains", brains + [model_id], swap=False, persist=True)
|
llamaswap.set_group("brains", brains + [model_id], swap=False, persist=True)
|
||||||
@@ -130,17 +133,20 @@ def set_agent_brain(model_id: str) -> dict:
|
|||||||
return {"ok": False, "reason": str(exc)}
|
return {"ok": False, "reason": str(exc)}
|
||||||
return {"ok": True, "old": old, "new": model_id, "note": "ist bereits das Agent-Hirn"}
|
return {"ok": True, "old": old, "new": model_id, "note": "ist bereits das Agent-Hirn"}
|
||||||
try:
|
try:
|
||||||
|
from services.llamaswap import DEFAULT_TTL, set_ttl
|
||||||
|
# Alias, Gruppe und ttl als EIN Schreibvorgang (24.09.2026) — jeder einzelne entlud alle Modelle.
|
||||||
|
with llamaswap.sammeln():
|
||||||
llamaswap.set_role(model_id, "hermes") # 1) Alias
|
llamaswap.set_role(model_id, "hermes") # 1) Alias
|
||||||
brains = (llamaswap.list_groups().get("brains") or {}).get("members") or []
|
brains = (llamaswap.list_groups().get("brains") or {}).get("members") or []
|
||||||
new_members = [x for x in brains if x not in (old, model_id)] + [model_id]
|
new_members = [x for x in brains if x not in (old, model_id)] + [model_id]
|
||||||
llamaswap.set_group("brains", new_members, swap=False, persist=True) # 2) warm
|
llamaswap.set_group("brains", new_members, swap=False, persist=True) # 2) warm
|
||||||
# 2b) TTL härten: neues Hirn nie auto-entladen; altes Hirn auf Default entspannen.
|
# 2b) TTL härten: neues Hirn nie auto-entladen; altes Hirn auf Default entspannen.
|
||||||
from services.llamaswap import DEFAULT_TTL, set_ttl
|
|
||||||
set_ttl(model_id, 0)
|
set_ttl(model_id, 0)
|
||||||
if old:
|
if old:
|
||||||
set_ttl(old, DEFAULT_TTL)
|
set_ttl(old, DEFAULT_TTL)
|
||||||
except PermissionError as exc:
|
except PermissionError as exc:
|
||||||
return {"ok": False, "reason": str(exc)}
|
return {"ok": False, "reason": str(exc)}
|
||||||
|
if hermes_umstellen:
|
||||||
update_brain_model("hermes") # 3) Config + Restart
|
update_brain_model("hermes") # 3) Config + Restart
|
||||||
# Weiche Budget-Warnung (kein Hard-Block): passt Hirn + größtes on-demand zusammen ins GTT?
|
# Weiche Budget-Warnung (kein Hard-Block): passt Hirn + größtes on-demand zusammen ins GTT?
|
||||||
warning = None
|
warning = None
|
||||||
|
|||||||
@@ -93,6 +93,29 @@ def config_sperre():
|
|||||||
datei.close()
|
datei.close()
|
||||||
|
|
||||||
|
|
||||||
|
# Sammel-Schreiben (24.09.2026): Jeder Schreibvorgang lässt llama-swap neu laden und ALLE Modelle
|
||||||
|
# entladen. Ein Modelltausch bestand aus bis zu sieben Schreibvorgängen (Eintragen, Befehl, Zwilling,
|
||||||
|
# Alias, Gruppe, ttl …). Innerhalb von sammeln() lesen alle Änderungen dieselbe Config aus dem
|
||||||
|
# Speicher, und geschrieben wird einmal am Ende — oder gar nicht, wenn etwas scheitert.
|
||||||
|
_SAMMEL = threading.local()
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def sammeln():
|
||||||
|
with config_sperre():
|
||||||
|
if getattr(_SAMMEL, "aktiv", False): # verschachtelt: der äußere schreibt
|
||||||
|
yield
|
||||||
|
return
|
||||||
|
_SAMMEL.aktiv, _SAMMEL.cfg = True, None
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
cfg = _SAMMEL.cfg
|
||||||
|
finally:
|
||||||
|
_SAMMEL.aktiv, _SAMMEL.cfg = False, None
|
||||||
|
if cfg is not None:
|
||||||
|
_schreiben(cfg)
|
||||||
|
|
||||||
|
|
||||||
def _mit_sperre(fn):
|
def _mit_sperre(fn):
|
||||||
@functools.wraps(fn)
|
@functools.wraps(fn)
|
||||||
def huelle(*args, **kwargs):
|
def huelle(*args, **kwargs):
|
||||||
@@ -103,6 +126,14 @@ def _mit_sperre(fn):
|
|||||||
|
|
||||||
# --- Lesen -------------------------------------------------------------------
|
# --- Lesen -------------------------------------------------------------------
|
||||||
def read_config() -> dict:
|
def read_config() -> dict:
|
||||||
|
if getattr(_SAMMEL, "aktiv", False):
|
||||||
|
if _SAMMEL.cfg is None:
|
||||||
|
_SAMMEL.cfg = _lesen()
|
||||||
|
return _SAMMEL.cfg
|
||||||
|
return _lesen()
|
||||||
|
|
||||||
|
|
||||||
|
def _lesen() -> dict:
|
||||||
if not CONFIG_PATH.exists():
|
if not CONFIG_PATH.exists():
|
||||||
return {"models": {}}
|
return {"models": {}}
|
||||||
from ruamel.yaml import YAML
|
from ruamel.yaml import YAML
|
||||||
@@ -260,7 +291,15 @@ def _augment_vision(cmd: str, model_path: str, mmproj_path: str | None) -> str:
|
|||||||
|
|
||||||
def write_config(cfg: dict) -> None:
|
def write_config(cfg: dict) -> None:
|
||||||
"""Atomar schreiben (tmp + os.replace), damit llama-swap mit -watch-config nie
|
"""Atomar schreiben (tmp + os.replace), damit llama-swap mit -watch-config nie
|
||||||
eine halbe Datei sieht. Fehlende Schreibrechte → klare Meldung."""
|
eine halbe Datei sieht. Fehlende Schreibrechte → klare Meldung. Innerhalb von sammeln()
|
||||||
|
wird nur vorgemerkt; geschrieben wird am Ende einmal."""
|
||||||
|
if getattr(_SAMMEL, "aktiv", False):
|
||||||
|
_SAMMEL.cfg = cfg
|
||||||
|
return
|
||||||
|
_schreiben(cfg)
|
||||||
|
|
||||||
|
|
||||||
|
def _schreiben(cfg: dict) -> None:
|
||||||
try:
|
try:
|
||||||
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
tmp = CONFIG_PATH.with_name(CONFIG_PATH.name + ".tmp")
|
tmp = CONFIG_PATH.with_name(CONFIG_PATH.name + ".tmp")
|
||||||
|
|||||||
@@ -1383,8 +1383,9 @@ def _tausche_ein(k: dict) -> dict:
|
|||||||
else:
|
else:
|
||||||
ziel = quelle # schon verschoben (früherer, abgebrochener Versuch)
|
ziel = quelle # schon verschoben (früherer, abgebrochener Versuch)
|
||||||
pfade = _lokale_pfade(k, ziel)
|
pfade = _lokale_pfade(k, ziel)
|
||||||
# Eintragen, Befehl ersetzen und Zwilling anlegen unter EINER Config-Sperre (24.09.2026).
|
# Eintragen, Befehl, Zwilling, Rollen, Gruppe und ttl als EIN Schreibvorgang (24.09.2026): Vorher
|
||||||
with llamaswap.config_sperre():
|
# schrieb ein Tausch die Config bis zu siebenmal, und jedes Mal entlud llama-swap alle Modelle.
|
||||||
|
with llamaswap.sammeln():
|
||||||
modell_id = llamaswap.register_model(pfade["gguf"], role=ALIAS[rolle],
|
modell_id = llamaswap.register_model(pfade["gguf"], role=ALIAS[rolle],
|
||||||
ctx=int(k.get("ctx") or CTX_ROLLE[rolle]),
|
ctx=int(k.get("ctx") or CTX_ROLLE[rolle]),
|
||||||
mmproj_path=None, jinja=True, set_alias=False)
|
mmproj_path=None, jinja=True, set_alias=False)
|
||||||
@@ -1396,27 +1397,32 @@ def _tausche_ein(k: dict) -> dict:
|
|||||||
if pfade.get("mmproj"):
|
if pfade.get("mmproj"):
|
||||||
_zwilling_eintragen(cfg, rolle, modell_id, pfade, k)
|
_zwilling_eintragen(cfg, rolle, modell_id, pfade, k)
|
||||||
llamaswap.write_config(cfg)
|
llamaswap.write_config(cfg)
|
||||||
hinweis = None
|
antwort: dict = {}
|
||||||
if rolle == "hirn":
|
if rolle == "hirn":
|
||||||
from services.agent import set_agent_brain
|
from services.agent import set_agent_brain
|
||||||
antwort = set_agent_brain(modell_id) # hermes-Alias, brains-Gruppe, ttl 0, Hermes-Config
|
# hermes-Alias, brains-Gruppe, ttl 0 — Hermes selbst erst nach dem Schreiben (unten).
|
||||||
|
antwort = set_agent_brain(modell_id, hermes_umstellen=False)
|
||||||
if not antwort.get("ok"):
|
if not antwort.get("ok"):
|
||||||
raise RuntimeError(antwort.get("reason") or "Hirn-Wechsel fehlgeschlagen")
|
raise RuntimeError(antwort.get("reason") or "Hirn-Wechsel fehlgeschlagen")
|
||||||
# Das Hirn ist heute hermes UND fast (Chat-Spur, warmup.sh, Oberfläche). Bliebe fast am alten
|
# Das Hirn ist heute hermes UND fast (Chat-Spur, warmup.sh, Oberfläche). Bliebe fast am alten
|
||||||
# Modell, lüde warmup.sh es nachts neben das neue Hirn.
|
# Modell, lüde warmup.sh es nachts neben das neue Hirn.
|
||||||
if alt and "fast" in {str(a).lower() for a in alt.get("aliases") or []}:
|
if alt and "fast" in {str(a).lower() for a in alt.get("aliases") or []}:
|
||||||
llamaswap.add_role(modell_id, "fast")
|
llamaswap.add_role(modell_id, "fast")
|
||||||
hinweis = " ".join(h for h in (antwort.get("warning"), _warmset_umstellen((alt or {}).get("name"), modell_id))
|
|
||||||
if h) or None
|
|
||||||
else:
|
else:
|
||||||
if not llamaswap.set_role(modell_id, "coder"):
|
if not llamaswap.set_role(modell_id, "coder"):
|
||||||
raise RuntimeError(f"Die Rolle coder ließ sich nicht auf {modell_id} setzen.")
|
raise RuntimeError(f"Die Rolle coder ließ sich nicht auf {modell_id} setzen.")
|
||||||
# heavy ist heute derselbe Coder (OpenCode plant damit). Bliebe es am alten Modell, lägen zwei Coder
|
# heavy ist heute derselbe Coder (OpenCode plant damit). Bliebe es am alten Modell, lägen zwei
|
||||||
# bereit und der alte ließe sich nie löschen.
|
# Coder bereit und der alte ließe sich nie löschen.
|
||||||
if alt and "heavy" in {str(a).lower() for a in alt.get("aliases") or []}:
|
if alt and "heavy" in {str(a).lower() for a in alt.get("aliases") or []}:
|
||||||
llamaswap.add_role(modell_id, "heavy")
|
llamaswap.add_role(modell_id, "heavy")
|
||||||
if alt and alt.get("ttl") is not None:
|
if alt and alt.get("ttl") is not None:
|
||||||
llamaswap.set_ttl(modell_id, int(alt["ttl"]))
|
llamaswap.set_ttl(modell_id, int(alt["ttl"]))
|
||||||
|
hinweis = None
|
||||||
|
if rolle == "hirn":
|
||||||
|
from services.agent import update_brain_model
|
||||||
|
update_brain_model("hermes") # Hermes-Config + Neustart, jetzt mit fertiger Config
|
||||||
|
hinweis = " ".join(h for h in (antwort.get("warning"), _warmset_umstellen((alt or {}).get("name"), modell_id))
|
||||||
|
if h) or None
|
||||||
return {"modell_id": modell_id, "pfad": str(ziel), "alt": (alt or {}).get("name"), "hinweis": hinweis}
|
return {"modell_id": modell_id, "pfad": str(ziel), "alt": (alt or {}).get("name"), "hinweis": hinweis}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
"""Sammel-Schreiben der llama-swap-Config (24.09.2026): Ein Modelltausch schrieb die Config bis zu
|
||||||
|
siebenmal, und jedes Mal entlud llama-swap alle Modelle. In llamaswap.sammeln() gibt es genau einen
|
||||||
|
Schreibvorgang — oder keinen, wenn etwas scheitert."""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from services import llamaswap
|
||||||
|
|
||||||
|
CONFIG = """\
|
||||||
|
models:
|
||||||
|
alt:
|
||||||
|
cmd: llama-server -m /srv/models/alt.gguf
|
||||||
|
aliases:
|
||||||
|
- hermes
|
||||||
|
- fast
|
||||||
|
ttl: 0
|
||||||
|
neu:
|
||||||
|
cmd: llama-server -m /srv/models/neu.gguf
|
||||||
|
ttl: 300
|
||||||
|
groups:
|
||||||
|
brains:
|
||||||
|
swap: false
|
||||||
|
persistent: true
|
||||||
|
members:
|
||||||
|
- alt
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def config(tmp_path, monkeypatch):
|
||||||
|
datei = tmp_path / "config.yaml"
|
||||||
|
datei.write_text(CONFIG, encoding="utf-8")
|
||||||
|
monkeypatch.setattr(llamaswap, "CONFIG_PATH", datei)
|
||||||
|
schreibvorgaenge: list[int] = []
|
||||||
|
echt = llamaswap._schreiben
|
||||||
|
|
||||||
|
def zaehlen(cfg):
|
||||||
|
schreibvorgaenge.append(1)
|
||||||
|
echt(cfg)
|
||||||
|
|
||||||
|
monkeypatch.setattr(llamaswap, "_schreiben", zaehlen)
|
||||||
|
return datei, schreibvorgaenge
|
||||||
|
|
||||||
|
|
||||||
|
def test_ohne_sammeln_schreibt_jede_aenderung(config):
|
||||||
|
_, schreibvorgaenge = config
|
||||||
|
llamaswap.set_role("neu", "coder")
|
||||||
|
llamaswap.set_ttl("neu", 600)
|
||||||
|
assert len(schreibvorgaenge) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_sammeln_schreibt_einmal_mit_allen_aenderungen(config):
|
||||||
|
datei, schreibvorgaenge = config
|
||||||
|
with llamaswap.sammeln():
|
||||||
|
llamaswap.set_role("neu", "hermes")
|
||||||
|
llamaswap.add_role("neu", "fast")
|
||||||
|
llamaswap.set_group("brains", ["neu"], swap=False, persist=True)
|
||||||
|
llamaswap.set_ttl("neu", 0)
|
||||||
|
llamaswap.set_ttl("alt", 300)
|
||||||
|
# Innerhalb des Sammelns sieht jede Abfrage den gesammelten Stand …
|
||||||
|
assert "hermes" in next(m for m in llamaswap.list_models() if m["name"] == "neu")["aliases"]
|
||||||
|
# … die Datei aber noch den alten.
|
||||||
|
assert "- hermes" in datei.read_text(encoding="utf-8").split("neu:")[0]
|
||||||
|
assert schreibvorgaenge == []
|
||||||
|
assert len(schreibvorgaenge) == 1
|
||||||
|
modelle = {m["name"]: m for m in llamaswap.list_models()}
|
||||||
|
assert {"hermes", "fast"} <= set(modelle["neu"]["aliases"]) and modelle["neu"]["ttl"] == 0
|
||||||
|
assert "hermes" not in modelle["alt"]["aliases"] and modelle["alt"]["ttl"] == 300
|
||||||
|
assert llamaswap.list_groups()["brains"]["members"] == ["neu"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_sammeln_schreibt_nichts_wenn_etwas_scheitert(config):
|
||||||
|
datei, schreibvorgaenge = config
|
||||||
|
vorher = datei.read_text(encoding="utf-8")
|
||||||
|
with pytest.raises(RuntimeError), llamaswap.sammeln():
|
||||||
|
llamaswap.set_role("neu", "hermes")
|
||||||
|
raise RuntimeError("Hirn-Wechsel fehlgeschlagen")
|
||||||
|
assert schreibvorgaenge == [] and datei.read_text(encoding="utf-8") == vorher
|
||||||
|
|
||||||
|
|
||||||
|
def test_verschachtelt_schreibt_der_aeussere(config):
|
||||||
|
_, schreibvorgaenge = config
|
||||||
|
with llamaswap.sammeln():
|
||||||
|
llamaswap.set_ttl("neu", 1)
|
||||||
|
with llamaswap.sammeln():
|
||||||
|
llamaswap.set_ttl("neu", 2)
|
||||||
|
assert schreibvorgaenge == []
|
||||||
|
assert len(schreibvorgaenge) == 1
|
||||||
|
assert next(m for m in llamaswap.list_models() if m["name"] == "neu")["ttl"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_andere_threads_sehen_den_gesammelten_stand_nicht(config):
|
||||||
|
gesehen: list[list[str]] = []
|
||||||
|
with llamaswap.sammeln():
|
||||||
|
llamaswap.set_role("neu", "coder")
|
||||||
|
t = threading.Thread(target=lambda: gesehen.append(
|
||||||
|
next(m for m in llamaswap._lesen()["models"] if m == "neu") and
|
||||||
|
list(llamaswap._lesen()["models"]["neu"].get("aliases") or [])))
|
||||||
|
t.start()
|
||||||
|
t.join()
|
||||||
|
assert gesehen == [[]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_hirn_umstellung_schreibt_einmal(config, monkeypatch):
|
||||||
|
from services import agent
|
||||||
|
|
||||||
|
_, schreibvorgaenge = config
|
||||||
|
monkeypatch.setattr(agent, "hermes_brain_info", dict)
|
||||||
|
umgestellt: list[str] = []
|
||||||
|
monkeypatch.setattr(agent, "update_brain_model", lambda alias: umgestellt.append(alias) or True)
|
||||||
|
antwort = agent.set_agent_brain("neu", hermes_umstellen=False)
|
||||||
|
assert antwort["ok"] and antwort["old"] == "alt"
|
||||||
|
assert len(schreibvorgaenge) == 1 and umgestellt == []
|
||||||
|
modelle = {m["name"]: m for m in llamaswap.list_models()}
|
||||||
|
assert "hermes" in modelle["neu"]["aliases"] and modelle["neu"]["ttl"] == 0
|
||||||
|
assert llamaswap.list_groups()["brains"]["members"] == ["neu"]
|
||||||
|
agent.set_agent_brain("neu") # schon das Hirn: härtet nur nach, Hermes wird nicht angefasst
|
||||||
|
assert umgestellt == []
|
||||||
@@ -374,7 +374,10 @@ def _uebernahme_vorbereiten(zustand, monkeypatch, rolle: str, alt: dict) -> tupl
|
|||||||
monkeypatch.setattr(radar.llamaswap, "add_role", lambda mid, r: aufrufe.append(("dazu", mid, r)) or True)
|
monkeypatch.setattr(radar.llamaswap, "add_role", lambda mid, r: aufrufe.append(("dazu", mid, r)) or True)
|
||||||
monkeypatch.setattr(radar, "_steward_neu_starten", lambda: aufrufe.append(("steward",)) or None)
|
monkeypatch.setattr(radar, "_steward_neu_starten", lambda: aufrufe.append(("steward",)) or None)
|
||||||
monkeypatch.setattr(radar.llamaswap, "set_ttl", lambda mid, ttl: aufrufe.append(("ttl", mid, ttl)) or True)
|
monkeypatch.setattr(radar.llamaswap, "set_ttl", lambda mid, ttl: aufrufe.append(("ttl", mid, ttl)) or True)
|
||||||
monkeypatch.setattr(agent, "set_agent_brain", lambda mid: aufrufe.append(("hirn", mid)) or {"ok": True})
|
monkeypatch.setattr(agent, "set_agent_brain",
|
||||||
|
lambda mid, hermes_umstellen=True: aufrufe.append(("hirn", mid, hermes_umstellen)) or {"ok": True})
|
||||||
|
# Nie die echte Hermes-Config: Auf der Box läuft dieser Test im Deploy-Prüftor.
|
||||||
|
monkeypatch.setattr(agent, "update_brain_model", lambda alias: aufrufe.append(("hermes-config", alias)) or True)
|
||||||
monkeypatch.setattr(radar, "STEWARD_WARMSET", zustand / "warmset.conf")
|
monkeypatch.setattr(radar, "STEWARD_WARMSET", zustand / "warmset.conf")
|
||||||
return aufrufe, cfg
|
return aufrufe, cfg
|
||||||
|
|
||||||
@@ -386,8 +389,10 @@ def test_uebernehmen_hirn_nutzt_den_hirn_wechsel_und_nimmt_fast_mit(zustand, mon
|
|||||||
encoding="utf-8")
|
encoding="utf-8")
|
||||||
antwort = radar.uebernehmen("o")
|
antwort = radar.uebernehmen("o")
|
||||||
assert antwort["ok"] and antwort["modell_id"] == "o"
|
assert antwort["ok"] and antwort["modell_id"] == "o"
|
||||||
assert [a[0] for a in aufrufe] == ["eintragen", "schreiben", "hirn", "dazu", "steward"]
|
# Hermes wird erst umgestellt, wenn die llama-swap-Config einmal geschrieben ist (24.09.2026).
|
||||||
assert aufrufe[0][2:] == ("hermes", False) and aufrufe[3] == ("dazu", "o", "fast")
|
assert [a[0] for a in aufrufe] == ["eintragen", "schreiben", "hirn", "dazu", "hermes-config", "steward"]
|
||||||
|
assert aufrufe[0][2:] == ("hermes", False) and aufrufe[2] == ("hirn", "o", False)
|
||||||
|
assert aufrufe[3] == ("dazu", "o", "fast") and aufrufe[4] == ("hermes-config", "hermes")
|
||||||
befehl = str(cfg["models"]["o"]["cmd"])
|
befehl = str(cfg["models"]["o"]["cmd"])
|
||||||
assert "--parallel 2" in befehl and "-ctk q8_0" in befehl and "--mmproj" not in befehl
|
assert "--parallel 2" in befehl and "-ctk q8_0" in befehl and "--mmproj" not in befehl
|
||||||
# Wie im Betrieb seit 24.09.: Bilder sieht der neue Bild-Zwilling (Projektor, ohne Draft), der alte fliegt raus.
|
# Wie im Betrieb seit 24.09.: Bilder sieht der neue Bild-Zwilling (Projektor, ohne Draft), der alte fliegt raus.
|
||||||
|
|||||||
+23
-1
@@ -108,6 +108,27 @@ Fremde Dienste, die der Box-Wart nur steuert oder überwacht: `llama-swap` (Syst
|
|||||||
- **Ohne systemd** (PC, Tests, `MC_JOBS_ART=prozess`) läuft der Auftrag als Kindprozess und überlebt keinen
|
- **Ohne systemd** (PC, Tests, `MC_JOBS_ART=prozess`) läuft der Auftrag als Kindprozess und überlebt keinen
|
||||||
Neustart. Ein Probelauf daneben (`MC_PROBELAUF=1`) nimmt keine Aufträge auf.
|
Neustart. Ein Probelauf daneben (`MC_PROBELAUF=1`) nimmt keine Aufträge auf.
|
||||||
|
|
||||||
|
## Ziele, Bausteine und Update-Verlauf (seit 24.09., Phase 2d)
|
||||||
|
|
||||||
|
- **Gemeinsames Modell** `backend/kern/ziele.py`: Ein Ziel ist ein Gerät (KI-Box, später Proxmox-Host, Container,
|
||||||
|
Arcane-VM) mit Bausteinen. Jeder Baustein hat einen Stand (`neu`, `aktuell`, `unbekannt` mit Grund,
|
||||||
|
`festgehalten`, `wird-geprueft`), Versionen und den Knopf, der das Update anstößt (Methode, Pfad, Rückfrage).
|
||||||
|
- **Box-Adapter** `services/box_updates.py` (`ki_box_ziel()`): Betriebssystem, Motor, llama-swap und Hermes aus dem
|
||||||
|
Zwischenspeicher der Update-Prüfung (10 Minuten, nach jedem Update sofort neu), Pins als `festgehalten`,
|
||||||
|
gescheiterte Prüfungen als `unbekannt`. `GET /api/ziele` liefert die KI-Box; der Homelab-Teil liefert seine
|
||||||
|
Ziele ab Phase 3 unter `/api/homelab/ziele`, die Oberfläche legt beide zu einer Liste zusammen.
|
||||||
|
- **Strukturierter Update-Verlauf** `<Datenordner>/mc2-update-verlauf.jsonl`: je Baustein eine JSON-Zeile
|
||||||
|
(`lauf`, `anlass`, `ts`, `baustein`, `ergebnis`, `text`). Es schreiben `autoupdate.sh` (Helfer `ergebnis`/`verlauf`;
|
||||||
|
die Telegram-Texte bleiben Zeichen für Zeichen gleich) und die Update-Knöpfe (Abschluss-Haken
|
||||||
|
`wartung:verlauf` der Aufträge, bei „Alle aktualisieren" mit dem Teil, an dem die Kette scheiterte).
|
||||||
|
`services/update_verlauf.py` liest ab dem ersten strukturierten Eintrag nur noch ihn, ältere Läufe weiter aus
|
||||||
|
`~/mc2-notify.log`. Den Hermes-Auftrag des Sonntags-Laufs startet `autoupdate.sh` mit `?verlauf=0`, damit er
|
||||||
|
nicht doppelt erscheint.
|
||||||
|
- **Sammel-Schreiben der llama-swap-Config** `llamaswap.sammeln()`: Alle Änderungen darin lesen dieselbe Config aus
|
||||||
|
dem Speicher; geschrieben wird einmal am Ende, bei einem Fehler gar nicht. Ein Radar-Tausch (Eintragen, Befehl,
|
||||||
|
Zwilling, Alias, Gruppe, ttl) und die Hirn-Umstellung sind damit je ein Schreibvorgang statt bis zu sieben;
|
||||||
|
jeder Schreibvorgang lässt llama-swap alle Modelle entladen. Hermes wird erst danach umgestellt.
|
||||||
|
|
||||||
## Modelle und Modell-Rollen
|
## Modelle und Modell-Rollen
|
||||||
|
|
||||||
- **Rollen-Aliase statt Namen:** Clients fragen `hermes`/`fast` (Hirn), `coder`/`heavy` (Coder), `vision` und
|
- **Rollen-Aliase statt Namen:** Clients fragen `hermes`/`fast` (Hirn), `coder`/`heavy` (Coder), `vision` und
|
||||||
@@ -142,7 +163,8 @@ Fremde Dienste, die der Box-Wart nur steuert oder überwacht: `llama-swap` (Syst
|
|||||||
| `/srv/models/mc2-geheimnisse.json` | MC2 (Einstellungen) | Hugging-Face-Zugang, Rechte 0600 |
|
| `/srv/models/mc2-geheimnisse.json` | MC2 (Einstellungen) | Hugging-Face-Zugang, Rechte 0600 |
|
||||||
| `/srv/models/mc2-discover.json` | MC2, Radar | Cache der Hugging-Face-Entdeckung (12 h) |
|
| `/srv/models/mc2-discover.json` | MC2, Radar | Cache der Hugging-Face-Entdeckung (12 h) |
|
||||||
| `/srv/models/mc2-deploy.log` | `deploy.sh` | eine Zeile je Deploy, auch gescheiterte |
|
| `/srv/models/mc2-deploy.log` | `deploy.sh` | eine Zeile je Deploy, auch gescheiterte |
|
||||||
| `~/mc2-notify.log` | `notify.sh` (anhängen), `morgenmeldung.sh` (über 3 MB auf 2 MB kürzen) | Quelle des Update-Verlaufs; der Wortlaut „QUEUED für Morgen-Digest" muss bleiben |
|
| `~/mc2-notify.log` | `notify.sh` (anhängen), `morgenmeldung.sh` (über 3 MB auf 2 MB kürzen) | Quelle des Update-Verlaufs für Läufe vor dem 24.09.; der Wortlaut „QUEUED für Morgen-Digest" muss bleiben |
|
||||||
|
| `/srv/models/mc2-update-verlauf.jsonl` | `autoupdate.sh`, MC2 (Update-Knöpfe) | nur anhängen, eine JSON-Zeile je Baustein |
|
||||||
| `~/.hermes/night-queue.txt` | `notify.sh` (anhängen), `morgenmeldung.sh` (übernehmen, senden) | nicht gesendeter Stapel bleibt als `.senden` liegen |
|
| `~/.hermes/night-queue.txt` | `notify.sh` (anhängen), `morgenmeldung.sh` (übernehmen, senden) | nicht gesendeter Stapel bleibt als `.senden` liegen |
|
||||||
|
|
||||||
## Meldeweg
|
## Meldeweg
|
||||||
|
|||||||
@@ -25,13 +25,10 @@ Hintergrund zur Homelab-Technik: [ARCHITEKTUR.md](../ARCHITEKTUR.md), Abschnitt
|
|||||||
|
|
||||||
## Offene Einzelpunkte (Box-Wart)
|
## Offene Einzelpunkte (Box-Wart)
|
||||||
|
|
||||||
1. **Ein Modelltausch schreibt die llama-swap-Config mehrfach.** „Übernehmen" im Radar trägt das Modell unter einer
|
1. **Session-Token des Hermes-Dashboards (nur mit User-Ja).** Das Drop-in
|
||||||
Sperre ein; Hirn-Umstellung, Aliase `fast`/`heavy` und ttl folgen als eigene Schreibvorgänge. Jeder lädt
|
|
||||||
llama-swap neu und entlädt alle Modelle. Ziel: ein Schreibvorgang je Tausch.
|
|
||||||
2. **Session-Token des Hermes-Dashboards (nur mit User-Ja).** Das Drop-in
|
|
||||||
`hermes-builtin-ui.service.d/session-token.conf` aus der früheren Desktop-Anbindung ist weiter gesetzt; `backup.sh`
|
`hermes-builtin-ui.service.d/session-token.conf` aus der früheren Desktop-Anbindung ist weiter gesetzt; `backup.sh`
|
||||||
und `restore.sh` sichern es samt Kopie `desktop-gateway-token`. Entfernen ist Security-Config.
|
und `restore.sh` sichern es samt Kopie `desktop-gateway-token`. Entfernen ist Security-Config.
|
||||||
3. **Rote Wächter-Hinweise warten nachts bis 07:00.** Der Wächter meldet mit Betreff „[Box-Problem]" ohne `-d`; zwischen
|
2. **Rote Wächter-Hinweise warten nachts bis 07:00.** Der Wächter meldet mit Betreff „[Box-Problem]" ohne `-d`; zwischen
|
||||||
00:00 und 06:59 landet das in der Morgenmeldung. Klären, ob Rot sofort raus soll.
|
00:00 und 06:59 landet das in der Morgenmeldung. Klären, ob Rot sofort raus soll.
|
||||||
6. **Code verweist auf verschobene Doku:** `deploy/mc2-backup.service` (`Documentation=` auf `docs/BACKUP.md`),
|
6. **Code verweist auf verschobene Doku:** `deploy/mc2-backup.service` (`Documentation=` auf `docs/BACKUP.md`),
|
||||||
`backend/services/backup.py` (`docs/BACKUP.md`), `backend/services/maintenance.py` (`docs/BEDIENUNG.md`,
|
`backend/services/backup.py` (`docs/BACKUP.md`), `backend/services/maintenance.py` (`docs/BEDIENUNG.md`,
|
||||||
|
|||||||
Reference in New Issue
Block a user