PROTOCOL.md + away-channel fallthrough
PROTOCOL.md: generated from internal/voice/wire.go — documents the voice wire format (TCP, length-prefixed JSON, methods, pushes, errors) for multi-client implementors. Must stay in sync with wire.go. dispatcher: when voice sink returns ErrVoiceNoSession, skip voice and continue to remaining channels instead of aborting. sev4-present already has ntfy in the routing table (continues naturally). sev1-3 present have only voice — the loop ends with no dispatches, which matches the spec (care/ops-soft drop on no-voice). voicesink: maps voice.ErrNoSession to delivery.ErrVoiceNoSession so the dispatcher can detect it without importing the voice package.
This commit is contained in:
@@ -27,10 +27,20 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/kami/maven/internal/loop"
|
||||
"github.com/kami/maven/internal/store"
|
||||
)
|
||||
|
||||
// ErrVoiceNoSession — sentinel returned by the voice sink when no live
|
||||
// client session exists at push time. The dispatcher treats this as a
|
||||
// skip-continue: the voice channel is unavailable for this delivery, but
|
||||
// remaining channels (ntfy, telegram) should still fire. The voicesink
|
||||
// maps voice.ErrNoSession to this sentinel so the dispatcher doesn't need
|
||||
// to import the voice package.
|
||||
var ErrVoiceNoSession = errors.New("delivery: voice: no live session")
|
||||
|
||||
// Channel — one delivery transport. Drop is an explicit no-op (the routing
|
||||
// table chose to suppress, which is a decision, not a failure — "a missed
|
||||
// water nudge is noise"). a nil Sink for a wired channel is a daemon config
|
||||
|
||||
@@ -2,7 +2,9 @@ package delivery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/kami/maven/internal/loop"
|
||||
@@ -104,6 +106,10 @@ func (d *Dispatcher) DispatchNudge(ctx context.Context, pn PhrasedNudge, now tim
|
||||
continue
|
||||
}
|
||||
if err := sink.Send(ctx, s); err != nil {
|
||||
if errors.Is(err, ErrVoiceNoSession) {
|
||||
log.Printf("dispatcher: no live voice session for %s, falling through", c.Rule.Name)
|
||||
continue
|
||||
}
|
||||
return out, fmt.Errorf("send %s: %w", ch, err)
|
||||
}
|
||||
// record AFTER successful send — a failed send must not pollute the
|
||||
@@ -151,6 +157,10 @@ func (d *Dispatcher) DispatchReminder(ctx context.Context, pr PhrasedReminder, n
|
||||
continue
|
||||
}
|
||||
if err := sink.Send(ctx, s); err != nil {
|
||||
if errors.Is(err, ErrVoiceNoSession) {
|
||||
log.Printf("dispatcher: no live voice session for reminder %d, falling through", rd.Reminder.ID)
|
||||
continue
|
||||
}
|
||||
return out, fmt.Errorf("send %s: %w", ch, err)
|
||||
}
|
||||
out = append(out, Dispatch{Sendable: s})
|
||||
|
||||
@@ -94,8 +94,8 @@ func (s *Sink) Send(ctx context.Context, send delivery.Sendable) error {
|
||||
}
|
||||
if err := s.sess.PushToMostRecent(ctx, push); err != nil {
|
||||
if errors.Is(err, voice.ErrNoSession) {
|
||||
log.Printf("voicesink: no live voice session for %s — rerouting deferred, dropping", send.RuleName)
|
||||
return err
|
||||
log.Printf("voicesink: no live voice session for %s, falling through to away channels", send.RuleName)
|
||||
return delivery.ErrVoiceNoSession
|
||||
}
|
||||
return fmt.Errorf("voicesink: push: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user