Never send a nudge body off-box when the summary is empty (#368)
Away channels (ntfy, telegram) leave the box, so an empty Summary now sends a fixed generic line plus the rule name instead of the full Body. The dispatcher strips detail before any sink sees it, so a sink added later cannot leak by reading the wrong field. Voice is local and unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
This commit is contained in:
@@ -160,6 +160,7 @@ func (d *Dispatcher) DispatchNudge(ctx context.Context, pn PhrasedNudge, now tim
|
|||||||
RepeatUntilAck: ch == ChannelTelegram && c.Severity >= loop.Sev4,
|
RepeatUntilAck: ch == ChannelTelegram && c.Severity >= loop.Sev4,
|
||||||
Ts: now,
|
Ts: now,
|
||||||
}
|
}
|
||||||
|
s = minimalForAway(s)
|
||||||
sink := d.sinkFor(ch)
|
sink := d.sinkFor(ch)
|
||||||
if sink == nil {
|
if sink == nil {
|
||||||
continue
|
continue
|
||||||
@@ -225,6 +226,7 @@ func (d *Dispatcher) DispatchReminder(ctx context.Context, pr PhrasedReminder, n
|
|||||||
Summary: pr.Summary,
|
Summary: pr.Summary,
|
||||||
Ts: now,
|
Ts: now,
|
||||||
}
|
}
|
||||||
|
s = minimalForAway(s)
|
||||||
sink := d.sinkFor(ch)
|
sink := d.sinkFor(ch)
|
||||||
if sink == nil {
|
if sink == nil {
|
||||||
continue
|
continue
|
||||||
@@ -315,6 +317,7 @@ func (d *Dispatcher) RepeatUnacked(ctx context.Context, keys []string, now time.
|
|||||||
RepeatUntilAck: true,
|
RepeatUntilAck: true,
|
||||||
Ts: now,
|
Ts: now,
|
||||||
}
|
}
|
||||||
|
s = minimalForAway(s)
|
||||||
attemptID := d.beginOutbox(ctx, "nudge", key, 0, ChannelTelegram, messageForChannel(s), now)
|
attemptID := d.beginOutbox(ctx, "nudge", key, 0, ChannelTelegram, messageForChannel(s), now)
|
||||||
if err := d.cfg.Telegram.Send(ctx, s); err != nil {
|
if err := d.cfg.Telegram.Send(ctx, s); err != nil {
|
||||||
d.completeOutbox(ctx, attemptID, store.DeliveryFailed, now)
|
d.completeOutbox(ctx, attemptID, store.DeliveryFailed, now)
|
||||||
@@ -342,20 +345,44 @@ func (d *Dispatcher) sinkFor(ch Channel) Sink {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenericAwayMessage — what an away channel gets when the phraser gave us no
|
||||||
|
// summary. no gendered forms, so it stays right whoever reads it.
|
||||||
|
const GenericAwayMessage = "что-то требует внимания"
|
||||||
|
|
||||||
|
// isAway — this channel leaves the box, so it only ever gets a minimal body.
|
||||||
|
func isAway(ch Channel) bool {
|
||||||
|
return ch == ChannelNtfy || ch == ChannelTelegram
|
||||||
|
}
|
||||||
|
|
||||||
// messageForChannel — away channels get the minimal summary (no shoulder-surf
|
// messageForChannel — away channels get the minimal summary (no shoulder-surf
|
||||||
// exfil — "disk low on homesrv," not detail); voice gets the full body (local).
|
// exfil — "disk low on homesrv," not detail); voice gets the full body (local).
|
||||||
// a missing summary falls back to body — a terse full message is better than
|
// an empty summary must NOT fall back to the body: the resident model is small
|
||||||
// no message, and the phraser should have produced a summary for away-bound
|
// and drops fields often, and the away path crosses the "never phones home"
|
||||||
// severities. this is the "minimal body" rule from the spec, enforced at the
|
// boundary. so we send a fixed generic line plus the rule name instead. voice
|
||||||
// last mile so a phraser bug can't accidentally exfil via the relay.
|
// is local, so it keeps the full body.
|
||||||
func messageForChannel(s Sendable) string {
|
func messageForChannel(s Sendable) string {
|
||||||
switch s.Channel {
|
if !isAway(s.Channel) {
|
||||||
case ChannelNtfy, ChannelTelegram:
|
|
||||||
if s.Summary != "" {
|
|
||||||
return s.Summary
|
|
||||||
}
|
|
||||||
return s.Body
|
|
||||||
default:
|
|
||||||
return s.Body
|
return s.Body
|
||||||
}
|
}
|
||||||
|
if s.Summary != "" {
|
||||||
|
return s.Summary
|
||||||
|
}
|
||||||
|
if s.RuleName != "" {
|
||||||
|
return GenericAwayMessage + ": " + s.RuleName
|
||||||
|
}
|
||||||
|
return GenericAwayMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// minimalForAway — strips detail from a Sendable bound for an away channel
|
||||||
|
// before any sink sees it. the sinks pick Summary themselves too, but this is
|
||||||
|
// where the boundary actually is: a sink added later must not be able to leak
|
||||||
|
// the full body just by reading the wrong field.
|
||||||
|
func minimalForAway(s Sendable) Sendable {
|
||||||
|
if !isAway(s.Channel) {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
msg := messageForChannel(s)
|
||||||
|
s.Body = msg
|
||||||
|
s.Summary = msg
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user