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:
1 parent
79314dfb0f
commit
97650ad0ec
13 files changed
+430
-26
No files matched your search
+57
-8
@@ -1,8 +1,10 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Verwurfs-Gründe im HTTP-Vertrag von POST /speak
|
||||
@@ -10,6 +12,7 @@ import (
|
||||
const (
|
||||
DiscardRateLimit = "rate_limit" // priority=low über dem Fenster-Budget
|
||||
DiscardQuietHours = "quiet_hours" // Ruhezeit aktiv (discard oder toast)
|
||||
DiscardExpired = "expired" // länger gewartet als queue.max_age (Ansage wäre veraltet)
|
||||
)
|
||||
|
||||
// RateLimiter ist das Zeitfenster des Daemon-Gesetzes gegen Ansage-Spam:
|
||||
@@ -90,17 +93,31 @@ type Laws struct {
|
||||
// echte OS-Toast folgt mit der Notification-Anbindung (dokumentierte
|
||||
// Einschränkung, siehe Task-05-Report).
|
||||
QuietMode string
|
||||
|
||||
// MaxText ist die Obergrenze der Ansage-Länge in Zeichen (Runen).
|
||||
// Längere Texte werden beim Eintritt an einer Wortgrenze gekürzt und
|
||||
// als gekürzt gemeldet — ein Agent, der die Langantwort vorliest,
|
||||
// kostet so weder Minuten noch API-Budget; der Nutzer hört den Anfang.
|
||||
// 0 = unbegrenzt.
|
||||
MaxText int
|
||||
|
||||
// MaxAge ist die längste Wartezeit einer Ansage in der Queue. Was
|
||||
// länger parkt (Rate-Limit, Stau), ist veraltet und wird beim
|
||||
// Fälligwerden verworfen (DiscardExpired) statt gesprochen. 0 =
|
||||
// unbegrenzt. now:true wartet nie lange und ist davon ausgenommen.
|
||||
MaxAge time.Duration
|
||||
}
|
||||
|
||||
// verdict ist die Gesetzes-Entscheidung über ein fälliges Item.
|
||||
type verdict int
|
||||
|
||||
const (
|
||||
verdictPlay verdict = iota // abspielen
|
||||
verdictWait // parken bis zum freien Slot
|
||||
verdictDiscardRate // verwerfen: low über dem Budget
|
||||
verdictDiscardQuiet // verwerfen: Ruhezeit (discard)
|
||||
verdictToast // Toast statt Audio (Ruhezeit)
|
||||
verdictPlay verdict = iota // abspielen
|
||||
verdictWait // parken bis zum freien Slot
|
||||
verdictDiscardRate // verwerfen: low über dem Budget
|
||||
verdictDiscardQuiet // verwerfen: Ruhezeit (discard)
|
||||
verdictToast // Toast statt Audio (Ruhezeit)
|
||||
verdictDiscardExpired // verwerfen: länger gewartet als MaxAge
|
||||
)
|
||||
|
||||
// decide wendet die Gesetze auf ein fälliges Item an (rein, ohne Zustand):
|
||||
@@ -109,14 +126,19 @@ const (
|
||||
// 1. now:true bricht bewusst aus — überspringt Rate-Limit UND Ruhezeit
|
||||
// (dokumentierter Ausbruch für Dringendes; startet trotzdem ein neues
|
||||
// Rate-Fenster).
|
||||
// 2. Ruhezeit: discard verwirft, toast ersetzt — gilt auch für high,
|
||||
// 2. Veraltet: Hat das Item länger als MaxAge gewartet, wird es verworfen
|
||||
// — eine Ansage von vor fünf Minuten ist keine Nachricht mehr.
|
||||
// 3. Ruhezeit: discard verwirft, toast ersetzt — gilt auch für high,
|
||||
// denn Burst ist ein Rate-Recht, kein Ruhe-Recht.
|
||||
// 3. Rate-Limit: freier Slot spielt; high spielt mit Burst-Recht; low
|
||||
// 4. Rate-Limit: freier Slot spielt; high spielt mit Burst-Recht; low
|
||||
// wird verworfen; normal (und high ohne Recht) parken bis zum Slot.
|
||||
func (l Laws) decide(item Item, now time.Time, slotFree bool) verdict {
|
||||
if item.Now {
|
||||
return verdictPlay
|
||||
}
|
||||
if l.MaxAge > 0 && !item.enqueued.IsZero() && now.Sub(item.enqueued) > l.MaxAge {
|
||||
return verdictDiscardExpired
|
||||
}
|
||||
if l.Quiet.Active(now) {
|
||||
if l.QuietMode == QuietModeToast {
|
||||
return verdictToast
|
||||
@@ -148,6 +170,10 @@ type Admission struct {
|
||||
// Toast meldet, dass die Ansage in der Ruhezeit durch eine
|
||||
// Benachrichtigung ersetzt wurde (quiet_mode = "toast").
|
||||
Toast bool
|
||||
|
||||
// Truncated meldet, dass der Text auf MaxText gekürzt wurde (die
|
||||
// gekürzte Fassung ist aufgenommen).
|
||||
Truncated bool
|
||||
}
|
||||
|
||||
// LawState ist die /status-Sicht auf die Anti-Spam-Gesetze: konfiguriertes
|
||||
@@ -159,5 +185,28 @@ type LawState struct {
|
||||
Quiet QuietHours
|
||||
QuietActive bool
|
||||
QuietMode string
|
||||
QuietUntil time.Time // Nullzeit = Ruhezeit inaktiv
|
||||
QuietUntil time.Time // Nullzeit = Ruhezeit inaktiv
|
||||
MaxText int // 0 = unbegrenzt
|
||||
MaxAge time.Duration // 0 = unbegrenzt
|
||||
}
|
||||
|
||||
// truncateText kürzt s auf höchstens max Runen, bevorzugt an einer
|
||||
// Wortgrenze (sofern die nicht in der ersten Hälfte liegt), und meldet, ob
|
||||
// gekürzt wurde. max <= 0 = unbegrenzt.
|
||||
func truncateText(s string, max int) (string, bool) {
|
||||
if max <= 0 {
|
||||
return s, false
|
||||
}
|
||||
runes := []rune(s)
|
||||
if len(runes) <= max {
|
||||
return s, false
|
||||
}
|
||||
cut := max
|
||||
for i := max; i > max/2; i-- {
|
||||
if unicode.IsSpace(runes[i]) {
|
||||
cut = i
|
||||
break
|
||||
}
|
||||
}
|
||||
return strings.TrimRight(string(runes[:cut]), " \t\r\n,;:-–—"), true
|
||||
}
|
||||
Reference in new issue
Block a user