Run pattern detection from the digestion tick, not just voice (#43)
detectPattern only ever fired as a side effect of a voice fact-write, so a recurring pattern already sitting in history went unnoticed until he happened to mention it again by voice — the opposite of proactive. Split the pipeline: extraction (fact -> normalized event) stays where a fact is written, in voice.go, since it's tied to that write regardless of who's talking. Detection (events -> stable pattern -> proposed_routines row) moves into shared code (patterns.go's detectAndPropose) that both the voice path and the new tick.go:detectPatterns call. The tick runs it every cycle over every action+object pair on record (store.DistinctEventPairs, added), so a pattern gets noticed on the daemon's own schedule. Idempotence and the dismiss-must-stick requirement turned out to already be handled by the store, not something the tick needs to reinvent: proposed_routines has UNIQUE(action, object) and CreateProposedRoutine does ON CONFLICT DO NOTHING, and DismissProposedRoutine flips status in place without deleting the row. So a pair already proposed, accepted, OR dismissed is a silent no-op on every later tick — a dismissed pattern can never resurface, and re-running the scan never spams the /routines page. Kept the voice-path call (immediate spoken confirmation is a nice feature UX-wise and is now redundant-but-harmless with the tick, since both paths share the same guarded detectAndPropose). Tick-side detection only ever writes a row; it does not notify, ring, or speak, keeping Maven "not a nag, not autonomous" — the /routines page is still the only place a proposal becomes visible, and only accepting it starts producing nudges (fireAcceptedRoutines). Also fixed the stale vikunja#46 reference in proposed_routines.go — the TODO it named is what this commit does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
This commit is contained in:
@@ -35,6 +35,36 @@ func (s *Store) CreateEvent(ctx context.Context, factID int64, action, object st
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// EventPair identifies one action+object grouping in the events table — the
|
||||
// unit the pattern detector reasons about.
|
||||
type EventPair struct {
|
||||
Action string
|
||||
Object string
|
||||
}
|
||||
|
||||
// DistinctEventPairs returns every distinct action+object pair that has at
|
||||
// least one event, in no particular order. This is what lets the proactive
|
||||
// digestion tick run the pattern detector over everything accumulated so far
|
||||
// instead of only the pair touched by the utterance that just landed
|
||||
// (Vikunja #43) — the tick has no "current utterance," so it has to ask the
|
||||
// store what to look at.
|
||||
func (s *Store) DistinctEventPairs(ctx context.Context) ([]EventPair, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT DISTINCT action, object FROM events`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("distinct event pairs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []EventPair
|
||||
for rows.Next() {
|
||||
var p EventPair
|
||||
if err := rows.Scan(&p.Action, &p.Object); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// EventsFor returns all events matching action+object, ordered by ts ascending
|
||||
// (oldest first — the order the pattern detector needs for interval computation).
|
||||
func (s *Store) EventsFor(ctx context.Context, action, object string) ([]Event, error) {
|
||||
|
||||
@@ -51,9 +51,10 @@ var (
|
||||
// keep finding the pattern, and every re-propose is refused here. Maven is not
|
||||
// a nag.
|
||||
//
|
||||
// TODO(vikunja#46): the detector currently only writes here from the voice
|
||||
// path. Once digestion runs the detector on its own tick, that tick should
|
||||
// call this too, so a pattern gets noticed even with nobody at the mic.
|
||||
// Vikunja #43: this is called both from the voice fact-write path (for the
|
||||
// immediate spoken confirmation) and from the digestion tick's proactive
|
||||
// scan (cmd/mavend/tick.go's detectPatterns, via patterns.go's
|
||||
// detectAndPropose), so a pattern gets noticed even with nobody at the mic.
|
||||
func (s *Store) CreateProposedRoutine(ctx context.Context, action, object string, intervalDays float64, ts time.Time) (int64, error) {
|
||||
res, err := s.db.ExecContext(ctx,
|
||||
`INSERT INTO proposed_routines (action, object, interval_days, status, created_ts)
|
||||
|
||||
Reference in New Issue
Block a user