Coach-Daemon (SILV-448) — Runbook
Stand 04.09.2026 · Strato. Der Coach-Daemon beantwortet die Knopf-Anfragen der App
(POST /api/coach/ask, Vertrag coach-app-vertrag) über Dennis'
Claude-Max-Abo mit dem Python Agent SDK — keine API-Tokens (Dennis, hart). Form und
Begründung: coach-service-abo-form.
Was läuft wo
| Teil | Ort | Zweck |
|---|---|---|
app/coach_daemon.py |
Host, Venv app/.venv-coach (claude-agent-sdk 0.2.152 + gebündelte CLI 2.1.259, httpx, starlette/uvicorn, pyyaml) |
Daemon: /ask, /health, /reload |
app/coach-daemon.sh |
Host | Einzelinstanz (flock), Log app/data/coach-daemon.log |
Crontab * * * * * coach-daemon.sh |
Host | Guard: startet neu, falls tot (Re-Run = No-Op); Neustart ≤ 60 s nach Absturz (verifiziert) |
~/running-coaching |
Host (Checkout, lesend) | Skills (docs/coach-skills/, später .claude/skills/coach/) + knowledge/ für den Prompt |
~/.claude/.credentials.json |
Host | Abo-Login (OAuth); Refresh-Token läuft bis 03.10.2026 |
app/data/coach-cwd/ |
Host | leeres Arbeitsverzeichnis der CLI (kein CLAUDE.md, keine Settings) |
Netz: der Daemon lauscht nur auf der Docker-Bridge 172.17.0.1:9310 (VPS öffentlich,
ufw aus → nie 0.0.0.0). Container erreicht ihn als host.docker.internal:9310 (extra_hosts:
host-gateway, compose), Host-Worker als http://172.17.0.1:9310. Jeder Aufruf braucht
X-Internal-Token.
Ablauf eines Knopfs
- Backend
coach_api._dispatchpushtPOST /askmit{request_id, user_id, button, effort, model, answer_max_lines, writes, photo_mode, context, callback}(Timeout 3 s) → Daemon antwortet sofort202und arbeitet asynchron. - Usage-Gate:
GET api.anthropic.com/api/oauth/usage(Bearer aus der Credentials-Datei, Cache 5 min; der Endpoint ist selbst rate-limitiert → bei 429 15 min Pause, letzter Wert bleibt).five_hour.utilization ≥ 90 %(COACH_USAGE_GATE) →meta+coach_error code=quotamit „frei wieder ab HH:MM“ (Berlin). Kein Ausweichen auf ein kleineres Modell. - Warmen Client holen (Pool je Effort:
low=1, medium=1, high=0,COACH_POOL; Effort ist ein CLI-Prozess-Flag → ein Pool je Stufe; Clients > 10 min werden erneuert). Kein warmer → frisch starten (+~1,5 s).metamitharness ok|cold,usage_pct,model,effort,warm,expected_ttft_s(17/30/40 je low/medium/high — Benchmark). - Prompt (Spec §7): system_prompt = kurzer Harness-Kopf +
_basis.md+ Kern (kommunikation.md,memory/*ohne Index,ziele.md„Fünf Regeln“ inkl. Ampel,ernaehrung.mdBudgetlogik/Zielwerte/Logging/Supplemente,athlet.mdkurz,datenquellen.md„Zwei Budgets“) +mahlzeiten.md+ Wellness-Regeln (plan-2026-08.mdAmpel) + Ausgabeformat §3.4 — stabil je Repo-Stand (~10k Tokens, Cache-Prefix). User-Prompt = Skill (ohne Frontmatter; ohne Skill-Datei ein generischer Stub) + Stand-Kurz (stand.md: Kopfzeile, Anker-Tabelle, Physiologie-Tabelle, Waage/Ruhepuls/KFA, Protein-Ziel) + die Kontextblöcke aus dem Push als JSON + Eingabe. - SDK:
tools=[],max_turns=1,setting_sources=None,permission_mode=dontAsk, Auto-Memory aus,no-session-persistence,include_partial_messages=True, Modellclaude-fable-5-1(COACH_MODEL).photo_mode=raw→ Bild ausapp/data/images/<basename>als base64-Block (sonst nur die Foto-Schätzung als Text). - Streaming: das Modell liefert JSON; der Daemon zieht nur den Inhalt von
answer_mdinkrementell heraus (dekodiert) und schickt ihn gebündelt (~120 ms) alscoach_token. Am Ende JSON parsen →coach_donemitmeta{model, effort, context_tokens, cached_tokens, output_tokens, latency_ms, ttft_ms, usage_pct, repo}. Ein Client je Anfrage, danach weg. - Fehlerbilder →
coach_error:quota(Gate oder Rate-Limitrejected),refusal(stop_reason=refusal),login(401/Auth — dazu einmal je Stunde Push an Dennis „Abo-Login kaputt“, Heartbeatpool_ready=0→/connectioncold),unparseable(kein Coach-JSON),upstream(sonstiges),harness_down(Fallback-Worker erreicht den Daemon nicht).
Fallback: kommt der Push nicht an, legt das Backend den ai_jobs-Job coach_ask an;
ai_jobs.job_coach_ask holt GET /api/coach/internal/{rid}/request (Push-Body neu gebaut)
und reicht ihn dem Daemon; ist der immer noch weg → sofort coach_error harness_down (statt
150 s Stream-Timeout). Kein zweiter KI-Weg.
Heartbeat alle 30 s → POST /api/coach/internal/heartbeat {pool_ready, usage_pct, model,
version}; /connection.harness = ok (Pool warm) · cold (Pool leer/Login kaputt) · down (kein
Heartbeat seit 90 s). Katalog: beim Start und bei Repo-Änderung PUT /api/coach/buttons
(Backend-Defaults aus coach_app.DEFAULT_BUTTONS, überschrieben durch Skill-Frontmatter;
version = <Datum>.<git-short>). Der Heartbeat-Loop erkennt einen neuen HEAD (nach git
pull) und lädt Prompt-Kit + Pool neu (POST /reload erzwingt das).
Bedienung
curl -s http://172.17.0.1:9310/health | python3 -m json.tool # Pool, Usage, Login, Zähler
tail -f app/data/coach-daemon.log # je Anfrage: ttft/total/ctx/cached
cd ~/running-coaching && git pull # neue Skills/Knowledge → Reload automatisch (≤ 30 s)
pkill -f '^\.venv-coach/bin/python coach_daemon' # Neustart (Cron-Guard holt ihn ≤ 60 s)
Login erneuern (Push „Coach: Abo-Login kaputt“ oder login_ok=false): auf dem VPS als dennis
claude starten und /login — Dennis macht den Browser-Schritt selbst. Danach Daemon neu starten.
Abo-Fenster teilen: der Coach zieht aus demselben 5-h-Fenster wie die Claude-Code-Sessions
auf diesem Konto (StratoClaude/MacClaude). Ein langer Bau-Tag kann das Fenster leeren; dann sagt
der Coach ab 90 % ehrlich ab (quota, Reset-Zeit im Text). Das ist gewollt (Dennis: kein
Ausweichen), aber beim Planen großer Sessions mitdenken.
Grenzen / offen
ask_backläuft als neuerask(neuer Client, Prefix aus dem Cache) — nicht „im selben SDK-Client“ wie in der Spec skizziert; der Vertrag 447 hat das so festgelegt.- Nur
hungerhat einen Skill im Repo; die 17 anderen Knöpfe laufen mit dem generischen Stub, bis der Coach (running-coaching) die Skills anlegt. Katalog-Frontmatter gewinnt dann automatisch. - Kern liegt bei ~10k statt 7,5k Tokens (Mahlzeiten + Ausgabeformat immer im Prefix) — Trim ist Backlog 44 im Coach-Repo.
- Latenz: Fable denkt immer (nicht abschaltbar) → erste Zeile nach ~17 s (low), 22 s (medium), 40 s (high); das 3-s-Ziel der Spec ist damit nicht erreichbar. Zahlen + Empfehlung: coach-benchmark-fable-effort.