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:
@@ -299,6 +299,16 @@ func (p *LLMPhraser) PhraseQuery(ctx context.Context, utterance string, notes []
|
||||
if text != "" {
|
||||
return text, nil
|
||||
}
|
||||
if raw == "" {
|
||||
// Same guard the knowledge branch above has had since it was written,
|
||||
// and this branch did not: the server answered and the model wrote
|
||||
// nothing, which returned ("", nil) — an empty answer reported as a
|
||||
// successful phrasing. The daemon's callers happen to check for the
|
||||
// empty string, so it read as a silent fallback there; the eval scored
|
||||
// it as bad phrasing rather than as the failure it is, and nothing on
|
||||
// either path logged that the model had produced no tokens.
|
||||
return SourcesFallback(strings.Join(notes, "; ")), errEmptyResponse
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
@@ -375,7 +385,13 @@ func (p *LLMPhraser) PhraseChat(ctx context.Context, utterance string, history [
|
||||
if i := strings.IndexByte(resp, '\n'); i >= 0 {
|
||||
resp = resp[:i]
|
||||
}
|
||||
return strings.TrimSpace(resp), nil
|
||||
if resp = strings.TrimSpace(resp); resp == "" {
|
||||
// The model was up and wrote nothing. Same rule as PhraseQuery: the
|
||||
// fallback keeps the turn alive and the failure stays visible, rather
|
||||
// than ("", nil) telling the caller the chat path succeeded.
|
||||
return ChatFallback(), fmt.Errorf("phrase chat: %w", errEmptyResponse)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (p *LLMPhraser) PhraseReminder(ctx context.Context, d loop.ReminderDecision) (delivery.PhrasedReminder, error) {
|
||||
@@ -414,11 +430,9 @@ func (p *LLMPhraser) PhraseReminder(ctx context.Context, d loop.ReminderDecision
|
||||
if mood == "" {
|
||||
mood = "neutral"
|
||||
}
|
||||
summary := body
|
||||
if len(summary) > 60 {
|
||||
summary = summary[:57] + "..."
|
||||
}
|
||||
return delivery.PhrasedReminder{Decision: d, Body: body, Summary: summary, Mood: mood}, nil
|
||||
return delivery.PhrasedReminder{
|
||||
Decision: d, Body: body, Summary: reminderSummary(body), Mood: mood,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *LLMPhraser) chat(ctx context.Context, userPrompt string) (string, error) {
|
||||
|
||||
Reference in New Issue
Block a user