Zalohovani vsech podstatnych souboru

This commit is contained in:
lachtan
2026-06-10 06:39:52 +02:00
parent 1e10891945
commit 67e29c8b88
69 changed files with 9115 additions and 0 deletions

View 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.

View 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.*

View File

@@ -0,0 +1,69 @@
# Přehled modelů Ollama Cloud (červen 2026)
**Datum:** 20260607
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í nanobotagenta. 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ů (SWEBench, TerminalBench, Code Arena, MCPAtlas, HLE atd.).
---
## 1. Tabulka přehledu
| Model | Rychlost (tok/s) | Inteligence (benchmark) | Toolcalling | Čeština / Multilingual | Kontext | Multimodální | Licence | Verbosita | Známé bugy / blokátory | Cena (OpenRouter proxy) | Longhorizon stabilita | Selfhost možnost |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| **glm-5.1:cloud** | ~198 (přímé měření) | SWEBench Pro 58.4% Code Arena Elo1530 | 99.6% schema adherence, žádné známé selhání | Žádná oficiální podpora češtiny, ale žádný drift | 200K (198K Ollama) | ❌ (textonly) | MIT | Nízká | | ~$4/M output (Ollama Pro flatrate $20/mo) | Ověřeno stovkami kol nejlepší | Ano (vyžaduje ~30GB VRAM) |
| **deepseek-v4-flash:cloud** | ~3050* (odhad) | SWEBench ~75% (odhad) | Dobrá | Žádná explicitní podpora češtiny, ale silná multilingvní skóre (MMMLU90.3) | 1M | ❌ | MIT | Střední | | $0.14/M in | Neověřeno (uživatelské benchmarky) | Ano (efektivní MoE, 13B aktivních) |
| **qwen3.5:397b-cloud** | ~1020* (odhad) | SWEBench 6670% | Dobrá | **201 jazyk včetně češtiny** (oficiální) | 1M | ✅ | Apache2.0 | Střední | Uživatelé hlásí pomalost a přesnostní problémy | (žádná proxy cena, Ollama Pro) | Neověřeno | Ano (Apache2.0, vyžaduje velké GPU) |
| **deepseek-v4-pro:cloud** | ~15.4 (přímé měření) | SWEBench 80.6% (nejvyšší) | Dobrá | Žádná explicitní podpora češtiny | 1M | ❌ | MIT | Střední | Extrémní latence (57s TTFT), vysoká variabilita | $1.74/M in | Neznámo | Ano (MIT) |
| **devstral-2:123b-cloud** | ~4060* (odhad) | SWEBench 72.2% TerminalBench 77.3% (nejvyšší) | Dobrá | Žádná explicitní podpora češtiny | 128K | ❌ | Apache2.0 | Střední | | | Neověřeno | Ano (Apache2.0) |
| **gemma4:31b-cloud** | ~80120* (odhad) | SWEBench ~52% Code Arena nízké | Native function calling | 140+ jazyků (neuvádí češtinu) | 256K | ✅ | Apache2.0 | Nízká | | | Neověřeno | Ano (31B dense) |
| **nemotron-3-ultra:cloud** | ~5080* (odhad) | SWEBench ~6079% (odhad) | Neznámo | Žádná oficiální podpora češtiny | 200K | ❌ | NVIDIA Open License | Nízká | Příliš nový žádná data o toolcallingu | $0.60/M in | Neověřeno | Ano (vyžaduje NVIDIAspecifické kvantování) |
| **nemotron-3-super:cloud** | ~60100* (odhad) | SWEBench 60.47% (odhad) | Neznámo | Žádná podpora češtiny | 1M | ❌ | NVIDIA Open License | Nízká | | | Neověřeno | Ano (vyžaduje NVIDIAspecifické kvantování) |
| **minimax-m3:cloud** | ~4060* (odhad) | (žádná veřejná benchmark data) | **Kritický bug** selhání toolresult zpráv (issue #16389) | Žádná explicitní podpora češtiny | 512K | ✅ | Open weights (brzy) | | Toolresult bug **nepoužitelné** | $0.60/M in | Neověřeno | Ano (otevřené váhy) |
| **kimi-k2.6:cloud** | ~3050* (odhad) | SWEBench 80.2% HLE 54.0% | Dobrá | Žádná podpora češtiny, **náhodný čínský drift** (kritické) | 256K | ✅ | Modified MIT | Střední | Čínský výstupní drift, OpenRouter kontextbug (32K) | $0.60/M in | 200300 tool calls (design) | Ano (MITlike) |
| **qwen3.6:cloud** | ~? (nepřímý odhad) | | | 201 jazyků (včetně češtiny) | 256K | ✅ | Apache2.0 | Střední | | | | Ano |
| **qwen3-coder-next:cloud** | ~4060* (odhad) | SWEBench ~70.6% (odhad) | Dobrá | 201 jazyků (včetně češtiny) | 512K | ❌ | Apache2.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** | ~6080* (odhad) | | | Strong (Google) | 1M | ✅ | 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 1M 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 toolresult 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.
### Watchlist (sledujte vývoj)
- **`nemotron-3-ultra:cloud`** slibné specifikace, ale chybí reálná data o toolcalling a dlouhodobé stabilitě.
- **`qwen3-coder-next:cloud`** zaměřeno na kódování, 512K kontext, dobrá podpora jazyků.
---
## 3. Metodologie a zdroje
- **Seznam modelů:** https://ollama.com/models?c=cloud (scraped 20260607).
- **Benchmarky a metriky:** `knowledge/models.md`, `results/2026-06-07_ollama-cloud-agent-model-comparison.md`, veřejné benchmarky (SWEBench, TerminalBench, Code Arena, MCPAtlas, HLE, MMMLU, CEval).
- **Bugtrackery:** 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 selfhost:** informace z oficiálních modelových repozitářů (Hugging Face, NVIDIA, ZAI).
---
*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ů.*

View 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)

View 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 15 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`. |

View 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``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 |

View 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 14 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 14 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 P2P3 — užitečné, ale nejsou blokátory.*