pi-system/doku/fix-plan-orchestrator-wecker-v2026-06-02-18-19.md

142 lines
7.6 KiB
Markdown
Raw Normal View 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".