Make delivery and integration failures explicit

Persist reminder presentations and retry state, atomically complete collapsed deliveries, fall back across away reaches, and block permanent failures visibly (V-715, V-678). Fail closed when enabled integrations lack credentials and keep remote arms explicitly dark (V-691). Give mavweb one sanitized, request-correlated error contract (V-689). Owner explicitly requested direct commits to master.
This commit is contained in:
2026-08-13 02:50:59 +04:00
parent da9114b623
commit 35c6ff5a71
67 changed files with 3174 additions and 477 deletions
+14 -4
View File
@@ -20,7 +20,8 @@
//
// - reminders are a SEPARATE class — two delivery paths. reminders bypass
// the restraint gate ("wake me 7" fires in quiet hours; that's the point).
// snooze still applies. voice when present, ntfy when away. fire once.
// snooze still applies. voice when present; when away, try ntfy then
// telegram as alternatives and stop after the first success. fire once.
//
// Architecture mirrors the loop's gather/pure split: the routing table is a
// PURE function of (severity, presence); the Dispatcher holds the impure Sinks
@@ -43,6 +44,12 @@ import (
// to import the voice package.
var ErrVoiceNoSession = errors.New("delivery: voice: no live session")
// ErrPermanent is the class of transport failures that waiting cannot repair:
// a revoked credential or an endpoint that refuses this sender. Dispatchers
// may still try a different reach for the same message, but the failed reach
// must not be put on an automatic retry clock until its configuration changes.
var ErrPermanent = errors.New("delivery: permanent failure")
// 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
@@ -91,8 +98,11 @@ func ChannelsFor(sev loop.Severity, presence store.Bucket) []Channel {
// ChannelsForReminder — reminders are a SEPARATE class that bypasses the gate.
// "wake me 7" fires in quiet hours; that's the point. presence still routes
// reachability: voice when present, ntfy when away. fires once — no repeat
// (repeat-til-ack is a sev4 ops-hard behavior, not a reminder behavior).
// reachability: voice when present, then an ordered ntfy→telegram alternative
// chain when away. The dispatcher stops after the first successful alternative,
// so a reminder still fires once rather than being broadcast on both channels.
// There is no repeat (repeat-til-ack is a sev4 ops-hard behavior, not a reminder
// behavior).
//
// reminders don't carry a Severity — they're user-stated future intent, not
// loop-derived insistence. the routing is presence-only: reachability without
@@ -100,7 +110,7 @@ func ChannelsFor(sev loop.Severity, presence store.Bucket) []Channel {
// per-reminder override, not a table entry.
func ChannelsForReminder(presence store.Bucket) []Channel {
if presence == store.Away {
return []Channel{ChannelNtfy}
return []Channel{ChannelNtfy, ChannelTelegram}
}
return []Channel{ChannelVoice}
}