The digestion tick already runs the pattern detector over all recorded
events (67563ed) and writes a proposed_routines row. What was missing is
the other half of #247: a proposal that nobody is at the mic for reaches
nothing but the /routines page, so a pattern noticed at 03:00 is only
seen if he goes looking.
This wires the tick's proposals into the existing care-delivery path
rather than a second channel: sev1 nudge, loop.Gate, dispatcher, same
routing table as an accepted routine. Restraints, since a feature that
speaks unprompted is the easiest way to turn Maven into a nag:
- off unless configured — the new pattern_proposals block, absent by
default, and deploy/mavend.json ships notify: false;
- at most one announcement per tick however many patterns surfaced;
- at most one per cooldown (24h default) across all pairs;
- sev1, so quiet hours, away and snooze suppress it;
- suppressed means dropped, not queued — /routines still has it;
- once per pair for good, since proposed_routines is
UNIQUE(action, object) and the row survives dismissal.
The body is pattern.PhraseRoutine's literal Russian, not LLM-worded, so
an inferred routine cannot arrive describing something never observed.
Also raises pattern.MinEvents from 3 to 4 — the interval-quality item on
#43. Two intervals with a ±50% band is a coincidence with a mean, not a
pattern, and now that a scan of all history can announce itself the cost
of a false positive is a permanent dismissal of that pair.
This commit is contained in:
@@ -140,6 +140,12 @@ type Config struct {
|
||||
// item. See internal/morning for the evaluation engine. Empty ⇒ disabled.
|
||||
MorningRoutines []MorningRoutineConfig `json:"morning_routines,omitempty"`
|
||||
|
||||
// PatternProposals — whether a routine the digestion tick inferred on its
|
||||
// own may be announced, and how often. nil / absent ⇒ silent detection
|
||||
// only: proposals are written for /routines and never announced. See
|
||||
// PatternProposalConfig.
|
||||
PatternProposals *PatternProposalConfig `json:"pattern_proposals,omitempty"`
|
||||
|
||||
// Praxis — the ecosystem attention-state service. When configured, maven
|
||||
// calls the Praxis HTTP tools API for attention listing and item lifecycle.
|
||||
// Maven never touches Praxis's database directly (ecosystem invariant: no
|
||||
@@ -352,6 +358,40 @@ type DigestConfig struct {
|
||||
SeverityCeiling int `json:"severity_ceiling,omitempty"` // max sev batched
|
||||
}
|
||||
|
||||
// PatternProposalConfig — announcement policy for routines the digestion tick
|
||||
// inferred by itself (Vikunja #247, #43).
|
||||
//
|
||||
// Detection is always on and always silent by default: the tick writes a
|
||||
// proposed_routines row and the /routines page shows it. Notify is what turns
|
||||
// "she noticed" into "she said something", and it is OFF unless configured —
|
||||
// Maven is not a nag and not autonomous, so a behaviour that speaks without
|
||||
// being asked has to be switched on deliberately, like weather and telegram.
|
||||
//
|
||||
// When Notify is on, the announcement is still heavily restrained:
|
||||
// - at most one proposal per tick, however many were detected;
|
||||
// - at most one per Cooldown across all pairs (not per pair), so a batch of
|
||||
// freshly-detected patterns cannot turn into a queue of interruptions;
|
||||
// - through the ordinary care-class gate (quiet hours / away / snooze), at
|
||||
// sev1 — the lowest severity there is. A proposal is the least urgent
|
||||
// thing Maven can say.
|
||||
//
|
||||
// A pair is only ever announced once, because it is only ever proposed once:
|
||||
// proposed_routines is UNIQUE(action, object) and the row survives dismissal.
|
||||
type PatternProposalConfig struct {
|
||||
// Notify — announce newly inferred routines. Default false.
|
||||
Notify bool `json:"notify,omitempty"`
|
||||
|
||||
// Cooldown — minimum spacing between two proposal announcements. 0 ⇒
|
||||
// DefaultProposalCooldown (24h).
|
||||
Cooldown Duration `json:"cooldown,omitempty"`
|
||||
}
|
||||
|
||||
// AnnounceProposals reports whether inferred routines may be announced. Safe
|
||||
// on a nil receiver — an absent config block means silent detection.
|
||||
func (p *PatternProposalConfig) AnnounceProposals() bool {
|
||||
return p != nil && p.Notify
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -448,6 +488,11 @@ const (
|
||||
DefaultLLMRouter = true
|
||||
|
||||
DefaultFactEnrichmentInterval = 30 * time.Second
|
||||
|
||||
// DefaultProposalCooldown — one inferred-routine announcement per day at
|
||||
// most. A proposal is never urgent; if two patterns surface in the same
|
||||
// hour, the second one waits, and the /routines page has it either way.
|
||||
DefaultProposalCooldown = 24 * time.Hour
|
||||
)
|
||||
|
||||
// Load reads the JSON config at path and applies defaults. A missing file is
|
||||
@@ -520,6 +565,12 @@ func (c *Config) applyDefaults() {
|
||||
c.Digest.SeverityCeiling = 2
|
||||
}
|
||||
|
||||
// Absent block stays nil (⇒ silent detection). Present-but-partial gets the
|
||||
// cooldown default, so `{"notify": true}` is enough to switch it on.
|
||||
if c.PatternProposals != nil && c.PatternProposals.Cooldown <= 0 {
|
||||
c.PatternProposals.Cooldown = Duration(DefaultProposalCooldown)
|
||||
}
|
||||
|
||||
if c.Voice != nil {
|
||||
if c.Voice.RouterThreshold <= 0 {
|
||||
c.Voice.RouterThreshold = DefaultRouterThreshold
|
||||
|
||||
Reference in New Issue
Block a user