refactor: ein Binary — talk startet den Daemon selbst, talkd und Degraded Mode entfallen

Zwei Binaries erzeugten den halben Komplexitätsballast: Degraded Mode mit
Lock-Datei und Stale-Lock-Race, „läuft talkd?", Autostart-Konfiguration je
OS, doppelte Chain-Konstruktion in cmd/talk und cmd/talkd — und der
Degraded Mode kannte weder Rate-Limit noch Ruhezeiten.

Jetzt:
- internal/speak: der eine Sprech-Kern (Config + Sink + Kette → Ansage);
  Daemon und Direkt-Pfad benutzen ihn, es gibt keine zweite
  Zusammensetzung der Kette mehr.
- internal/daemon: Run (der bisherige talkd-Kern), Spawn (detached per
  Setsid bzw. DETACHED_PROCESS, stdout/stderr → daemon.log), PID-Datei
  daemon.pid, Shutdown-Trigger.
- `talk daemon run|start|stop`: Vordergrund (systemd/launchd), expliziter
  Hintergrund-Start, Stop per POST /shutdown — stop wartet, bis HTTP weg
  und die PID-Datei gelöscht ist (Prozess wirklich beendet).
- Autostart: say/wrap starten den Daemon beim ersten Sprechbefehl selbst
  (daemon.autostart, Default an; Env TALK_AUTOSTART). Verliert ein
  paralleler Aufruf das Port-Rennen, erkennt er das am Kind-Ende und nutzt
  den Daemon des anderen — kein Timeout-Warten.
- Direkt-Pfad statt Degraded Mode: ohne erreichbaren oder startbaren
  Daemon spricht talk direkt (Mute respektiert, kein Lock, Hinweis +
  Grund + Log-Pfad auf stderr). Steuerbefehle starten den Daemon bewusst
  nicht; `talk status` zeigt „läuft nicht" (Exit 1).
- cmd/talkd und cmd/talk/degraded.go entfernt; README-Daemon-Abschnitt
  neu.

Tests: Harness baut nur noch talk und startet `talk daemon run`;
TALK_AUTOSTART=false als Grundausstattung aller Tests (kein Test
hinterlässt Daemons), autostart_test.go schaltet es bewusst an: Autostart
beim ersten say, Idempotenz, paralleler Start in EINE Queue, expliziter
start/stop, Port fremd belegt → Direkt-Pfad ohne Timeout. Cross-Compile
aller sechs Targets mit CGO_ENABLED=0 verifiziert.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
m3ta-chironandClaude Fable 5.1 committed 2026-10-24 09:49:16 +02:00
1 parent b86632006e
commit bc26a73aeb
29 files changed
+1145 -584

No files matched your search

+8 -5
View File
@@ -19,7 +19,9 @@ import (
// runMute implementiert `talk mute`: POST /mute an den Daemon. Ist kein
// Daemon erreichbar, wird die Mute-Datei direkt geschrieben — dieselbe
// Persistenz-Quelle, die der Daemon bei jeder Ansage neu liest; der
// Zustand greift also auch offline und überlebt Daemon-Restarts.
// Zustand greift also auch offline und überlebt Daemon-Restarts. Die
// Steuer-Befehle starten den Daemon bewusst NICHT (nur Sprechen tut das):
// Stummschalten braucht keinen laufenden Prozess.
func runMute(args []string, opts *sayOptions, stdin io.Reader, stdout, stderr io.Writer) int {
return setMute(true, stdout, stderr)
}
@@ -72,9 +74,9 @@ func printMuteState(muted bool, stdout io.Writer) {
// runStatus implementiert `talk status`: plain Block aus GET /status —
// Daemon-Adresse + Version, Queue-Länge, aktuelles Item, Mute-State,
// aktiver Provider, Gesetz-State (Rate-Limit, Ruhezeit). Ohne Daemon:
// klarer Fehler (status ist eine Anfrage an den Daemon; der Degraded Mode
// hat keinen Status).
// aktiver Provider, Gesetz-State (Rate-Limit, Ruhezeit). Läuft kein Daemon,
// sagt status genau das (Exit 1) und startet ihn nicht — status soll die
// Wirklichkeit zeigen, nicht verändern.
func runStatus(args []string, opts *sayOptions, stdin io.Reader, stdout, stderr io.Writer) int {
cfg, err := config.Load(config.Options{Getenv: getenv})
if err != nil {
@@ -86,7 +88,8 @@ func runStatus(args []string, opts *sayOptions, stdin io.Reader, stdout, stderr
defer cancel()
st, err := client.Status(ctx)
if err != nil {
fmt.Fprintf(stderr, "talk: Daemon nicht erreichbar unter %s (läuft talkd? say fällt in den Degraded Mode)\n", client.BaseURL)
fmt.Fprintf(stdout, "daemon: läuft nicht (%s)\n", client.BaseURL)
fmt.Fprintln(stderr, "talk: kein Daemon — er startet beim nächsten Sprechbefehl von selbst (oder: talk daemon start)")
return exitFailure
}