pi-system/doku/fix-plan-orchestrator-wecker-v2026-06-02-18-19.md
Raimund Bauer aa1539c744 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
2026-06-02 18:33:02 +02:00

141 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:2115: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:0016:00:51 (~35 min) | Subagent arbeitet, KEIN Dialog offen, Orchestrator idle | nein |
| **A — offener Bestätigungs-Dialog** | 16:00:5116: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 ~3060 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 14 verifiziert den Fix.
Bis dahin gilt der Fix als „implementiert, nicht verifiziert".