From aa1539c7442e988cc7873c6de308739fbc474b24 Mon Sep 17 00:00:00 2001 From: Raimund Bauer Date: Tue, 2 Jun 2026 18:33:02 +0200 Subject: [PATCH] =?UTF-8?q?fix/extensions:=20rule-enforcer=20erzwingt=20In?= =?UTF-8?q?variante=20=E2=80=94=20Orchestrator=20nie=20idle=20w=C3=A4hrend?= =?UTF-8?q?=20Subagent=20l=C3=A4uft?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root Cause (log-belegt, 36,6-min-Lücke 15:25–16:01): watch_subagents ist ein Polling-Tool; die Überwachung lebt nur solange das LLM es neu aufruft. Eine User-Zwischenfrage riss die Schleife ab, der Orchestrator ging idle. Der passive Alert-File-Pfad (nur in before_agent_start gelesen) feuert bei idle nie → Orchestrator schlief 36 min bis der Mensch tippte. Fix: 30s-Check erkennt laufende Subagenten per Prozessbaum (lebt ein pi-Kind unter dem tmux-Pane?), nicht per Keyword. Idle + laufender Subagent → aktive Weckung via pi.sendUserMessage() (löst garantiert einen Turn aus) + ui.notify. Idle-Erkennung zeitbasiert (45s, > 30s Pollintervall), ctx-unabhängig. Verifiziert: Syntax, Modul-Load, Handler-Registrierung, Prompt-Injection, W06, Prozessbaum-Erkennung an echten Sessions. NICHT verifiziert: Live-Weckpfad (erfordert Orchestrator-Test). Plan: doku/fix-plan-orchestrator-wecker-v2026-06-02-18-19.md --- ...n-orchestrator-wecker-v2026-06-02-18-19.md | 141 +++++++++++ extensions/rule-enforcer.ts | 218 +++++++++++++----- 2 files changed, 304 insertions(+), 55 deletions(-) create mode 100644 doku/fix-plan-orchestrator-wecker-v2026-06-02-18-19.md diff --git a/doku/fix-plan-orchestrator-wecker-v2026-06-02-18-19.md b/doku/fix-plan-orchestrator-wecker-v2026-06-02-18-19.md new file mode 100644 index 0000000..75541b1 --- /dev/null +++ b/doku/fix-plan-orchestrator-wecker-v2026-06-02-18-19.md @@ -0,0 +1,141 @@ +# Fix-Plan: Orchestrator wacht nicht auf, wenn Subagent auf Bestätigung wartet + +**Version:** v2026-06-02-18-19 +**Status:** Plan — Umsetzung in derselben Session direkt im Anschluss +**Betroffene Datei:** `extensions/rule-enforcer.ts` (Repo) → deployed nach `~/.pi/agent/extensions/rule-enforcer.ts` + +--- + +## 1. Vorfall (log-belegt) + +Quelle: Orchestrator-Session-Log +`~/.pi/agent/sessions/--media-xray-NEU-Code-20260602-locosoft-recherche--/2026-06-02T14-29-08-398Z_019e88bd-...jsonl` + +``` +15:21–15:24 watch_subagents-Schleife läuft sauber (alle ~30s ein toolResult, + Orchestrator ruft watch_subagents jedes Mal neu auf). ✅ +15:24:27 USER unterbricht mit neuer Anfrage. +15:25:00 Orchestrator startet Subagent lexware-arbeitsamt, schreibt Text + "SubAgent lexware-arbeitsamt läuft:…" → TURN ENDET, Orchestrator idle. + ── 36,6 min KEIN watch_subagents, KEINE Aktivität ── +16:01:33 USER tippt → Orchestrator wacht auf. +``` + +Subagent-Log (`…lexware-arbeitsamt…jsonl`): letzter Eintrag **16:00:51 assistant OHNE +folgendes toolResult** = blockierender Bestätigungs-Dialog (Tool-Call gestellt, confirm-Dialog +offen, Tool nie ausgeführt). + +## 2. Root Cause (bewiesen) + +`watch_subagents` ist ein **Polling-Tool, das nach ~30s zurückkehrt**. Die „Dauerüberwachung" +existiert nur, solange das LLM es **immer wieder selbst neu aufruft**. + +Um 15:24:27 hat die User-Zwischenfrage die Kette unterbrochen. Der Orchestrator beantwortete +sie, kündigte um 15:25:00 den Subagenten an — und **re-armte die watch_subagents-Schleife +nicht**. Der Turn endete mit einem Text → Orchestrator idle → Schleife für immer tot, bis ein +externer Trigger (der Mensch) kam. + +Das `setInterval(30s)` der Extension lief weiter und schrieb in die Alert-Datei +`/tmp/.pi-subagent-alert`. Aber die Datei wird **nur in `before_agent_start` gelesen** +(rule-enforcer.ts:196), und `before_agent_start` feuert **nur bei Turn-Start**. Kein Turn → +Alert nie gelesen. **Der passive Datei-Mechanismus kann einen idle Orchestrator strukturell +nicht aufwecken.** + +## 3. Zwei unterschiedliche Lücken (wichtig fürs Design) + +| Lücke | Zeitraum | Zustand | Bisher erkannt? | +|---|---|---|---| +| **B — verwaiste Überwachung** | 15:25:00–16:00:51 (~35 min) | Subagent arbeitet, KEIN Dialog offen, Orchestrator idle | nein | +| **A — offener Bestätigungs-Dialog** | 16:00:51–16:01:33 (~40 s) | Subagent wartet auf „Erlauben?", Orchestrator idle | nur passiv (wirkungslos bei idle) | + +## 4. Belegte API-Grundlage (Pi 0.78.0) + +`~/.pi/.../dist/core/extensions/types.d.ts`: +- Z. 843: `pi.sendUserMessage(content, opts?)` — *„Send a user message to the agent. **Always + triggers a turn.** When the agent is streaming, use deliverAs to specify how to queue."* +- Z. 835: `pi.sendMessage(msg, {triggerTurn?, deliverAs?})` +- Z. 221: `ctx.isIdle()` — *„Whether the agent is idle (not streaming)"* (nur in Event-Handlern + via `ctx`, **nicht** auf `pi` selbst → muss aus einem Handler in eine Closure-Variable + übernommen werden) +- Z. 75: `ctx.ui.notify(message, "warning")` — sichtbare Meldung an den Menschen + +## 5. Korrektur des Designs (Benutzer-Einwand, berechtigt) + +Ursprünglich wollte ich nur den offenen Dialog (Trigger A) abfangen und „Subagent läuft, +Orchestrator idle" auf später verschieben. **Falsch.** Die korrekte Invariante ist: + +> **Solange mindestens ein Subagent läuft, darf der Orchestrator nicht idle sein.** + +„Idle Orchestrator + laufender Subagent" ist kein tolerierbarer Zustand, sondern der Bug selbst +— und genau das war heute 35 min lang der Fall, *bevor* überhaupt ein Dialog aufkam. Der +offene Dialog ist nur ein Spezialfall: ein wartender Subagent ist ein noch **lebender** +`pi`-Prozess; die Prozess-Erkennung erfasst ihn ohnehin. + +### Zuverlässige „läuft ein Subagent?"-Erkennung (empirisch verifiziert 2026-06-02 18:2x) + +`pane_current_command` ist **unbrauchbar** — meldet `bash`, obwohl Pi läuft (Pi ist Kindprozess +der `bash -c "… GlmPi …"`). Verifizierte Methode = **Prozessbaum**: + +``` +für jede tmux-Session mit pane_current_path unter /home/xray/.pi/subagents/: + pane_pid holen → hat sie ein Kind mit comm == "pi"? → Subagent läuft. +``` +Test an 2 echten Sessions: beide korrekt als „läuft" erkannt (pi-Kind 12335/13687). Fertige +Session (bash auf `read`, kein pi-Kind) → korrekt „läuft nicht". Kein Keyword-Matching. + +## 6. Fix — diese Session (invarianten-basiert) + +Der 30s-Check: +1. `laufendeSubagenten()` per Prozessbaum bestimmen. Keiner → nichts tun (idle ist erlaubt). +2. Alert-Datei schreiben (passiver Pfad — greift, wenn der Orchestrator gerade in einem Turn ist). +3. **Idle-Erkennung (ctx-unabhängig):** Aktivitäts-Zeitstempel `letzteAktivitaet` wird in + message_start/update/end, tool_execution_*, turn_* aktualisiert. Idle = seit + `IDLE_SCHWELLE_MS` (45 s) kein Event. (45 s > 30 s watch_subagents-Pollintervall ⇒ keine + Fehlweckung während aktiver Überwachung.) Zusätzlich: wenn `letzterCtx.isIdle() === false` + → definitiv beschäftigt, nicht wecken. +4. Idle **und** Subagent läuft → aktiv wecken: `pi.sendUserMessage(…)` (löst garantiert einen + Turn aus) + `letzterCtx.ui.notify(…)` für den Menschen. Dialog-Inhalt reichert die Meldung an. + +Eigenschaften: +- **Bias zum Wecken:** verpasster Wecker = teuer (heute 35 min), überflüssiger = nur kurz lästig. +- **Spam-Schutz:** höchstens 1 aktive Weckung pro `WECK_MIN_ABSTAND_MS` (60 s). +- **Selbstterminierend:** Hält der Orchestrator nach der Weckung seine watch_subagents-Schleife, + bleibt er non-idle → keine weiteren Weckungen. Erst wenn alle Subagenten fertig sind, endet + die Überwachung regulär. +- **ctx-Robustheit:** Idle wird primär zeitbasiert erkannt, nicht über ein evtl. veraltetes + `ctx`-Objekt. `isIdle()` dient nur als zusätzliches „beschäftigt"-Veto. + +### Bekannter Trade-off (ehrlich) +Hält der Orchestrator die Schleife trotz Weckung wiederholt nicht, wird er ~1×/60 s erneut +geweckt, solange der Subagent läuft. Das bläht den Kontext (je eine User-Nachricht pro Weckung). +Im Normalfall (Orchestrator nimmt die Schleife nach 1. Weckung wieder auf) bleibt es bei **einer** +Weckung. Responsivität wird höher gewichtet als Kontext-Sparsamkeit — bewusst. + +## 7. Rollback + +Vor der Änderung: Git-Commit des Ist-Stands (Repo) + Backup der deployten Datei. +``` +# Repo zurück: +git checkout HEAD -- extensions/rule-enforcer.ts +# Deploy zurück (Backup wird vom install-Schritt mit Timestamp angelegt): +cp ~/.pi/agent/extensions/rule-enforcer.ts.bak- ~/.pi/agent/extensions/rule-enforcer.ts +# Wirksam nach Pi-Neustart bzw. /reload. +``` + +## 8. Erwartetes Verhalten & Live-Test (durch Benutzer) + +**Erwartung:** Orchestrator ist idle, ein Subagent zeigt „Erlauben?". Innerhalb von ≤30 s +erscheint im Orchestrator eine Auto-Weckung (sichtbare notify + neuer Turn), der Orchestrator +ruft `watch_subagents` und reagiert auf den Dialog — **ohne** dass der Mensch tippen muss. + +**Testszenario (nur mit echtem Orchestrator+Subagent valide):** +1. Pi-Orchestrator neu starten (lädt geänderte Extension) bzw. `/reload`. +2. Subagent starten, der einen Bestätigungs-Dialog auslöst (z. B. eine Aktion, die + `confirm-deletion.ts` triggert). +3. Orchestrator **nicht** anfassen, idle lassen. +4. Beobachten: Kommt innerhalb ~30–60 s eine Auto-Weckung und reagiert der Orchestrator? + +**Ehrlich markiert:** Ob `pi.sendUserMessage()` aus einem `setInterval` heraus in der +laufenden Pi-Version genau dieses Verhalten zeigt, ist durch die Typdefinition belegt, aber +**noch nicht in einem Live-Lauf bestätigt**. Erst der Test in Schritt 1–4 verifiziert den Fix. +Bis dahin gilt der Fix als „implementiert, nicht verifiziert". diff --git a/extensions/rule-enforcer.ts b/extensions/rule-enforcer.ts index cfce5e5..27d6956 100644 --- a/extensions/rule-enforcer.ts +++ b/extensions/rule-enforcer.ts @@ -1,18 +1,22 @@ /** * Rule Enforcer Extension * - * Zwei Mechanismen: + * Mechanismen: * * 1. before_agent_start — injiziert Kern-Regeln in jeden System-Prompt-Turn * + Selbstkorrektur wenn letzter Turn W06 verletzt hat - * + Subagenten-Alerts aus Alert-File (geschrieben von SubConfirm/setInterval) + * + Subagenten-Lage aus Alert-File (passiver Pfad) * * 2. message_end — scannt Pi's Ausgabe auf W06-Deliberations-Muster, * setzt Flag für nächsten before_agent_start * - * 3. setInterval (30s) — prüft tmux-Sessions auf offene Bestätigungs-Dialoge - * und schreibt in Alert-File, damit Pi beim nächsten Turn reagiert. - * Ergänzt SubConfirm-Daemon — auch wenn SubConfirm nicht läuft. + * 3. setInterval (30s) — erzwingt die Invariante: + * "Solange ein Subagent läuft, darf der Orchestrator nicht idle sein." + * Erkennt laufende Subagenten per PROZESSBAUM (lebt ein `pi`-Kind unter dem + * tmux-Pane?), nicht per Keyword. Ist der Orchestrator idle, obwohl ein + * Subagent läuft, wird er AKTIV via pi.sendUserMessage() geweckt — das löst + * garantiert einen Turn aus. Der passive Alert-File-Pfad allein reicht nicht, + * weil before_agent_start bei einem idle Orchestrator nie feuert. * * Nur aktiv wenn PI_ORCHESTRATOR=1. */ @@ -25,6 +29,14 @@ const ORCHESTRATOR_AKTIV = !!process.env.PI_ORCHESTRATOR; const ALERT_FILE = "/tmp/.pi-subagent-alert"; const CHECK_INTERVAL_MS = 30_000; +// Idle = seit so langer Zeit kein Pi-Event. 45s > 30s watch_subagents-Pollintervall, +// damit aktive Überwachung nicht fälschlich als idle gilt. +const IDLE_SCHWELLE_MS = 45_000; +// Höchstens eine aktive Weckung in diesem Abstand. +const WECK_MIN_ABSTAND_MS = 60_000; + +const SUBAGENT_PFAD_MARKER = "/.pi/subagents/"; + // --------------------------------------------------------------------------- // Kern-Regeln — werden in jeden System-Prompt-Turn injiziert // --------------------------------------------------------------------------- @@ -36,7 +48,7 @@ const KERN_REGELN = ` **W07** Subagenten nicht aufgeben: Bei Problemen Benutzer kurz informieren, auf Anweisung warten. Nie eigenständig auf direkte Ausführung umschwenken. **W09** sleep verboten: Stattdessen watch_subagents nutzen oder Benutzer informieren. **W10** Subagent nie doppelt starten: Einmal starten, dann watch_subagents abwarten. -**WATCH** watch_subagents SOFORT nach jeder Antwort erneut aufrufen — permanent, ohne Ausnahme. +**WATCH** watch_subagents SOFORT nach jeder Antwort erneut aufrufen — permanent, ohne Ausnahme. Solange ein Subagent läuft, darfst du NIE idle werden. `.trim(); // --------------------------------------------------------------------------- @@ -78,7 +90,7 @@ function textAusNachricht(message: any): string { } // --------------------------------------------------------------------------- -// Alert-File lesen und leeren +// Alert-File (passiver Pfad) // --------------------------------------------------------------------------- function leseAlerts(): string | null { @@ -95,55 +107,90 @@ function leseAlerts(): string | null { function schreibeAlert(text: string): void { try { - const existing = fs.existsSync(ALERT_FILE) - ? fs.readFileSync(ALERT_FILE, "utf-8") - : ""; - const neuerInhalt = existing.trim() - ? existing.trim() + "\n" + text - : text; - fs.writeFileSync(ALERT_FILE, neuerInhalt); + fs.writeFileSync(ALERT_FILE, text); } catch {} } // --------------------------------------------------------------------------- -// tmux-Sessions auf Bestätigungs-Dialoge prüfen +// Laufende Subagenten per PROZESSBAUM erkennen +// +// pane_current_command ist unbrauchbar (meldet "bash", obwohl pi als Kind läuft). +// Stattdessen: für jede tmux-Session unter ~/.pi/subagents/ prüfen, ob unter dem +// Pane-Shell-PID ein lebender `pi`-Prozess hängt. // --------------------------------------------------------------------------- -function pruefeSubagentenBestaetigungen(): string | null { +interface LaufenderSubagent { + name: string; + dialog: string | null; // Pane-Auszug, falls ein Bestätigungs-Dialog erkennbar ist +} + +function laeuftPiUnter(panePid: number): boolean { try { - const raw = execSync("tmux ls -F '#{session_name}' 2>/dev/null || true", { + const kids = execSync(`pgrep -P ${panePid} 2>/dev/null || true`, { encoding: "utf-8", - timeout: 2000, + timeout: 1500, }).trim(); - if (!raw) return null; + const pids = kids.split(/\s+/).filter(Boolean); + for (const k of pids) { + const n = Number(k); + if (!Number.isInteger(n)) continue; + const comm = execSync(`ps -o comm= -p ${n} 2>/dev/null || true`, { + encoding: "utf-8", + timeout: 1000, + }).trim(); + if (comm === "pi") return true; + // pi könnte eine Ebene tiefer laufen (z.B. via node-Wrapper) + if (laeuftPiUnter(n)) return true; + } + return false; + } catch { + return false; + } +} - const sessions = raw.split("\n").map((s) => s.trim()).filter(Boolean); - const wartend: string[] = []; +function laufendeSubagenten(): LaufenderSubagent[] { + try { + const raw = execSync( + "tmux list-panes -a -F '#{session_name}::#{pane_pid}::#{pane_current_path}' 2>/dev/null || true", + { encoding: "utf-8", timeout: 2500 }, + ).trim(); + if (!raw) return []; - for (const name of sessions) { + const ergebnis: LaufenderSubagent[] = []; + const gesehen = new Set(); + + for (const line of raw.split("\n")) { + const teile = line.split("::"); + if (teile.length < 3) continue; + const name = teile[0].trim(); + const panePid = Number(teile[1].trim()); + const path = teile[2].trim(); + if (!name || gesehen.has(name)) continue; + if (!path.includes(SUBAGENT_PFAD_MARKER)) continue; + if (!Number.isInteger(panePid)) continue; + if (!laeuftPiUnter(panePid)) continue; // Pi nicht (mehr) aktiv ⇒ kein laufender Subagent + + gesehen.add(name); + + // Optional: Dialog-Inhalt zur Anreicherung der Weckmeldung + let dialog: string | null = null; try { const pane = execSync( - `tmux capture-pane -t '${name}' -p 2>/dev/null | tail -10`, + `tmux capture-pane -t '${name}' -p 2>/dev/null | tail -8`, { encoding: "utf-8", timeout: 1000 }, ).trim(); - - const wartetAufBestaetigung = - pane.includes("Erlauben?") || - pane.includes("→ Yes") || - (pane.includes("navigate") && pane.includes("select")) || - pane.includes("ACHTUNG:") || - pane.includes("Überschreiben?") || - pane.includes("Bestätigung"); - - if (wartetAufBestaetigung) { - wartend.push(` • ${name}: WARTET AUF BESTÄTIGUNG`); + if ( + /Erlauben\?|→ Yes|Überschreiben\?|ACHTUNG:|Bestätigung/.test(pane) + ) { + dialog = pane; } } catch {} - } - return wartend.length > 0 ? wartend.join("\n") : null; + ergebnis.push({ name, dialog }); + } + return ergebnis; } catch { - return null; + return []; } } @@ -155,47 +202,108 @@ export default function (pi: ExtensionAPI) { if (!ORCHESTRATOR_AKTIV) return; let letzterVerstoss: string | null = null; + let letzterCtx: any = null; + let letzteAktivitaet = Date.now(); + let letzteWeckung = 0; - // ── 30s Background-Check ───────────────────────────────────────────────── - // Prüft tmux-Sessions alle 30s auf offene Dialoge, unabhängig von Pi's - // Tool-Calls. Schreibt in Alert-File → wird in before_agent_start injiziert. + function markAktiv(ctx?: any): void { + letzteAktivitaet = Date.now(); + if (ctx) letzterCtx = ctx; + } + + // ── 30s Background-Check: Invariante erzwingen ─────────────────────────── const interval = setInterval(() => { - const wartend = pruefeSubagentenBestaetigungen(); - if (wartend) { - schreibeAlert(`[30s-Check] Subagent wartet auf Bestätigung:\n${wartend}`); - } + const subagenten = laufendeSubagenten(); + if (subagenten.length === 0) return; // kein laufender Subagent ⇒ idle ist erlaubt + + const dialoge = subagenten.filter((s) => s.dialog); + const beschreibung = subagenten + .map( + (s) => + ` • ${s.name}${s.dialog ? " — WARTET AUF BESTÄTIGUNG" : " — läuft"}`, + ) + .join("\n"); + + // Passiver Pfad: greift, falls der Orchestrator gerade in einem Turn ist + schreibeAlert( + `[30s-Check] Laufende Subagenten — Orchestrator muss überwachen:\n${beschreibung}`, + ); + + // Idle-Erkennung: primär zeitbasiert (ctx-unabhängig) + const idleMs = Date.now() - letzteAktivitaet; + if (idleMs < IDLE_SCHWELLE_MS) return; // Orchestrator aktiv ⇒ nichts tun + + // Zusätzliches Veto, falls ctx zuverlässig "beschäftigt" meldet + try { + if (letzterCtx && typeof letzterCtx.isIdle === "function") { + if (letzterCtx.isIdle() === false) return; + } + } catch {} + + const jetzt = Date.now(); + if (jetzt - letzteWeckung < WECK_MIN_ABSTAND_MS) return; + letzteWeckung = jetzt; + + const dringlich = dialoge.length > 0; + const dialogText = dialoge + .map((d) => `--- ${d.name} ---\n${d.dialog}`) + .join("\n"); + + try { + letzterCtx?.ui?.notify?.( + dringlich + ? "🚨 Subagent wartet auf Bestätigung — Orchestrator wird geweckt." + : "⏱️ Subagent läuft, Orchestrator war idle — wird geweckt.", + "warning", + ); + } catch {} + + try { + pi.sendUserMessage( + `🚨 [Auto-Weckung] Es läuft mindestens ein Subagent, aber du warst idle. ` + + `Das ist nicht erlaubt, solange Subagenten laufen.\n${beschreibung}\n` + + (dringlich + ? `\nEin Subagent wartet auf eine Bestätigung:\n${dialogText}\n→ Sofort watch_subagents aufrufen, den Dialog prüfen und nach Regel bestätigen ODER den Benutzer informieren.\n` + : ``) + + `→ Nimm die watch_subagents-Überwachung sofort wieder auf und halte sie, bis ALLE Subagenten fertig sind.`, + ); + } catch {} }, CHECK_INTERVAL_MS); pi.on("session_shutdown", async () => { clearInterval(interval); }); - // ── message_end: W06-Scan ───────────────────────────────────────────────── - // Scannt Pi's abgeschlossene Antwort auf Deliberations-Muster. - // Setzt Flag für nächsten before_agent_start-Turn. - pi.on("message_end", async (event) => { + // ── Aktivitäts-Tracking für die zeitbasierte Idle-Erkennung ────────────── + pi.on("turn_start", async (_e, ctx) => markAktiv(ctx)); + pi.on("turn_end", async (_e, ctx) => markAktiv(ctx)); + pi.on("message_start", async (_e, ctx) => markAktiv(ctx)); + pi.on("message_update", async (_e, ctx) => markAktiv(ctx)); + pi.on("tool_execution_start", async (_e, ctx) => markAktiv(ctx)); + pi.on("tool_execution_end", async (_e, ctx) => markAktiv(ctx)); + + // ── message_end: W06-Scan + Aktivität ──────────────────────────────────── + pi.on("message_end", async (event, ctx) => { + markAktiv(ctx); const text = textAusNachricht(event.message); if (hatDeliberation(text)) { letzterVerstoss = "W06 (Internes Deliberieren)"; } }); - // ── before_agent_start: System-Prompt-Injection ─────────────────────────── - // Injiziert Kern-Regeln + Selbstkorrektur + Subagenten-Alerts - // in den System-Prompt jedes Turns. - pi.on("before_agent_start", async (event) => { + // ── before_agent_start: System-Prompt-Injection ────────────────────────── + pi.on("before_agent_start", async (event, ctx) => { + markAktiv(ctx); let zusatz = `\n\n---\n${KERN_REGELN}`; - // Selbstkorrektur wenn letzter Turn W06 verletzt hat if (letzterVerstoss) { zusatz += `\n\n⚠️ REGELVERSTOSS erkannt (${letzterVerstoss}): Diese Antwort OHNE Gedankenkommentar. Nur Ergebnis oder Entscheidung.`; letzterVerstoss = null; } - // Subagenten-Alerts const alerts = leseAlerts(); if (alerts) { - zusatz += `\n\n🚨 SUBAGENT WARTET AUF BESTÄTIGUNG:\n${alerts}\n→ Sofort watch_subagents aufrufen oder Benutzer informieren.`; + zusatz += `\n\n🚨 SUBAGENT-LAGE:\n${alerts}\n→ watch_subagents aufrufen und die Überwachung halten, bis alle Subagenten fertig sind.`; } return { systemPrompt: event.systemPrompt + zusatz };