Zalohovani vsech podstatnych souboru
This commit is contained in:
693
results/2026-06-02_remind-skill-analysis-and-improvements.md
Normal file
693
results/2026-06-02_remind-skill-analysis-and-improvements.md
Normal file
@@ -0,0 +1,693 @@
|
||||
# /remind Skill — Codebase Analysis & Improvement Report
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
The /remind skill consists of three scripts (`remind_edit.py`, `remind_send.py`, `random_times.py`) plus tests. The `random_times.py` module is well-structured and tested. The two main scripts (`remind_edit.py`, `remind_send.py`) suffer from:
|
||||
|
||||
- Manual YAML string construction instead of proper serialization
|
||||
- No tests at all
|
||||
- Missing core features (list, edit, deduplication, dry-run)
|
||||
- Race conditions and data-loss risks
|
||||
- One-time reminders firing repeatedly within the same minute
|
||||
|
||||
This report identifies 20+ concrete improvements with code examples.
|
||||
|
||||
---
|
||||
|
||||
## 2. Critical Issues
|
||||
|
||||
### 2.1 One-time `at` reminders fire repeatedly (BUG)
|
||||
|
||||
`remind_send.py` uses a 60-second window:
|
||||
|
||||
```python
|
||||
def should_fire(candidate: datetime, now: datetime) -> bool:
|
||||
return abs((now - candidate).total_seconds()) < 60
|
||||
```
|
||||
|
||||
With a 1-minute cron, an `at: "2026-06-02T09:20:00"` reminder fires at 09:20:00 **and** 09:20:01..09:20:59 if the cron job happens to run multiple times or with slight delay. The log shows this:
|
||||
|
||||
```
|
||||
2026-06-02T09:20:01 cedule proti kouření ve výtahu
|
||||
```
|
||||
|
||||
Only one line, but if the cron ran twice in the same minute, it would duplicate.
|
||||
|
||||
**Fix:** Track fired one-time reminders in a state file, or narrow the window to `<= 30` and ensure the cron runs at :00.
|
||||
|
||||
```python
|
||||
# Better: stateful deduplication for one-time reminders
|
||||
FIRED_STATE_PATH = Path(__file__).parent.parent.parent / "db" / "remind_fired.sqlite"
|
||||
|
||||
# Or simpler: narrow window + minute-level dedup via log check
|
||||
```
|
||||
|
||||
### 2.2 Non-atomic YAML writes = data loss risk
|
||||
|
||||
`remind_edit.py` writes directly to `reminder.yaml`:
|
||||
|
||||
```python
|
||||
with open(REMINDER_FILE, "w") as f:
|
||||
yaml.dump(data, f)
|
||||
```
|
||||
|
||||
If the process crashes mid-write, the file is truncated/corrupted.
|
||||
|
||||
**Fix:** Atomic write via temp file + rename:
|
||||
|
||||
```python
|
||||
import os
|
||||
|
||||
def atomic_write(path: Path, data: dict, yaml: YAML) -> None:
|
||||
tmp = path.with_suffix(".tmp")
|
||||
with open(tmp, "w") as f:
|
||||
yaml.dump(data, f)
|
||||
os.replace(tmp, path)
|
||||
```
|
||||
|
||||
### 2.3 Concurrent edit + send = race condition
|
||||
|
||||
`remind_send.py` reads `reminder.yaml` every minute. `remind_edit.py` writes to it. No file locking means the reader could get a partially-written file.
|
||||
|
||||
**Fix:** Use `filelock` (already available via uv) or atomic writes (above) + read retry.
|
||||
|
||||
### 2.4 `remind_edit.py` has no `list` command (advertised but missing)
|
||||
|
||||
`SKILL.md` documents `list` and `remove` commands, but `remind_edit.py` only implements `add` and `remove`. There is no `list`.
|
||||
|
||||
**Fix:** Add `list` to `main()`:
|
||||
|
||||
```python
|
||||
elif command == "list":
|
||||
for i, r in enumerate(data.get("reminders", []), 1):
|
||||
print(f"{i}. {r.get('text', '(no text)')}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Code Quality — Shorten & Improve
|
||||
|
||||
### 3.1 Remove custom `LiteralScalarString` (redundant)
|
||||
|
||||
`remind_edit.py` defines:
|
||||
|
||||
```python
|
||||
class LiteralScalarString(str):
|
||||
__slots__ = ()
|
||||
```
|
||||
|
||||
ruamel.yaml already provides `ruamel.yaml.scalarstring.LiteralScalarString`. The custom class is unnecessary and confusing.
|
||||
|
||||
**Fix:**
|
||||
|
||||
```python
|
||||
from ruamel.yaml.scalarstring import LiteralScalarString
|
||||
```
|
||||
|
||||
### 3.2 `format_reminder` manually builds YAML (fragile)
|
||||
|
||||
Current code concatenates strings to produce YAML:
|
||||
|
||||
```python
|
||||
def format_reminder(text, schedule):
|
||||
lines = [f"- text: {text}"]
|
||||
for key, value in schedule.items():
|
||||
if isinstance(value, list):
|
||||
lines.append(f" {key}:")
|
||||
for item in value:
|
||||
lines.append(f" - {item}")
|
||||
else:
|
||||
lines.append(f" {key}: {value}")
|
||||
return "\n".join(lines)
|
||||
```
|
||||
|
||||
This breaks on special characters (quotes, colons, newlines in text), doesn't handle indentation consistently, and duplicates YAML serialization logic.
|
||||
|
||||
**Fix:** Build a dict and let ruamel.yaml serialize it:
|
||||
|
||||
```python
|
||||
def build_reminder(text: str, schedule: dict) -> dict:
|
||||
reminder = {"text": LiteralScalarString(text)}
|
||||
for key, value in schedule.items():
|
||||
if key in ("at", "at_times", "cron_exprs") and isinstance(value, list):
|
||||
reminder[key] = [LiteralScalarString(v) for v in value]
|
||||
elif key in ("at", "window") and isinstance(value, str):
|
||||
reminder[key] = LiteralScalarString(value)
|
||||
else:
|
||||
reminder[key] = value
|
||||
return reminder
|
||||
```
|
||||
|
||||
Then append to `data["reminders"]` and dump the whole document.
|
||||
|
||||
### 3.3 `parse_schedule` is a long if-elif chain
|
||||
|
||||
```python
|
||||
def parse_schedule(args):
|
||||
if not args:
|
||||
return {"cron_exprs": ["0 9 * * *"]}
|
||||
elif args[0] == "at":
|
||||
...
|
||||
elif args[0] == "times":
|
||||
...
|
||||
elif args[0] == "cron":
|
||||
...
|
||||
else:
|
||||
...
|
||||
```
|
||||
|
||||
**Fix:** Dispatch table:
|
||||
|
||||
```python
|
||||
SCHEDULE_PARSERS = {
|
||||
"at": lambda args: {"at": args[1]},
|
||||
"times": lambda args: {"at_times": args[1:]},
|
||||
"cron": lambda args: {"cron_exprs": args[1:]},
|
||||
}
|
||||
|
||||
def parse_schedule(args: list[str]) -> dict:
|
||||
if not args:
|
||||
return {"cron_exprs": ["0 9 * * *"]}
|
||||
parser = SCHEDULE_PARSERS.get(args[0])
|
||||
if parser:
|
||||
return parser(args)
|
||||
# fallback: treat all args as cron expressions
|
||||
return {"cron_exprs": args}
|
||||
```
|
||||
|
||||
### 3.4 `remove_reminder` dual-match logic is confusing
|
||||
|
||||
```python
|
||||
def remove_reminder(data, text):
|
||||
reminders = data.get("reminders", [])
|
||||
for i, reminder in enumerate(reminders):
|
||||
if reminder.get("text") == text:
|
||||
del reminders[i]
|
||||
return True
|
||||
for i, reminder in enumerate(reminders):
|
||||
if text.lower() in reminder.get("text", "").lower():
|
||||
del reminders[i]
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
This silently falls back to substring match, which could delete the wrong reminder.
|
||||
|
||||
**Fix:** Be explicit. Support exact match and `--grep` flag:
|
||||
|
||||
```python
|
||||
def remove_reminder(data: dict, text: str, grep: bool = False) -> bool:
|
||||
reminders = data.get("reminders", [])
|
||||
for i, reminder in enumerate(reminders):
|
||||
reminder_text = reminder.get("text", "")
|
||||
if (not grep and reminder_text == text) or (grep and text.lower() in reminder_text.lower()):
|
||||
del reminders[i]
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
### 3.5 `main()` in `remind_edit.py` is a big if-elif
|
||||
|
||||
**Fix:** Same dispatch pattern:
|
||||
|
||||
```python
|
||||
COMMANDS = {
|
||||
"add": cmd_add,
|
||||
"remove": cmd_remove,
|
||||
"list": cmd_list,
|
||||
}
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
if not args:
|
||||
print("Usage: ...")
|
||||
sys.exit(1)
|
||||
cmd = COMMANDS.get(args[0])
|
||||
if not cmd:
|
||||
print(f"Unknown command: {args[0]}")
|
||||
sys.exit(1)
|
||||
cmd(args[1:])
|
||||
```
|
||||
|
||||
### 3.6 `remind_send.py` `should_fire` window too wide
|
||||
|
||||
With 1-minute cron granularity, a 60-second window allows double-firing if there's any jitter. Use 30 seconds:
|
||||
|
||||
```python
|
||||
def should_fire(candidate: datetime, now: datetime, window_sec: int = 30) -> bool:
|
||||
delta = (now - candidate).total_seconds()
|
||||
return 0 <= delta < window_sec
|
||||
```
|
||||
|
||||
This also ensures we only fire **after** the scheduled time, not before (which `abs()` allowed).
|
||||
|
||||
### 3.7 `remind_send.py` catches bare `Exception`
|
||||
|
||||
```python
|
||||
except Exception as e:
|
||||
print(f"Error sending reminder: {e}", file=sys.stderr)
|
||||
```
|
||||
|
||||
**Fix:** Catch specific exceptions (`telegram.error.TelegramError`, `NetworkError`).
|
||||
|
||||
### 3.8 `sys.path.insert` hacks in both scripts
|
||||
|
||||
Both scripts do:
|
||||
|
||||
```python
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
```
|
||||
|
||||
This is a code smell. Since these are run via `uv run`, they should either:
|
||||
- Be part of a proper Python package with `__init__.py`
|
||||
- Or use `PYTHONPATH` in the cron job
|
||||
- Or import via relative imports if refactored into a package
|
||||
|
||||
**Fix:** Add a `pyproject.toml` in `skills/remind/` declaring the scripts directory as part of the package, or set `PYTHONPATH` in the cron:
|
||||
|
||||
```cron
|
||||
* * * * * PYTHONPATH=/home/nanobot/.nanobot/workspace/skills/remind/scripts uv run /home/nanobot/.nanobot/workspace/skills/remind/scripts/remind_send.py
|
||||
```
|
||||
|
||||
Then use normal imports: `from random_times import compute_fire_times`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Missing Functionality
|
||||
|
||||
### 4.1 No `list` command in `remind_edit.py`
|
||||
|
||||
Users cannot view reminders without `cat reminder.yaml`.
|
||||
|
||||
### 4.2 No `edit` command
|
||||
|
||||
To change a reminder, users must remove and re-add. An `edit` command would be useful:
|
||||
|
||||
```python
|
||||
def edit_reminder(data: dict, old_text: str, new_text: str, new_schedule: dict | None = None) -> bool:
|
||||
for reminder in data.get("reminders", []):
|
||||
if reminder.get("text") == old_text:
|
||||
reminder["text"] = new_text
|
||||
if new_schedule:
|
||||
# Remove old schedule keys, add new ones
|
||||
for key in list(reminder.keys()):
|
||||
if key != "text":
|
||||
del reminder[key]
|
||||
reminder.update(new_schedule)
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
### 4.3 No deduplication / "fired" tracking for one-time reminders
|
||||
|
||||
`at` and `at_times` reminders should fire exactly once. Currently they rely on the 60s window and cron granularity.
|
||||
|
||||
**Fix:** SQLite state tracking:
|
||||
|
||||
```python
|
||||
# db/remind_state.sqlite
|
||||
# table fired (text TEXT, fired_at TEXT PRIMARY KEY)
|
||||
```
|
||||
|
||||
Or simpler: append a `fired:` list to each reminder in `reminder.yaml` (but this modifies user data). Better: separate state file.
|
||||
|
||||
### 4.4 No dry-run mode in `remind_send.py`
|
||||
|
||||
Users cannot preview what would fire without actually sending Telegram messages.
|
||||
|
||||
**Fix:** Add `--dry-run` flag:
|
||||
|
||||
```python
|
||||
if dry_run:
|
||||
print(f"[DRY-RUN] Would fire: {text} at {now}")
|
||||
else:
|
||||
fire_reminder(text)
|
||||
```
|
||||
|
||||
### 4.5 No way to see today's schedule
|
||||
|
||||
Users can't ask "what reminders do I have today?"
|
||||
|
||||
**Fix:** Add a `today` or `schedule` command to `remind_edit.py` that computes and prints all fire times for the current day.
|
||||
|
||||
### 4.6 No support for disabling reminders
|
||||
|
||||
Users must delete reminders to stop them. A `disabled: true` flag would be useful.
|
||||
|
||||
### 4.7 No validation before write
|
||||
|
||||
`remind_edit.py` doesn't validate that the produced YAML is loadable by `remind_send.py`. A malformed entry could break the cron job silently.
|
||||
|
||||
**Fix:** After building the reminder dict, run it through `random_times.compute_fire_times` (if it has `random`) or `croniter` (if it has `cron_exprs`) to validate:
|
||||
|
||||
```python
|
||||
def validate_reminder(reminder: dict) -> None:
|
||||
if "random" in reminder:
|
||||
compute_fire_times(date.today(), reminder["text"], reminder["random"])
|
||||
if "cron_exprs" in reminder:
|
||||
for expr in reminder["cron_exprs"]:
|
||||
croniter(expr)
|
||||
```
|
||||
|
||||
### 4.8 No backup before edit
|
||||
|
||||
**Fix:** Keep last N backups:
|
||||
|
||||
```python
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
|
||||
def backup_reminders(path: Path) -> None:
|
||||
backup = path.with_suffix(f".yaml.{datetime.now():%Y%m%d%H%M%S}.bak")
|
||||
shutil.copy2(path, backup)
|
||||
```
|
||||
|
||||
### 4.9 `random_times.py` lacks step syntax in days parser
|
||||
|
||||
Cron supports `*/2`, `1-5/2`. `_parse_days` doesn't handle this.
|
||||
|
||||
**Fix:**
|
||||
|
||||
```python
|
||||
def _parse_days(spec: object) -> set[int]:
|
||||
text = str(spec).strip()
|
||||
if text == "*":
|
||||
return set(range(7))
|
||||
result: set[int] = set()
|
||||
for part in text.split(","):
|
||||
part = part.strip()
|
||||
step = 1
|
||||
if "/" in part:
|
||||
part, step_str = part.split("/", 1)
|
||||
step = int(step_str)
|
||||
if "-" in part:
|
||||
low_str, high_str = part.split("-", 1)
|
||||
low, high = int(low_str), int(high_str)
|
||||
result.update(_normalize_dow(d) for d in range(low, high + 1, step))
|
||||
else:
|
||||
result.add(_normalize_dow(int(part)))
|
||||
return result
|
||||
```
|
||||
|
||||
### 4.10 No `__main__` guard in `random_times.py`
|
||||
|
||||
Not critical since it's a library, but good practice.
|
||||
|
||||
---
|
||||
|
||||
## 5. Testing Gaps
|
||||
|
||||
| Component | Tests? | Coverage |
|
||||
|-----------|--------|----------|
|
||||
| `random_times.py` | Yes | Good (determinism, gaps, filters, errors) |
|
||||
| `remind_edit.py` | **No** | Zero |
|
||||
| `remind_send.py` | **No** | Zero |
|
||||
|
||||
### 5.1 Tests needed for `remind_edit.py`
|
||||
|
||||
- `parse_schedule` with all input variants
|
||||
- `build_reminder` / `format_reminder` roundtrip
|
||||
- `remove_reminder` exact vs substring
|
||||
- YAML dump/load roundtrip preserves formatting
|
||||
- Atomic write doesn't corrupt file
|
||||
|
||||
### 5.2 Tests needed for `remind_send.py`
|
||||
|
||||
- `should_fire` boundary conditions
|
||||
- `fire_reminder` with mocked Telegram bot
|
||||
- `main` with mocked `reminder.yaml` and mocked bot
|
||||
- One-time reminder deduplication
|
||||
- Random reminder integration with `random_times`
|
||||
|
||||
### 5.3 Test infrastructure
|
||||
|
||||
`conftest.py` only adds `sys.path`. It should also provide fixtures:
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def sample_yaml(tmp_path):
|
||||
path = tmp_path / "reminder.yaml"
|
||||
path.write_text("reminders:\n- text: test\n at: 2026-06-01T10:00:00\n")
|
||||
return path
|
||||
|
||||
@pytest.fixture
|
||||
def mock_bot(monkeypatch):
|
||||
class FakeBot:
|
||||
def send_message(self, chat_id, text):
|
||||
self.last_call = (chat_id, text)
|
||||
bot = FakeBot()
|
||||
monkeypatch.setattr("remind_send.Bot", lambda token: bot)
|
||||
return bot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Architecture Improvements
|
||||
|
||||
### 6.1 Consolidate into a single CLI
|
||||
|
||||
The user has considered consolidating remind into a single script. Current split:
|
||||
- `remind_edit.py` = user-facing CLI
|
||||
- `remind_send.py` = cron daemon
|
||||
- `random_times.py` = shared library
|
||||
|
||||
This split is actually reasonable. But `remind_edit.py` and `remind_send.py` share no code. Consider extracting common YAML I/O:
|
||||
|
||||
```python
|
||||
# remind_common.py
|
||||
from pathlib import Path
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
REMINDER_FILE = Path(__file__).parent.parent.parent / "reminder.yaml"
|
||||
|
||||
def load_reminders() -> dict:
|
||||
yaml = YAML()
|
||||
yaml.preserve_quotes = True
|
||||
with open(REMINDER_FILE) as f:
|
||||
return yaml.load(f) or {"reminders": []}
|
||||
|
||||
def save_reminders(data: dict) -> None:
|
||||
yaml = YAML()
|
||||
yaml.default_flow_style = False
|
||||
yaml.indent(mapping=2, sequence=4, offset=2)
|
||||
atomic_write(REMINDER_FILE, data, yaml)
|
||||
```
|
||||
|
||||
### 6.2 Use SQLite for state (not YAML)
|
||||
|
||||
The user is evaluating SQLite vs YAML for remind data storage. Current YAML approach:
|
||||
- **Pros:** Human-readable, easy to edit by hand, version-control friendly
|
||||
- **Cons:** No schema validation, race conditions, no querying, append-only log is separate
|
||||
|
||||
**Recommendation:** Keep YAML for the reminder definitions (human-editable), but use SQLite for runtime state (fired tracking, history query):
|
||||
|
||||
```python
|
||||
# db/remind_state.sqlite
|
||||
CREATE TABLE fired (
|
||||
id INTEGER PRIMARY KEY,
|
||||
text TEXT NOT NULL,
|
||||
scheduled_at TEXT NOT NULL,
|
||||
fired_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX idx_scheduled ON fired(scheduled_at);
|
||||
```
|
||||
|
||||
This gives:
|
||||
- Exact-once firing for one-time reminders
|
||||
- Queryable history ("when did X last fire?")
|
||||
- No modification to `reminder.yaml`
|
||||
|
||||
### 6.3 Refactor `remind_send.py` into a class
|
||||
|
||||
Current procedural style makes testing hard. A class-based design:
|
||||
|
||||
```python
|
||||
class ReminderEngine:
|
||||
def __init__(self, yaml_path: Path, bot: Bot | None = None, dry_run: bool = False):
|
||||
self.yaml_path = yaml_path
|
||||
self.bot = bot
|
||||
self.dry_run = dry_run
|
||||
self.now = datetime.now(TIMEZONE)
|
||||
|
||||
def load(self) -> list[dict]:
|
||||
...
|
||||
|
||||
def should_fire(self, candidate: datetime) -> bool:
|
||||
...
|
||||
|
||||
def fire(self, text: str) -> None:
|
||||
...
|
||||
|
||||
def run(self) -> list[str]:
|
||||
fired = []
|
||||
for reminder in self.load():
|
||||
for candidate in self.candidates(reminder):
|
||||
if self.should_fire(candidate) and not self.already_fired(reminder, candidate):
|
||||
self.fire(reminder["text"])
|
||||
fired.append(reminder["text"])
|
||||
return fired
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Specific Code Examples
|
||||
|
||||
### 7.1 Atomic write for `remind_edit.py`
|
||||
|
||||
```python
|
||||
import os
|
||||
from pathlib import Path
|
||||
from tempfile import mkstemp
|
||||
|
||||
def atomic_write_yaml(path: Path, data: dict, yaml: YAML) -> None:
|
||||
fd, tmp = mkstemp(dir=path.parent, suffix=".tmp")
|
||||
try:
|
||||
with os.fdopen(fd, "w") as f:
|
||||
yaml.dump(data, f)
|
||||
os.replace(tmp, path)
|
||||
except Exception:
|
||||
os.unlink(tmp)
|
||||
raise
|
||||
```
|
||||
|
||||
### 7.2 Proper `LiteralScalarString` usage
|
||||
|
||||
```python
|
||||
from ruamel.yaml.scalarstring import LiteralScalarString
|
||||
|
||||
def build_reminder(text: str, schedule: dict) -> dict:
|
||||
reminder = {"text": LiteralScalarString(text)}
|
||||
for key, value in schedule.items():
|
||||
if isinstance(value, list):
|
||||
reminder[key] = [LiteralScalarString(v) for v in value]
|
||||
elif isinstance(value, str):
|
||||
reminder[key] = LiteralScalarString(value)
|
||||
else:
|
||||
reminder[key] = value
|
||||
return reminder
|
||||
```
|
||||
|
||||
### 7.3 Deduplication for one-time reminders
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
|
||||
STATE_DB = Path(__file__).parent.parent.parent / "db" / "remind_state.sqlite"
|
||||
|
||||
def ensure_state_db() -> None:
|
||||
STATE_DB.parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(STATE_DB)
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS fired (
|
||||
text TEXT NOT NULL,
|
||||
scheduled_at TEXT NOT NULL,
|
||||
fired_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (text, scheduled_at)
|
||||
)
|
||||
""")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def already_fired(text: str, scheduled_at: datetime) -> bool:
|
||||
conn = sqlite3.connect(STATE_DB)
|
||||
row = conn.execute(
|
||||
"SELECT 1 FROM fired WHERE text = ? AND scheduled_at = ?",
|
||||
(text, scheduled_at.isoformat())
|
||||
).fetchone()
|
||||
conn.close()
|
||||
return row is not None
|
||||
|
||||
def record_fired(text: str, scheduled_at: datetime) -> None:
|
||||
conn = sqlite3.connect(STATE_DB)
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO fired (text, scheduled_at) VALUES (?, ?)",
|
||||
(text, scheduled_at.isoformat())
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
```
|
||||
|
||||
### 7.4 Narrowed `should_fire` + dedup
|
||||
|
||||
```python
|
||||
def should_fire(candidate: datetime, now: datetime, window_sec: int = 30) -> bool:
|
||||
delta = (now - candidate).total_seconds()
|
||||
return 0 <= delta < window_sec
|
||||
|
||||
# In main loop for one-time reminders:
|
||||
if "at" in reminder:
|
||||
candidate = parse_at(reminder["at"])
|
||||
if should_fire(candidate, now) and not already_fired(text, candidate):
|
||||
fire_reminder(text)
|
||||
record_fired(text, candidate)
|
||||
```
|
||||
|
||||
### 7.5 `remind_edit.py` with dispatch table
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from ruamel.yaml import YAML
|
||||
from ruamel.yaml.scalarstring import LiteralScalarString
|
||||
|
||||
from random_times import compute_fire_times
|
||||
from croniter import croniter
|
||||
|
||||
REMINDER_FILE = Path(__file__).parent.parent.parent / "reminder.yaml"
|
||||
|
||||
# --- commands ---
|
||||
|
||||
def cmd_add(args: list[str]) -> None:
|
||||
text = " ".join(args)
|
||||
schedule = parse_schedule([]) # default cron
|
||||
add_reminder(text, schedule)
|
||||
|
||||
def cmd_remove(args: list[str]) -> None:
|
||||
text = " ".join(args)
|
||||
remove_reminder(text)
|
||||
|
||||
def cmd_list(_args: list[str]) -> None:
|
||||
data = load_reminders()
|
||||
for i, r in enumerate(data.get("reminders", []), 1):
|
||||
print(f"{i}. {r.get('text', '(no text)')}")
|
||||
|
||||
COMMANDS = {
|
||||
"add": cmd_add,
|
||||
"remove": cmd_remove,
|
||||
"list": cmd_list,
|
||||
}
|
||||
|
||||
def main() -> None:
|
||||
args = sys.argv[1:]
|
||||
if not args or args[0] not in COMMANDS:
|
||||
print(f"Usage: {sys.argv[0]} [{'|'.join(COMMANDS)}] ...")
|
||||
sys.exit(1)
|
||||
COMMANDS[args[0]](args[1:])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Prioritized Action Plan
|
||||
|
||||
| Priority | Task | Effort | Impact |
|
||||
|----------|------|--------|--------|
|
||||
| **P0** | Fix one-time reminder double-firing (narrow window + dedup) | Small | High — prevents spam |
|
||||
| **P0** | Add atomic writes to `remind_edit.py` | Small | High — prevents data loss |
|
||||
| **P1** | Add `list` command to `remind_edit.py` | Small | Medium — advertised feature |
|
||||
| **P1** | Replace custom `LiteralScalarString` with ruamel's | Tiny | Low — code cleanliness |
|
||||
| **P1** | Replace manual YAML string building with dict+dump | Medium | High — robustness |
|
||||
| **P1** | Add validation before write | Small | Medium — catches errors early |
|
||||
| **P2** | Add tests for `remind_edit.py` and `remind_send.py` | Medium | High — enables refactoring |
|
||||
| **P2** | Extract common YAML I/O to `remind_common.py` | Small | Medium — DRY |
|
||||
| **P2** | Add `--dry-run` to `remind_send.py` | Small | Medium — safer testing |
|
||||
| **P3** | Add SQLite state tracking for fired reminders | Medium | Medium — exact-once, queryable history |
|
||||
| **P3** | Add `edit` command | Small | Low — convenience |
|
||||
| **P3** | Add `disabled` flag | Small | Low — convenience |
|
||||
| **P3** | Support cron step syntax in `_parse_days` | Small | Low — completeness |
|
||||
|
||||
---
|
||||
|
||||
## 9. Summary
|
||||
|
||||
The `random_times.py` module is solid. The main pain points are in `remind_edit.py` (manual YAML construction, no atomic writes, missing commands) and `remind_send.py` (double-firing risk, no deduplication, no tests). The highest-impact fixes are: (1) atomic YAML writes, (2) one-time reminder deduplication, and (3) replacing manual YAML string building with proper serialization. Adding tests for the two untested scripts is essential before any major refactoring.
|
||||
187
results/2026-06-07_ollama-cloud-agent-model-comparison.md
Normal file
187
results/2026-06-07_ollama-cloud-agent-model-comparison.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# Ollama Cloud Agent Model Comparison — Nanobot Deployment
|
||||
|
||||
**Date:** 2026-06-07
|
||||
**Baseline:** `glm-5.1:cloud`
|
||||
**Scope:** Evaluate all Ollama Cloud models against 12 criteria for sustained nanobot agent use.
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
**GLM-5.1:cloud remains the best default** for nanobot agent deployment on Ollama Cloud. It offers the best balance of speed (~198 tok/s), proven agentic reliability, MIT license, 200K context, and no known language-drift or tool-calling blockers.
|
||||
|
||||
**Viable alternatives (with tradeoffs):**
|
||||
- **`deepseek-v4-flash:cloud`** — if you need 1M context and can tolerate slower speed. MIT license, open weights.
|
||||
- **`qwen3.5:397b-cloud`** — if you need multimodal + 1M context + explicit Czech support (201 languages). Apache 2.0. Reported as slow with accuracy issues on Ollama Cloud.
|
||||
- **`devstral-2:123b-cloud`** — if the workload is purely coding-heavy and 128K context is sufficient. Strong SWE-Bench / Terminal-Bench scores. Apache 2.0.
|
||||
|
||||
**Not recommended due to blockers:**
|
||||
- `minimax-m3:cloud` — critical tool-result message bug on Ollama Cloud (ollama/ollama #16389).
|
||||
- `kimi-k2.6:cloud` — random Chinese output drift (critical risk for Czech use).
|
||||
- `deepseek-v4-pro:cloud` — strongest benchmarks but 15.4 tok/s and 57s TTFT cold-start make it impractical for interactive agent work.
|
||||
|
||||
**GLM-5.2 status:** Not released. No official announcement from Z.AI as of June 2026.
|
||||
|
||||
---
|
||||
|
||||
## Comparison Table
|
||||
|
||||
| Model | Speed (tok/s) | TTFT | SWE-Bench V | SWE-Bench Pro | Terminal-Bench | MCP-Atlas | HLE | Code Arena Elo | Tool Reliability | Czech / Multilingual | Context | Multimodal | License | Verbosity | Known Bugs | Pricing (OpenRouter proxy) | Long-Horizon | Self-Host |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| **glm-5.1:cloud** | ~198 | Low | ~58.4 (Pro) | 58.4 | 63.5 | 71.8 | 52.3 | 1530 | Excellent | No Czech claim; no drift observed | 200K (198K Ollama) | No | MIT | Low | None known | ~$4/M out | Proven | Yes |
|
||||
| **deepseek-v4-pro:cloud** | ~15.4 | 57s cold | 80.6 | — | 67.9 | 74.2 | 56.2 | — | Good | Strong multilingual (MMMLU 90.3) | 1M | No | MIT | Medium | Extreme variance | $1.74/M in | Unknown | Yes |
|
||||
| **deepseek-v4-flash:cloud** | ~30-50* | Moderate | ~75* | — | ~60* | — | — | — | Good | Strong multilingual | 1M | No | MIT | Medium | None known | $0.14/M in | Unknown | Yes |
|
||||
| **qwen3.5:397b-cloud** | ~10-20* | High | ~66-70 | — | ~59.3 | — | — | — | Good | **201 languages incl. Czech** | 1M | Yes | Apache 2.0 | Medium | "Too slow, accuracy issues" per user benchmark | — | Unknown | Yes |
|
||||
| **qwen3.5:cloud** | ~20-40* | Moderate | ~60-65 | — | ~55 | — | — | — | Good | 201 languages | 256K | Yes | Apache 2.0 | Medium | None known | — | Unknown | Yes |
|
||||
| **minimax-m3:cloud** | ~40-60* | Low | — | — | — | — | — | — | **Broken** | Undeclared | 512K | Yes | Open weights (pending) | — | **Tool result messages fail (#16389)** | $0.60/M in | N/A | Yes (pending) |
|
||||
| **kimi-k2.6:cloud** | ~30-50* | Moderate | 80.2 | — | 66.7 | — | 54.0 | — | Good | No Czech claim; **random Chinese drift** | 256K | Yes | Modified MIT | Medium | Chinese output bug; OR context bug (32K) | $0.60/M in | 200-300 tool calls | Yes |
|
||||
| **kimi-k2-thinking:cloud** | ~25-40* | Moderate | — | — | — | — | — | — | Good | No Czech claim | 256K | No | Modified MIT | High | Older (Nov 2025) | — | 200-300 seq tool calls | Yes |
|
||||
| **kimi-k2.5:cloud** | ~30-50* | Moderate | — | — | — | — | — | — | Good | No Czech claim | 256K | Yes | Modified MIT | Medium | Older (Jan 2026) | — | Unknown | Yes |
|
||||
| **nemotron-3-ultra:cloud** | ~50-80* | Low | ~60-79* | — | — | 74.2 | — | — | Unknown | Undeclared | 200K | No | NVIDIA Open | Low | Too new (Jun 4 2026) | $0.60/M in | Unknown | Yes (NVFP4) |
|
||||
| **nemotron-3-super:cloud** | ~60-100* | Low | 60.47 | — | — | — | — | — | Unknown | Undeclared | 1M | No | NVIDIA Open | Low | None known | — | Unknown | Yes |
|
||||
| **gemma4:31b-cloud** | ~80-120* | Low | ~52.0 | — | ~29.2 | — | — | — | Native FC | 140+ languages | 256K | Yes | Apache 2.0 | Low | None known | — | Unknown | Yes |
|
||||
| **devstral-2:123b-cloud** | ~40-60* | Moderate | 72.2 | — | 77.3 | — | — | — | Good | Undeclared | 128K | No | Apache 2.0 | Medium | None known | — | Unknown | Yes |
|
||||
| **gpt-oss:120b-cloud** | ~30-50* | Moderate | ~41.9 | — | — | — | — | — | Good | Undeclared | 128K | No | Apache 2.0 | Medium | Older (Aug 2025) | $0.039/M in | Unknown | Yes |
|
||||
| **gemini-3-flash-preview:cloud** | ~60-80* | Low | — | — | — | — | — | — | Good | Strong (Google) | 1M | Yes | Proprietary | Low | Older (Dec 2025) | — | Unknown | No |
|
||||
| **qwen3-coder-next:cloud** | ~40-60* | Moderate | ~70.6 | — | — | — | — | — | Good | 201 languages | 512K | No | Apache 2.0 | Medium | None known | — | Unknown | Yes |
|
||||
|
||||
*Speed estimates marked with * are inferred from similar-size MoE models or provider benchmarks, not direct Ollama Cloud measurements. GLM-5.1's ~198 tok/s is the only Ollama Cloud-specific speed figure found in the knowledge base.
|
||||
|
||||
---
|
||||
|
||||
## Detailed Analysis by Criterion
|
||||
|
||||
### 1. Speed (TTFT, throughput, wall-clock latency)
|
||||
- **GLM-5.1** is the clear speed leader on Ollama Cloud at ~198 tok/s.
|
||||
- **DeepSeek V4-Pro** is the slowest at ~15.4 tok/s with extreme variance and 57s cold-start TTFT.
|
||||
- **Gemma 4 31B** and **Nemotron 3 Super** are likely the fastest among alternatives due to small active parameter counts (31B dense, 12B active MoE).
|
||||
- **Qwen3.5:397B** is reported as "too slow" in user benchmarks.
|
||||
|
||||
### 2. Intelligence (Benchmarks)
|
||||
- **SWE-Bench Verified leaders:** DeepSeek V4-Pro (80.6%), Kimi K2.6 (80.2%), Devstral 2 (72.2%), Qwen3.5-397B (~66-70%), Nemotron 3 Super (60.47%), Gemma 4 31B (~52%), GPT-OSS 120B (~41.9%).
|
||||
- **SWE-Bench Pro:** GLM-5.1 leads open models at 58.4%.
|
||||
- **Terminal-Bench 2.0:** DeepSeek V4-Pro (67.9%), Kimi K2.6 (66.7%), Devstral 2 (77.3% — highest reported), GLM-5.1 (63.5%).
|
||||
- **MCP-Atlas:** Nemotron 3 Ultra (74.2%), DeepSeek V4-Pro (74.2%), GLM-5.1 (71.8%).
|
||||
- **HLE:** Kimi K2.6 (54.0%), GLM-5.1 (52.3%).
|
||||
- **Code Arena Elo:** GLM-5.1 at 1530 (#3 globally for agentic web dev).
|
||||
|
||||
### 3. Tool Calling Reliability & Schema Adherence
|
||||
- **GLM-5.1:** 99.6% schema adherence (per prior research), no known tool-calling failures.
|
||||
- **MiniMax M3:** **Critical blocker** — fails on tool result messages via Ollama Cloud OpenAI-compatible endpoint (issue #16389, 6 days old as of Jun 7). Returns empty responses.
|
||||
- **Kimi K2.6:** Good tool-call reliability but known OpenRouter context-length bug (reports 32K instead of 256K) that may affect Ollama.
|
||||
- **Gemma 4 31B:** Native function calling support.
|
||||
- **Nemotron 3 Ultra:** Too new; no verified tool-calling data yet.
|
||||
|
||||
### 4. Czech / Multilingual Support & Language Drift Risk
|
||||
- **Qwen3.5** (all variants): Explicitly claims 201 languages including Czech. Best documented multilingual support.
|
||||
- **Gemma 4 31B:** Claims 140+ languages, Apache 2.0.
|
||||
- **DeepSeek V4:** Strong multilingual (MMMLU 90.3, C-Eval 93.1) but no explicit Czech claim.
|
||||
- **GLM-5.1:** No explicit Czech claim, but **no known language drift** in practice.
|
||||
- **Kimi K2.6:** **Critical risk** — multiple user reports of random Chinese output even with English prompts. No explicit Czech support claim.
|
||||
- **MiniMax M3:** No explicit multilingual claim.
|
||||
- **Nemotron / Devstral / GPT-OSS:** No explicit Czech claims.
|
||||
|
||||
### 5. Context Window Size
|
||||
- **1M tokens:** deepseek-v4-pro, deepseek-v4-flash, nemotron-3-super, qwen3.5:397b-cloud, gemini-3-flash-preview
|
||||
- **512K:** minimax-m3, qwen3-coder-next
|
||||
- **256K:** kimi-k2.6, kimi-k2-thinking, kimi-k2.5, qwen3.5:cloud, gemma4:31b-cloud, devstral-2
|
||||
- **200K:** glm-5.1, nemotron-3-ultra
|
||||
- **128K:** gpt-oss:120b-cloud
|
||||
|
||||
### 6. Multimodality
|
||||
- **Multimodal:** minimax-m3, kimi-k2.6, kimi-k2.5, qwen3.5:397b-cloud, qwen3.5:cloud, gemma4:31b-cloud, gemini-3-flash-preview
|
||||
- **Text-only:** glm-5.1, deepseek-v4-pro, deepseek-v4-flash, kimi-k2-thinking, qwen3-coder-next, devstral-2, gpt-oss, nemotron-3-super/ultra
|
||||
|
||||
### 7. Open Weights & License
|
||||
- **MIT:** GLM-5.1, GLM-5, DeepSeek V4-Pro/Flash
|
||||
- **Apache 2.0:** Qwen3.5/Qwen3.6/Qwen3-coder, Gemma 4, GPT-OSS 120B, Devstral 2
|
||||
- **Modified MIT:** Kimi K2.6, Kimi K2-thinking, Kimi K2.5
|
||||
- **NVIDIA Open License:** Nemotron 3 Ultra/Super
|
||||
- **Proprietary:** Gemini-3-flash-preview
|
||||
- **MiniMax M3:** Open weights promised ~10 days after launch (early June 2026) — likely available by now.
|
||||
|
||||
### 8. Verbosity (Tokens per Answer)
|
||||
- **Low:** GLM-5.1 ("nejmenší verbosity z MoE rodiny"), Nemotron 3 Ultra/Super (up to 30% fewer tokens per turn), Gemma 4 31B
|
||||
- **Medium:** DeepSeek V4, Qwen3.5, Devstral 2, Kimi K2.6
|
||||
- **High:** Kimi K2-thinking (reasoning model)
|
||||
|
||||
### 9. Known Bugs / Blockers on Ollama Cloud
|
||||
- **MiniMax M3:** Tool result message failures (#16389) — **deploy blocker**.
|
||||
- **Kimi K2.6:** Random Chinese output drift — **deploy blocker for Czech use**.
|
||||
- **DeepSeek V4-Pro:** Extreme latency variance, 57s cold-start TTFT — usability issue.
|
||||
- **Qwen3.5:397B:** User-reported "too slow, accuracy issues" on Ollama Cloud.
|
||||
- **GLM-5.1:** No known bugs.
|
||||
|
||||
### 10. Pricing (OpenRouter proxy — Ollama Cloud is flat-rate $20/mo Pro)
|
||||
- **Cheapest input:** GPT-OSS 120B ($0.039/M), DeepSeek V4-Flash ($0.14/M)
|
||||
- **Mid-range:** DeepSeek V4-Pro ($1.74/M), MiniMax M3 ($0.60/M), Kimi K2.6 ($0.60/M), Nemotron 3 Ultra ($0.60/M)
|
||||
- **Most expensive:** GLM-5.1 (~$4/M output)
|
||||
- **Note:** Ollama Cloud Pro is flat-rate $20/month; per-token pricing only matters if switching to API/OpenRouter fallback.
|
||||
|
||||
### 11. Long-Horizon Agent Stability (Multi-turn, hundreds of tool calls)
|
||||
- **GLM-5.1:** Proven over "hundreds of rounds" — best sustained productivity per user experience.
|
||||
- **Kimi K2-thinking:** Explicitly designed for 200-300 sequential tool calls.
|
||||
- **Kimi K2.6:** Supports 200-300 sequential tool calls.
|
||||
- **Nemotron 3 Ultra:** Marketed for "long-running agents" but too new for verification.
|
||||
- **DeepSeek V4:** Unknown for sustained multi-turn agent use on Ollama Cloud.
|
||||
|
||||
### 12. Self-Host Fallback Possibility
|
||||
- **All models except Gemini-3-flash-preview** have open weights available on Hugging Face.
|
||||
- **NVFP4 quantization:** Nemotron 3 Ultra/Super require NVIDIA-specific formats.
|
||||
- **Hardware requirements:**
|
||||
- GLM-5.1: ~198K context on Ollama; self-host requires significant VRAM.
|
||||
- DeepSeek V4-Flash: 284B total / 13B active — efficient MoE, viable on consumer hardware.
|
||||
- Qwen3.5:397B: 397B total / 17B active — large but efficient.
|
||||
- Gemma 4 31B: Dense 31B — fits on 24GB GPU.
|
||||
- Devstral 2 123B: Large but coding-optimized.
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Primary Default (No Change)
|
||||
**`glm-5.1:cloud`** remains the best default nanobot agent model on Ollama Cloud.
|
||||
|
||||
**Why:**
|
||||
- Fastest measured speed (~198 tok/s)
|
||||
- Best proven track record for sustained agent sessions
|
||||
- MIT license
|
||||
- No known bugs or language drift
|
||||
- Strong benchmark suite (SWE-Bench Pro 58.4, Terminal-Bench 63.5, MCP-Atlas 71.8, Code Arena Elo 1530)
|
||||
- Low verbosity = lower token burn
|
||||
|
||||
### Alternative Tier 1 (Specific Needs)
|
||||
1. **`deepseek-v4-flash:cloud`** — Choose if you need 1M context for large codebase analysis or long-document processing. MIT license, open weights, cheaper than Pro. Tradeoff: slower than GLM-5.1 (~30-50 tok/s estimated).
|
||||
2. **`qwen3.5:397b-cloud`** — Choose if you need multimodal input (screenshots, diagrams) or explicit Czech language support (201 languages claimed). Apache 2.0, 1M context. Tradeoff: slow, reported accuracy issues on Ollama Cloud.
|
||||
|
||||
### Alternative Tier 2 (Niche Use)
|
||||
3. **`devstral-2:123b-cloud`** — Choose for pure coding-heavy workloads with strong benchmark scores (SWE-Bench 72.2%, Terminal-Bench 77.3%). Tradeoff: 128K context limit, no multimodal.
|
||||
4. **`gemma4:31b-cloud`** — Choose if you need a fast, lightweight alternative with native function calling and 140+ language support. Tradeoff: weaker agent benchmarks (SWE-Bench ~52%, Terminal-Bench ~29%).
|
||||
|
||||
### Avoid (Blockers)
|
||||
- **`minimax-m3:cloud`** — Tool-calling bug makes it unusable for agent work until Ollama fixes #16389.
|
||||
- **`kimi-k2.6:cloud`** — Chinese language drift is unacceptable for Czech-language agent use.
|
||||
- **`deepseek-v4-pro:cloud`** — 15.4 tok/s and 57s TTFT make it impractical for interactive agent sessions despite top benchmarks.
|
||||
|
||||
### Watch List
|
||||
- **`nemotron-3-ultra:cloud`** — Too new (released June 4, 2026). Promising specs (550B/55B, 1M ctx, low verbosity) but needs real-world agent validation on Ollama Cloud.
|
||||
- **`qwen3-coder-next:cloud`** — Strong coding focus, 512K context, Apache 2.0. Good candidate if coding is the primary workload.
|
||||
|
||||
---
|
||||
|
||||
## GLM-5.2 Status
|
||||
|
||||
**Not released.** As of June 7, 2026, Z.AI has made no official announcement of GLM-5.2. Reddit speculation from April 2026 suggested 50-83 days from GLM-5.1 launch (April 7, 2026), implying a June-July 2026 window, but no confirmation exists. It is not available on Ollama Cloud.
|
||||
|
||||
---
|
||||
|
||||
## Sources & Methodology
|
||||
|
||||
- Ollama Cloud model listings: ollama.com/search?c=cloud
|
||||
- Benchmark aggregators: llm-stats.com, benchlm.ai, benchmark.space, swebench.com
|
||||
- Vendor technical reports: NVIDIA Nemotron 3 Ultra (Jun 4, 2026), DeepSeek V4 (Apr 24, 2026), Qwen3.5/3.6 blog posts, Kimi K2.6 blog, Z.AI GLM-5.1 page
|
||||
- Community benchmarks: ollama-cloud-benchmark GitHub (erikwangz), dev.to user benchmarks
|
||||
- Bug trackers: ollama/ollama #16389 (MiniMax M3), Cursor/Reddit user reports (Kimi K2.6 Chinese drift)
|
||||
- Pricing: OpenRouter proxy rates (Ollama Cloud itself is flat-rate $20/mo Pro)
|
||||
|
||||
*Note on speed: Only GLM-5.1 has a direct Ollama Cloud speed measurement in our knowledge base (~198 tok/s). All other speed figures are estimates inferred from MoE active-parameter counts, provider benchmarks, or similar-platform measurements. Actual Ollama Cloud performance may vary due to load, cold starts, and quantization.*
|
||||
69
results/2026-06-07_ollama-cloud-model-report.md
Normal file
69
results/2026-06-07_ollama-cloud-model-report.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Přehled modelů Ollama Cloud (červen 2026)
|
||||
|
||||
**Datum:** 2026‑06‑07
|
||||
|
||||
Tento report shrnuje všechny modely dostupné na stránce *Ollama Cloud* (https://ollama.com/models?c=cloud) a hodnotí je podle 12 kritérií relevantních pro nasazení nanobot‑agenta. Hodnocení vychází z interního knowledge/models.md, detailní srovnávací tabulky v `results/2026-06-07_ollama-cloud-agent-model-comparison.md` a veřejně dostupných benchmarků (SWE‑Bench, Terminal‑Bench, Code Arena, MCP‑Atlas, HLE atd.).
|
||||
|
||||
---
|
||||
|
||||
## 1. Tabulka přehledu
|
||||
|
||||
| Model | Rychlost (tok/s) | Inteligence (benchmark) | Tool‑calling | Čeština / Multilingual | Kontext | Multimodální | Licence | Verbosita | Známé bugy / blokátory | Cena (OpenRouter proxy) | Long‑horizon stabilita | Self‑host možnost |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| **glm-5.1:cloud** | ~198 (přímé měření) | SWE‑Bench Pro 58.4 % – Code Arena Elo 1530 | 99.6 % schema adherence, žádné známé selhání | Žádná oficiální podpora češtiny, ale žádný drift | 200 K (198 K Ollama) | ❌ (text‑only) | MIT | Nízká | – | ~$4 /M output (Ollama Pro flat‑rate $20 /mo) | Ověřeno stovkami kol – nejlepší | Ano (vyžaduje ~30 GB VRAM) |
|
||||
| **deepseek-v4-flash:cloud** | ~30‑50* (odhad) | SWE‑Bench ~75 % (odhad) | Dobrá | Žádná explicitní podpora češtiny, ale silná multilingvní skóre (MMMLU 90.3) | 1 M | ❌ | MIT | Střední | – | $0.14 /M in | Neověřeno (uživatelské benchmarky) | Ano (efektivní MoE, 13 B aktivních) |
|
||||
| **qwen3.5:397b-cloud** | ~10‑20* (odhad) | SWE‑Bench 66‑70 % | Dobrá | **201 jazyk včetně češtiny** (oficiální) | 1 M | ✅ | Apache 2.0 | Střední | Uživatelé hlásí pomalost a přesnostní problémy | – (žádná proxy cena, Ollama Pro) | Neověřeno | Ano (Apache 2.0, vyžaduje velké GPU) |
|
||||
| **deepseek-v4-pro:cloud** | ~15.4 (přímé měření) | SWE‑Bench 80.6 % (nejvyšší) | Dobrá | Žádná explicitní podpora češtiny | 1 M | ❌ | MIT | Střední | Extrémní latence (57 s TTFT), vysoká variabilita | $1.74 /M in | Neznámo | Ano (MIT) |
|
||||
| **devstral-2:123b-cloud** | ~40‑60* (odhad) | SWE‑Bench 72.2 % – Terminal‑Bench 77.3 % (nejvyšší) | Dobrá | Žádná explicitní podpora češtiny | 128 K | ❌ | Apache 2.0 | Střední | – | – | Neověřeno | Ano (Apache 2.0) |
|
||||
| **gemma4:31b-cloud** | ~80‑120* (odhad) | SWE‑Bench ~52 % – Code Arena nízké | Native function calling | 140+ jazyků (neuvádí češtinu) | 256 K | ✅ | Apache 2.0 | Nízká | – | – | Neověřeno | Ano (31 B dense) |
|
||||
| **nemotron-3-ultra:cloud** | ~50‑80* (odhad) | SWE‑Bench ~60‑79 % (odhad) | Neznámo | Žádná oficiální podpora češtiny | 200 K | ❌ | NVIDIA Open License | Nízká | Příliš nový – žádná data o tool‑callingu | $0.60 /M in | Neověřeno | Ano (vyžaduje NVIDIA‑specifické kvantování) |
|
||||
| **nemotron-3-super:cloud** | ~60‑100* (odhad) | SWE‑Bench 60.47 % (odhad) | Neznámo | Žádná podpora češtiny | 1 M | ❌ | NVIDIA Open License | Nízká | – | – | Neověřeno | Ano (vyžaduje NVIDIA‑specifické kvantování) |
|
||||
| **minimax-m3:cloud** | ~40‑60* (odhad) | – (žádná veřejná benchmark data) | **Kritický bug** – selhání tool‑result zpráv (issue #16389) | Žádná explicitní podpora češtiny | 512 K | ✅ | Open weights (brzy) | – | Tool‑result bug – **nepoužitelné** | $0.60 /M in | Neověřeno | Ano (otevřené váhy) |
|
||||
| **kimi-k2.6:cloud** | ~30‑50* (odhad) | SWE‑Bench 80.2 % – HLE 54.0 % | Dobrá | Žádná podpora češtiny, **náhodný čínský drift** (kritické) | 256 K | ✅ | Modified MIT | Střední | Čínský výstupní drift, OpenRouter kontext‑bug (32 K) | $0.60 /M in | 200‑300 tool calls (design) | Ano (MIT‑like) |
|
||||
| **qwen3.6:cloud** | ~? (nepřímý odhad) | – | – | 201 jazyků (včetně češtiny) | 256 K | ✅ | Apache 2.0 | Střední | – | – | – | Ano |
|
||||
| **qwen3-coder-next:cloud** | ~40‑60* (odhad) | SWE‑Bench ~70.6 % (odhad) | Dobrá | 201 jazyků (včetně češtiny) | 512 K | ❌ | Apache 2.0 | Střední | – | – | – | Ano |
|
||||
| **lfm2.5:cloud** | – (žádná data) | – | – | – | – | – | – | – | – | – | – | – |
|
||||
| **lfm2:cloud** | – | – | – | – | – | – | – | – | – | – | – | – |
|
||||
| **glm-4.7:cloud** | – | – | – | – | – | – | – | – | – | – | – | – |
|
||||
| **glm-4.7-flash:cloud** | – | – | – | – | – | – | – | – | – | – | – | – |
|
||||
| **translategemma:cloud** | – | – | – | – | – | – | – | – | – | – | – | – |
|
||||
| **gemini-3-flash-preview:cloud** | ~60‑80* (odhad) | – | – | Strong (Google) | 1 M | ✅ | Proprietární | Nízká | – | – | – | Ne (proprietární) |
|
||||
|
||||
*Poznámka: hvězdičkou označené rychlosti jsou odhady založené na podobných modelových velikostech a veřejných benchmarkech, protože přímé měření na Ollama Cloud není v knowledge base dostupné.*
|
||||
|
||||
---
|
||||
|
||||
## 2. Doporučení
|
||||
|
||||
### Primární výchozí model (bez změny)
|
||||
**`glm-5.1:cloud`** – nejrychlejší, nejstabilnější, MIT licence, žádné známé bugy, ověřená dlouhodobá agentní stabilita.
|
||||
|
||||
### Alternativy první úrovně (specifické potřeby)
|
||||
1. **`deepseek-v4-flash:cloud`** – pokud potřebujete 1 M kontextu a nižší cenu, akceptujete střední rychlost a žádnou multimodalitu.
|
||||
2. **`qwen3.5:397b-cloud`** – pokud je pro vás klíčová podpora češtiny a multimodální vstup (obrázky, diagramy). Připravte se na pomalejší odezvu a možná mírná přesnost.
|
||||
|
||||
### Alternativy druhé úrovně (niche)
|
||||
- **`devstral-2:123b-cloud`** – výborný pro čistě kódovací úlohy, silné benchmarky, ale omezený kontext a žádná multimodalita.
|
||||
- **`gemma4:31b-cloud`** – lehký, rychlý, nízká verbosita, dobrá funkční volání, ale slabší agentní skóre.
|
||||
|
||||
### Modely k vyhnutí (blokátory)
|
||||
- **`minimax-m3:cloud`** – kritický bug v tool‑result zprávách, nedostupný pro agentní práci.
|
||||
- **`kimi-k2.6:cloud`** – náhodný čínský výstup, nepřijatelný pro české nasazení.
|
||||
- **`deepseek-v4-pro:cloud`** – extrémní latence a variabilita, i přes špičkové benchmarky.
|
||||
|
||||
### Watch‑list (sledujte vývoj)
|
||||
- **`nemotron-3-ultra:cloud`** – slibné specifikace, ale chybí reálná data o tool‑calling a dlouhodobé stabilitě.
|
||||
- **`qwen3-coder-next:cloud`** – zaměřeno na kódování, 512 K kontext, dobrá podpora jazyků.
|
||||
|
||||
---
|
||||
|
||||
## 3. Metodologie a zdroje
|
||||
- **Seznam modelů:** https://ollama.com/models?c=cloud (scraped 2026‑06‑07).
|
||||
- **Benchmarky a metriky:** `knowledge/models.md`, `results/2026-06-07_ollama-cloud-agent-model-comparison.md`, veřejné benchmarky (SWE‑Bench, Terminal‑Bench, Code Arena, MCP‑Atlas, HLE, MMMLU, C‑Eval).
|
||||
- **Bug‑trackery:** GitHub issue #16389 (MiniMax M3), Reddit/Cursor reporty o Kimi K2.6.
|
||||
- **Ceny:** OpenRouter proxy rates (viz `results/..._agent-model-comparison.md`), Ollama Cloud Pro tarif $20 /mo.
|
||||
- **Licence a self‑host:** informace z oficiálních modelových repozitářů (Hugging Face, NVIDIA, Z‑AI).
|
||||
|
||||
---
|
||||
|
||||
*Report byl vygenerován automaticky na základě dostupných interních a veřejných dat. Pro konkrétní nasazení doporučuji provést vlastní rychlostní testy na vašem hardware a ověřit aktuální stav bugů.*
|
||||
362
results/2026-06-07_todo-skill-unification-analysis.md
Normal file
362
results/2026-06-07_todo-skill-unification-analysis.md
Normal file
@@ -0,0 +1,362 @@
|
||||
# Analýza: /todo skill a unifikace /note, /remind, /keep
|
||||
|
||||
## 1. Současný stav — co každý skill dělá
|
||||
|
||||
| Skill | Storage | Příkazy | Klíčová vlastnost | Problémy |
|
||||
|-------|---------|---------|-------------------|----------|
|
||||
| **/keep** | `keep.md` (plain markdown) | `add`, `list` | Okamžitá persist, žádná struktura | Append-only, žádné mazání/úpravy, žádné kategorie, plaintext |
|
||||
| **/note** | `db/note.sqlite` | `add`, `list`, `search`, `delete`, `edit` | Plné CRUD, kategorie, vyhledávání | Není "task-oriented", žádný status/due date |
|
||||
| **/remind** | `reminder.yaml` + `.reminder_state.json` | `add`, `delete` | Časové plánování, Telegram notifikace | YAML race conditions, žádný `list`, žádné `edit`, fragile dedup |
|
||||
| **/todo** *(navrhovaný)* | — | — | Seznam úkolů bez časového plánování | Neexistuje |
|
||||
|
||||
### 1.1 Překryv funkcionality
|
||||
|
||||
```
|
||||
/keep add "koupit mléko" → plaintext záznam
|
||||
/note add "koupit mléko" --cat shopping → strukturovaný záznam
|
||||
/todo add "koupit mléko" → úkol (co se liší od note?)
|
||||
/remind add "koupit mléko" at 18:00 → úkol + časová notifikace
|
||||
```
|
||||
|
||||
**Základní entita je stejná:** text + metadata. Rozdíl je v *chování* (notifikace, status tracking).
|
||||
|
||||
---
|
||||
|
||||
## 2. Požadavky na /todo
|
||||
|
||||
Z uživatelova popisu: "podobný jako remind, jen tam není to přesné časové odesílání".
|
||||
|
||||
To znamená:
|
||||
- Přidat úkol
|
||||
- Označit jako hotový
|
||||
- Seznam aktivních/dokončených úkolů
|
||||
- Smazat úkol
|
||||
- Možná priorita, kategorie, due date (bez notifikace)
|
||||
|
||||
**To je 90% funkcionality /note + jeden sloupec `status`.**
|
||||
|
||||
---
|
||||
|
||||
## 3. Architektonické varianty
|
||||
|
||||
### Varianta A: Jeden univerzální skill `/task` (nebo `/item`)
|
||||
|
||||
**Koncept:** Jeden SQLite DB, jedna tabulka `items`:
|
||||
|
||||
```sql
|
||||
CREATE TABLE items (
|
||||
id INTEGER PRIMARY KEY,
|
||||
type TEXT CHECK(type IN ('note','todo','reminder','keep')),
|
||||
content TEXT NOT NULL,
|
||||
category TEXT,
|
||||
status TEXT CHECK(status IN ('active','done','archived')),
|
||||
due_at TIMESTAMP, -- pro todo + reminder
|
||||
schedule TEXT, -- cron expr pro reminder
|
||||
notify_channel TEXT, -- telegram, etc.
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
**Příkazy:**
|
||||
```
|
||||
/task note add "obsah" --cat prace
|
||||
/task todo add "udělat review" --priority high --due 2026-06-10
|
||||
/task remind add "zavolat" --at 2026-06-08T10:00
|
||||
/task keep add "zapamatuj si heslo"
|
||||
/task list --type todo --status active
|
||||
/task done <id>
|
||||
/task delete <id>
|
||||
```
|
||||
|
||||
**Výhody:**
|
||||
- Jednotné API — uživatel se učí jeden skill
|
||||
- Jeden storage — žádná duplicita dat
|
||||
- Flexibilní — úkol může "proměnit" z todo na remind přidáním schedule
|
||||
- Fulltext search přes všechny typy najednou
|
||||
- Jedna codebase na CRUD
|
||||
|
||||
**Nevýhody:**
|
||||
- Velká změna — migrace 3 existujících skillů
|
||||
- `/remind` potřebuje minutový cron — to nelze udělat uvnitř LLM agenta
|
||||
- Risk "one size fits none" — kompromisy v UI každého typu
|
||||
- Složitější permission model (co když chci remind bez todo?)
|
||||
|
||||
**Verdikt:** Příliš monolitické. `/remind` cron mechanismus je technický důvod pro separaci.
|
||||
|
||||
---
|
||||
|
||||
### Varianta B: Zachovat separaci, přidat orchestraci
|
||||
|
||||
**Koncept:** Existující skilly zůstanou. Nový skill `/task` (nebo `/items`) je "meta-skill" — analyzuje záměr a deleguje na správný pod-skill.
|
||||
|
||||
```
|
||||
Uživatel: "připomeň mi zítra v 10 zavolat"
|
||||
→ /task rozpozná "připomeň" + čas → volá /remind
|
||||
|
||||
Uživatel: "zapiš si že Ollama má 5h limit"
|
||||
→ /task rozpozná "zapiš si" → volá /note
|
||||
|
||||
Uživatel: "mám udělat review PR"
|
||||
→ /task rozpozná úkol bez času → volá /todo
|
||||
```
|
||||
|
||||
**Výhody:**
|
||||
- Zachovává specializaci každého skillu
|
||||
- Postupná adopce — nemusí se migrovat existující data
|
||||
- `/remind` zůstane samostatný pro cron
|
||||
|
||||
**Nevýhody:**
|
||||
- Orchestrace přes LLM je nespolehlivá (záměr se může špatně klasifikovat)
|
||||
- Uživatel stále potřebuje znát 4 commandy
|
||||
- Duplicitní kód (list, delete, search se opakují v každém skillu)
|
||||
- "Magie" — uživatel neví, kam se data vlastně uložila
|
||||
|
||||
**Verdikt:** Přidává komplexitu bez jasného benefitu. Klasifikace záměru je problém, který LLM agent řeší už teď implicitně.
|
||||
|
||||
---
|
||||
|
||||
### Varianta C: Konvergence — sloučit /keep do /note, /todo jako rozšíření /note
|
||||
|
||||
**Koncept:**
|
||||
1. **/keep** se stane aliasem na `/note add --cat keep` + `/note list --cat keep`
|
||||
2. **/note** se rozšíří o sloupec `status` (NULL = note, 'active'/'done' = todo)
|
||||
3. **/todo** je nový skill, ale volá stejný SQLite DB jako /note — jen s default filtrem `status IS NOT NULL`
|
||||
4. **/remind** zůstane samostatný (YAML + cron), ale může číst z note DB pro kontext
|
||||
|
||||
**Schéma rozšíření:**
|
||||
```sql
|
||||
ALTER TABLE notes ADD COLUMN status TEXT CHECK(status IN ('active','done','archived'));
|
||||
ALTER TABLE notes ADD COLUMN due_date TIMESTAMP; -- optional, bez notifikace
|
||||
ALTER TABLE notes ADD COLUMN priority INTEGER DEFAULT 0; -- -1 low, 0 normal, 1 high
|
||||
```
|
||||
|
||||
**Příkazy:**
|
||||
```
|
||||
/note add "Ollama limit 5h" --cat knowledge → klasická poznámka
|
||||
/note add "koupit mléko" --cat shopping --status active --due 2026-06-10 → todo v note DB
|
||||
/todo add "udělat review" --priority high → shortcut pro note s status=active
|
||||
/todo list → note list --status active
|
||||
/todo done <id> → note edit <id> --status done
|
||||
/keep add "heslo je xyz" → alias: note add --cat keep
|
||||
```
|
||||
|
||||
**Výhody:**
|
||||
- /note a /todo sdílejí storage — žádná duplicita
|
||||
- /keep se zjednoduší (odpadne custom markdown parser)
|
||||
- Uživatel může používat /note pro vše, nebo /todo pro rychlý přístup
|
||||
- Postupná migrace — /keep.md se může naimportovat do note DB
|
||||
- /remind zůstane nezměněný (žádný cron refactoring)
|
||||
|
||||
**Nevýhody:**
|
||||
- /todo skill je technicky tenká vrstva nad /note — může působit zbytečně
|
||||
- Dvě cesty k jednomu cíli (`/note add --status active` vs `/todo add`)
|
||||
|
||||
**Verdikt:** Nejpragmatičtější. Zachovává existující investici, minimalizuje duplicitu.
|
||||
|
||||
---
|
||||
|
||||
### Varianta D: /todo jako samostatný skill s vlastním storage
|
||||
|
||||
**Koncept:** Úplně nový skill, vlastní SQLite DB `db/todo.sqlite`, žádná vazba na /note.
|
||||
|
||||
**Výhody:**
|
||||
- Čistá separace concerns
|
||||
- Nezávislý vývoj
|
||||
- Jednoduché schéma optimalizované pro task tracking
|
||||
|
||||
**Nevýhody:**
|
||||
- Další DB, další skill, další maintenance
|
||||
- Uživatel musí rozhodnout: dát to do /note, /todo, nebo /remind?
|
||||
- Překryv s /note je obrovský (90% kódu by bylo stejné)
|
||||
|
||||
**Verdikt:** Nepřijatelné. Vytváří problém, který řešíš.
|
||||
|
||||
---
|
||||
|
||||
## 4. Doporučená architektura
|
||||
|
||||
### Fáze 1: Rozšířit /note o task tracking (okamžitě)
|
||||
|
||||
Rozšířit `note.py` o:
|
||||
- `status` sloupec (NULL = note, 'active'/'done'/'archived' = task)
|
||||
- `due_date` sloupec (optional)
|
||||
- `priority` sloupec (optional)
|
||||
- Příkazy: `--status`, `--due`, `--priority` v `add` a `edit`
|
||||
- `list` filtry: `--status`, `--due-before`, `--priority`
|
||||
|
||||
### Fáze 2: Vytvořit /todo jako thin wrapper (lehký skill)
|
||||
|
||||
`/todo` skill s vlastním SKILL.md, ale volá stejný `note.py` skript s přednastavenými parametry:
|
||||
|
||||
```bash
|
||||
# /todo add "udělat review" → interně:
|
||||
uv run scripts/note.py add "udělat review" --status active
|
||||
|
||||
# /todo list → interně:
|
||||
uv run scripts/note.py list --status active --sort priority,due_date
|
||||
|
||||
# /todo done <id> → interně:
|
||||
uv run scripts/note.py edit <id> --status done
|
||||
```
|
||||
|
||||
Toto je podobné patternu, který používá např. `git switch` jako alias na `git checkout`.
|
||||
|
||||
### Fáze 3: Deprecate /keep (postupně)
|
||||
|
||||
- Přidat do /note kategorii `keep`
|
||||
- Migrace: `keep.md` → import do note DB s cat=keep
|
||||
- /keep skill zůstane jako read-only legacy, nebo se stane aliasem
|
||||
|
||||
### Fáze 4: /remind integrace (volitelně, později)
|
||||
|
||||
- /remind může číst z note DB — když uživatel řekne "připomeň mi úkol #5", /remind najde note s id=5 a vytvoří reminder
|
||||
- Nebo: /remind může ukládat do note DB místo YAML (ale cron skript by musel číst SQLite — možné, ale větší změna)
|
||||
|
||||
---
|
||||
|
||||
## 5. Technické detaily /todo skillu
|
||||
|
||||
### 5.1 Schéma dat (rozšířené /note)
|
||||
|
||||
```sql
|
||||
CREATE TABLE notes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
content TEXT NOT NULL,
|
||||
category TEXT,
|
||||
status TEXT CHECK(status IN ('active','done','archived')),
|
||||
due_date TIMESTAMP,
|
||||
priority INTEGER DEFAULT 0 CHECK(priority IN (-1, 0, 1)),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_notes_status ON notes(status);
|
||||
CREATE INDEX idx_notes_due ON notes(due_date);
|
||||
CREATE INDEX idx_notes_priority ON notes(priority);
|
||||
CREATE INDEX idx_notes_category ON notes(category);
|
||||
```
|
||||
|
||||
### 5.2 Příkazy /todo
|
||||
|
||||
| Příkaz | Akce | Ekvivalent v /note |
|
||||
|--------|------|-------------------|
|
||||
| `todo add "text" [--cat] [--priority] [--due]` | Vytvoří aktivní úkol | `note add "text" --status active` |
|
||||
| `todo list [--cat] [--all]` | Seznam aktivních | `note list --status active` |
|
||||
| `todo done <id>` | Označí hotové | `note edit <id> --status done` |
|
||||
| `todo undo <id>` | Vrátí do aktivních | `note edit <id> --status active` |
|
||||
| `todo delete <id>` | Smaže | `note delete <id>` |
|
||||
| `todo search <query>` | Fulltext | `note search <query> --status active` |
|
||||
|
||||
### 5.3 Proč thin wrapper místo vlastního skriptu?
|
||||
|
||||
- **Jedna codebase:** Bugfix v note.py se projeví v obou skillech
|
||||
- **Jedna migrace:** Když se změní schéma, stačí jeden skript
|
||||
- **Konzistence:** `todo search` najde i poznámky, pokud uživatel chce
|
||||
- **Jednoduchost:** /todo SKILL.md je ~50 řádek, žádný Python kód
|
||||
|
||||
---
|
||||
|
||||
## 6. Srovnání variant
|
||||
|
||||
| Kritérium | A: Monolit | B: Orchestrace | C: Konvergence | D: Samostatný |
|
||||
|-----------|-----------|----------------|----------------|---------------|
|
||||
| Jednotné UI | ✅ | ⚠️ magie | ✅ /note+/todo | ❌ |
|
||||
| Jednotný storage | ✅ | ❌ | ✅ | ❌ |
|
||||
| Zachová /remind cron | ❌ | ✅ | ✅ | ✅ |
|
||||
| Minimální změna existujícího | ❌ | ✅ | ✅ | ✅ |
|
||||
| Žádná duplicita kódu | ✅ | ❌ | ✅ | ❌ |
|
||||
| Postupná migrace | ❌ | ✅ | ✅ | ✅ |
|
||||
| Uživatel se učí 1 command | ✅ | ❌ | ⚠️ 2 (/note, /todo) | ❌ |
|
||||
| Spolehlivost | ⚠️ komplex | ❌ LLM klasifikace | ✅ | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 7. Konkrétní doporučení
|
||||
|
||||
**Implementuj variantu C s /todo jako thin wrapper nad /note.**
|
||||
|
||||
### Kroky:
|
||||
|
||||
1. **Rozšířit `note.py`:**
|
||||
- Přidat `status`, `due_date`, `priority` do schématu (s migrací existující DB)
|
||||
- Přidat `--status`, `--due`, `--priority` do `add` a `edit`
|
||||
- Přidat `--status`, `--due-before`, `--priority` do `list`
|
||||
- Upravit výstup `list` — pro status != NULL zobrazit `[ ]` / `[x]` prefix
|
||||
|
||||
2. **Vytvořit `/todo` skill:**
|
||||
- SKILL.md s příkazy, které volají `note.py` s přednastavenými parametry
|
||||
- Žádný vlastní Python kód (nebo minimální wrapper skript)
|
||||
- `todo add` → `note add --status active`
|
||||
- `todo list` → `note list --status active --sort priority,due_date`
|
||||
- `todo done` → `note edit --status done`
|
||||
|
||||
3. **Deprecate `/keep`:**
|
||||
- Přidat do /note podporu pro `--cat keep`
|
||||
- Volitelně: import skript pro `keep.md`
|
||||
- /keep SKILL.md upravit na aliasy
|
||||
|
||||
4. **Ponechat `/remind` nezměněný:**
|
||||
- YAML + cron je technicky odůvodněný
|
||||
- Později: integrační bod — /remind může číst z note DB
|
||||
|
||||
### Příklad použití po implementaci:
|
||||
|
||||
```
|
||||
# Rychlá poznámka
|
||||
> note add "Ollama limit 5h" --cat knowledge
|
||||
|
||||
# Úkol bez deadlinu
|
||||
> todo add "refactor auth module" --priority high
|
||||
|
||||
# Úkol s deadlinem (bez notifikace)
|
||||
> todo add "odeslat fakturu" --due 2026-06-10 --priority high
|
||||
|
||||
# Připomínka s notifikací
|
||||
> remind add "odeslat fakturu" at 2026-06-10T09:00
|
||||
|
||||
# Seznam všech aktivních úkolů
|
||||
> todo list
|
||||
[ ] #12 refactor auth module [high]
|
||||
[ ] #15 odeslat fakturu [high] due: 2026-06-10
|
||||
|
||||
# Seznam všech poznámek a úkolů
|
||||
> note list --cat knowledge
|
||||
#7 Ollama limit 5h [knowledge]
|
||||
|
||||
# Hotovo
|
||||
> todo done 12
|
||||
|
||||
# Hledání přes všechno
|
||||
> note search "faktura"
|
||||
#15 [active] odeslat fakturu
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Rizika a mitigace
|
||||
|
||||
| Riziko | Mitigace |
|
||||
|--------|----------|
|
||||
| Migrace existující note DB | `note.py` musí detekovat staré schéma a přidat sloupce automaticky |
|
||||
| /todo jako wrapper je "podvod" | Dokumentovat v SKILL.md — uživatel chápe, že /todo je pohled na /note |
|
||||
| Uživatel ztratí přehled co kam dát | Jasné pravidlo: potřebuješ notifikaci? → /remind. Úkol bez notifikace? → /todo. Čistá informace? → /note. |
|
||||
| /keep uživatelé ztratí data | Import skript + /keep zůstane read-only dočasně |
|
||||
|
||||
---
|
||||
|
||||
## 9. Závěr
|
||||
|
||||
**Nejlepší cesta je konvergence, ne monolit.**
|
||||
|
||||
- `/note` se stane univerzálním storage pro všechny "item" typy (poznámky, úkoly, keep)
|
||||
- `/todo` je pohled (view) na `/note` — uživatelsky přívětivý, technicky tenký
|
||||
- `/remind` zůstane samostatný kvůli cron architektuře
|
||||
- `/keep` se postupně absorbuje do `/note --cat keep`
|
||||
|
||||
Toto dává:
|
||||
- **Jednotný storage** (SQLite)
|
||||
- **Jednu codebase** na CRUD (note.py)
|
||||
- **Specializované UI** pro každý use case (/note, /todo, /remind)
|
||||
- **Postupnou migraci** bez big-bang
|
||||
- **Technickou správnost** (cron zůstává mimo LLM agenta)
|
||||
260
results/2026-06-10_remind-skill-sqlite-redesign.md
Normal file
260
results/2026-06-10_remind-skill-sqlite-redesign.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# /remind skill — návrh přechodu z YAML na SQLite
|
||||
|
||||
## 1. Proč SQLite
|
||||
|
||||
| Aspekt | YAML (současné) | SQLite (navrhované) |
|
||||
|--------|-----------------|---------------------|
|
||||
| Atomicita | tmp+rename, žádné transakce | `BEGIN` … `COMMIT` |
|
||||
| Query | Načíst celý soubor do paměti | SELECT s JOIN a indexy |
|
||||
| Dedup | Externí `.reminder_state.json` | Tabulka `reminder_fires` |
|
||||
| Datové typy | Vše string | INTEGER, TEXT ISO, CHECK |
|
||||
| Edit | Chybí (celý záznam se přepisuje) | UPDATE / DELETE per sloupec |
|
||||
| Testy | File-based, side-effects | `:memory:` databáze |
|
||||
| Audit | Žádný | `reminder_fires.status` + `error_message` |
|
||||
|
||||
## 2. Navrhované schéma
|
||||
|
||||
```sql
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
-- Hlavní entita -----------------------------------------------------------
|
||||
CREATE TABLE reminders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
timezone TEXT NOT NULL DEFAULT 'Europe/Prague',
|
||||
created_at TEXT NOT NULL, -- ISO-8601
|
||||
updated_at TEXT NOT NULL, -- ISO-8601
|
||||
deleted_at TEXT -- soft-delete, NULL = aktivní
|
||||
);
|
||||
|
||||
-- One-time scheduly -------------------------------------------------------
|
||||
CREATE TABLE schedule_at (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
reminder_id INTEGER NOT NULL REFERENCES reminders(id) ON DELETE CASCADE,
|
||||
at_datetime TEXT NOT NULL, -- ISO-8601 (lokální čas dle reminders.timezone)
|
||||
enabled INTEGER NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
-- Recurring cron scheduly -------------------------------------------------
|
||||
CREATE TABLE schedule_cron (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
reminder_id INTEGER NOT NULL REFERENCES reminders(id) ON DELETE CASCADE,
|
||||
cron_expr TEXT NOT NULL, -- standardní cron, např. "0 9 * * 1-5"
|
||||
enabled INTEGER NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
-- Random scheduly ---------------------------------------------------------
|
||||
CREATE TABLE schedule_random (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
reminder_id INTEGER NOT NULL REFERENCES reminders(id) ON DELETE CASCADE,
|
||||
times_per_day INTEGER NOT NULL,
|
||||
window_start_min INTEGER NOT NULL, -- 0..1439 (minuty od půlnoci)
|
||||
window_end_min INTEGER NOT NULL, -- 0..1440 (výhradně horní mez)
|
||||
days_filter TEXT, -- např. "1-5", NULL = každý den
|
||||
from_date TEXT, -- YYYY-MM-DD, NULL = okamžitě
|
||||
until_date TEXT, -- YYYY-MM-DD, NULL = navždy
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
CHECK(times_per_day >= 1),
|
||||
CHECK(window_start_min >= 0 AND window_start_min < 1440),
|
||||
CHECK(window_end_min > 0 AND window_end_min <= 1440),
|
||||
CHECK(window_start_min < window_end_min)
|
||||
);
|
||||
|
||||
-- Audit / dedup / delivery log --------------------------------------------
|
||||
CREATE TABLE reminder_fires (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
reminder_id INTEGER NOT NULL REFERENCES reminders(id) ON DELETE CASCADE,
|
||||
schedule_id INTEGER NOT NULL, -- ID v příslušné schedule_* tabulce
|
||||
schedule_type TEXT NOT NULL CHECK(schedule_type IN ('at','cron','random')),
|
||||
fire_time TEXT NOT NULL, -- ISO-8601, plánovaný čas výstřelu
|
||||
delivered_at TEXT, -- ISO-8601, skutečný čas doručení
|
||||
status TEXT NOT NULL DEFAULT 'pending'
|
||||
CHECK(status IN ('pending','delivered','failed')),
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
-- Indexy ------------------------------------------------------------------
|
||||
CREATE INDEX idx_reminders_text ON reminders(text);
|
||||
CREATE INDEX idx_fire_lookup ON reminder_fires(
|
||||
reminder_id, schedule_type, schedule_id, fire_time, status
|
||||
);
|
||||
CREATE INDEX idx_at_datetime ON schedule_at(reminder_id, at_datetime);
|
||||
CREATE INDEX idx_cron_expr ON schedule_cron(reminder_id, cron_expr);
|
||||
```
|
||||
|
||||
## 3. Lepší datové typy oproti YAML
|
||||
|
||||
| Pole (YAML) | SQLite sloupec | Proč lepší |
|
||||
|-------------|----------------|------------|
|
||||
| `times_per_day: "5"` (string v YAML) | `times_per_day INTEGER` | Nativní číselná validace, CHECK constraint |
|
||||
| `window: "09:00-21:00"` (string) | `window_start_min INTEGER`, `window_end_min INTEGER` | Umožňuje matematiku (`fire_minute BETWEEN 540 AND 1260`), sortable |
|
||||
| `at: "2026-06-10T10:00:00"` | `at_datetime TEXT` | Sice stále TEXT, ale ISO formát je porovnatelný a sortable; SQLite nemá nativní datetime |
|
||||
| `days: "1-5"` | `days_filter TEXT` | Zůstává TEXT — parsuje se až při běhu; alternativně normalizovat na `random_days(day_of_week INT)`, ale pro 1–5 položek to je overkill |
|
||||
| `from` / `until` | `from_date TEXT`, `until_date TEXT` | ISO date je sortable; pro query stačí `date <= '2026-06-10'` |
|
||||
|
||||
**Poznámka k časům:** SQLite nemá nativní `DATETIME` typ. Doporučuji ukládat jako **TEXT v ISO-8601** (např. `2026-06-10T10:00:00+02:00`) místo Unix timestampu — je to čitelné, sortable a přímo použitelné s `datetime.fromisoformat()`.
|
||||
|
||||
## 4. Jednotlivé typy časů — proč 1:N a ne jedna tabulka
|
||||
|
||||
Současný YAML model:
|
||||
```yaml
|
||||
- text: "water the plants"
|
||||
cron_exprs: ["0 19 * * *"]
|
||||
random: {times_per_day: 2, window: "08:00-12:00"}
|
||||
```
|
||||
|
||||
V DB to rozdělíme na **jeden řádek `reminders`** + **řádky v `schedule_cron` a `schedule_random`**. Důvody:
|
||||
|
||||
- **Normalizace**: Každý schedule má svůj životní cyklus — jde zapnout/vypnout, editovat, mazat bez dotyku ostatních.
|
||||
- **Dedup**: `reminder_fires` odkazuje na konkrétní `schedule_id` + `schedule_type`. Víme přesně, který cron nebo random výstřel už byl doručen.
|
||||
- **Extensibility**: Přidání nového typu schedule = nová tabulka, není potřeba migrovat existující data.
|
||||
|
||||
## 5. Dává smysl ukládat cron jako cron string?
|
||||
|
||||
**Ano.**
|
||||
|
||||
- Cron je de facto standard, `croniter` ho umí parsovat i expandovat (`get_prev` / `get_next`).
|
||||
- Rozparsování na `cron_minute INT`, `cron_hour INT` atd. by ztratilo expresivitu (`*/15`, `L`, ranges, step values).
|
||||
- Ukládání jako cron string je kompaktní a čitelné.
|
||||
|
||||
Random schedule **nelze** vyjádřit jako cron — je to vlastní algoritmus `compute_fire_times()`. Proto má samostatnou tabulku s parametry.
|
||||
|
||||
## 6. Dedup a state — z `.reminder_state.json` do DB
|
||||
|
||||
Současný mechanismus:
|
||||
```python
|
||||
key = hashlib.sha1(text.encode()).hexdigest()[:8]
|
||||
last = state.get(key) # "2026-06-10T09:20:00"
|
||||
```
|
||||
|
||||
Problém: hash textu je hrubý — změna textu znamená nový key, stejný text = stejný key pro všechny scheduly.
|
||||
|
||||
Nový mechanismus v SQLite:
|
||||
```sql
|
||||
-- Před odesláním:
|
||||
SELECT 1 FROM reminder_fires
|
||||
WHERE reminder_id = ? AND schedule_id = ? AND schedule_type = ?
|
||||
AND fire_time = ? AND status = 'delivered';
|
||||
```
|
||||
|
||||
- Přesná dedup **per schedule**, ne per text.
|
||||
- `status = 'failed'` umožňuje retry při příštím běhu.
|
||||
- `error_message` zachytí proč Telegram API selhalo.
|
||||
- `delivered_at` je audit trail.
|
||||
|
||||
## 7. Query pro remind_send.py (místo načítání celého YAML)
|
||||
|
||||
```sql
|
||||
-- Najít všechny due fires za posledních 60 sekund
|
||||
SELECT
|
||||
r.id AS reminder_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 sa.enabled = 1
|
||||
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'
|
||||
)
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Cron: vypočítat v Pythonu přes croniter, ale DB řekne které expr existují
|
||||
SELECT r.id, r.text, 'cron', sc.id, sc.cron_expr
|
||||
FROM reminders r
|
||||
JOIN schedule_cron sc ON sc.reminder_id = r.id
|
||||
WHERE r.enabled = 1 AND sc.enabled = 1;
|
||||
-- croniter.get_prev() se provede v Pythonu, pak se porovná s now-60s
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Random: všechny aktivní random scheduly
|
||||
SELECT r.id, r.text, 'random', sr.id, NULL
|
||||
FROM reminders r
|
||||
JOIN schedule_random sr ON sr.reminder_id = r.id
|
||||
WHERE r.enabled = 1 AND sr.enabled = 1
|
||||
AND (sr.from_date IS NULL OR sr.from_date <= date('now'))
|
||||
AND (sr.until_date IS NULL OR sr.until_date >= date('now'));
|
||||
-- compute_fire_times(date.today(), ...) se provede v Pythonu
|
||||
```
|
||||
|
||||
## 8. CLI změny
|
||||
|
||||
`remind_edit.py` zachová stejné CLI rozhraní, backend se změní:
|
||||
|
||||
| Subcommand | Změna |
|
||||
|------------|-------|
|
||||
| `list` | SQL JOIN místo `yaml.safe_load` + JSON dump |
|
||||
| `add` | `INSERT INTO reminders` + `INSERT INTO schedule_*` v jedné transakci |
|
||||
| `remove --keyword` | `SELECT id FROM reminders WHERE text LIKE '%keyword%'` → `DELETE` nebo `UPDATE deleted_at` |
|
||||
| **nové** `edit --keyword` | `UPDATE reminders.text` nebo přidání/odebrání schedulů |
|
||||
| **nové** `enable` / `disable` | `UPDATE reminders SET enabled = 0/1` |
|
||||
|
||||
## 9. Další věci k uvážení
|
||||
|
||||
### 9.1 Timezone
|
||||
- Všechny `at_datetime` a `fire_time` by měly být **aware** (s offsetem `+02:00`) nebo explicitně v `reminders.timezone`.
|
||||
- Cron výrazy jsou vždy v lokální čase — `croniter` běží nad `datetime.now(TZ)`.
|
||||
- Doporučení: ukládat jako **TEXT s offsetem** (`2026-06-10T10:00:00+02:00`), při query převádět v Pythonu.
|
||||
|
||||
### 9.2 WAL mode
|
||||
```sql
|
||||
PRAGMA journal_mode = WAL;
|
||||
```
|
||||
Umožní čtení během zápisu. Pro remind_send.py (každou minutu SELECT) + remind_edit.py (občasný INSERT/UPDATE) je to kritické.
|
||||
|
||||
### 9.3 Schema versioning
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS _schema_version (version INTEGER PRIMARY KEY);
|
||||
INSERT INTO _schema_version VALUES (1);
|
||||
```
|
||||
Při startu skriptu zkontrolovat verzi a spustit migrace.
|
||||
|
||||
### 9.4 Testování
|
||||
- SQLite podporuje `:memory:` databázi — testy mohou běžet bez file I/O.
|
||||
- `remind_edit.py` dostane parametr `--db PATH` (default `workspace/db/reminders.sqlite`).
|
||||
|
||||
### 9.5 Migrace z YAML
|
||||
Jednorázový skript:
|
||||
1. Načíst `reminder.yaml`
|
||||
2. `BEGIN TRANSACTION`
|
||||
3. Pro každý reminder: `INSERT INTO reminders` → získat `lastrowid`
|
||||
4. Podle polí `at` / `at_times` / `cron_exprs` / `random` vložit do příslušných schedule tabulek
|
||||
5. `COMMIT`
|
||||
6. Přejmenovat `reminder.yaml` → `reminder.yaml.bak`
|
||||
|
||||
### 9.6 Soft delete vs hard delete
|
||||
- `deleted_at TEXT` místo `DELETE FROM reminders` — zachová historii a umožní "undo".
|
||||
- `remind_edit.py remove` by default nastaví `deleted_at`, `--hard` by provedl skutečný DELETE.
|
||||
|
||||
### 9.7 FTS5 (volitelně)
|
||||
Pokud bude >100 reminderů, `CREATE VIRTUAL TABLE reminders_fts USING fts5(text)` urychlí fulltext vyhledávání pro `remove --keyword`.
|
||||
|
||||
### 9.8 Konfigurace cesty k DB
|
||||
```python
|
||||
DEFAULT_DB = Path(__file__).resolve().parent.parent.parent.parent / "db" / "reminders.sqlite"
|
||||
```
|
||||
Podle pravidel v AGENTS.md: *Always store SQLite databases under `db/*.sqlite`*.
|
||||
|
||||
## 10. Shrnutí rozhodnutí
|
||||
|
||||
| Otázka | Rozhodnutí |
|
||||
|--------|------------|
|
||||
| Ukládat cron jako string? | **Ano** — standard, expresivní, croniter to zvládne. |
|
||||
| Random do cron stringu? | **Ne** — random je vlastní algoritmus, ukládat parametry. |
|
||||
| Jedna tabulka vs schedule tabulky? | **3 schedule tabulky** (at, cron, random) — 1:N vztah. |
|
||||
| Dedup externě nebo v DB? | **V DB** — `reminder_fires` per schedule. |
|
||||
| Časy jako TEXT nebo INTEGER? | **TEXT ISO-8601** — čitelné, sortable, Python-compatible. |
|
||||
| Window jako string nebo minuty? | **INTEGER minuty** — umožňuje SQL matematiku. |
|
||||
| Hard delete nebo soft delete? | **Soft delete** (`deleted_at`) — audit trail. |
|
||||
| Transakce? | **Ano** — každý `add` / `remove` / `edit` v `BEGIN…COMMIT`. |
|
||||
141
results/remind-skill-audit-2026-06-02.md
Normal file
141
results/remind-skill-audit-2026-06-02.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Deep Research Audit: /remind Skill
|
||||
|
||||
**Datum:** 2026-06-02
|
||||
**Model:** GLM-5.1:cloud
|
||||
**Scope:** SKILL.md, remind_edit.py, remind_send.py, random_times.py, testy, reminder.yaml, .reminder_state.json, log, crontab
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Kritické problémy
|
||||
|
||||
### 1. Žádný `update` příkaz
|
||||
`remind_edit.py` má jen `list`, `add`, `remove`. Když chceš změnit čas existujícího reminderu, musíš ho smazat a vytvořit znovu. To je nebezpečné — `remove` matchuje substring, takže při recreate můžeš trefit špatný záznam nebo vytvořit duplikát.
|
||||
|
||||
**Návrh:** Přidat `update --keyword "..." --cron/--at/--random-*` příkaz, který najde reminder a upraví jen zadaná pole.
|
||||
|
||||
### 2. `at` remindery se nikdy nesmažou (no garbage collection)
|
||||
Jednorázové `at` remindery zůstávají v `reminder.yaml` navždy. Po odeslání se jen přestanou spouštět, ale leží v YAML a loadují se každý minutovým cronem. Po čase tam bude stovky mrtvých záznamů.
|
||||
|
||||
**Návrh:** `remind_send.py` by měl po úspěšném doručení `at` reminderu zapsat flag nebo rovnou zavolat `remind_edit.py remove`. Nebo lépe — přidat `purge` subcommand, který smaže všechny `at` remindery s `at_time < now`.
|
||||
|
||||
### 3. Žádné stabilní ID — `remove` matchuje substring
|
||||
`remove --keyword "boty"` by smazal "objednat boty xshoes", ale taky "koupit boty pro dědu". Substring match na textu je křehký.
|
||||
|
||||
**Návrh:** Přidat `id` pole (hash nebo UUID) přiřazené při `add`. `remove` i `update` by primárně pracovaly s `--id`. `--keyword` by zůstal jako fallback.
|
||||
|
||||
### 4. Deduplikace přes SHA1(text)[:8] — kolize a křehkost
|
||||
`.reminder_state.json` klíč je `hashlib.sha1(text.encode())[:8]` — 4 bajty hex. Při ~65k reminderů je kolize pravděpodobná. Horší: když se text změní (i jen překlep), dedup key se změní a reminder se odešle znovu.
|
||||
|
||||
**Návrh:** Použít stabilní `id` z bodu 3 jako klíč do state. SHA1[:8] zahodit.
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Střední problémy
|
||||
|
||||
### 5. Hardcoded `CHAT_ID` v remind_send.py
|
||||
`CHAT_ID = "8826147089"` je natvrdo v kódu. Když se změní uživatel nebo přidá druhý, musí se upravovat zdroják.
|
||||
|
||||
**Návrh:** Číst `chat_id` z `config.json` (tam už je token), nebo z `reminder.yaml` jako globální `default_chat_id`.
|
||||
|
||||
### 6. Žádná validace `at` časů v budoucnosti
|
||||
`remind_edit.py` přijme `--at "2020-01-01T00:00:00"` bez chyby. Zápis v minulosti nedává smysl a nikdy se nespustí.
|
||||
|
||||
**Návrh:** Validovat `at > now()` v `cmd_add`. Případně alespoň varování na stderr.
|
||||
|
||||
### 7. Žádný max-retry / TTL pro neodeslané remindery
|
||||
Když Telegram API vrátí chybu, `remind_send.py` zkusí znovu příští minutu — ale jen pokud `last` state nebyl nastaven. Když selže 100x po sobě, zkusí to 100x. Žádný TTL ani exponential backoff.
|
||||
|
||||
**Návrh:** Přidat retry count do state. Po 3 selháních označit jako `failed` a přestat zkoušet. Nebo jednoduše: po 5 minutách od first fire time přestat retryovat.
|
||||
|
||||
### 8. Identity check bug v `cmd_remove`
|
||||
```python
|
||||
data["reminders"] = [r for r in data["reminders"] if r is not removed]
|
||||
```
|
||||
`is not` je identity check. Funguje, protože `matches[0]` je reference na stejný dict v seznamu, ale je to křehké — jakýkoliv refaktoring (deep copy, reload) to rozbije.
|
||||
|
||||
**Návrh:** Použít index nebo `id`-based filter.
|
||||
|
||||
### 9. Chybí dokumentace k `at_times` (multi-at)
|
||||
SKILL.md dokumentuje `--at` jako "repeatable", ale `remind_send.py` zpracovává `at_times` pole, zatímco SKILL.md ho nezmíní jako samostatný koncept. Uživatel (nebo LLM) může být zmatený.
|
||||
|
||||
**Návrh:** Doplnit SKILL.md o příklad multi-at.
|
||||
|
||||
---
|
||||
|
||||
## 🔵 Zlepšení kódu
|
||||
|
||||
### 10. Přechod z YAML na SQLite
|
||||
YAML je lidsky čitelný, ale:
|
||||
- Atomic write přes `.tmp` + `os.replace` je správný, ale zbytečně složitý
|
||||
- YAML nemá schema, snadno se rozbije ruční editací
|
||||
- Dotazy (list, search) vyžadují full load
|
||||
|
||||
**Návrh:** Přesunout data do `db/reminders.sqlite` (konvence `db/*.sqlite`). YAML nechat jako read-only export nebo zahodit. `remind_edit.py` by pracoval s SQLite, `remind_send.py` taky. Výhody: ID autoincrement, atomicity zdarma, snadný search, žádný parse overhead.
|
||||
|
||||
### 11. Cachování Telegram tokenu
|
||||
`_telegram_token()` čte a parsuje `config.json` každou minutu. Soubor se nemění.
|
||||
|
||||
**Návrh:** Načíst jednou při startu, cachovat v modulu. Nebo ještě lépe — environment variable `TELEGRAM_BOT_TOKEN`.
|
||||
|
||||
### 12. Log enrichment
|
||||
`reminder.log` má jen `timestamp text`. Chybí: delivery status, fire time vs actual send time, reminder ID.
|
||||
|
||||
**Návrh:** Formát: `{ts} {id} {fire_time} {status} {text}`
|
||||
|
||||
### 13. `--dry-run` flag pro `add`
|
||||
Užitečné pro LLM skill workflow — ukáže, co by se přidalo, bez zápisu.
|
||||
|
||||
### 14. Test coverage — chybí testy pro remind_edit.py a remind_send.py
|
||||
Testy pokrývají jen `random_times.py`. `remind_edit.py` (CRUD) a `remind_send.py` (dedup, fire detection) nemají žádné testy.
|
||||
|
||||
**Návrh:** Přidat unit testy pro:
|
||||
- `cmd_add` s různými kombinacemi flagů
|
||||
- `cmd_remove` s 0/1/N matches
|
||||
- `_due_fire` s různými typy reminderů
|
||||
- Dedup state management
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Chybějící funkce
|
||||
|
||||
### 15. Pause / disable reminder
|
||||
Nemáš způsob jak reminder dočasně vypnout bez smazání. Běžný use case: "nech mě týden na pokoji".
|
||||
|
||||
**Návrh:** Přidat `enabled: true/false` pole. `remind_send.py` by skipoval `enabled: false`. Příkaz `remind_edit.py pause --id X` / `resume --id X`.
|
||||
|
||||
### 16. Cron s end date
|
||||
Cron remindery běží navždy. Chybí `until` datum pro cron (podobně jako `random` má `from`/`until`).
|
||||
|
||||
**Návrh:** Přidat `until` pole na úroveň reminderu. `remind_send.py` by po `until` datumu reminder přeskočil.
|
||||
|
||||
### 17. Snooze
|
||||
Když reminder přijde a uživatel není připraven, nemá jak ho odložit. To by vyžadovalo interakci s Telegram botem (callback button), což je mimo současný scope, ale je to přirozené rozšíření.
|
||||
|
||||
### 18. `list --due` nebo `list --next`
|
||||
Užitečné zobrazit jen remindery, které se spustí v následujících N hodin. SKILL.md to neumožňuje.
|
||||
|
||||
**Návrh:** Přidat `list --due-within 2h` nebo `list --next 5`.
|
||||
|
||||
### 19. Per-reminder timezone
|
||||
SKILL.md říká "Timezone is always Europe/Prague". To je OK pro jednoho uživatele, ale kód je tight-coupled — `TZ` je konstanta v `remind_send.py`. Pro multi-user by to muselo být konfigurovatelné.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Prioritizovaný implementační plán
|
||||
|
||||
| Priorita | Co | Proč |
|
||||
|----------|----|------|
|
||||
| **P0** | Stabilní ID + dedup fix (body 3, 4) | Bez toho hrozí kolize a duplikátní doručení |
|
||||
| **P0** | Garbage collection `at` reminderů (bod 2) | YAML poroste donekonečna |
|
||||
| **P0** | Identity check fix v remove (bod 8) | Tichý bug, dnes funguje náhodou |
|
||||
| **P1** | `update` příkaz (bod 1) | Zásadní UX zlepšení, snižuje riziko chyb |
|
||||
| **P1** | Validace `at` v budoucnosti (bod 6) | Prevence nesmyslných vstupů |
|
||||
| **P1** | Retry TTL (bod 7) | Prevence nekonečných retry |
|
||||
| **P2** | SQLite backend (bod 10) | Architektonické zlepšení, ale není urgentní |
|
||||
| **P2** | Testy pro edit/send (bod 14) | Spolehlivost |
|
||||
| **P2** | `pause`/`resume` (bod 15) | Užitečná funkce |
|
||||
| **P2** | `until` pro cron (bod 16) | Užitečná funkce |
|
||||
| **P3** | Chat ID z configu (bod 5) | Multi-user příprava |
|
||||
| **P3** | Log enrichment (bod 12) | Debugovatelnost |
|
||||
| **P3** | `--dry-run` (bod 13) | Vývojářská ergonomie |
|
||||
| **P3** | `list --due` (bod 18) | Nice-to-have |
|
||||
87
results/remind-skill-top5-priorities-2026-06-02.md
Normal file
87
results/remind-skill-top5-priorities-2026-06-02.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# /remind Skill — Top 5 Priorities (Merged from Two Audits)
|
||||
|
||||
**Datum:** 2026-06-02
|
||||
**Model:** GLM-5.1:cloud
|
||||
**Zdroje:** `results/2026-06-02_remind-skill-analysis-and-improvements.md` + `results/remind-skill-audit-2026-06-02.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. Stabilní ID + dedup fix (nahradit SHA1[:8] + substring match)
|
||||
|
||||
**Problém:** Dva propojené bugy:
|
||||
- `.reminder_state.json` používá `SHA1(text)[:8]` jako dedup klíč — 4 bajty hex, kolize při ~65k reminderů. Změna textu (i překlep) vytvoří nový klíč → duplikátní doručení.
|
||||
- `remove` matchuje substring — `remove --keyword "boty"` smaže i "koupit boty pro dědu".
|
||||
- `cmd_remove` používá `is not` identity check — funguje jen díky referenční shodě, po refaktoringu (deep copy, reload) se rozbije.
|
||||
|
||||
**Řešení:**
|
||||
- Přidat `id` pole (UUID nebo short hash z text+timestamp) přiřazené při `add`.
|
||||
- `remove` i `update` primárně přes `--id`, `--keyword` jako fallback.
|
||||
- State file klíč → stabilní `id` místo SHA1[:8].
|
||||
- `cmd_remove` filtrovat přes `id` nebo index, ne přes `is not`.
|
||||
|
||||
**Dopad:** Zabrání tichým datovým ztrátám a duplikátům. Bez toho je celý skill nespolehlivý.
|
||||
|
||||
---
|
||||
|
||||
## 2. Garbage collection `at` reminderů + deduplikace při doručení
|
||||
|
||||
**Problém:** Dva propojené bugy:
|
||||
- Jednorázové `at` remindery zůstávají v `reminder.yaml` navždy. Po odeslání se jen přestanou spouštět, ale loadují se každý minutovým cronem. Po měsících tam budou stovky mrtvých záznamů.
|
||||
- `should_fire()` má 60s okno — s minutovým cronem může `at` reminder doručit dvakrát (např. při dvojím spuštění cronu nebo časovém posunu). Log ukazuje, že to zatím proběhlo OK, ale není to garantováno.
|
||||
|
||||
**Řešení:**
|
||||
- `remind_send.py` po úspěšném doručení `at` reminderu: buď ho smazat z YAML, nebo přidat `purge` subcommand pro ruční cleanup.
|
||||
- Zužit existující state file pro dedup: zúžit okno na `0 <= delta < 30` a kontrolovat, zda už byl ve stejném minutovém okně doručen (state file už existuje, jen má špatný klíč — viz bod 1).
|
||||
- Alternativně: SQLite state tabulka `fired(text, scheduled_at, fired_at)` s `PRIMARY KEY(text, scheduled_at)`.
|
||||
|
||||
**Dopad:** Zabrání spamu a nekonečnému růstu YAML souboru.
|
||||
|
||||
---
|
||||
|
||||
## 3. Atomic writes + odstranění ruční YAML konstrukce
|
||||
|
||||
**Problém:** Dva propojené problémy v `remind_edit.py`:
|
||||
- Zápis do `reminder.yaml` je neatomický — `with open(REMINDER_FILE, "w")` může při crashu zanechat prázdný/s poškozený soubor = ztráta všech reminderů.
|
||||
- `format_reminder()` ručně skládá YAML stringy (`f"- text: {text}"`) — neescapuje speciální znaky (uvozovky, dvojtečky, newlines), nedrží konzistentní odsazení, duplikuje logiku ruamel.yaml.
|
||||
|
||||
**Řešení:**
|
||||
- Atomic write: `tmp = path.with_suffix(".tmp")` → `yaml.dump(data, f)` → `os.replace(tmp, path)`.
|
||||
- Nahradit `format_reminder()` builděním dictu a `yaml.dump()` celého dokumentu. Použít `ruamel.yaml.scalarstring.LiteralScalarString` přímo z knihovny (smazat vlastní třídu).
|
||||
- Přidat validaci před zápisem (schema check).
|
||||
|
||||
**Dopad:** Zabrání ztrátě dat a tichým YAML parse chybám. Největší robustness win s minimálním úsilím.
|
||||
|
||||
---
|
||||
|
||||
## 4. `update` příkaz + `list` příkaz
|
||||
|
||||
**Problém:**
|
||||
- `remind_edit.py` má jen `add` a `remove`. Změna času = smazat a vytvořit znovu — rizikové (viz bod 1, substring match).
|
||||
- `list` je dokumentovaný v SKILL.md, ale v kódu neexistuje. Uživatel (nebo LLM) nemá jak zkontrolovat aktuální stav.
|
||||
|
||||
**Řešení:**
|
||||
- Přidat `update --id X [--cron ...] [--at ...] [--random-* ...]` — najde reminder a upraví jen zadaná pole.
|
||||
- Přidat `list` — vypíše všechny remindery s ID, textem a typem schedule.
|
||||
- Přidat `--dry-run` k `add` a `update` pro bezpečné testování.
|
||||
|
||||
**Dopad:** Zásadní UX zlepšení, snižuje riziko chyb při úpravách, doplňuje chybějící dokumentovanou funkci.
|
||||
|
||||
---
|
||||
|
||||
## 5. Testy pro `remind_edit.py` a `remind_send.py`
|
||||
|
||||
**Problém:** Testy pokrývají jen `random_times.py`. Dva hlavní skripty (CRUD operace, dedup, fire detection, YAML I/O) nemají žádné testy. Jakákoliv změna v bodech 1–4 bez testů = riziko regresí.
|
||||
|
||||
**Řešení:** Přidat unit testy pro:
|
||||
- `cmd_add` s různými kombinacemi flagů (cron, at, random)
|
||||
- `cmd_remove` s 0/1/N matches, substring kolize
|
||||
- `should_fire` s různými typy reminderů a okraji časových oken
|
||||
- Dedup state management (nový i starý formát)
|
||||
- Atomic write (crash uprostřed zápisu)
|
||||
- Validace `at` v budoucnosti
|
||||
|
||||
**Dopad:** Bez testů je jakýkoliv refaktoring hazard. S testy se body 1–4 dají implementovat bez strachu z regresí.
|
||||
|
||||
---
|
||||
|
||||
*Zbylé návrhy (SQLite backend, pause/resume, cron until, retry TTL, log enrichment, chat_id z configu, per-reminder TZ) jsou P2–P3 — užitečné, ale nejsou blokátory.*
|
||||
Reference in New Issue
Block a user