feat: IPC additions for reminders/events/routines, ack tracking, tool management

- Extend IPC wire protocol: add ListReminders, ListEvents, ListProposedRoutines,
  DismissProposedRoutine, AcceptProposedRoutine IPC methods with request/response
  types. Update wire.go with new message kinds.
- Add ack_sends table (migration #5): tracks sev4 telegram repeat-til-ack
  delivery state with rule name + timestamp, indexed for dedup.
- Add Store.DeleteTool: permanently removes a tool row (for dismissing proposed
  tools), idempotent on missing tool.
- Update tick.go: wire new IPC handlers into daemon tick.
This commit is contained in:
kami
2026-07-10 15:49:10 +04:00
parent 4c40a183cf
commit 25357bf267
9 changed files with 375 additions and 2 deletions
+71
View File
@@ -0,0 +1,71 @@
package store
import (
"context"
"fmt"
"time"
)
// WasAcked returns true when the rule has no pending (un-acked) telegram
// nudges. A resolved nudge (acted/snoozed/ignored) means the user has seen
// and dealt with it — the alarm is considered acked.
func (s *Store) WasAcked(ctx context.Context, key string) (bool, error) {
var n int
err := s.db.QueryRowContext(ctx,
`SELECT COUNT(*) FROM nudges
WHERE rule = ? AND channel = 'telegram' AND outcome = 'pending'`, key).Scan(&n)
if err != nil {
return false, fmt.Errorf("was acked %s: %w", key, err)
}
return n == 0, nil
}
// MarkSent records that a sev4 telegram nudge was sent (or re-sent) for the
// given rule at the given time. Used by the repeat-til-ack loop to clock the
// repeat interval.
func (s *Store) MarkSent(ctx context.Context, key string, ts time.Time) error {
_, err := s.db.ExecContext(ctx,
`INSERT INTO ack_sends (rule, sent_at) VALUES (?, ?)`,
key, ts.UnixMilli())
if err != nil {
return fmt.Errorf("mark sent %s: %w", key, err)
}
return nil
}
// LastSent returns the most recent send timestamp for the given rule's
// telegram nudge. Returns zero time if nothing has been sent yet (the initial
// send goes through RecordNudge, not MarkSent, so the first MarkSent comes on
// the repeat path — LastSent may legitimately be zero until then).
func (s *Store) LastSent(ctx context.Context, key string) (time.Time, error) {
var millis int64
err := s.db.QueryRowContext(ctx,
`SELECT MAX(sent_at) FROM ack_sends WHERE rule = ?`, key).Scan(&millis)
if err != nil {
return time.Time{}, fmt.Errorf("last sent %s: %w", key, err)
}
if millis == 0 {
return time.Time{}, nil
}
return time.UnixMilli(millis).UTC(), nil
}
// MarkAcked marks ALL pending telegram nudges for the rule as "acted" —
// stopping the repeat-til-ack loop. Called when the user acknowledges the
// alarm (voice acknowledgment, Telegram callback, etc.).
func (s *Store) MarkAcked(ctx context.Context, key string) error {
now := time.Now()
res, err := s.db.ExecContext(ctx,
`UPDATE nudges SET outcome = 'acted', outcome_ts = ?
WHERE rule = ? AND channel = 'telegram' AND outcome = 'pending'`,
now.UnixMilli(), key)
if err != nil {
return fmt.Errorf("mark acked %s: %w", key, err)
}
n, _ := res.RowsAffected()
if n == 0 {
// no pending nudges — already acked or never sent; not an error.
return nil
}
return nil
}
+24
View File
@@ -26,6 +26,30 @@ ALTER TABLE reminders ADD COLUMN next_fire_ts INTEGER;`, // #2
meta TEXT NOT NULL DEFAULT '{}',
created_ts INTEGER NOT NULL
);`, // #3 — long-term vector memory (persistent backend for internal/memory)
`CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
fact_id INTEGER NOT NULL REFERENCES facts(id),
action TEXT NOT NULL,
object TEXT NOT NULL,
ts INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_events_action_object ON events (action, object, ts DESC);
CREATE TABLE IF NOT EXISTS proposed_routines (
id INTEGER PRIMARY KEY AUTOINCREMENT,
action TEXT NOT NULL,
object TEXT NOT NULL,
interval_days REAL NOT NULL,
status TEXT NOT NULL DEFAULT 'proposed' CHECK (status IN ('proposed','accepted','dismissed')),
created_ts INTEGER NOT NULL,
reminder_id INTEGER REFERENCES reminders(id),
UNIQUE(action, object)
);`, // #4 — event extraction + pattern inference
`CREATE TABLE IF NOT EXISTS ack_sends (
rule TEXT NOT NULL,
sent_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_ack_sends_rule ON ack_sends (rule, sent_at DESC);`, // #5 — sev4 telegram repeat-til-ack tracking
}
// migrate applies every migration with a number greater than the DB's current
+9
View File
@@ -88,6 +88,15 @@ func (s *Store) EnableTool(ctx context.Context, name string, cmd []string, destr
return nil
}
// DeleteTool permanently removes a tool row. Used for "dismiss" on proposed
// tools — there's no dismissed status, the proposal is simply gone and maven
// can re-propose it later if the same gap is encountered. Idempotent: deleting
// a tool that doesn't exist is a no-op.
func (s *Store) DeleteTool(ctx context.Context, name string) error {
_, err := s.db.ExecContext(ctx, `DELETE FROM tools WHERE name = ?`, name)
return err
}
// DisableTool sets a tool's status from 'enabled' back to 'proposed'. This is
// the "disable" act on the authed surface — the tool stays in the store (its
// provenance preserved) but won't run until re-enabled. Idempotent: disabling