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

7.6 KiB
Raw Permalink Blame History

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".