#!/usr/bin/env bash
# claude-headless-retry.sh — Headless-Claude-Lauf mit exponentiellem Backoff bei temporaeren API-Fehlern (Kais TG 12705, 24.10.2026).
# Im Container als /tmp/vs-857/run-with-retry.sh. Aufruf:
#   nohup bash /tmp/vs-857/run-with-retry.sh "<label>" <brief-datei> <log> <workdir> [max_retries=5] >/dev/null 2>&1 &
# Startparameter identisch zu den bisherigen Startern (Modell, Effort, Settings mit Deny-Liste, stream-json). Deny-Liste bleibt unveraendert.
# Protokoll: <log> (START/ENDE-Zeilen + stream-json), <log>.status (ein Ereignis je Zeile), <log>.pid. Fortsetzung per --resume <session_id>.
# Umgebungsvariablen: RESUME_SID (Fortsetzung eines abgebrochenen Laufs per --resume), FRESH_CONTINUE=1 (frischer Lauf mit Fortsetzungs-Praeambel),
#   ATTACH_PID + ATTACH_START_LINE (an einen bereits laufenden claude-Prozess anhaengen: warten, auswerten, bei Bedarf wiederholen),
#   SETTINGS, MODEL, EFFORT, BASE_WAIT, MAX_WAIT, CLAUDE_BIN.
# Hinweis: Claude Code wiederholt API-Fehler intern bis zu 7x im Sekundenbereich (api_retry); dieser Wrapper ist die aeussere Schicht (Minuten) mit Resume.
set -u
LABEL=${1:?label fehlt}; BRIEF=${2:?brief-datei fehlt}; LOG=${3:?log fehlt}; WORKDIR=${4:?workdir fehlt}; MAXR=${5:-5}
SETTINGS=${SETTINGS:-/tmp/vs-857/claude-settings-857.json}
MODEL=${MODEL:-claude-fable-5-1}; EFFORT=${EFFORT:-max}
BASE_WAIT=${BASE_WAIT:-60}; MAX_WAIT=${MAX_WAIT:-900}
CLAUDE_BIN=${CLAUDE_BIN:-claude}
export PATH="/home/dev/.local/bin:$PATH"
cd "$WORKDIR" || { echo "workdir fehlt: $WORKDIR" >&2; exit 1; }
[ -f "$BRIEF" ] || { echo "brief fehlt: $BRIEF" >&2; exit 1; }
STATUS="$LOG.status"; PIDF="$LOG.pid"
ts(){ date -u +%FT%TZ; }
st(){ printf '%s %s\n' "$(ts)" "$*" >> "$STATUS"; }
PROMPT=$(cat "$BRIEF")
CONT="Der vorige Lauf wurde durch einen temporären API-Fehler unterbrochen. Prüfe zuerst den Ist-Zustand im Worktree (git status, git diff --stat, welche Punkte des Briefs bereits umgesetzt sind), wiederhole keine abgeschlossenen Schritte und setze den Auftrag aus dem ursprünglichen Brief an der Unterbrechungsstelle fort. Schließe mit dem Bericht ab, wie im Brief verlangt."
# Ergebniszeile: die Schluesselreihenfolge variiert (beginnt oft mit duration_api_ms), daher NICHT am Zeilenanfang ankern.
RES_RE='"type":"result"'
# API-Signaturen im Text einer Fehler-Ergebniszeile (is_error=true):
# "out of usage credits" (Max-Plan-Fenster) loest sich erfahrungsgemaess binnen Minuten und ist deshalb wiederholbar.
API_RESULT_RE='API Error|Overloaded|overloaded|rate.?limit|out of usage credits|usage limit|ECONNRESET|ETIMEDOUT|fetch failed|ENOTFOUND|socket hang up|timed out|upstream connect error|\b5(00|02|03|04|29)\b'
# API-Signaturen im Logende, wenn gar keine Ergebniszeile kam (Absturz). rate_limit_event ist eine Info-Zeile und zaehlt NICHT.
API_TAIL_RE='api_retry|"error":"overloaded"|API Error|Overloaded|ECONNRESET|ETIMEDOUT|fetch failed|ENOTFOUND|socket hang up'
attempt=0; sid="${RESUME_SID:-}"; wait=$BASE_WAIT
echo "$(ts) START $LABEL model=$MODEL effort=$EFFORT promptlen=${#PROMPT} retry-wrapper max_retries=$MAXR${ATTACH_PID:+ attach_pid=$ATTACH_PID}" >> "$LOG"
st "START label=$LABEL max_retries=$MAXR${ATTACH_PID:+ attach_pid=$ATTACH_PID}"
while :; do
  attempt=$((attempt+1))
  if [ $attempt -eq 1 ] && [ -n "${ATTACH_PID:-}" ]; then
    pid=$ATTACH_PID; start_line=${ATTACH_START_LINE:-0}; echo "$pid" > "$PIDF"
    st "ATTACHED attempt=1 pid=$pid start_line=$start_line resume=${sid:-none}"
    while kill -0 "$pid" 2>/dev/null; do sleep 5; done
    rc="?"
  else
    start_line=$(wc -l < "$LOG")
    if [ -n "$sid" ]; then
      "$CLAUDE_BIN" -p "$CONT" --resume "$sid" --model "$MODEL" --effort "$EFFORT" --settings "$SETTINGS" --dangerously-skip-permissions --output-format stream-json --verbose >> "$LOG" 2>&1 &
    else
      if [ $attempt -eq 1 ] && [ "${FRESH_CONTINUE:-0}" != "1" ]; then P="$PROMPT"; else P="$CONT

