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

+23 -6
View File
@@ -65,9 +65,9 @@ schreibt die Mute-Datei direkt.
| Endpunkt | Wirkung |
| --- | --- |
| `POST /speak` | `{text, priority?, provider?, voice?, lang?, now?}` → `202 {"id":"…"}`; bei Mute: `202 {"id":"","muted":true}`; bei Gesetz-Verwurf: `202 {"id":"","discarded":"rate_limit"\|"quiet_hours"}` (+ `"toast":true` im Toast-Modus) — verworfen, dokumentiertes Verhalten |
| `POST /speak` | `{text, priority?, provider?, voice?, lang?, now?}` → `202 {"id":"…"}` (+ `"truncated":true`, wenn auf `max_text` gekürzt); bei Mute: `202 {"id":"","muted":true}`; bei Gesetz-Verwurf: `202 {"id":"","discarded":"rate_limit"\|"quiet_hours"}` (+ `"toast":true` im Toast-Modus) — verworfen, dokumentiertes Verhalten |
| `OPTIONS /speak` | CORS-Preflight → `204` mit Freigabe-Headern, wenn die `Origin` in `server.allowed_origins` steht (siehe unten); sonst `204` ohne CORS-Header |
| `GET /status` | `{"version", "queue", "queue_ids", "current", "muted", "provider", "rate_limit": {"interval", "allow_high_burst", "next_slot"}, "quiet_hours": {"window", "active", "mode", "until"}, "mcp": true, "cors": [origins]}` |
| `GET /status` | `{"version", "queue", "queue_ids", "current", "muted", "provider", "rate_limit": {"interval", "allow_high_burst", "next_slot"}, "quiet_hours": {"window", "active", "mode", "until"}, "limits": {"max_text", "max_age"}, "mcp": true, "cors": [origins]}` |
| `POST /cancel` | bricht die laufende Ansage ab + leert die Queue → `200 {"cancelled":n}` |
| `POST /mute` / `POST /unmute` | persistenter Mute-State → `200 {"muted":bool}` |
| `POST /shutdown` | fährt den Daemon herunter (`talk daemon stop`) → `200 {"ok":true}` |
@@ -117,21 +117,38 @@ sind Opt-in wie Mute). In der Ruhezeit verfällt Audio, auch für high
Notification-Anbindung).
- `now:true` **bricht auch die Ruhezeit** (bewusster Ausbruch).
**Rangordnung:** manuelles Mute gewinnt immer > `now` > Ruhezeit >
Rate-Limit. Konfiguration (config.toml, Env schlägt Datei):
**Länge und Alter:** Zwei weitere Gesetze gegen das, was Prompts gern tun
— die Langantwort vorlesen und Ansagen stapeln:
- `max_text` (Default **400 Zeichen**, etwa zwei bis drei Sätze): Längere
Texte werden beim Eintritt an einer Wortgrenze **gekürzt** und so
gesprochen; die Antwort meldet `"truncated": true`, die CLI warnt auf
stderr, das MCP-Ergebnis sagt es im Text. Der Nutzer hört den Anfang
statt zehn Minuten Vortrag, das API-Budget bleibt begrenzt.
- `max_age` (Default **2 Minuten**): Was länger in der Queue parkt (Rate-
Limit, Stau), ist keine Nachricht mehr und wird beim Fälligwerden
**verworfen** (`discarded: "expired"` im Protokoll; die 202-Antwort beim
Eintritt bleibt eine Zusage, denn beim Eintritt ist nichts veraltet).
`now:true` wartet nie lange und ist ausgenommen.
**Rangordnung:** manuelles Mute gewinnt immer > `now` > veraltet (`max_age`)
> Ruhezeit > Rate-Limit; `max_text` greift beim Eintritt auf alles.
Konfiguration (config.toml, Env schlägt Datei):
```toml
[queue]
rate_limit = "30s" # Dauer; "0s" = unbegrenzt (TALK_RATE_LIMIT)
allow_high_burst = true # high darf verstoßen (TALK_ALLOW_HIGH_BURST)
max_text = 400 # Zeichen; 0 = unbegrenzt (TALK_MAX_TEXT)
max_age = "2m" # Wartezeit; "0s" = unbegrenzt (TALK_MAX_AGE)
quiet_hours = "18:00-08:00" # leer = aus; Env TALK_QUIET_HOURS ("off" = aus)
quiet_mode = "discard" # discard | toast (TALK_QUIET_MODE)
```
`talk status` zeigt den Gesetz-State: `rate-limit: 30s (high-burst
erlaubt), nächster slot …` und `ruhezeit: 18:00-08:00 (modus discard,
aktiv bis 08:00)`.
erlaubt), nächster slot …`, `ruhezeit: 18:00-08:00 (modus discard,
aktiv bis 08:00)` und `limits: max. 400 Zeichen, max. Wartezeit 2m0s`.
### CORS-Tür + MCP-Endpoint (Task 06)