nudge: add digest/batching mode for care nudges

When enabled, eligible nudges (sev ≤ ceiling) are queued in memory
instead of sent immediately. Every 'window' duration (or when
'max_items' reached), the queue is flushed as a single digest
notification with concatenated bodies.

Config:
  digest:
    enabled: true           # default false
    window: 30m             # flush window (default 30m)
    max_items: 5            # flush at this count (default 5)
    severity_ceiling: 2     # max sev batched (default 2; sev3+ bypass)

Changes:
- config.go: add DigestConfig struct with defaults
- tick.go: QueuedNudge type, digest queue/flush in TickLoop,
  shouldQueue/maybeFlush/flushDigest helpers
- main.go: pass cfg.Digest to newTickLoop
- tick_test.go: 6 new tests covering queue, flush, bypass, dedup
This commit is contained in:
kami
2026-07-05 13:07:28 +04:00
parent 5afff001c3
commit 354990fed0
5 changed files with 388 additions and 18 deletions
+29
View File
@@ -115,6 +115,10 @@ type Config struct {
// fact independently; both the schedule AND the toggle activate quiet.
// nil ⇒ quiet hours only activate via the voice toggle.
QuietHours *QuietHoursConfig `json:"quiet_hours,omitempty"`
// Digest — notification batching / digest mode. nil ⇒ digest disabled
// (every nudge is sent as it fires — legacy behaviour).
Digest *DigestConfig `json:"digest,omitempty"`
}
// QuietHoursConfig — a recurring daily quiet-window. Times are local to the
@@ -185,6 +189,18 @@ type ToolConfig struct {
Destructive bool `json:"destructive,omitempty"`
}
// DigestConfig — notification batching / digest mode. When enabled, eligible
// nudges (severity ≤ SeverityCeiling) are queued in memory instead of sent
// immediately. Every Window duration (or when MaxItems reached), the queue is
// flushed as a single digest notification. nil ⇒ digest disabled (legacy
// behaviour — every nudge is sent as it fires).
type DigestConfig struct {
Enabled bool `json:"enabled,omitempty"`
Window Duration `json:"window,omitempty"` // e.g. "30m"
MaxItems int `json:"max_items,omitempty"` // flush at this count
SeverityCeiling int `json:"severity_ceiling,omitempty"` // max sev batched
}
// PhraserConfig — the LLM-backed phraser seam. The daemon spawns llama-server
// as a managed subprocess and sends chat-completion requests to phrase nudge
// and reminder messages. nil ⇒ the template-based Stub is used instead.
@@ -315,6 +331,19 @@ func (c *Config) applyDefaults() {
}
}
if c.Digest == nil {
c.Digest = &DigestConfig{Enabled: false}
}
if c.Digest.Window == 0 {
c.Digest.Window = Duration(30 * time.Minute)
}
if c.Digest.MaxItems == 0 {
c.Digest.MaxItems = 5
}
if c.Digest.SeverityCeiling == 0 {
c.Digest.SeverityCeiling = 2
}
if c.Voice != nil {
if c.Voice.RouterThreshold <= 0 {
c.Voice.RouterThreshold = DefaultRouterThreshold