7.2 KiB
Přechod /remind skillu z YAML na SQLite
Kontext
Současný /remind skill ukládá reminder data do reminder.yaml a dedup state do .reminder_state.json. YAML má známé problémy: žádné transakce, celý soubor se načítá do paměti, dedup je hrubý (hash textu), chybí audit trail pro CRUD operace. Cílem je přejít na SQLite jako primární storage s textovým logem pro audit.
Postup
1. Schéma a inicializace databáze
Cesta k DB: workspace/db/reminders.sqlite (podle pravidel v AGENTS.md).
Vytvořit skills/remind/scripts/db.py:
- Funkce
init_db(path)— spustí CREATE TABLE/INDEX z schématu níže - Funkce
get_db(path)— vrací connection s WAL mode a foreign keys - Funkce
log_operation(operation, reminder_id, details)— append doworkspace/log/reminder.log
Schéma:
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS reminders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL CHECK(text <> ''),
enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0, 1)),
timezone TEXT NOT NULL DEFAULT 'Europe/Prague',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
deleted_at TEXT
);
CREATE TABLE IF NOT EXISTS schedule_at (
id INTEGER PRIMARY KEY AUTOINCREMENT,
reminder_id INTEGER NOT NULL REFERENCES reminders(id) ON DELETE CASCADE,
at_datetime TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS schedule_cron (
id INTEGER PRIMARY KEY AUTOINCREMENT,
reminder_id INTEGER NOT NULL REFERENCES reminders(id) ON DELETE CASCADE,
cron_expr TEXT NOT NULL CHECK(cron_expr <> '')
);
CREATE TABLE IF NOT EXISTS schedule_random (
id INTEGER PRIMARY KEY AUTOINCREMENT,
reminder_id INTEGER NOT NULL REFERENCES reminders(id) ON DELETE CASCADE,
times_per_day INTEGER NOT NULL CHECK(times_per_day >= 1),
window_start INTEGER NOT NULL CHECK(window_start >= 0 AND window_start < 1440),
window_end INTEGER NOT NULL CHECK(window_end > 0 AND window_end <= 1440),
days_filter TEXT,
from_date TEXT,
until_date TEXT,
CHECK(window_start < window_end)
);
CREATE TABLE IF NOT EXISTS reminder_fires (
id INTEGER PRIMARY KEY AUTOINCREMENT,
reminder_id INTEGER NOT NULL REFERENCES reminders(id) ON DELETE CASCADE,
schedule_id INTEGER NOT NULL,
schedule_type TEXT NOT NULL CHECK(schedule_type IN ('at', 'cron', 'random')),
fire_time TEXT NOT NULL,
delivered_at TEXT,
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'delivered', 'failed')),
error_message TEXT
);
CREATE INDEX IF NOT EXISTS idx_reminders_text ON reminders(text);
CREATE INDEX IF NOT EXISTS idx_at_datetime ON schedule_at(reminder_id, at_datetime);
CREATE INDEX IF NOT EXISTS idx_cron_expr ON schedule_cron(reminder_id, cron_expr);
CREATE INDEX IF NOT EXISTS idx_random_dates ON schedule_random(from_date, until_date);
CREATE INDEX IF NOT EXISTS idx_fire_lookup ON reminder_fires(
reminder_id, schedule_type, schedule_id, fire_time, status
);
2. Přepsat remind_edit.py
Změnit backend z YAML na SQLite, zachovat CLI rozhraní. Všechny operace v jedné SQLite transakci.
Subcommandy:
list— SQL JOIN místoyaml.safe_load. Výstup: JSON pole reminderů se scheduly.add—INSERT INTO reminders→ získatlastrowid→INSERT INTO schedule_*podle parametrů. Podporuje kombinaciat,cron,randomv jednom reminderu.remove --keyword—SELECT id FROM reminders WHERE text LIKE '%keyword%'→UPDATE deleted_at = now(soft delete). Pokud keyword matchne více, vypsat seznam a vyžádat potvrzení.edit --keyword—UPDATE reminders.textnebo přidání/odebrání schedulů. Pokud keyword matchne více, vypsat seznam a vyžádat potvrzení.enable --keyword/disable --keyword—UPDATE reminders SET enabled = 0/1.
Log formát do workspace/log/reminder.log:
2026-06-10T06:40:00 [ADD] id=42 text="..." at=2026-06-11T09:00:00
2026-06-10T06:41:00 [REMOVE] id=42 text="..."
2026-06-10T06:42:00 [EDIT] id=42 text="..."
2026-06-10T06:43:00 [ENABLE] id=42
2026-06-10T06:44:00 [DISABLE] id=42
3. Přepsat remind_send.py
Změnit z YAML+JSON state na SQLite. Cesta k DB přes argument nebo default workspace/db/reminders.sqlite.
Algoritmus:
- Query pro všechny due fires (at + cron + random) v jednom SELECT s UNION ALL.
- Pro každý fire: zkontrolovat
reminder_fires— pokud existuje řádek se stejnýmreminder_id + schedule_id + schedule_type + fire_timeastatus = 'delivered', přeskočit. - Odeslat Telegram notifikaci.
INSERT INTO reminder_fires (... status='delivered', delivered_at=now).- Při chybě odeslání:
INSERT INTO reminder_fires (... status='failed', error_message=e)— příští běh retry. - Log do
reminder.log:2026-06-10T09:00:00 [DELIVER] id=42 text="..."
Cron job — příkaz zůstává stejný (uv run skills/remind/scripts/remind_send.py), skript si sám najde DB.
Query pattern (at):
SELECT r.id, r.text, 'at' AS schedule_type, sa.id AS schedule_id, sa.at_datetime AS fire_time
FROM reminders r JOIN schedule_at sa ON sa.reminder_id = r.id
WHERE r.enabled = 1 AND r.deleted_at IS NULL
AND sa.at_datetime > datetime('now', '-60 seconds')
AND sa.at_datetime <= datetime('now')
AND NOT EXISTS (
SELECT 1 FROM reminder_fires rf
WHERE rf.reminder_id = r.id AND rf.schedule_id = sa.id
AND rf.schedule_type = 'at' AND rf.fire_time = sa.at_datetime
AND rf.status = 'delivered'
)
Query pattern (cron): Načíst všechny aktivní cron expr, v Pythonu přes croniter vypočítat poslední fire time, porovnat s now - 60s.
Query pattern (random): Načíst všechny aktivní random scheduly, v Pythonu přes compute_fire_times(date.today(), ...) vypočítat fire times, porovnat s now - 60s.
4. Migrační skript
Vytvořit scripts/migrate_yaml_to_sqlite.py:
- Načte
reminder.yaml BEGIN TRANSACTION- Pro každý reminder:
INSERT INTO reminders→ získatlastrowid - Podle polí
at/at_times/cron_exprs/randomvložit do příslušných schedule tabulek COMMIT- Přejmenovat
reminder.yaml→reminder.yaml.bak
5. Testy
tests/test_db.py— inicializace schématu, INSERT/SELECT/UPDATE/DELETE, foreign keys, constraintstests/test_remind_edit.py— add, remove, list, edit, enable/disable s:memory:databázítests/test_remind_send.py— due detection, dedup, retry failed, log output- Aktualizovat
tests/test_random_times.py— stále platný, random_times.py se nemění
6. Aktualizovat SKILL.md a dokumentaci
- Popsat nové DB storage místo YAML
- Popsat
reminder.logformát - Aktualizovat
reminder.example.yamlna SQL příklady nebo odstranit
Ověření
- Spustit
pytest skills/remind/tests/— všechny testy procházejí - Spustit migrační skript —
reminder.yamlse převede,.bakvznikne - Ručně přidat reminder přes
remind_edit.py add— ověřit v DB přessqlite3 - Spustit
remind_send.py— ověřit delivery a log - Ověřit dedup: spustit send dvakrát ve stejné minutě — druhý běh nesmí poslat duplikát
- Ověřit
remind_edit.py list— zobrazí všechny aktivní reminder - Ověřit
remind_edit.py remove --keyword— soft delete, záznam zůstane v DB sdeleted_at