pi-system/bin/SubConfirm

142 lines
4.9 KiB
Text
Raw Normal View History

#!/bin/bash
# SubConfirm — Proaktiver Stasis-Detektor für Subagenten
#
# Läuft als Hintergrund-Daemon und prüft alle 30 Sekunden alle tmux-Sessions.
# Wenn eine Session ihren Output seit >30s nicht verändert hat (Stasis),
# wird der vollständige Pane-Inhalt in die Alert-Datei geschrieben UND
# direkt in die Orchestrator-Session injiziert (tmux send-keys).
#
# Architektur:
# SubConfirm → /tmp/.pi-subagent-alert → arbeitsweise-guard.ts → Orchestrator
# SubConfirm → tmux send-keys → Orchestrator-Session (direkter Push)
#
# Nutzung:
# SubConfirm --orchestrator "session-name" & # Mit Push in Orchestrator-Session
# SubConfirm --interval 15 & # Kürzeres Intervall
# SubConfirm --skip "main-session" & # Session ausschließen
# pkill -f SubConfirm # Beenden
#
# Der --orchestrator und --skip Parameter sind oft gleich:
# NAME=$(tmux display-message -p '#S')
# SubConfirm --orchestrator "$NAME" --skip "$NAME" &
set -euo pipefail
INTERVAL=30
SKIP_SESSION=""
ORCHESTRATOR_SESSION=""
REPORT_COOLDOWN=90 # Sekunden zwischen wiederholten Meldungen zur selben Session
ALERT_FILE="/tmp/.pi-subagent-alert"
STATE_DIR="/tmp/.pi-subconfirm-state"
PID_FILE="/tmp/.pi-subconfirm.pid"
while [[ $# -gt 0 ]]; do
case "$1" in
--interval) INTERVAL="$2"; shift 2 ;;
--skip) SKIP_SESSION="$2"; shift 2 ;;
--orchestrator) ORCHESTRATOR_SESSION="$2"; shift 2 ;;
*) shift ;;
esac
done
mkdir -p "$STATE_DIR"
echo $$ > "$PID_FILE"
log() {
echo "[SubConfirm $(date '+%H:%M:%S')] $1" >&2
}
# Prüft ob Pi im Orchestrator-Terminal gerade auf Eingabe wartet.
# Pi zeigt am Ende des Pane-Inhalts einen Eingabe-Cursor (leere Zeile oder ">").
orchestrator_is_idle() {
[ -z "$ORCHESTRATOR_SESSION" ] && return 1
local last
last=$(tmux capture-pane -t "$ORCHESTRATOR_SESSION" -p 2>/dev/null \
| sed 's/\x1b\[[0-9;]*[mGKHF]//g' \
| grep -v '^[[:space:]]*$' \
| tail -3)
# Pi ist idle wenn die letzte sichtbare Zeile den Status-Bar zeigt
# (kein "Working..." oder Spinner sichtbar)
echo "$last" | grep -qE '(Working\.\.\.|⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏)' && return 1
return 0
}
# Injiziert eine Nachricht in die Orchestrator-Session via tmux send-keys.
inject_to_orchestrator() {
local msg="$1"
[ -z "$ORCHESTRATOR_SESSION" ] && return
orchestrator_is_idle || return
# Nachricht tippen + Enter → Pi verarbeitet es als User-Input
tmux send-keys -t "$ORCHESTRATOR_SESSION" "$msg" Enter 2>/dev/null || true
log "Injiziert in Orchestrator-Session: ${msg:0:60}..."
}
alert() {
local session="$1"
local pane_display="$2"
local short_msg="⏸️ SUBAGENT-STASIS [$session] — braucht Eingabe"
# 1. In Alert-Datei schreiben (Guard zeigt das beim nächsten Tool-Call)
cat >> "$ALERT_FILE" <<EOF
$(date '+%H:%M:%S') $short_msg
Pane-Inhalt [$session]:
$pane_display
Mögliche Reaktionen:
Bestätigen (Yes): tmux send-keys -t "$session" "" Enter
Ablehnen (No): tmux send-keys -t "$session" "Down" && tmux send-keys -t "$session" "" Enter
Ignorieren: (nächste Meldung in ${REPORT_COOLDOWN}s)
EOF
# 2. Desktop-Notification
zenity --notification --text="SubConfirm: $short_msg" 2>/dev/null &
echo -e "\a" 2>/dev/null || true
# 3. Direkt in Orchestrator-Session injizieren wenn idle
inject_to_orchestrator "$short_msg — bitte prüfen und reagieren (tmux capture-pane -t '$session' -p | tail -20)"
}
log "Gestartet (PID $$, Intervall: ${INTERVAL}s, Skip: '${SKIP_SESSION}', Orchestrator: '${ORCHESTRATOR_SESSION}')"
while true; do
sleep "$INTERVAL"
SESSIONS=$(tmux ls -F '#{session_name}' 2>/dev/null || echo "")
[ -z "$SESSIONS" ] && continue
while IFS= read -r session; do
[ -z "$session" ] && continue
[ "$session" = "$SKIP_SESSION" ] && continue
# Pane-Inhalt holen (letzte 25 Zeilen, ANSI-Escape-Codes entfernen)
CURRENT=$(tmux capture-pane -t "$session" -p 2>/dev/null \
| sed 's/\x1b\[[0-9;]*[mGKHF]//g' \
| grep -v '^[[:space:]]*$' \
| tail -25 \
| tr '\n' '§')
[ -z "$CURRENT" ] && continue
STATE_FILE="${STATE_DIR}/${session//\//_}.state"
REPORT_FILE="${STATE_DIR}/${session//\//_}.reported"
PREV=$(cat "$STATE_FILE" 2>/dev/null || echo "")
if [ "$CURRENT" = "$PREV" ]; then
NOW=$(date +%s)
LAST_REPORT=$(cat "$REPORT_FILE" 2>/dev/null || echo 0)
if [ $((NOW - LAST_REPORT)) -gt "$REPORT_COOLDOWN" ]; then
PANE_DISPLAY=$(echo "$CURRENT" | tr '§' '\n')
alert "$session" "$PANE_DISPLAY"
log "Stasis gemeldet: $session"
echo "$NOW" > "$REPORT_FILE"
fi
else
echo "$CURRENT" > "$STATE_FILE"
rm -f "$REPORT_FILE"
fi
done <<< "$SESSIONS"
done