Phase 2 Drei-Welten-Plan: No-Progress-Bremse + Betrieb-Playbook + mem0-Lern-Buendelung

- no-progress-bremse.py: generische, turn-uebergreifende Schleifenbremse (pre/post_tool_call);
  Muster-Hash statt hartkodierter Fehler-Strings, Selbst-Lernen via neue-signaturen.jsonl.
  Schliesst die Luecke der nativen guardrails (pro Turn, nur klassifizierte Fehler).
- ensure-profile-hooks.py: kann Eintraege in BESTEHENDE Event-Bloecke einfuegen
  (zweiter pre_tool_call-Key haette den Tabu-Guard lautlos verschluckt) + registriert Bremse.
- betrieb-playbook-Skill: SSH-/Deploy-Diagnose-Checklisten (Passphrase-Falle zuerst),
  Triage-Regel im Steckbrief-Hook; Skill wird in alle Profile gesynct.
- mc2-memory: Lern-Extraktion gebuendelt (4 Turns oder 180 s idle = EIN Lauf) statt pro Turn -
  Extraktion konkurrierte mit Lucys Hirn um die Slots.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-19 14:21:18 +02:00
parent 331aafa25d
commit 903f3bc074
6 changed files with 290 additions and 24 deletions
+38 -5
View File
@@ -49,6 +49,12 @@ MIN_USER_LEN = int(os.environ.get("MC2_MEMORY_MIN_USER_LEN", "25")) # zu Kurzes
# wandern und Tage später ungefragt wiederverwendet werden.
SKIP_UNTRUSTED = os.environ.get("MC2_MEMORY_SKIP_UNTRUSTED", "1") not in ("0", "false", "False", "no")
UNTRUSTED_MARKERS = ("[Bildschirm-Sicht",)
# Lern-Buendelung (Phase 2, 19.07.2026): Die LLM-Extraktion lief bisher nach JEDEM Turn und
# konkurrierte dabei mit Lucys Hirn um die Slots (gleiches Modell, --parallel 2). Jetzt werden
# Turns gesammelt und als EIN Gespraechs-Block extrahiert: nach BATCH_TURNS Turns oder wenn
# BATCH_IDLE_S Sekunden Ruhe war. Ein Batch = ein Extraktionslauf statt N.
BATCH_TURNS = max(1, int(os.environ.get("MC2_MEMORY_BATCH_TURNS", "4")))
BATCH_IDLE_S = float(os.environ.get("MC2_MEMORY_BATCH_IDLE", "180"))
# Triviale Turns, die keinen dauerhaften Fakt tragen (reine Begrüßung/Quittung, Aufwärm-/Test-
# Kommandos) → gar nicht erst zum Auto-Lernen schicken. Spart Extraktions-Läufe und verhindert
@@ -77,6 +83,10 @@ class MC2MemoryProvider(MemoryProvider):
self._worker: Optional[threading.Thread] = None
self._stop = threading.Event()
self._recall_cache: Dict[str, str] = {}
# Turn-Puffer der Lern-Buendelung (siehe BATCH_TURNS/BATCH_IDLE_S).
self._buf: List[List[Dict[str, str]]] = []
self._buf_lock = threading.Lock()
self._buf_last_add = 0.0
# -- Identität / Verfügbarkeit -------------------------------------------
def name(self) -> str:
@@ -164,13 +174,29 @@ class MC2MemoryProvider(MemoryProvider):
a = (a + f"\n[genutzte Tools: {', '.join(tools[:6])}]").strip()
if a:
msgs.append({"role": "assistant", "content": a[:4000]})
try:
self._q.put_nowait(msgs)
except queue.Full:
log.debug("mc2-memory learn queue full — Turn übersprungen")
with self._buf_lock:
self._buf.append(msgs)
self._buf_last_add = time.time()
full = len(self._buf) >= BATCH_TURNS
if full:
self._drain_buffer()
# Neue Fakten können gelandet sein → Recall-Cache invalidieren.
self._recall_cache.clear()
def _drain_buffer(self) -> None:
"""Gepufferte Turns als EINEN Gespraechs-Block in die Lern-Queue geben."""
with self._buf_lock:
if not self._buf:
return
merged: List[Dict[str, str]] = []
for turn in self._buf:
merged.extend(turn)
self._buf.clear()
try:
self._q.put_nowait(merged)
except queue.Full:
log.debug("mc2-memory learn queue full — Batch übersprungen")
def _post_learn(self, msgs: list) -> None:
try:
httpx.post(f"{MC_URL}/api/memory/learn",
@@ -182,8 +208,14 @@ class MC2MemoryProvider(MemoryProvider):
def _run(self) -> None:
while not self._stop.is_set():
try:
msgs = self._q.get(timeout=1.0)
msgs = self._q.get(timeout=5.0)
except queue.Empty:
# Idle-Flush: Ruhe im Gespraech → angesammelte Turns jetzt lernen.
with self._buf_lock:
idle = (self._buf and
time.time() - self._buf_last_add >= BATCH_IDLE_S)
if idle:
self._drain_buffer()
continue
try:
self._post_learn(msgs)
@@ -193,6 +225,7 @@ class MC2MemoryProvider(MemoryProvider):
def _flush(self) -> None:
"""Offene Turns garantiert rausschreiben — bei Session-Ende/CLI-Exit, wo der
Prozess sofort beendet wird (daemon-Worker würde sonst mitten im POST sterben)."""
self._drain_buffer()
self._stop.set()
if self._worker and self._worker.is_alive():
self._worker.join(timeout=95.0) # laufenden Worker-POST zu Ende lassen