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
+23
-3
@@ -65,7 +65,8 @@ type Item struct {
|
||||
// high) — sie unterbricht aber nie die laufende Ansage.
|
||||
Now bool
|
||||
|
||||
seq uint64 // Enqueue-Reihenfolge: FIFO-Tiebrecher innerhalb eines Levels
|
||||
seq uint64 // Enqueue-Reihenfolge: FIFO-Tiebrecher innerhalb eines Levels
|
||||
enqueued time.Time // Eintrittszeit (Basis für Laws.MaxAge)
|
||||
}
|
||||
|
||||
// rank liefert die Abarbeitungs-Klasse: now über high über normal über low.
|
||||
@@ -160,7 +161,12 @@ func (q *Queue) Enqueue(item Item) (Admission, error) {
|
||||
if item.Priority == "" {
|
||||
item.Priority = PriorityNormal
|
||||
}
|
||||
switch q.laws.decide(item, time.Now(), q.limit.Allow()) {
|
||||
var truncated bool
|
||||
if item.Text, truncated = truncateText(item.Text, q.laws.MaxText); truncated {
|
||||
defer q.logf("queue: ansage (%q …) auf %d Zeichen gekürzt (max_text)", firstRunes(item.Text, 40), q.laws.MaxText)
|
||||
}
|
||||
item.enqueued = time.Now()
|
||||
switch q.laws.decide(item, item.enqueued, q.limit.Allow()) {
|
||||
case verdictDiscardQuiet:
|
||||
q.mu.Unlock()
|
||||
q.logf("queue: ansage (%q, priorität %s) verworfen — ruhezeit %s aktiv (modus discard)",
|
||||
@@ -185,7 +191,16 @@ func (q *Queue) Enqueue(item Item) (Admission, error) {
|
||||
q.items = append(q.items, item)
|
||||
q.mu.Unlock()
|
||||
q.notEmpty.Signal()
|
||||
return Admission{ID: item.ID}, nil
|
||||
return Admission{ID: item.ID, Truncated: truncated}, nil
|
||||
}
|
||||
|
||||
// firstRunes liefert die ersten n Runen von s (Protokoll-Vorschau).
|
||||
func firstRunes(s string, n int) string {
|
||||
r := []rune(s)
|
||||
if len(r) <= n {
|
||||
return s
|
||||
}
|
||||
return string(r[:n])
|
||||
}
|
||||
|
||||
// Pending liefert eine Momentaufnahme der wartenden Items in
|
||||
@@ -272,6 +287,8 @@ func (q *Queue) LawState() LawState {
|
||||
QuietActive: q.laws.Quiet.Active(now),
|
||||
QuietMode: q.laws.QuietMode,
|
||||
QuietUntil: q.laws.Quiet.Until(now),
|
||||
MaxText: q.laws.MaxText,
|
||||
MaxAge: q.laws.MaxAge,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,6 +365,9 @@ func (q *Queue) loop() {
|
||||
case verdictToast:
|
||||
q.logf("queue: ansage %s (%q) durch toast ersetzt — ruhezeit %s aktiv (modus toast)",
|
||||
item.ID, item.Text, q.laws.Quiet)
|
||||
case verdictDiscardExpired:
|
||||
q.logf("queue: ansage %s (%q) verworfen — %s gewartet, älter als max_age %s",
|
||||
item.ID, item.Text, time.Since(item.enqueued).Round(time.Second), q.laws.MaxAge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user