feat(queue): Gesetze max_text und max_age — gekürzte Langtexte, verfallene Stau-Ansagen

Zwei Löcher im Anti-Spam-Gesetz, die ein Prompt trivial nutzt: Ein Agent
kann 64 KB Text einreichen (ElevenLabs stellt die Rechnung, die Ansage
dauert Minuten), und geparkte normal-Ansagen spielen unbegrenzt spät —
„Build fertig" zweieinhalb Minuten nach dem Build ist keine Nachricht.

- queue.max_text (Default 400 Zeichen, Env TALK_MAX_TEXT): längere Texte
  werden beim Eintritt an einer Wortgrenze gekürzt und so gesprochen; die
  /speak-Antwort meldet truncated, die CLI warnt auf stderr, das MCP-
  Ergebnis sagt es im Text. Runen, nicht Bytes.
- queue.max_age (Default 2m, Env TALK_MAX_AGE): Items, die beim
  Fälligwerden länger gewartet haben, werden verworfen (discarded
  "expired", Protokoll nennt Wartezeit und Grenze). now:true ist
  ausgenommen. Rang: now > veraltet > Ruhezeit > Rate-Limit.
- /status trägt "limits": {max_text, max_age}; `talk status` zeigt sie.
- Negative Werte sind Config-Fehler; 0 = unbegrenzt.

Tests: truncateText-Tabelle (Wortgrenze, Satzzeichen, Runen), Enqueue-
Kürzung, decide-Vorrang inkl. now-Ausnahme, geparktes Item verfällt;
Config-Schichten; Prozessgrenze: Provider bekommt die gekürzte Fassung,
dritte von drei Stau-Ansagen wird nie gesprochen.

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:54:40 +02:00
1 parent 79314dfb0f
commit 97650ad0ec
13 files changed
+430 -26

No files matched your search

+55
View File
@@ -731,3 +731,58 @@ func TestLoadDaemonAutostart(t *testing.T) {
t.Error("TALK_AUTOSTART=vielleicht: kein Fehler, want klarer Fehler")
}
}
// TestLoadLimits deckt queue.max_text / queue.max_age ab: Defaults 400 / 2m,
// TOML und Env (Env schlägt Datei), 0 = unbegrenzt, Unsinn und negative
// Werte sind klare Fehler.
func TestLoadLimits(t *testing.T) {
fehlendeDatei := filepath.Join(t.TempDir(), "gibt-es-nicht.toml")
writeTOML := func(t *testing.T, content string) string {
path := filepath.Join(t.TempDir(), "config.toml")
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
return path
}
cfg, err := Load(Options{File: fehlendeDatei, Getenv: env(nil)})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Queue.MaxText != DefaultMaxText || cfg.Queue.MaxAge != DefaultMaxAge {
t.Errorf("Defaults = %d/%v, want %d/%v", cfg.Queue.MaxText, cfg.Queue.MaxAge, DefaultMaxText, DefaultMaxAge)
}
cfg, err = Load(Options{File: writeTOML(t, "[queue]\nmax_text = 0\nmax_age = \"45s\"\n"), Getenv: env(nil)})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Queue.MaxText != 0 || cfg.Queue.MaxAge != 45*time.Second {
t.Errorf("TOML = %d/%v, want 0/45s", cfg.Queue.MaxText, cfg.Queue.MaxAge)
}
cfg, err = Load(Options{
File: writeTOML(t, "[queue]\nmax_text = 100\n"),
Getenv: env(map[string]string{"TALK_MAX_TEXT": "250", "TALK_MAX_AGE": "0s"}),
})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Queue.MaxText != 250 || cfg.Queue.MaxAge != 0 {
t.Errorf("Env = %d/%v, want 250/0 (Env schlägt Datei)", cfg.Queue.MaxText, cfg.Queue.MaxAge)
}
for name, kv := range map[string]map[string]string{
"TALK_MAX_TEXT=abc": {"TALK_MAX_TEXT": "abc"},
"TALK_MAX_TEXT=-1": {"TALK_MAX_TEXT": "-1"},
"TALK_MAX_AGE=bald": {"TALK_MAX_AGE": "bald"},
"TALK_MAX_AGE=-1s": {"TALK_MAX_AGE": "-1s"},
} {
if _, err := Load(Options{File: fehlendeDatei, Getenv: env(kv)}); err == nil {
t.Errorf("%s: kein Fehler, want klarer Fehler", name)
}
}
if _, err := Load(Options{File: writeTOML(t, "[queue]\nmax_text = -5\n"), Getenv: env(nil)}); err == nil {
t.Error("max_text = -5 in TOML: kein Fehler, want klarer Fehler")
}
}