$PROMPT"; fi
      "$CLAUDE_BIN" -p "$P" --model "$MODEL" --effort "$EFFORT" --settings "$SETTINGS" --dangerously-skip-permissions --output-format stream-json --verbose >> "$LOG" 2>&1 &
    fi
    pid=$!; echo "$pid" > "$PIDF"
    st "RUNNING attempt=$attempt pid=$pid resume=${sid:-none}"
    wait "$pid"; rc=$?
  fi
  sleep 1
  seg=$(tail -n +"$((start_line+1))" "$LOG")
  [ -n "$sid" ] || sid=$(printf '%s\n' "$seg" | grep -m1 -o '"session_id":"[^"]*"' | cut -d'"' -f4)
  result=$(printf '%s\n' "$seg" | grep -E "$RES_RE" | tail -1)
  is_err=$(printf '%s' "$result" | grep -o '"is_error":[a-z]*' | head -1 | cut -d: -f2)
  if [ -n "$result" ] && [ "$is_err" = "false" ]; then
    echo "$(ts) ENDE $LABEL rc=$rc attempt=$attempt" >> "$LOG"; st "DONE rc=$rc attempt=$attempt session=${sid:-?}"; rm -f "$PIDF"; exit 0
  fi
  retryable=0; reason=""
  if [ -n "$result" ]; then
    reason=$(printf '%s\n' "$result" | grep -o '"result":"[^"]\{0,160\}' | head -1 | cut -c11-)
    printf '%s\n' "$reason" | grep -qE "$API_RESULT_RE" && retryable=1
    [ -n "$reason" ] || reason="result-ohne-text"
  else
    reason=$(printf '%s\n' "$seg" | tail -n 40 | grep -oE "($API_TAIL_RE)[^\"]{0,120}" | tail -1)
    [ -n "$reason" ] && retryable=1
    [ -n "$reason" ] || reason="kein-result"
  fi
  if [ $retryable -eq 1 ]; then
    if [ $attempt -ge $((MAXR+1)) ]; then
      echo "$(ts) ENDE $LABEL rc=$rc attempt=$attempt FAILED max-retries" >> "$LOG"; st "FAILED reason=max-retries attempts=$attempt last=$reason"; rm -f "$PIDF"; exit 2
    fi
    st "RETRY attempt=$((attempt+1)) wait=${wait}s rc=$rc reason=$reason"
    echo "$(ts) RETRY $LABEL attempt=$((attempt+1)) wait=${wait}s reason=$reason" >> "$LOG"
    sleep "$wait"; wait=$(( wait*2 > MAX_WAIT ? MAX_WAIT : wait*2 ))
    continue
  fi
  echo "$(ts) ENDE $LABEL rc=$rc attempt=$attempt FAILED non-retryable" >> "$LOG"; st "FAILED reason=non-retryable rc=$rc result=$reason"; rm -f "$PIDF"; exit 1
done
