phraser: the reminder summary is cut in runes, and silence is an error (V-620)
Three defects in internal/phraser, all of the shape "reports done when
nothing happened".
The reminder summary was cut in bytes: `len(s) > 60` and `s[:57]`, in two
copies (Stub.PhraseReminder and LLMPhraser.PhraseReminder). On Russian a
letter is two bytes, so the cut fell at about 28 letters instead of 60 and
landed inside a letter about half the time. Sendable.Summary is what
voicesink hands to piper and what the telegram sink posts, so the half rune
was spoken and sent. One rune-counting helper now, shared by both. The test
that covered this was ASCII, which is what let the arithmetic stand.
The evidence branch of PhraseQuery and the bare-prose tail of PhraseChat
both returned ("", nil) when the server answered and the model wrote no
tokens. The knowledge branch has guarded that with errEmptyResponse since it
was written; these two did not. The daemon's callers check for the empty
string and paper over it, so the visible cost was the eval, which scored a
silent model as bad phrasing rather than as a failure, and a log line that
never appeared.
Stub.PhraseReminder set no Mood. Its sibling PhraseNudge sets "neutral" and
says in a comment why: the Stub is a production fallback and owes the output
contract a value. The zero value is not one of the five moods.
No prompt and no spoken wording changed, so the phrasing eval is unmoved.
This commit is contained in:
@@ -115,11 +115,32 @@ func (s *Stub) PhraseReminder(_ context.Context, d loop.ReminderDecision) (deliv
|
||||
if text == "" {
|
||||
text = "reminder"
|
||||
}
|
||||
summary := text
|
||||
if len(summary) > 60 {
|
||||
summary = summary[:57] + "..."
|
||||
// Mood, for the same reason PhraseNudge sets it: the Stub is a production
|
||||
// fallback, so it owes the output contract a value. This one was left at the
|
||||
// zero value, which is not one of the five moods.
|
||||
return delivery.PhrasedReminder{
|
||||
Decision: d, Body: text, Summary: reminderSummary(text), Mood: "neutral",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// summaryLimit — how much of a reminder goes to the away channels.
|
||||
const summaryLimit = 60
|
||||
|
||||
// reminderSummary shortens a reminder body to the away-channel summary.
|
||||
//
|
||||
// Counted in runes. Both copies of this counted bytes — `len(s) > 60` and
|
||||
// `s[:57]` — and on a Russian reminder that is wrong twice. A Cyrillic letter is
|
||||
// two bytes, so the cut fell at about 28 letters rather than 60; and byte 57
|
||||
// lands inside a letter about half the time, so the summary ended in half a
|
||||
// rune. That is not cosmetic: Sendable.Summary is the text voicesink hands to
|
||||
// piper and the text the telegram sink posts, so the broken byte was spoken and
|
||||
// sent.
|
||||
func reminderSummary(body string) string {
|
||||
r := []rune(body)
|
||||
if len(r) <= summaryLimit {
|
||||
return body
|
||||
}
|
||||
return delivery.PhrasedReminder{Decision: d, Body: text, Summary: summary}, nil
|
||||
return string(r[:summaryLimit-3]) + "..."
|
||||
}
|
||||
|
||||
// phraseNudge — the per-rule templates. each reads the context the predicate
|
||||
|
||||
Reference in New Issue
Block a user