#!/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" </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