fix/extensions: rule-enforcer erzwingt Invariante — Orchestrator nie idle während Subagent läuft
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
This commit is contained in:
parent
88bc53c307
commit
aa1539c744
2 changed files with 304 additions and 55 deletions
141
doku/fix-plan-orchestrator-wecker-v2026-06-02-18-19.md
Normal file
141
doku/fix-plan-orchestrator-wecker-v2026-06-02-18-19.md
Normal file
|
|
@ -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-<ts> ~/.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".
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
/**
|
/**
|
||||||
* Rule Enforcer Extension
|
* Rule Enforcer Extension
|
||||||
*
|
*
|
||||||
* Zwei Mechanismen:
|
* Mechanismen:
|
||||||
*
|
*
|
||||||
* 1. before_agent_start — injiziert Kern-Regeln in jeden System-Prompt-Turn
|
* 1. before_agent_start — injiziert Kern-Regeln in jeden System-Prompt-Turn
|
||||||
* + Selbstkorrektur wenn letzter Turn W06 verletzt hat
|
* + 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,
|
* 2. message_end — scannt Pi's Ausgabe auf W06-Deliberations-Muster,
|
||||||
* setzt Flag für nächsten before_agent_start
|
* setzt Flag für nächsten before_agent_start
|
||||||
*
|
*
|
||||||
* 3. setInterval (30s) — prüft tmux-Sessions auf offene Bestätigungs-Dialoge
|
* 3. setInterval (30s) — erzwingt die Invariante:
|
||||||
* und schreibt in Alert-File, damit Pi beim nächsten Turn reagiert.
|
* "Solange ein Subagent läuft, darf der Orchestrator nicht idle sein."
|
||||||
* Ergänzt SubConfirm-Daemon — auch wenn SubConfirm nicht läuft.
|
* 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.
|
* 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 ALERT_FILE = "/tmp/.pi-subagent-alert";
|
||||||
const CHECK_INTERVAL_MS = 30_000;
|
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
|
// 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.
|
**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.
|
**W09** sleep verboten: Stattdessen watch_subagents nutzen oder Benutzer informieren.
|
||||||
**W10** Subagent nie doppelt starten: Einmal starten, dann watch_subagents abwarten.
|
**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();
|
`.trim();
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -78,7 +90,7 @@ function textAusNachricht(message: any): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Alert-File lesen und leeren
|
// Alert-File (passiver Pfad)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function leseAlerts(): string | null {
|
function leseAlerts(): string | null {
|
||||||
|
|
@ -95,55 +107,90 @@ function leseAlerts(): string | null {
|
||||||
|
|
||||||
function schreibeAlert(text: string): void {
|
function schreibeAlert(text: string): void {
|
||||||
try {
|
try {
|
||||||
const existing = fs.existsSync(ALERT_FILE)
|
fs.writeFileSync(ALERT_FILE, text);
|
||||||
? fs.readFileSync(ALERT_FILE, "utf-8")
|
|
||||||
: "";
|
|
||||||
const neuerInhalt = existing.trim()
|
|
||||||
? existing.trim() + "\n" + text
|
|
||||||
: text;
|
|
||||||
fs.writeFileSync(ALERT_FILE, neuerInhalt);
|
|
||||||
} catch {}
|
} 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 {
|
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",
|
encoding: "utf-8",
|
||||||
timeout: 2000,
|
timeout: 1500,
|
||||||
}).trim();
|
}).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);
|
function laufendeSubagenten(): LaufenderSubagent[] {
|
||||||
const wartend: string[] = [];
|
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<string>();
|
||||||
|
|
||||||
|
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 {
|
try {
|
||||||
const pane = execSync(
|
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 },
|
{ encoding: "utf-8", timeout: 1000 },
|
||||||
).trim();
|
).trim();
|
||||||
|
if (
|
||||||
const wartetAufBestaetigung =
|
/Erlauben\?|→ Yes|Überschreiben\?|ACHTUNG:|Bestätigung/.test(pane)
|
||||||
pane.includes("Erlauben?") ||
|
) {
|
||||||
pane.includes("→ Yes") ||
|
dialog = pane;
|
||||||
(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`);
|
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
|
||||||
|
|
||||||
return wartend.length > 0 ? wartend.join("\n") : null;
|
ergebnis.push({ name, dialog });
|
||||||
|
}
|
||||||
|
return ergebnis;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,47 +202,108 @@ export default function (pi: ExtensionAPI) {
|
||||||
if (!ORCHESTRATOR_AKTIV) return;
|
if (!ORCHESTRATOR_AKTIV) return;
|
||||||
|
|
||||||
let letzterVerstoss: string | null = null;
|
let letzterVerstoss: string | null = null;
|
||||||
|
let letzterCtx: any = null;
|
||||||
|
let letzteAktivitaet = Date.now();
|
||||||
|
let letzteWeckung = 0;
|
||||||
|
|
||||||
// ── 30s Background-Check ─────────────────────────────────────────────────
|
function markAktiv(ctx?: any): void {
|
||||||
// Prüft tmux-Sessions alle 30s auf offene Dialoge, unabhängig von Pi's
|
letzteAktivitaet = Date.now();
|
||||||
// Tool-Calls. Schreibt in Alert-File → wird in before_agent_start injiziert.
|
if (ctx) letzterCtx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 30s Background-Check: Invariante erzwingen ───────────────────────────
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
const wartend = pruefeSubagentenBestaetigungen();
|
const subagenten = laufendeSubagenten();
|
||||||
if (wartend) {
|
if (subagenten.length === 0) return; // kein laufender Subagent ⇒ idle ist erlaubt
|
||||||
schreibeAlert(`[30s-Check] Subagent wartet auf Bestätigung:\n${wartend}`);
|
|
||||||
}
|
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);
|
}, CHECK_INTERVAL_MS);
|
||||||
|
|
||||||
pi.on("session_shutdown", async () => {
|
pi.on("session_shutdown", async () => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── message_end: W06-Scan ─────────────────────────────────────────────────
|
// ── Aktivitäts-Tracking für die zeitbasierte Idle-Erkennung ──────────────
|
||||||
// Scannt Pi's abgeschlossene Antwort auf Deliberations-Muster.
|
pi.on("turn_start", async (_e, ctx) => markAktiv(ctx));
|
||||||
// Setzt Flag für nächsten before_agent_start-Turn.
|
pi.on("turn_end", async (_e, ctx) => markAktiv(ctx));
|
||||||
pi.on("message_end", async (event) => {
|
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);
|
const text = textAusNachricht(event.message);
|
||||||
if (hatDeliberation(text)) {
|
if (hatDeliberation(text)) {
|
||||||
letzterVerstoss = "W06 (Internes Deliberieren)";
|
letzterVerstoss = "W06 (Internes Deliberieren)";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── before_agent_start: System-Prompt-Injection ───────────────────────────
|
// ── before_agent_start: System-Prompt-Injection ──────────────────────────
|
||||||
// Injiziert Kern-Regeln + Selbstkorrektur + Subagenten-Alerts
|
pi.on("before_agent_start", async (event, ctx) => {
|
||||||
// in den System-Prompt jedes Turns.
|
markAktiv(ctx);
|
||||||
pi.on("before_agent_start", async (event) => {
|
|
||||||
let zusatz = `\n\n---\n${KERN_REGELN}`;
|
let zusatz = `\n\n---\n${KERN_REGELN}`;
|
||||||
|
|
||||||
// Selbstkorrektur wenn letzter Turn W06 verletzt hat
|
|
||||||
if (letzterVerstoss) {
|
if (letzterVerstoss) {
|
||||||
zusatz += `\n\n⚠️ REGELVERSTOSS erkannt (${letzterVerstoss}): Diese Antwort OHNE Gedankenkommentar. Nur Ergebnis oder Entscheidung.`;
|
zusatz += `\n\n⚠️ REGELVERSTOSS erkannt (${letzterVerstoss}): Diese Antwort OHNE Gedankenkommentar. Nur Ergebnis oder Entscheidung.`;
|
||||||
letzterVerstoss = null;
|
letzterVerstoss = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subagenten-Alerts
|
|
||||||
const alerts = leseAlerts();
|
const alerts = leseAlerts();
|
||||||
if (alerts) {
|
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 };
|
return { systemPrompt: event.systemPrompt + zusatz };
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue